Compare commits

..

95 Commits

Author SHA1 Message Date
bicarus b23640222c api: add CORS policy (#878)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Adds `Access-Control-Allow-Origin: *` to the video stream server's
responses.

## Testing
Tested with substream project.
2026-08-20 02:58:22 -07:00
bicarus 3f6862908e api: drop stale websocket / stream connections (#877) 2026-08-19 13:37:41 -07:00
bicarus 0934cce225 api: h.264 video stream (#876)
## Link to GitHub Issue or related Pull Request, if one exists
fixes #875

## Description of change
Adds `-apistream`, an optional HTTP video stream of the mirrored screen.
It listens on the API port +2.

Two endpoints, sharing the same `screen`, `fps` and `q` parameters:

    /stream.mjpg    JPEG frames, for clients with no container support
/stream.h264 H.264 annex-b, for an app driving MediaCodec or
VideoToolbox itself

One encoder per connection, fed by a per-screen pump that always hands
over the newest frame, so a slow reader drops frames instead of building
a backlog. `capture.get_jpg` behaviour is unchanged.

Additional documentation for developers:
https://github.com/spice2x/spice2x.github.io/wiki/Video-Stream

## Testing
2026-08-19 03:10:02 -07:00
bicarus 7c50fcc79e graphics: remove libjpeg from XP builds, fetch at build time (#874)
Replaces the vendored `external/libjpeg-turbo` tree (191 files) with the
`mingw-w64-libjpeg-turbo` AUR package, already built into the Docker
deps image.

The WinXP toolchains set `CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY`
against their own sysroot, so they cannot see AUR mingw packages. Those
targets now build without JPEG support: `jpeg_encoder::encode` compiles
to a stub returning `false`, which callers already treat as "no frame
available". The practical effect is that `capture.get_jpg` over the API
is unavailable on WinXP builds; every other target is unchanged.

CMake names `libjpeg.a` outright, since the package also ships an import
library and linking that would add a runtime DLL dependency.
2026-08-18 08:57:18 -07:00
clamp 0f4ab63101 patchmanager: fix signature patches reporting neither on or off (#873)
## Link to GitHub Issue or related Pull Request, if one exists
None

## Description of change
Patch Manager showed valid signature patches as broken: **"Bad patch;
patch is neither on or off"** instead of Enabled/Disabled, so they could
not be toggled.

`SignaturePatch::to_memory` had two bugs. In the configurator it cached
a file offset as `data_offset_ptr`, so status checks `memcmp`'d a fake
address. It also passed JSON `offset` into `find_pattern` while still
indexing the signature/replacement from 0, which mis-aligned every patch
with `offset != 0`. This change locates the signature start, applies
`offset` afterward, compares only the replacement window, and leaves the
pointer null so `is_patch_active` re-resolves from `data_offset`.

Not proposed as built-in patches. The JSON below is the reproduction
case: each entry uses `offset > 0` and a replacement shorter than the
signature.

## Testing
Reproduced in Patch Manager against `bm2dx.dll` using the signature JSON
below. Before the fix, every patch reported "neither on or off". After
the fix, each patch locates, shows Disabled/Enabled, and toggling writes
only the replacement bytes at `signature_match + offset`.

## Demo
<details>
<summary>Signature JSON used to reproduce (offset + short
replacement)</summary>

```json
[
  {
    "info": "streaming / getcm patches (type=signature)",
    "gameCode": "LDJ",
    "notes": "Each site uses a unique signature (usage=0 only)."
  },
  {
    "type": "group",
    "id": "streaming-getcm",
    "name": "Streaming getcm",
    "description": "Enable all children so streaming.common merges without Banner FS and getcm can fire without visiting Test Mode.",
    "gameCode": "LDJ"
  },
  {
    "name": "Streaming: merge common without Banner",
    "description": "NOP jz in streaming.common callback so CM work table is filled even when Banner FS is still null.",
    "caution": "Required. Without this, early common responses are discarded and getcm stays empty.",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "E8????????4885C00F84????????488D0D????????488D15????????41B848080000",
    "replacement": "909090909090",
    "offset": 8,
    "usage": 0
  },
  {
    "name": "Streaming: scheduler without Banner (common)",
    "description": "NOP jz after Banner getter on the common branch of the periodic scheduler.",
    "caution": "Enable with the getcm scheduler sibling. Unique via imul of common-interval dword.",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "E8????????4885C0742469057E791C0AE8030000",
    "replacement": "9090",
    "offset": 8,
    "usage": 0
  },
  {
    "name": "Streaming: scheduler without Banner (getcm)",
    "description": "NOP jz after Banner getter on the getcm branch of the periodic scheduler.",
    "caution": "Enable with the common scheduler sibling. Unique via imul of getcm-interval dword. Without this, getcm never schedules while Banner FS is null.",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "E8????????4885C07424690574B91D0AE8030000",
    "replacement": "9090",
    "offset": 8,
    "usage": 0
  },
  {
    "name": "Streaming: fall into getcm after common",
    "description": "NOP jmp-after-common so the same scheduler tick can evaluate getcm instead of returning early.",
    "caution": "Pair with Banner scheduler skips (or a live Banner FS).",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "891D????????E9????????E8????????4885C074246905",
    "replacement": "9090909090",
    "offset": 6,
    "usage": 0
  },
  {
    "name": "Streaming: getcm interval 1s #1",
    "description": "Default getcm poll interval 1800s to 1s (first init store).",
    "caution": "Enable #1 and #2 together. Trailing BF3C000000 distinguishes this init site.",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "C705????????18150000C705????????08070000C705????????201C0000BF3C000000",
    "replacement": "01000000",
    "offset": 16,
    "usage": 0
  },
  {
    "name": "Streaming: getcm interval 1s #2",
    "description": "Default getcm poll interval 1800s to 1s (second init store).",
    "caution": "Enable #1 and #2 together. Trailing 448925 distinguishes this init site.",
    "gameCode": "LDJ",
    "type": "signature",
    "group": "streaming-getcm",
    "dllName": "bm2dx.dll",
    "signature": "C705????????18150000C705????????08070000C705????????201C0000448925",
    "replacement": "01000000",
    "offset": 16,
    "usage": 0
  }
]
```
</details>

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-18 08:16:12 -07:00
bicarus 8b2f38307b graphics: rewrite screenshot and api capture image processing (#870)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Significantly speeds up API screen capture and D3D9 screenshots saving.
Two reasons for doing this:

1. We now have a 4K game (GITADORA) and existing capture code was taking
multiple seconds.
2. Renewed user interest on streaming as we have a couple more companion
apps in active development.

**API screen capture (streaming), 1280x720:** 14.3ms -> 6.3ms per frame.
Back buffer copies go to pooled `D3DPOOL_SYSTEMMEM` surfaces via
`GetRenderTargetData` instead of allocating a lockable render target
every frame, and TooJpeg is replaced with libjpeg-turbo (encode 9.8ms ->
3.0ms). MSAA remains unsupported

**Screenshots for GITADORA arena model, across 4 screens with one of
them 4K**: 4068ms -> 124ms. `D3DXSaveSurfaceToFileA` is replaced with
fpng (encode 4043ms -> 76ms) and the screens encode in parallel.
Dropping D3DX also removes the `d3dx9_43.dll` ... `d3dx9_24.dll` probing
loop, so screenshots no longer fail outright on machines with no D3DX9
runtime installed.

Screenshot surfaces are read on the present thread, so no D3D call
reaches another thread for screenshots. This fixes a hang in DDR X2
introduced earlier in the branch: its device has no internal locking,
and reading the surface on a pool thread while the present thread sat
inside `GetRenderTargetData` left the game's own render thread
deadlocked.


## Testing

- **GITADORA** (arena model, D3D9Ex, 4K main plus three subscreens,
windowed) with
`-screenshotsub`: three sets of four screenshots, images verified
correct. Completion
order differs between sets, so the screens really are encoding in
parallel.
- **LovePlus** (KLP, plain D3D9, 768x1360): covers the inline path used
by games whose
  image processing must not leave the present thread. 
- **API screen capture** through a companion app: live video correct
throughout.
- **Print Screen** bound as the screenshot key: the clipboard copy
succeeded on every shot.
- Quitting the game after capturing leaves no `IDirect3DDevice9`
reference count warning,
  so the pooled readback surfaces are released along with the device.
2026-08-18 00:22:45 -07:00
bicarus 3863d5a4ed misc: various clean up for diagnosing launch failures (#872)
## Link to GitHub Issue or related Pull Request, if one exists
#345 

## Description of change

**IIDX TDJ rom probe no longer touches removable media** —
`C:\000rom.txt` and `D:\001rom.txt` are not emulated paths; they hit
whatever is actually mounted on the user's machine. `D:` is commonly an
optical drive or card reader, and the launcher clears
`SEM_FAILCRITICALERRORS` process-wide before attach, so an empty drive
raises the modal *"insert a disk"* dialog and blocks the attaching
thread. The probe now checks `GetDriveTypeW` and only reads fixed and
RAM disks.

**`iat_find` no longer calls `log_fatal` on an unparseable module** —
`iat_try(nullptr)` walks every loaded module, including foreign ones
(injected, manually mapped, header wiped by AV/EDR/overlays). A non-`MZ`
DOS header called `log_fatal`. There is nothing to hook in such a
module, so it is skipped.

**`logger::stop()` can no longer hang forever** — hook installation
suspends every other thread, including the logging thread. `stop()`
unconditionally joined that thread, so `log_fatal` and the 30-second
`show_popup` watchdog both wedged instead of terminating, and logging is
asynchronous so nothing reached log.txt either. It now waits with a
timeout, then detaches and flushes synchronously.

**`GetFileSizeEx` was never hooked** — the hook was registered under the
name `"GetFileSize"`, so it re-patched that slot instead.

**Warn when `-modules` is set** — it changes where the game is run from,
and is usually set accidentally.

## Testing
*how was the code tested?*
2026-08-17 22:41:07 -07:00
bicarus-dev 94574c485a exlude line ending enforcement for external dir 2026-08-16 21:27:03 -07:00
bicarus-dev f857926ec3 fix line endings 2026-08-16 21:23:41 -07:00
bicarus adf4cccd4a overlay: fix subscreen overlay rendering for popn (#869)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
For popn HC, some transparency effects, like the one for touch feedback,
rendered incorrectly; this is due to how alpha channel is handled for
subscreen overlay drawing on top of ImGui.

## Testing
Tested popn and iidx.
2026-08-16 04:24:14 -07:00
bicarus bf8e194685 sdvx: fix api touch and -sdvxnosub sub window (#868)
## Link to GitHub Issue or related Pull Request, if one exists
Related: #859 and #862.

## Description of change

Three SDVX (Valkyrie model) fixes:

* **API touch went to the wrong window.** SDVX registers touch on both
its Main Screen and Sub Screen windows, and the native injector kept
whichever attached last. The touch surface is now published explicitly:
sub screen window when windowed, main window in fullscreen.
* **Landscape never rotated API coordinates.** Synthetic contacts bypass
`transform::hardware_to_game`, so they missed the rotation a real finger
gets. Applied before injection now, as portrait already did. Extracted
to `sdvx_landscape_rotate` so both paths share it; gated on the native
path since `wintouchemu` rotates via the subscreen overlay instead.
* **`-sdvxnosub` didn't hide the sub window in fullscreen.**
`ShowWindow_hook` had branches for GITADORA, pop'n and IIDX but not
SDVX. Added the missing one.

## Testing
Tested Nabla
2026-08-16 04:03:42 -07:00
bicarus a2e508208c gitadora: fix synthetic touch (mouse and api) (#867)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix mouse and touch API handling for gitadora.

* hook native touch in every arena window mode (previously only using
wintouchemu for 1 windowed mode)
* publish the touch window (SMALL panel, or the main window drawing the
overlay) and only attach injection there
* route touch on the SMALL window straight through instead of the
overlay transform, and accept the mouse only there
* fix spiceapi by forcing 800x1280 touch canvas size
* run API capture before the subscreen present to fix api not showing
sub image


## Testing
Test:

gitadora 1/2/4 windowed mode, fullscreen

iidx /sdvx / popn windowed/fullscreen for regressions
2026-08-16 01:43:20 -07:00
bicarus f5888609a8 graphics: option to include subscreens in screenshot (#866)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Adds `-screenshotsub`. Off by default. When on, screenshots include the
subscreens. All subscreens are captured, even if they are hidden from
view. Works for all four screens of gitadora arena model as well.

This should also fix an issue with API / companion app not capturing
gitadora subscreen correctly.

Note: only done for DX9... DX11 will need another PR to make this work.

Unrelated to this PR, there seems to be a bug with gitadora not
accepting mouse or api touch input.

## Testing
2026-08-14 04:05:50 -07:00
bicarus c6cd72c528 graphics: exclude overlay from screenshots (#865)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change

Adds the `-screenshotoverlay` option.

By default, file screenshots exclude the Spice overlay. Enabling this
option captures screenshots after the overlay is rendered.

Also updates DX11 backend to allow screenshots even when overlay is
disabled.

## Testing
2026-08-13 23:21:46 -07:00
bicarus 82e0c053d0 overlay: create a dedicated thread for polling hotkeys (#864)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Create a new thread that polls for the following hotkeys:

* super exit (both alt+f4 and bound key)
* coin insert
* screenshot

The goal is to make the capture of these more reliable, because before
this PR it wasn't.

## Testing
2026-08-12 20:04:59 -07:00
James Liu 3b29227dbc gitadora: fix XG2 gibberish text showing (#863)
> [!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

## Description of change
Fix gibberish in dynamic Japanese text in GuitarFreaks/DrumMania XG2
(K32/K33) when Windows uses a non-Japanese system code page.

XG2 converts UTF-8 property strings through WideCharToMultiByte(CP_ACP)
before rendering. On systems where the active code page is not
Shift-JIS, these strings are converted using the system code page and
become corrupted or turn into question marks.

Spice2x already hooks WideCharToMultiByte and redirects these
conversions to CP932 for newer 64-bit Gitadora models. This change makes
the existing hook available to 32-bit builds and enables it specifically
for XG2 models K32 and K33.

The existing 64-bit Gitadora Arena and T44 conditions remain unchanged.

## Testing
Before
<img width="1280" height="745" alt="image"
src="https://github.com/user-attachments/assets/34aa2c93-89e8-4c4f-b267-6cbc236f19de"
/>
<img width="2054" height="1188" alt="image"
src="https://github.com/user-attachments/assets/d88b5770-5545-4402-b9e7-3126fe048fb9"
/>

After
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/d43b4e47-b6af-4b97-8898-53849c9a0ca4"
/>
<img width="1280" height="720" alt="image"
src="https://github.com/user-attachments/assets/884301e2-1edd-4ad2-92ac-ece656853fe7"
/>

- Tested GuitarFreaks XG2 on a non-Japanese (English US) Windows.
- Verified that Community Log preset comments render correctly in
Japanese.
- Verified that Cooperation Challenge descriptions, rewards, and
progress text render correctly.
- Confirmed that the previous mojibake and question marks no longer
appear.
- GitHub Actions build completed successfully for both architectures.
2026-08-12 00:08:05 -07:00
bicarus ce2f93d234 graphics: refactor DX9 screenshot (part 2) (#861) 2026-08-11 01:11:42 -07:00
bicarus 13a171f199 sdvx: fix api touch in windowed mode (#862)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #858 

## Description of change
Fix orientation being wrong in windowed mode when API touch is used

Fix mouse no longer working when API client is in use.

This PR only affects windowed mode SDVX.

## Testing
2026-08-10 18:58:48 -07:00
bicarus 3da352488e graphics: refactor screenshots (#860)
First commit - pure refactoring, no functional changes

Second commit - clean up & minor bug fix.
2026-08-09 20:47:38 -07:00
bicarus 2386047c2f sdvx: fix touchscreen and API touch in landscape mode (#859)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #858 

## Description of change
Landscape mode needs another adjustment applied to touch coordinates.

This fixex both native touch and wintouchemu paths.

## Testing
Tested landscape, also with full screen resolution changes.
2026-08-08 16:22:29 -07:00
bicarus baa550037a api: sdvx tape led (#857)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #852

## Description of change
Add SDVX valk cab tape LED output over API

## Testing
tested with custom python script over api
2026-08-07 00:38:17 -07:00
bicarus b53447bed5 popn: fix subscreen redraw option causing graphical glitches (#854)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Old behavior: Forced redraw presented the subscreen every main frame,
even when the game already presented it, causing duplicate presents and
tearing in popn (was fine in sdvx)

New behavior: Forced redraw acts as a fallback, presenting only when the
game skips or fails a subscreen update.

## Testing
Popn - no more glitching
Nabla - no regression
2026-08-06 04:14:31 -07:00
bicarus 4959a58de3 iidx: don't hook legacy camera unless requested (#856)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #855 

## Description of change
Legacy camera hooks are specifically for IIDX 25/26 and they only
perform redirection of device discovery (emulates USB vendor/device ID
and USB port). It has very limited use since most cameras are
practically unusable in IIDX 25/26. Don't enable it, unless explicitly
requested by the user.

New truth table for `-iidxcabcams` :

| Mode | `-iidx` off (cab setup) | `-iidx` on |
|---|---|---|
| `auto` | Native cameras enabled (same as `on`) | Cameras disabled
(same as `off`) |
| `off` | Cameras disabled | Cameras disabled |
| `on` | Native cameras enabled | Native cameras enabled |
| `legacy` | Native cameras enabled | IIDX 25/26 discovery emulation
enabled |

## Testing
2026-08-06 03:44:52 -07:00
bicarus 9141ff453b lang: detect forced UTF-8 ACP, try to opt-out (#853)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Fixes broken Japanese text when Windows `Use Unicode UTF-8 for worldwide
language support` setting is enabled. This setting changes the system
ACP to UTF-8 (65001), causing legacy Shift-JIS lead-byte checks to fail.

Windows 11: requests the legacy process code page through the manifest.

Windows 10: detects ACP 65001 (UTF-8), warns the user via deferred log,
and applies compatibility hook (only for popn pika model for now)

Windows 7 and below - UTF-8 option doesn't exist.

## Testing
2026-08-04 09:04:29 -07:00
bicarus f955ab984c iidx: allow main window to receive touches in windows TDJ subscreen mode (#850)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
This allows API touches to be recognized even if the main window is in
focus.

## Testing
2026-08-02 16:46:33 -07:00
bicarus-dev 435b9ce9aa output device name in message 2026-08-02 12:29:21 -07:00
bicarus 1ec7528dac gitadora: option to disable frame limiter (#848)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
There is a patch for GITADORA series that fixes frame pacing on modern
Windows. Turn it into a signature patch and embed it into the game.

## Testing
should work for all 64-bit gitadora versions, pre-GWD
2026-08-02 03:35:07 -07:00
bicarus-dev e7822aad8e remove volume boost from quick options 2026-08-02 01:04:11 -07:00
bicarus-dev 8140e83e46 mark -sdvxlandscape as experimental 2026-08-02 01:01:09 -07:00
bicarus cc0899290e sdvx: re-enable landscape mode (#847)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Un-deprecate SDVX landscape mode. The game calls `GetViewport` to figure
out the camera position, so if we return a sane value the game goes back
to rendering correctly.

## Testing
Tested nabla only
2026-08-02 00:50:04 -07:00
Tsung-Hsiang Chang 3012139028 gitadora: add native dual fullscreen for Arena (#846)
## Link to GitHub Issue or related Pull Request, if one exists

Fixed https://github.com/spice2x/spice2x.github.io/issues/740

## Description of change

- Enable native D3D9 adapter-group dual fullscreen for the GITADORA
Arena model when the two-screen MAIN + SMALL configuration is selected
without windowed mode.
- Present MAIN and SMALL on their respective monitor heads while keeping
the invisible LEFT and RIGHT targets offscreen.
- Scope the new fake-swap-chain query behavior to the hidden GITADORA
two-head targets, preserving existing behavior for other games and
configurations.

The existing borderless-windowed two-screen path could not consistently
keep input and game timing synchronized and also reduced rendering
performance. Native dual fullscreen avoids that windowed composition
path.

## Testing

- Manually tested GITADORA Arena with separate MAIN and SMALL monitors
in fullscreen mode. Windowed mode still works if the user chooses to use
that.
- Confirmed both displays render correctly, touch input works on the
SMALL screen, gameplay input stays synchronized, and gameplay holds a
steady 60 FPS.
- Built `spicetools_spice64` at commit `e4a98e9` with the repository
Docker toolchain.
- Confirmed no new compiler warnings.
- Confirmed the forbidden static DLL import check passes.
- Confirmed the Windows 7 DLL compatibility check reports `All DLLs
OK!`.

---------

Co-authored-by: vgod <428979+vgod@users.noreply.github.com>
2026-08-02 00:03:36 -07:00
bicarus c590b87cff jb: add t44 support (#845)
With help from certain sea creature.
2026-08-01 01:20:31 -07:00
bicarus 5cabd026ae misc: various performance clean up (#844)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

- Replace unnecessary precision timers with standard sleeps to reduce
wakeups and background CPU usage.
- Reuse buffers in raw input, touchscreen, and HID output paths to
eliminate steady-state allocations.
- Pre-index HID button groups and correctly process batched HID reports.

No functional changes.

## Testing
2026-07-28 21:05:32 -07:00
bicarus 7ccd938f9b overlay: mouse-as-touch needs to check if subscreen window is active (#842)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #840 

## Description of change
Currently, for subscreen games, mouse events are still delivered even
when the subscreen overlay window is not visible.

Change this so that the subscreen overlay must be active and under the
mouse cursor for the mouse-to-touch transformation to occur. Applies to
both native and wintouchemu.

## Testing
Needs to test everything again..

iidx:

- [ ]  full screen with overlay
- [ ]  single window with overlay
- [ ]  two window

Test: mouse, poke, api, real touch screen

sdvx:

- [ ] full screen with overlay
- [ ] windowed

popn

- [x] full screen with overlay
- [x] window with overlay
- [x] dedicated window

gitadora

- [x] single window with overlay
- [x] dedicated sub window

nostalgia

- [x] fullscreen
- [x] windowed

test: poke

wintouchemu
- [ ] Do all of the above again with wintouchemu
2026-07-28 01:14:47 -07:00
bicarus 88a210e82e iidx: camera hook improvements (#841)
## Link to GitHub Issue or related Pull Request, if one exists
#201

## Description of change

- Adds MJPEG camera support using Media Foundation decoding.
- Moves camera capture to asynchronous Source Reader callbacks.
- Supports native NV12 and YUY2 capture, preferring target-sized native
formats over MJPEG when possible.
- Improves capture and rendering performance through callback-paced
reads, bulk frame uploads, optimized row-based flips, and lazy
allocation of flip textures.
- Makes runtime media-type changes interrupt pending reads and safely
flush and reconfigure the Source Reader.
- Improves automatic mode selection based on aspect ratio, proximity to
1280x720, frame rate, and capture format.
- Hardens camera shutdown, flush handling, media-type changes, and
repeated capture failures.

## Testing
Tested with two cameras in tdj
2026-07-27 08:42:46 -07:00
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
247 changed files with 43969 additions and 26814 deletions
+46
View File
@@ -0,0 +1,46 @@
# Normalize every text file to LF in the repository.
# The build runs under Linux/MinGW containers, so LF is also used in the
# working tree; Windows editors and toolchains handle LF fine.
* text=auto eol=lf
# Windows-only files that must keep CRLF in the working tree.
*.bat text eol=crlf
*.cmd text eol=crlf
*.sln text eol=crlf
*.vcproj text eol=crlf
*.vcxproj text eol=crlf
*.props text eol=crlf
*.filters text eol=crlf
# Files that must keep LF even if a Windows editor rewrites them.
*.sh text eol=lf
*.in text eol=lf
*.cmake text eol=lf
*.mk text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
# Binary files - never touch the contents.
*.bin binary
*.ico binary
*.ttf binary
*.otf binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.bmp binary
*.zip binary
*.7z binary
*.gz binary
*.dll binary
*.exe binary
*.lib binary
*.a binary
*.o binary
*.obj binary
*.pdb binary
# Vendored code is stored and checked out byte-for-byte as upstream ships it,
# so re-importing a library never produces line-ending-only diffs.
src/spice2x/external/** -text
+31 -31
View File
@@ -1,32 +1,32 @@
on: [push, pull_request]
name: Continuous Integration
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fw-ci:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v4
- 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
- name: Calculate commit SHA
id: vars
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v4
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
on: [push, pull_request]
name: Continuous Integration
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fw-ci:
name: Build
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- 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.23
- name: Calculate commit SHA
id: vars
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile
run: ./build_docker.sh
- uses: actions/upload-artifact@v6
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
if-no-files-found: error
+49
View File
@@ -0,0 +1,49 @@
name: Draft Release
on:
workflow_dispatch:
permissions:
contents: write
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
release:
name: Build and Draft Release
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
with:
ref: main
fetch-depth: 0
- name: Clean leftover build artifacts
run: |
rm -rf .ccache dist bin cmake-build-*
- name: Compile
run: ./build_docker.sh
- name: Determine release name from dist filename
run: |
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28
name="${dist%.zip}"
# the tag is the date portion, e.g. 26-06-28
tag="${name#spice2x-}"
echo "RELEASE_NAME=$name" >> $GITHUB_ENV
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
- name: Create draft release
uses: softprops/action-gh-release@v3
with:
draft: true
prerelease: true
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_NAME }}
target_commitish: main
generate_release_notes: true
files: |
src/spice2x/dist/spice2x-*.zip
+7 -3
View File
@@ -11,13 +11,17 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
* For a new game that was never supported (not a new version of a game, but rather a new series): please wait 1 year after official AC release in Japan.
* For new version of an already supported game: proceed with caution and use generally-accepted community guidelines.
### Forbidden "features" that will never be accepted
### Pull requests that will not be accepted
* Performing decryption of encrypted files
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data
* Performing decryption of encrypted files.
* Including (bundling), distributing, or linking to game data, or making it easier to find/download game data.
* Any features that [phone home](https://en.wikipedia.org/wiki/Phoning_home)
* This includes automatic updaters.
* Exception: we have allowed the "patch import from URL" feature, but this has only been done under very careful considerations and plenty of warnings to the user.
* Built-in EA server that saves data. They should live in a separate project.
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
* Changes to the software license terms.
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
### Avoiding regressions
+137 -7
View File
@@ -256,6 +256,57 @@ add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
# sysroot and cannot see it, so those targets build without JPEG support.
add_library(spice_jpeg INTERFACE)
if(NOT SPICE_XP)
# search static archives only: the mingw package also ships an import library,
# and linking that one would pull in a libjpeg DLL at runtime
set(SPICE_JPEG_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
find_package(JPEG)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_JPEG_SUFFIXES})
if(JPEG_FOUND)
target_link_libraries(spice_jpeg INTERFACE JPEG::JPEG)
target_compile_definitions(spice_jpeg INTERFACE SPICE_JPEG=1)
else()
message(WARNING
"libjpeg-turbo not found: screen capture over the API is disabled")
endif()
endif()
# x264 for the API H.264 video stream, installed into the mingw sysroots by the
# deps image. The WinXP toolchains deliberately go without it and serve MJPEG only.
add_library(spice_x264 INTERFACE)
if(NOT SPICE_XP)
# search static archives only: the mingw package also ships an import library,
# and linking that one would pull in a libx264 DLL at runtime
set(SPICE_X264_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})
find_library(X264_LIBRARY NAMES x264 libx264)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${SPICE_X264_SUFFIXES})
find_path(X264_INCLUDE_DIR NAMES x264.h)
if(X264_LIBRARY AND X264_INCLUDE_DIR)
target_include_directories(spice_x264 INTERFACE "${X264_INCLUDE_DIR}")
target_link_libraries(spice_x264 INTERFACE "${X264_LIBRARY}")
target_compile_definitions(spice_x264 INTERFACE SPICE_H264=1)
else()
message(WARNING
"x264 not found: the api video stream will only offer MJPEG")
endif()
endif()
# fpng's SIMD needs the whole unit built for SSE4.1, which its runtime CPU check
# cannot undo, so keep it scalar rather than raising the CPU baseline
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
COMPILE_DEFINITIONS "FPNG_NO_SSE=1")
if(NOT MSVC)
set_source_files_properties(external/fpng/fpng.cpp PROPERTIES
COMPILE_OPTIONS "-fno-strict-aliasing")
endif()
# set link time optimizations (disabled for Debug builds for speed, disabled
# for RelWithDebInfo builds due to "lto1: error: two or more sections for"
# errors)
@@ -317,6 +368,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
# api
api/controller.cpp
api/websocket.cpp
api/capture_pump.cpp
api/h264_stream.cpp
api/stream_format.cpp
api/stream_server.cpp
api/request.cpp
api/response.cpp
api/module.cpp
@@ -332,6 +387,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
api/modules/control.cpp
api/modules/touch.cpp
api/modules/iidx.cpp
api/modules/sdvx.cpp
api/serial.cpp
api/modules/drs.cpp
api/modules/lcd.cpp
@@ -379,7 +435,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
external/tinyxml2/tinyxml2.cpp
external/http-parser/http_parser.c
external/usbhidusage/usb-hid-usage.c
external/toojpeg/toojpeg.cpp
external/fpng/fpng.cpp
external/scard/scard.cpp
# games
@@ -408,13 +464,17 @@ 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/bi2x_hook.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
@@ -430,6 +490,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
@@ -499,6 +560,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
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
@@ -525,10 +587,16 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp
hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp
hooks/graphics/jpeg_encoder.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_readback.cpp
hooks/graphics/backends/d3d9/d3d9_screenshot.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_gfdm.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
@@ -567,7 +635,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp
misc/eamuse.cpp
misc/extdev.cpp
misc/nativetouchhook.cpp
misc/hotkeys.cpp
misc/sciunit.cpp
misc/sde.cpp
misc/wintouchemu.cpp
@@ -605,12 +673,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
@@ -637,8 +720,14 @@ set(SOURCE_FILES ${SOURCE_FILES}
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
@@ -671,6 +760,33 @@ set(SOURCE_FILES ${SOURCE_FILES}
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
@@ -681,7 +797,7 @@ source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} PREFIX "Source Files" FILES ${SOUR
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)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
if(NOT MSVC)
@@ -721,7 +837,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_linux
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)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
@@ -739,7 +855,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
@@ -762,7 +878,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
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)
@@ -781,6 +897,7 @@ endif()
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})
# the configurator serves neither the API nor the video stream, so it needs no codecs
target_link_libraries(spicetools_cfg
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)
@@ -955,3 +1072,16 @@ set_target_properties(spicetools_spice64 spicetools_spice64_linux spicetools_stu
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()
+50
View File
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
- `Side Panel Right Inner`
- `Side Panel Right`
#### SDVX
- tapeled_get(name: str, ...)
- returns a list containing a dict of the current tape LED states. The dict keys are:
- `Title`
- `Upper Left Speaker`
- `Upper Right Speaker`
- `Left Wing`
- `Right Wing`
- `Control Panel`
- `Lower Left Speaker`
- `Lower Right Speaker`
- `Woofer`
- `V Unit`
#### LCD
- info()
- returns information about the serial LCD controller some games use
@@ -277,6 +291,42 @@ which also means that your hex edits are applicable directly.
- image_resize_set_scene(scene: int)
- sets the active scene for image resize state; set to 0 to disable resize
## Video Stream
Separate from the JSON API, spice can serve the mirrored screen as a video
stream over plain HTTP. Enable it with `-apistream`. It listens on the API port
plus two, in the same way the WebSocket server uses the API port plus one, so
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
Two formats are served:
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
http://host:1339/stream.h264 H.264 annex-b, no container
All accept the same optional query parameters:
- `screen` - which screen to mirror, 0-3. Defaults to the subscreen when the
game has one, otherwise the main screen.
- `fps` - frames per second, 1-60. Default 30.
- `q` - quality, 1-100. Default 70. This is the JPEG quality for `stream.mjpg`
and is mapped onto the H.264 rate factor for `stream.h264`, so the same number
does not mean the same thing for both.
For example:
http://host:1339/stream.h264?screen=1&fps=30&q=70
See the wiki for format tradeoffs, latency tuning, testing commands and client
notes.
The stream is view only. Touch and other input still go through the JSON API,
so a companion app needs both. There is no authentication on the stream port -
anyone who can reach it can watch the screen.
WinXP builds have no video stream. Neither encoder is compiled in, so every
endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for
the same reason.
## Native wrapper libraries
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
Python is the only one that is fully spec compliant.
+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;
}
+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 -1
View File
@@ -239,7 +239,7 @@ bool ICCADevice::parse_msg(MessageData *msg_in,
// SDVX Old cabinet mode
if (avs::game::is_model("KFC") && avs::game::SPEC[0] != 'G' && avs::game::SPEC[0] != 'H')
answer_type = 1;
if (avs::game::is_model("L44"))
if (avs::game::is_model({ "L44", "T44" }))
answer_type = 2;
// check answer type
+58
View File
@@ -0,0 +1,58 @@
#include "capture_pump.h"
#include <array>
#include <mutex>
#include "hooks/graphics/graphics.h"
namespace api::capture_pump {
namespace {
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
std::mutex CLAIMED_M;
std::array<bool, GRAPHICS_CAPTURE_SCREEN_NO> CLAIMED {};
bool valid_screen(int screen) {
return 0 <= screen && screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO);
}
}
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
uint64_t *timestamp, int *width, int *height) {
if (!valid_screen(screen)) {
return false;
}
std::lock_guard<std::mutex> lock(CONSUMER_M[screen]);
graphics_capture_trigger(screen);
return graphics_capture_receive_raw(
screen, out, divide, timestamp, width, height);
}
bool claim_screen(int screen) {
if (!valid_screen(screen)) {
return false;
}
std::lock_guard<std::mutex> lock(CLAIMED_M);
if (CLAIMED[screen]) {
return false;
}
CLAIMED[screen] = true;
return true;
}
void release_screen(int screen) {
if (!valid_screen(screen)) {
return;
}
std::lock_guard<std::mutex> lock(CLAIMED_M);
CLAIMED[screen] = false;
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
#include <memory>
namespace api::capture_pump {
struct Frame {
// packed 24bpp RGB, width * height * 3 bytes
std::shared_ptr<uint8_t[]> pixels;
uint64_t timestamp = 0;
int width = 0;
int height = 0;
};
// the graphics layer has one capture slot per screen, so concurrent waiters would steal
// each other's frames; everything that captures goes through here to keep it serialized
bool capture_direct(int screen, std::shared_ptr<uint8_t[]> &out, int divide,
uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr);
// a screen carries one stream at a time; false when another connection already holds it
bool claim_screen(int screen);
void release_screen(int screen);
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace api {
extern std::atomic_uint32_t CLIENT_COUNT;
inline bool has_clients() {
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
}
}
+9
View File
@@ -5,6 +5,7 @@
#include <utility>
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "util/crypt.h"
@@ -28,6 +29,7 @@
#include "modules/lcd.h"
#include "modules/lights.h"
#include "modules/memory.h"
#include "modules/sdvx.h"
#include "modules/touch.h"
#include "modules/resize.h"
#include "request.h"
@@ -36,6 +38,8 @@
using namespace rapidjson;
using namespace api;
std::atomic_uint32_t api::CLIENT_COUNT = 0;
Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty)
{
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::SDVX());
state->modules.push_back(new modules::Touch());
state->modules.push_back(new modules::Resize());
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
}
void Controller::free_state(api::ClientState *state) {
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
// free cipher
delete state->cipher;
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
}
void Controller::free_socket() {
+224
View File
@@ -0,0 +1,224 @@
#include "h264_stream.h"
#ifdef SPICE_H264
#include <vector>
#include <x264.h>
#include "util/logging.h"
namespace api {
namespace {
// BT.601 limited range, the range every decoder assumes for H.264 without
// explicit colour metadata
inline uint8_t rgb_to_y(int r, int g, int b) {
return static_cast<uint8_t>(((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
}
inline uint8_t rgb_to_u(int r, int g, int b) {
return static_cast<uint8_t>(((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);
}
inline uint8_t rgb_to_v(int r, int g, int b) {
return static_cast<uint8_t>(((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);
}
// a bare annex-b elementary stream, one encoder per connection so every client
// starts on its own keyframe. no container, so nothing here keeps a media clock
class H264Writer : public StreamWriter {
public:
H264Writer(int quality, int fps) : quality(quality), fps(fps) {}
~H264Writer() override {
this->close();
}
std::string content_type() const override {
return "video/h264";
}
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
// I420 needs even dimensions
const int width = frame.width & ~1;
const int height = frame.height & ~1;
if (width <= 0 || height <= 0) {
return true;
}
if (this->encoder == nullptr) {
if (!this->open(width, height)) {
return false;
}
} else if (width != this->width || height != this->height) {
// the encoder is fixed at the size it opened with; let the client reconnect
log_info("api::stream", "capture size changed, ending H.264 client");
return false;
}
this->convert(frame.pixels.get(), frame.width);
this->picture.i_pts = this->frame_index;
x264_nal_t *nals = nullptr;
int nal_count = 0;
x264_picture_t picture_out;
const int size = x264_encoder_encode(
this->encoder, &nals, &nal_count, &this->picture, &picture_out);
if (size < 0) {
log_warning("api::stream", "H.264 encode failed");
return false;
}
this->frame_index++;
if (size == 0) {
return true;
}
// x264 lays every NAL of the frame out back to back. an SEI or delimiter
// carries no picture, so only the parameter sets and the slice go through
this->annexb.clear();
for (int i = 0; i < nal_count; i++) {
switch (nals[i].i_type) {
case NAL_SEI:
case NAL_AUD:
case NAL_FILLER:
continue;
default:
break;
}
this->annexb.insert(this->annexb.end(),
nals[i].p_payload, nals[i].p_payload + nals[i].i_payload);
}
if (this->annexb.empty()) {
return true;
}
return send(this->annexb.data(), this->annexb.size());
}
private:
bool open(int width, int height) {
x264_param_t param;
if (x264_param_default_preset(&param, "ultrafast", "zerolatency") < 0) {
return false;
}
param.i_csp = X264_CSP_I420;
param.i_width = width;
param.i_height = height;
param.i_fps_num = this->fps;
param.i_fps_den = 1;
param.i_threads = 1;
param.b_annexb = 1;
// SPS/PPS ahead of every IDR, so a client can start decoding cold
param.b_repeat_headers = 1;
// a keyframe every two seconds bounds how long a new client waits
param.i_keyint_max = this->fps * 2;
param.i_log_level = X264_LOG_NONE;
param.rc.i_rc_method = X264_RC_CRF;
param.rc.f_rf_constant = 40.0f - (this->quality * 0.25f);
// baseline keeps hardware decode available on the widest range of phones
if (x264_param_apply_profile(&param, "baseline") < 0) {
return false;
}
this->encoder = x264_encoder_open(&param);
if (this->encoder == nullptr) {
log_warning("api::stream", "could not open the H.264 encoder");
return false;
}
if (x264_picture_alloc(&this->picture, X264_CSP_I420, width, height) < 0) {
this->close();
return false;
}
this->picture_ready = true;
this->width = width;
this->height = height;
return true;
}
void close() {
if (this->picture_ready) {
x264_picture_clean(&this->picture);
this->picture_ready = false;
}
if (this->encoder != nullptr) {
x264_encoder_close(this->encoder);
this->encoder = nullptr;
}
}
// packed 24bpp RGB to I420, averaging each 2x2 block for the chroma planes
void convert(const uint8_t *rgb, int source_width) {
uint8_t *plane_y = this->picture.img.plane[0];
uint8_t *plane_u = this->picture.img.plane[1];
uint8_t *plane_v = this->picture.img.plane[2];
const int stride_y = this->picture.img.i_stride[0];
const int stride_u = this->picture.img.i_stride[1];
const int stride_v = this->picture.img.i_stride[2];
for (int y = 0; y < this->height; y++) {
const uint8_t *row = rgb + static_cast<size_t>(y) * source_width * 3;
uint8_t *out_y = plane_y + static_cast<size_t>(y) * stride_y;
for (int x = 0; x < this->width; x++) {
const uint8_t *pixel = row + x * 3;
out_y[x] = rgb_to_y(pixel[0], pixel[1], pixel[2]);
}
}
for (int y = 0; y < this->height / 2; y++) {
const uint8_t *row0 = rgb + static_cast<size_t>(y * 2) * source_width * 3;
const uint8_t *row1 = row0 + static_cast<size_t>(source_width) * 3;
uint8_t *out_u = plane_u + static_cast<size_t>(y) * stride_u;
uint8_t *out_v = plane_v + static_cast<size_t>(y) * stride_v;
for (int x = 0; x < this->width / 2; x++) {
const uint8_t *p00 = row0 + (x * 2) * 3;
const uint8_t *p01 = p00 + 3;
const uint8_t *p10 = row1 + (x * 2) * 3;
const uint8_t *p11 = p10 + 3;
const int r = (p00[0] + p01[0] + p10[0] + p11[0] + 2) / 4;
const int g = (p00[1] + p01[1] + p10[1] + p11[1] + 2) / 4;
const int b = (p00[2] + p01[2] + p10[2] + p11[2] + 2) / 4;
out_u[x] = rgb_to_u(r, g, b);
out_v[x] = rgb_to_v(r, g, b);
}
}
}
int quality;
int fps;
int width = 0;
int height = 0;
int64_t frame_index = 0;
std::vector<uint8_t> annexb;
x264_t *encoder = nullptr;
x264_picture_t picture {};
bool picture_ready = false;
};
}
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps) {
return std::make_unique<H264Writer>(quality, fps);
}
}
#endif // SPICE_H264
+11
View File
@@ -0,0 +1,11 @@
#pragma once
#include <memory>
#include "stream_format.h"
namespace api {
// bare annex-b H.264; null when the build has no encoder
std::unique_ptr<StreamWriter> make_h264_writer(int quality, int fps);
}
+12 -6
View File
@@ -2,8 +2,10 @@
#include <functional>
#include <mutex>
#include <unordered_map>
#include "api/capture_pump.h"
#include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h"
#include "hooks/graphics/jpeg_encoder.h"
#include "util/crypt.h"
using namespace std::placeholders;
@@ -93,8 +95,6 @@ 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
int screen = 0;
@@ -120,10 +120,16 @@ namespace api::modules {
uint64_t timestamp = 0;
int width = 0;
int height = 0;
graphics_capture_trigger(screen);
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
CAPTURE_BUFFER.push_back(byte);
}, true, quality, true, divide, &timestamp, &width, &height);
std::shared_ptr<uint8_t[]> pixels;
bool success = capture_pump::capture_direct(
screen, pixels, divide, &timestamp, &width, &height);
if (success) {
CAPTURE_BUFFER.clear();
success = jpeg_encoder::encode(
CAPTURE_BUFFER, pixels.get(), width, height, quality);
}
if (success) {
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
+54 -54
View File
@@ -1,54 +1,54 @@
#include "ddr.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "games/ddr/ddr.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
DDR::DDR() : Module("ddr") {
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
}
/**
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
*/
void DDR::tapeled_get(Request &req, Response &res) {
static const char* device_names[11] = {
"p1_foot_up",
"p1_foot_right",
"p1_foot_left",
"p1_foot_down",
"p2_foot_up",
"p2_foot_right",
"p2_foot_left",
"p2_foot_down",
"top_panel",
"monitor_left",
"monitor_right"
};
Value response_object(kObjectType);
// Iterate through each device and dump its lights data into the response
for (size_t device = 0; device < 11; device++) {
size_t num_leds = 25;
if (device > 7)
num_leds = 50;
Value light_state(kArrayType);
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
for (size_t led = 0; led < num_leds; led++) {
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
}
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
}
res.add_data(response_object);
}
}
#include "ddr.h"
#include <functional>
#include "external/rapidjson/document.h"
#include "games/ddr/ddr.h"
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
DDR::DDR() : Module("ddr") {
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
}
/**
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
*/
void DDR::tapeled_get(Request &req, Response &res) {
static const char* device_names[11] = {
"p1_foot_up",
"p1_foot_right",
"p1_foot_left",
"p1_foot_down",
"p2_foot_up",
"p2_foot_right",
"p2_foot_left",
"p2_foot_down",
"top_panel",
"monitor_left",
"monitor_right"
};
Value response_object(kObjectType);
// Iterate through each device and dump its lights data into the response
for (size_t device = 0; device < 11; device++) {
size_t num_leds = 25;
if (device > 7)
num_leds = 50;
Value light_state(kArrayType);
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
for (size_t led = 0; led < num_leds; led++) {
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
}
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
}
res.add_data(response_object);
}
}
+17 -17
View File
@@ -1,17 +1,17 @@
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class DDR : public Module {
public:
DDR();
private:
// function definitions
void tapeled_get(Request &req, Response &res);
};
}
#pragma once
#include <vector>
#include "api/module.h"
#include "api/request.h"
namespace api::modules {
class DDR : public Module {
public:
DDR();
private:
// function definitions
void tapeled_get(Request &req, Response &res);
};
}
+4 -2
View File
@@ -106,14 +106,16 @@ namespace api::modules {
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
// Create an array for the light state
Value light_state(kArrayType);
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator());
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// Can't use StringRef here, turns some strings partially into null bytes for some reason
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
+4 -4
View File
@@ -1,13 +1,14 @@
#include "keypads.h"
#include <chrono>
#include <functional>
#include <thread>
#include <windows.h>
#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;
@@ -59,7 +60,6 @@ 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) {
@@ -93,11 +93,11 @@ namespace api::modules {
// set
eamuse_set_keypad_overrides(keypad, state);
timer.sleep(sleep_time);
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
// unset
eamuse_set_keypad_overrides(keypad, 0);
timer.sleep(sleep_time);
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
}
+67
View File
@@ -0,0 +1,67 @@
#include "sdvx.h"
#include <functional>
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
SDVX::SDVX() : Module("sdvx") {
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
for (auto &light : games::sdvx::TAPELED_MAPPING) {
lights_by_names.emplace(light.lightName, light);
}
}
/**
* tapeled_get()
* tapeled_get(name: str, ...)
*/
void SDVX::tapeled_get(Request &req, Response &res) {
Value response_object(kObjectType);
// all tape leds
if (req.params.Size() == 0) {
// iterate through each device and dump its lights data into the response
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
copy_tapeled_data(res, response_object, mapping);
}
} else {
// specified light names
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsString()) {
error_type(res, "name", "string");
return;
}
const auto name = param.GetString();
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
copy_tapeled_data(res, response_object, it->second.get());
}
}
}
res.add_data(response_object);
}
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
const tapeledutils::tape_led &mapping)
{
Value light_state(kArrayType);
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <string>
#include "api/module.h"
#include "api/request.h"
#include "external/robin_hood.h"
#include "games/sdvx/sdvx.h"
namespace api::modules {
class SDVX : public Module {
public:
SDVX();
private:
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
void tapeled_get(Request &req, Response &res);
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
const tapeledutils::tape_led &mapping);
};
}
+125 -10
View File
@@ -1,5 +1,7 @@
#include "touch.h"
#include <algorithm>
#include <cstdint>
#include <functional>
#include "external/rapidjson/document.h"
@@ -8,7 +10,11 @@
#include "misc/eamuse.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "touch/native/transform.h"
#include "util/utils.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h"
using namespace std::placeholders;
@@ -17,6 +23,54 @@ 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);
}
// map API coordinates onto the touch space SDVX reads, which depends on how it is displayed
static void sdvx_touch_errata(
int &x, int &y, bool use_native, int canvas_w, int canvas_h) {
// windowed coordinates already match the sub screen window they land on
if (GRAPHICS_WINDOWED) {
return;
}
// landscape mode: native injection hands the game these coordinates
// unchanged, so apply the rotation the touchscreen gets, while wintouchemu instead
// rotates them later through the subscreen overlay
if (GRAPHICS_FS_ORIENTATION_SWAP) {
if (use_native) {
POINT position { x, y };
if (nativetouch::transform::sdvx_landscape_rotate(&position, canvas_w, canvas_h)) {
x = position.x;
y = position.y;
}
}
return;
}
// rotate into the portrait touch space
const int x_raw = x;
x = canvas_w - y;
y = x_raw;
}
Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC");
@@ -26,6 +80,32 @@ 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) {
// windowed API coordinates land on the sub screen window as-is; fullscreen
// coordinates are rotated into the game's touch space by apply_touch_errata
const bool landscape_coordinates =
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
native_canvas_w = landscape_coordinates ? 1920 : 1080;
native_canvas_h = landscape_coordinates ? 1080 : 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;
} else if (games::gitadora::is_arena_model()) {
// GITADORA arena SMALL subscreen, either in its own window or in the overlay
native_canvas_w = games::gitadora::ARENA_SUBSCREEN_WIDTH;
native_canvas_h = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
}
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 +179,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,23 +207,54 @@ 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) {
int x_raw = x;
int y_raw = y;
if (is_tdj_fhd) {
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p)
// we don't know what screen is being shown on the companion and the API doesn't specify
// the target of the touch events so just assume it's the sub screen
x = x_raw * 1920 / 1280;
y = y_raw * 1080 / 720;
x = x * 1920 / 1280;
y = y * 1080 / 720;
} else if (is_sdvx) {
// for exceed gear, they are both 1080p screens, but need to apply transformation
x = 1080 - y_raw;
y = x_raw;
sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
}
}
}
+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);
};
@@ -7,6 +7,7 @@ from .coin import *
from .control import *
from .exceptions import *
from .iidx import *
from .sdvx import *
from .info import *
from .keypads import *
from .lights import *
@@ -0,0 +1,12 @@
from .connection import Connection
from .request import Request
def sdvx_tapeled_get(con: Connection, *light_names):
req = Request("sdvx", "tapeled_get")
for light_name in light_names:
req.add_param(light_name)
res = con.request(req)
return res.get_data()
+3 -3
View File
@@ -1,11 +1,12 @@
#include "controller.h"
#include "serial.h"
#include <chrono>
#include <string>
#include <thread>
#include <utility>
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/utils.h"
@@ -17,7 +18,6 @@ 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];
@@ -162,7 +162,7 @@ namespace api {
// slow down on reconnect
if (this->running) {
timer.sleep(retry_time);
std::this_thread::sleep_for(std::chrono::milliseconds(retry_time));
}
}
});
+70
View File
@@ -0,0 +1,70 @@
#include "stream_format.h"
#include <vector>
#include "h264_stream.h"
#include "hooks/graphics/jpeg_encoder.h"
namespace api {
namespace {
#ifdef SPICE_JPEG
constexpr const char *MJPEG_BOUNDARY = "spice2xframe";
// multipart/x-mixed-replace: every frame is a standalone JPEG, no inter-frame state
class MjpegWriter : public StreamWriter {
public:
explicit MjpegWriter(int quality) : quality(quality) {}
std::string content_type() const override {
return std::string("multipart/x-mixed-replace; boundary=") + MJPEG_BOUNDARY;
}
bool write(const StreamSend &send, const capture_pump::Frame &frame) override {
this->jpeg.clear();
if (!jpeg_encoder::encode(
this->jpeg, frame.pixels.get(),
frame.width, frame.height, this->quality)) {
// a frame the encoder rejects is not worth dropping the client over
return true;
}
const std::string part =
"--" + std::string(MJPEG_BOUNDARY) + "\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: " + std::to_string(this->jpeg.size()) + "\r\n"
"\r\n";
return send(part.data(), part.size())
&& send(this->jpeg.data(), this->jpeg.size())
&& send("\r\n", 2);
}
private:
int quality;
std::vector<uint8_t> jpeg;
};
#endif
}
// both parameters go unused on toolchains that compile in neither format
std::unique_ptr<StreamWriter> make_stream_writer(
const std::string &path, [[maybe_unused]] int quality, [[maybe_unused]] int fps) {
#ifdef SPICE_JPEG
if (path == "/stream.mjpg") {
return std::make_unique<MjpegWriter>(quality);
}
#endif
#ifdef SPICE_H264
if (path == "/stream.h264") {
return make_h264_writer(quality, fps);
}
#endif
return nullptr;
}
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include "capture_pump.h"
namespace api {
// writes bytes to the client; false once the connection is gone
using StreamSend = std::function<bool(const void *, size_t)>;
// one wire format, instantiated per connection so it can keep encoder state across frames
class StreamWriter {
public:
virtual ~StreamWriter() = default;
StreamWriter(const StreamWriter &) = delete;
StreamWriter &operator=(const StreamWriter &) = delete;
// value for the HTTP Content-Type response header
virtual std::string content_type() const = 0;
// for formats that open with an init segment; runs once before any frame
virtual bool begin(const StreamSend &send) { return true; }
virtual bool write(const StreamSend &send, const capture_pump::Frame &frame) = 0;
protected:
StreamWriter() = default;
};
// null when the path does not name a format this build supports
std::unique_ptr<StreamWriter> make_stream_writer(
const std::string &path, int quality, int fps);
}
+476
View File
@@ -0,0 +1,476 @@
#include <winsock2.h>
#include <ws2tcpip.h>
#include "stream_server.h"
#include <algorithm>
#include <cctype>
#include <chrono>
#include <limits>
#include <map>
#include <string>
#include <thread>
#include <vector>
#include "capture_pump.h"
#include "hooks/graphics/graphics.h"
#include "stream_format.h"
#include "util/logging.h"
#include "util/utils.h"
namespace api {
namespace {
struct HttpRequest {
std::string method;
std::string path;
std::map<std::string, std::string> query;
};
bool send_all(SOCKET socket, const void *data, size_t size) {
auto cursor = reinterpret_cast<const char *>(data);
size_t remaining = size;
while (remaining > 0) {
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
if (sent <= 0) {
return false;
}
cursor += sent;
remaining -= static_cast<size_t>(sent);
}
return true;
}
bool send_all(SOCKET socket, const std::string &text) {
return send_all(socket, text.data(), text.size());
}
// a viewer leaving is normally noticed by a failing send, so a stream with no frame
// to push has to ask the socket instead
bool client_gone(SOCKET socket) {
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(socket, &read_set);
// the socket is blocking with a receive timeout, so poll before touching it
timeval immediately {};
const int ready = select(0, &read_set, nullptr, nullptr, &immediately);
if (ready == 0) {
return false;
}
if (ready < 0) {
return true;
}
// consumed rather than peeked: a stray byte would otherwise sit in front of the
// FIN and keep hiding it for as long as the stream runs
char discard[256];
return recv(socket, discard, sizeof(discard), 0) <= 0;
}
std::string url_decode(const std::string &input) {
std::string out;
out.reserve(input.size());
for (size_t i = 0; i < input.size(); i++) {
if (input[i] == '+') {
out.push_back(' ');
} else if (input[i] == '%' && i + 2 < input.size()
&& isxdigit(static_cast<unsigned char>(input[i + 1]))
&& isxdigit(static_cast<unsigned char>(input[i + 2]))) {
out.push_back(static_cast<char>(
std::stoi(input.substr(i + 1, 2), nullptr, 16)));
i += 2;
} else {
out.push_back(input[i]);
}
}
return out;
}
void parse_query(const std::string &query, HttpRequest &request) {
size_t pos = 0;
while (pos < query.size()) {
auto end = query.find('&', pos);
if (end == std::string::npos) {
end = query.size();
}
const auto pair = query.substr(pos, end - pos);
const auto split = pair.find('=');
if (split != std::string::npos && split > 0) {
request.query[url_decode(pair.substr(0, split))] =
url_decode(pair.substr(split + 1));
}
pos = end + 1;
}
}
// reads the request head only; anything oversized or malformed is refused
bool read_request(SOCKET socket, size_t size_limit, HttpRequest &request) {
std::string head;
char buffer[1024];
while (head.find("\r\n\r\n") == std::string::npos) {
if (head.size() >= size_limit) {
return false;
}
// read no further than the limit, so the head cannot overshoot it
const size_t budget = std::min(sizeof(buffer), size_limit - head.size());
const int received = recv(socket, buffer, static_cast<int>(budget), 0);
if (received <= 0) {
return false;
}
head.append(buffer, static_cast<size_t>(received));
}
const auto line_end = head.find("\r\n");
const auto line = head.substr(0, line_end);
const auto method_end = line.find(' ');
if (method_end == std::string::npos) {
return false;
}
const auto target_end = line.find(' ', method_end + 1);
if (target_end == std::string::npos) {
return false;
}
request.method = line.substr(0, method_end);
auto target = line.substr(method_end + 1, target_end - method_end - 1);
const auto query_start = target.find('?');
if (query_start != std::string::npos) {
parse_query(target.substr(query_start + 1), request);
target = target.substr(0, query_start);
}
request.path = url_decode(target);
return true;
}
int query_int(const HttpRequest &request, const std::string &name, int fallback,
int min, int max) {
const auto pos = request.query.find(name);
if (pos == request.query.end()) {
return fallback;
}
try {
return std::clamp(std::stoi(pos->second), min, max);
} catch (const std::exception &) {
return fallback;
}
}
// an <img> can show a cross-origin stream without this, but a browser client that
// decodes the bytes itself has to fetch() them, and fetch is subject to CORS. errors
// carry it too, or the client sees an opaque failure instead of the status.
constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n";
void send_error(SOCKET socket, const char *status) {
const std::string response =
std::string("HTTP/1.0 ") + status + "\r\n"
+ cors_header +
"Content-Length: 0\r\n"
"Connection: close\r\n"
"\r\n";
send_all(socket, response);
}
}
StreamServer::StreamServer(unsigned short port)
: port(port)
{
if (!this->open_listener()) {
// the stream was asked for explicitly, so say plainly that it is not there
log_warning("api::stream",
"the video stream is not available on port {}", this->port);
return;
}
this->running = true;
this->acceptor = std::thread([this] {
this->accept_worker();
});
// deliberately not logging a full URL; local IPs would leak into shared logs
log_info("api::stream", "video stream is listening on port: {}", this->port);
log_warning("api::stream",
"the video stream is unauthenticated - anyone who can reach port {} can watch "
"the game screen", this->port);
}
bool StreamServer::open_listener() {
WSADATA wsa_data;
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
if (error != 0) {
log_warning("api::stream", "WSAStartup() returned {}", error);
return false;
}
this->wsa_started = true;
this->listener = socket(AF_INET, SOCK_STREAM, 0);
if (this->listener == INVALID_SOCKET) {
log_warning("api::stream", "could not create listener socket: {}",
get_last_error_string());
return false;
}
int opt_enable = 1;
if (setsockopt(this->listener, SOL_SOCKET, SO_REUSEADDR,
reinterpret_cast<const char *>(&opt_enable), sizeof(int)) == -1) {
log_warning("api::stream", "could not set socket option SO_REUSEADDR: {}",
get_last_error_string());
}
sockaddr_in server_address {};
server_address.sin_family = AF_INET;
server_address.sin_port = htons(this->port);
server_address.sin_addr.s_addr = INADDR_ANY;
if (bind(this->listener, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
log_warning("api::stream", "could not bind socket on port {}: {}",
this->port, get_last_error_string());
closesocket(this->listener);
this->listener = INVALID_SOCKET;
return false;
}
if (listen(this->listener, server_backlog) == -1) {
log_warning("api::stream", "could not listen on port {}: {}",
this->port, get_last_error_string());
closesocket(this->listener);
this->listener = INVALID_SOCKET;
return false;
}
return true;
}
StreamServer::~StreamServer() {
this->running = false;
if (this->listener != INVALID_SOCKET) {
closesocket(this->listener);
this->listener = INVALID_SOCKET;
}
// drops the client threads out of their blocking send/recv
{
std::lock_guard<std::mutex> lock(this->clients_m);
for (auto &client : this->clients) {
if (client.socket != INVALID_SOCKET) {
::shutdown(client.socket, SD_BOTH);
}
}
}
if (this->acceptor.joinable()) {
this->acceptor.join();
}
// joining is what guarantees no client thread outlives this object
for (auto &client : this->clients) {
if (client.thread.joinable()) {
client.thread.join();
}
}
if (this->wsa_started) {
WSACleanup();
}
}
void StreamServer::accept_worker() {
while (this->running) {
sockaddr_in client_address {};
int client_address_size = sizeof(sockaddr_in);
const SOCKET client = accept(
this->listener, (sockaddr *) &client_address, &client_address_size);
if (client == INVALID_SOCKET) {
// on shutdown the listener is closed under us; otherwise do not spin
if (this->running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
continue;
}
if (!this->running) {
closesocket(client);
break;
}
char address_data[INET_ADDRSTRLEN] {};
inet_ntop(AF_INET, &client_address.sin_addr, address_data, INET_ADDRSTRLEN);
std::string address(address_data);
// every client costs an encode and real bandwidth, so the cap protects the game
int slot = -1;
{
std::lock_guard<std::mutex> lock(this->clients_m);
for (size_t i = 0; i < this->clients.size(); i++) {
if (!this->clients[i].active) {
this->clients[i].active = true;
this->clients[i].socket = client;
slot = static_cast<int>(i);
break;
}
}
}
if (slot < 0) {
log_warning("api::stream", "client limit of {} hit", client_limit);
send_error(client, "503 Service Unavailable");
closesocket(client);
continue;
}
// this thread is the only one that touches the thread objects, so the slot's
// previous occupant gets reaped here rather than being detached
if (this->clients[slot].thread.joinable()) {
this->clients[slot].thread.join();
}
this->clients[slot].thread = std::thread([this, slot, client, address] {
this->client_worker(slot, client, address);
});
}
}
void StreamServer::client_worker(int slot, SOCKET socket, std::string address) {
DWORD timeout = request_timeout_ms;
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
timeout = send_timeout_ms;
setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
int opt_enable = 1;
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<const char *>(&opt_enable), sizeof(int));
// whatever sits in the send buffer is already stale, and the default holds about a
// third of a second of H.264 because the bitrate is so low. keeping it small makes a
// slow reader block the sender, which then skips to the newest frame instead of
// handing over a backlog
int send_buffer = send_buffer_bytes;
setsockopt(socket, SOL_SOCKET, SO_SNDBUF,
reinterpret_cast<const char *>(&send_buffer), sizeof(send_buffer));
HttpRequest request;
if (read_request(socket, request_size_limit, request)) {
if (request.method != "GET") {
send_error(socket, "405 Method Not Allowed");
} else {
const int fps = query_int(request, "fps", 30, 1, fps_limit);
const int quality = query_int(request, "q", 70, 1, 100);
auto writer = make_stream_writer(request.path, quality, fps);
if (!writer) {
send_error(socket, "404 Not Found");
} else {
std::vector<int> screens;
graphics_screens_get(screens);
// registration takes a raw swapchain index and never bounds it, so the
// capture range has to be enforced here rather than assumed
const auto streamable = [&screens](int screen) {
return screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
&& std::find(screens.begin(), screens.end(), screen)
!= screens.end();
};
// screen 1 is the subscreen in every game that has one; single-screen games
// only ever register screen 0, so resolve the default against what exists.
// left unclamped so a nonsense screen is reported as what was asked for
int screen = query_int(request, "screen", -1, 0,
std::numeric_limits<int>::max());
if (screen < 0) {
screen = streamable(1) ? 1 : 0;
}
// the default always lands on a screen that exists, so this is only ever
// an explicit request for one that cannot be captured
if (!streamable(screen)) {
log_warning("api::stream",
"screen {} is not available, refusing {}", screen, address);
send_error(socket, "404 Not Found");
} else if (!capture_pump::claim_screen(screen)) {
log_warning("api::stream",
"screen {} is already being streamed, refusing {}",
screen, address);
send_error(socket, "503 Service Unavailable");
} else {
log_info("api::stream",
"client connected: {} ({}, screen={}, fps={}, quality={})",
address, request.path, screen, fps, quality);
const std::string header =
"HTTP/1.0 200 OK\r\n"
"Connection: close\r\n"
+ std::string(cors_header) +
"Cache-Control: no-store, no-cache, must-revalidate\r\n"
"Pragma: no-cache\r\n"
"Content-Type: " + writer->content_type() + "\r\n"
"\r\n";
const StreamSend stream_send = [socket](const void *data, size_t size) {
return send_all(socket, data, size);
};
if (send_all(socket, header) && writer->begin(stream_send)) {
const auto interval = std::chrono::microseconds(1000000 / fps);
while (this->running) {
const auto started = std::chrono::steady_clock::now();
capture_pump::Frame frame;
const bool ok = capture_pump::capture_direct(
screen, frame.pixels, 1,
&frame.timestamp, &frame.width, &frame.height);
if (ok && frame.pixels) {
if (!writer->write(stream_send, frame)) {
break;
}
} else if (client_gone(socket)) {
break;
}
// a failed capture still paces, or a stalled game spins this
std::this_thread::sleep_until(started + interval);
}
}
capture_pump::release_screen(screen);
log_info("api::stream", "client disconnected: {}", address);
}
}
}
}
{
std::lock_guard<std::mutex> lock(this->clients_m);
this->clients[slot].socket = INVALID_SOCKET;
this->clients[slot].active = false;
}
closesocket(socket);
}
}
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <array>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <winsock2.h>
namespace api {
class StreamServer {
public:
explicit StreamServer(unsigned short port);
~StreamServer();
StreamServer(const StreamServer &) = delete;
StreamServer &operator=(const StreamServer &) = delete;
private:
// configuration
static constexpr int server_backlog = 4;
static constexpr int client_limit = 4;
static constexpr int request_size_limit = 8 * 1024;
static constexpr int request_timeout_ms = 5000;
static constexpr int send_timeout_ms = 5000;
// small enough that a low bitrate stream cannot hide a backlog of stale frames in it
static constexpr int send_buffer_bytes = 16 * 1024;
static constexpr int fps_limit = 60;
struct Client {
std::thread thread;
SOCKET socket = INVALID_SOCKET;
bool active = false;
};
void accept_worker();
bool open_listener();
void client_worker(int slot, SOCKET socket, std::string address);
unsigned short port;
SOCKET listener = INVALID_SOCKET;
bool wsa_started = false;
std::atomic_bool running { false };
std::thread acceptor;
std::mutex clients_m;
// socket and active are guarded by clients_m; only the acceptor touches thread
std::array<Client, client_limit> clients;
};
}
+29
View File
@@ -12,6 +12,20 @@ using namespace headsocket;
namespace api {
namespace {
// how long a single handshake read may stall before the connection is dropped;
// headsocket reads the request a byte at a time, so this is an idle timeout between
// bytes rather than a deadline for the whole handshake
constexpr int handshake_timeout_ms = 5000;
void set_recv_timeout(connection &conn, int milliseconds) {
DWORD timeout = static_cast<DWORD>(milliseconds);
setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
}
}
/*
* Client class declaration
*/
@@ -37,6 +51,21 @@ namespace api {
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
public:
WebSocketController *websocket;
protected:
bool handshake(connection &conn) override {
// headsocket runs the handshake on its single accept thread with a blocking
// recv, so a peer that connects and then says nothing would park that thread and
// leave every later connection sitting unaccepted in the backlog
set_recv_timeout(conn, handshake_timeout_ms);
const bool accepted = base_t::handshake(conn);
// from here the client thread owns the socket and wants to block on reads
set_recv_timeout(conn, 0);
return accepted;
}
};
void api::WebSocketServer::init() {}
+2 -1
View File
@@ -1114,6 +1114,7 @@ namespace avs {
#endif
// jubeat
{"jubeat.dll", 0x2000000},
{"jubeat2019.dll", 0x10000000},
// MUSECA
{"museca.dll", 0xC000000},
@@ -1924,7 +1925,7 @@ namespace avs {
#if !SPICE64
// sdvx4 bad log config fix
if (avs::game::DLL_NAME == "soundvoltex.dll" && // it's too early for avs::game::is_model
property_search_safe(config, config_node, "/log/enable_console")) {
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");
}
+1
View File
@@ -591,6 +591,7 @@ namespace avs {
// for proper reporting of Omnimix and other song packs
if (_stricmp(EA3_MODEL, "LDJ") == 0 ||
_stricmp(EA3_MODEL, "L44") == 0 ||
_stricmp(EA3_MODEL, "T44") == 0 ||
_stricmp(EA3_MODEL, "M39") == 0 ||
_stricmp(EA3_MODEL, "KFC") == 0)
{
+1
View File
@@ -6,6 +6,7 @@
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware>true</dpiAware>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
</asmv3:windowsSettings>
</asmv3:application>
<dependency>
+18 -1
View File
@@ -214,6 +214,19 @@ time (
echo "==========================="
)
echo ""
echo "Checking Win7 compatibility..."
echo "============================="
if ! command -v windows_dll_compat_checker &> /dev/null; then
echo "WARNING: windows_dll_compat_checker not found, skipping Win7 compatibility check"
else
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64.ini \
${BUILDDIR_64}/spicetools/64/spice64.exe
windows_dll_compat_checker -s PREMADE/konami_win7_museca_x86_64_32bit_dlls.ini \
${BUILDDIR_32}/spicetools/spicecfg.exe \
${BUILDDIR_32}/spicetools/32/spice.exe
fi
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
echo ""
@@ -229,7 +242,7 @@ then
fi
if ((BUILD_XP_32 > 0))
then
windows_dll_compat_checker -s PREMADE/winxp_x86_64_32bit_dlls.ini \
windows_dll_compat_checker -s PREMADE/konami_winxp_jubeat_i686.ini \
${BUILDDIR_WINXP_32}/spicetools/spicecfg.exe \
${BUILDDIR_WINXP_32}/spicetools/32/spice.exe
fi
@@ -294,6 +307,7 @@ mkdir -p ${OUTDIR_EXTRAS}/largeaddressaware
mkdir -p ${OUTDIR_EXTRAS}/linux
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/32
mkdir -p ${OUTDIR_EXTRAS}/sdk/samples/64
mkdir -p ${OUTDIR_EXTRAS}/updater
if ((BUILD_XP_32 > 0)) || ((BUILD_XP_64 > 0))
then
mkdir -p ${OUTDIR_EXTRAS}/winxp
@@ -354,6 +368,9 @@ mkdir ${OUTDIR_EXTRAS}/api
find ./api/resources/python -name "__pycache__" -exec rm -rf {} +
cp -r ./api/resources/* ${OUTDIR_EXTRAS}/api
# generate the standalone updater scripts from the shared template and drop them
bash ./build_updaters.sh ${OUTDIR_EXTRAS}/updater
# build distribution archive
if ((DIST_ENABLE > 0))
then
+57
View File
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# builds the standalone updater scripts from the shared PowerShell logic in
# update_spice.ps1. each output is a self-contained polyglot .bat: a small batch
# header (with the release channel baked in) followed by the shared PowerShell
# body after the marker line.
#
# usage: build_updaters.sh OUT_DIR
# OUT_DIR directory to write the generated .bat files into
set -eu
if [ $# -lt 1 ]; then
echo "usage: build_updaters.sh OUT_DIR" >&2
exit 1
fi
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PS_BODY="${SRC_DIR}/update_spice.ps1"
OUT_DIR="$1"
[ -f "$PS_BODY" ] || { echo "error: $PS_BODY not found" >&2; exit 1; }
MARKER='#___PS___'
gen() {
channel="$1"; label="$2"; out="$3"
{
cat <<HEADER
@echo off
setlocal
REM ============================================================================
REM spice2x updater - updates spice.exe, spice64.exe and spicecfg.exe in
REM this folder to the latest spice2x release from GitHub.
REM
REM release channel: ${label}
REM make sure spice/spicecfg are NOT running before updating.
REM ============================================================================
REM expose the script folder + channel to the embedded PowerShell, then run the
REM PowerShell part: re-read this file and execute everything after the marker.
set "SPICE_DIR=%~dp0"
set "SPICE_CHANNEL=${channel}"
powershell -NoProfile -ExecutionPolicy Bypass -Command "\$m='#___'+'PS'+'___'; \$code=((Get-Content -LiteralPath '%~f0' -Raw) -split \$m)[-1]; Invoke-Expression \$code"
set "RC=%ERRORLEVEL%"
endlocal & exit /b %RC%
${MARKER}
HEADER
cat "$PS_BODY"
} | sed -e 's/\r$//' -e 's/$/\r/' > "${OUT_DIR}/${out}"
echo " generated ${OUT_DIR}/${out}"
}
echo "Generating updater scripts from $(basename "$PS_BODY")..."
gen "" "stable (pre-releases excluded)" "update_spice.bat"
gen "beta" "beta (newest, pre-releases included)" "update_spice_beta.bat"
+72 -11
View File
@@ -3,7 +3,9 @@
#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"
@@ -62,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) {
@@ -76,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;
@@ -411,6 +466,12 @@ GameAPI::Buttons::State GameAPI::Buttons::getState(rawinput::RawInputManager *ma
BUTTON_PRESSED : BUTTON_NOT_PRESSED;
break;
}
case rawinput::DESTROYED:
// device was unplugged. release the button instead of leaving
// state at getLastState(), which would latch a held button on
// forever (e.g. a MIDI note held while the device is removed)
state = BUTTON_NOT_PRESSED;
break;
default:
break;
}
@@ -463,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);
@@ -478,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) {
+21 -1
View File
@@ -299,7 +299,27 @@ 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();
+11
View File
@@ -49,6 +49,7 @@ 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;
@@ -65,6 +66,7 @@ private:
unsigned short velocity_threshold = 0;
std::string getMidiNoteString();
std::string getBindingDisplayString(rawinput::RawInputManager *manager);
public:
std::string getVKeyString();
@@ -111,6 +113,7 @@ public:
device_identifier = "";
analog_type = BAT_NONE;
bat_threshold = 0;
modifier_mask = 0;
}
std::string getDisplayString(rawinput::RawInputManager* manager);
@@ -123,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;
}
+12
View File
@@ -165,6 +165,7 @@ bool Config::addGame(Game &game, bool save) {
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);
@@ -173,6 +174,7 @@ bool Config::addGame(Game &game, bool save) {
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");
@@ -185,6 +187,7 @@ bool Config::addGame(Game &game, bool save) {
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));
@@ -194,6 +197,7 @@ bool Config::addGame(Game &game, bool save) {
button->setBatThreshold(bat_threshold);
button->setVelocityThreshold(velocity_threshold);
button->setInvert(invert);
button->setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button->setDeviceIdentifier(devid);
}
@@ -214,6 +218,7 @@ bool Config::addGame(Game &game, bool save) {
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);
}
@@ -440,6 +445,7 @@ bool Config::addGame(Game &game, bool save) {
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);
}
@@ -552,6 +558,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", button.getVelocityThreshold());
gameButtonNode->SetAttribute("invert", button.getInvert());
gameButtonNode->SetAttribute("devid", button.getDeviceIdentifier().c_str());
gameButtonNode->SetAttribute("modifiers", button.getModifierMask());
break;
}
}
@@ -569,6 +576,7 @@ bool Config::updateBinding(const Game &game, const Button &button, int alternati
gameButtonNode->SetAttribute("velocity_threshold", 0);
gameButtonNode->SetAttribute("invert", false);
gameButtonNode->SetAttribute("devid", "");
gameButtonNode->SetAttribute("modifiers", 0);
gameButtonsNode->InsertEndChild(gameButtonNode);
}
}
@@ -964,6 +972,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
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);
@@ -971,6 +980,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
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
@@ -986,6 +996,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
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));
}
@@ -1005,6 +1016,7 @@ std::vector<Button> Config::getButtons(const std::string &gameName) {
button.setBatThreshold(bat_threshold);
button.setVelocityThreshold(velocity_threshold);
button.setInvert(invert);
button.setModifierMask(static_cast<uint8_t>(modifier_mask));
if (devid) {
button.setDeviceIdentifier(devid);
}
+21 -1
View File
@@ -3,6 +3,7 @@
#include <d3d9.h>
#include "overlay/overlay.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace cfg {
@@ -35,7 +36,26 @@ namespace cfg {
return false;
}
IDirect3D9 *d3d = Direct3DCreate9(D3D_SDK_VERSION);
// 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;
+46
View File
@@ -26,6 +26,7 @@ namespace overlay::windows {
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);
}
@@ -54,6 +55,10 @@ namespace overlay::windows {
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;
}
@@ -160,6 +165,28 @@ namespace overlay::windows {
}
}
// 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) {
@@ -244,6 +271,7 @@ namespace overlay::windows {
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) {
@@ -300,6 +328,24 @@ namespace overlay::windows {
}
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");
@@ -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}")
+9
View File
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
ninja \
cmake \
unzip \
nasm \
mingw-w64-crt \
mingw-w64-winpthreads \
mingw-w64-gcc \
@@ -23,3 +24,11 @@ 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
# libjpeg-turbo for JPEG encoding. Only the mingw-w64 toolchains get it;
# the WinXP targets build without JPEG support entirely.
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo"
# x264 for the API H.264 video stream. Only the mingw-w64 toolchains need it;
# the WinXP targets build without the stream encoder.
RUN su user -c "yay --noconfirm -S mingw-w64-x264"
+95 -95
View File
@@ -1,95 +1,95 @@
Copyright (c) 2017, keshikan (http://www.keshikan.net),
with Reserved Font Name "DSEG".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Copyright (c) 2017, keshikan (http://www.keshikan.net),
with Reserved Font Name "DSEG".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
File diff suppressed because one or more lines are too long
+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 */
+3222
View File
File diff suppressed because it is too large Load Diff
+122
View File
@@ -0,0 +1,122 @@
// fpng.h - unlicense (see end of fpng.cpp)
#pragma once
#include <stdlib.h>
#include <stdint.h>
#include <vector>
#ifndef FPNG_TRAIN_HUFFMAN_TABLES
// Set to 1 when using the -t (training) option in fpng_test to generate new opaque/alpha Huffman tables for the single pass encoder.
#define FPNG_TRAIN_HUFFMAN_TABLES (0)
#endif
namespace fpng
{
// ---- Library initialization - call once to identify if the processor supports SSE.
// Otherwise you'll only get scalar fallbacks.
void fpng_init();
// ---- Useful Utilities
// Returns true if the CPU supports SSE 4.1, and SSE support wasn't disabled by setting FPNG_NO_SSE=1.
// fpng_init() must have been called first, or it'll assert and return false.
bool fpng_cpu_supports_sse41();
// Fast CRC-32 SSE4.1+pclmul or a scalar fallback (slice by 4)
const uint32_t FPNG_CRC32_INIT = 0;
uint32_t fpng_crc32(const void* pData, size_t size, uint32_t prev_crc32 = FPNG_CRC32_INIT);
// Fast Adler32 SSE4.1 Adler-32 with a scalar fallback.
const uint32_t FPNG_ADLER32_INIT = 1;
uint32_t fpng_adler32(const void* pData, size_t size, uint32_t adler = FPNG_ADLER32_INIT);
// ---- Compression
enum
{
// Enables computing custom Huffman tables for each file, instead of using the custom global tables.
// Results in roughly 6% smaller files on average, but compression is around 40% slower.
FPNG_ENCODE_SLOWER = 1,
// Only use raw Deflate blocks (no compression at all). Intended for testing.
FPNG_FORCE_UNCOMPRESSED = 2,
};
// Fast PNG encoding. The resulting file can be decoded either using a standard PNG decoder or by the fpng_decode_memory() function below.
// pImage: pointer to RGB or RGBA image pixels, R first in memory, B/A last.
// w/h - image dimensions. Image's row pitch in bytes must is w*num_chans.
// num_chans must be 3 or 4.
bool fpng_encode_image_to_memory(const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, std::vector<uint8_t>& out_buf, uint32_t flags = 0);
#ifndef FPNG_NO_STDIO
// Fast PNG encoding to the specified file.
bool fpng_encode_image_to_file(const char* pFilename, const void* pImage, uint32_t w, uint32_t h, uint32_t num_chans, uint32_t flags = 0);
#endif
// ---- Decompression
enum
{
FPNG_DECODE_SUCCESS = 0, // file is a valid PNG file and written by FPNG and the decode succeeded
FPNG_DECODE_NOT_FPNG, // file is a valid PNG file, but it wasn't written by FPNG so you should try decoding it with a general purpose PNG decoder
FPNG_DECODE_INVALID_ARG, // invalid function parameter
FPNG_DECODE_FAILED_NOT_PNG, // file cannot be a PNG file
FPNG_DECODE_FAILED_HEADER_CRC32, // a chunk CRC32 check failed, file is likely corrupted or not PNG
FPNG_DECODE_FAILED_INVALID_DIMENSIONS, // invalid image dimensions in IHDR chunk (0 or too large)
FPNG_DECODE_FAILED_DIMENSIONS_TOO_LARGE, // decoding the file fully into memory would likely require too much memory (only on 32bpp builds)
FPNG_DECODE_FAILED_CHUNK_PARSING, // failed while parsing the chunk headers, or file is corrupted
FPNG_DECODE_FAILED_INVALID_IDAT, // IDAT data length is too small and cannot be valid, file is either corrupted or it's a bug
// fpng_decode_file() specific errors
FPNG_DECODE_FILE_OPEN_FAILED,
FPNG_DECODE_FILE_TOO_LARGE,
FPNG_DECODE_FILE_READ_FAILED,
FPNG_DECODE_FILE_SEEK_FAILED
};
// Fast PNG decoding of files ONLY created by fpng_encode_image_to_memory() or fpng_encode_image_to_file().
// If fpng_get_info() or fpng_decode_memory() returns FPNG_DECODE_NOT_FPNG, you should decode the PNG by falling back to a general purpose decoder.
//
// fpng_get_info() parses the PNG header and iterates through all chunks to determine if it's a file written by FPNG, but does not decompress the actual image data so it's relatively fast.
//
// pImage, image_size: Pointer to PNG image data and its size
// width, height: output image's dimensions
// channels_in_file: will be 3 or 4
//
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
int fpng_get_info(const void* pImage, uint32_t image_size, uint32_t& width, uint32_t& height, uint32_t& channels_in_file);
// fpng_decode_memory() decompresses 24/32bpp PNG files ONLY encoded by this module.
// If the image was written by FPNG, it will decompress the image data, otherwise it will return FPNG_DECODE_NOT_FPNG in which case you should fall back to a general purpose PNG decoder (lodepng, stb_image, libpng, etc.)
//
// pImage, image_size: Pointer to PNG image data and its size
// out: Output 24/32bpp image buffer
// width, height: output image's dimensions
// channels_in_file: will be 3 or 4
// desired_channels: must be 3 or 4
//
// If the image is 24bpp and 32bpp is requested, the alpha values will be set to 0xFF.
// If the image is 32bpp and 24bpp is requested, the alpha values will be discarded.
//
// Returns FPNG_DECODE_SUCCESS on success, otherwise one of the failure codes above.
// If FPNG_DECODE_NOT_FPNG is returned, you must decompress the file with a general purpose PNG decoder.
// If another error occurs, the file is likely corrupted or invalid, but you can still try to decompress the file with another decoder (which will likely fail).
int fpng_decode_memory(const void* pImage, uint32_t image_size, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
#ifndef FPNG_NO_STDIO
int fpng_decode_file(const char* pFilename, std::vector<uint8_t>& out, uint32_t& width, uint32_t& height, uint32_t& channels_in_file, uint32_t desired_channels);
#endif
// ---- Internal API used for Huffman table training purposes
#if FPNG_TRAIN_HUFFMAN_TABLES
const uint32_t HUFF_COUNTS_SIZE = 288;
extern uint64_t g_huff_counts[HUFF_COUNTS_SIZE];
bool create_dynamic_block_prefix(uint64_t* pFreq, uint32_t num_chans, std::vector<uint8_t>& prefix, uint64_t& bit_buf, int& bit_buf_size, uint32_t *pCodes, uint8_t *pCodesizes);
#endif
} // namespace fpng
+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
-10
View File
@@ -1,10 +0,0 @@
zlib License
Copyright (c) 2011-2016 Stephan Brumme
This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software.
If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
-665
View File
@@ -1,665 +0,0 @@
// //////////////////////////////////////////////////////////
// toojpeg.cpp
// written by Stephan Brumme, 2018-2019
// see https://create.stephan-brumme.com/toojpeg/
//
#include "toojpeg.h"
// - the "official" specifications: https://www.w3.org/Graphics/JPEG/itu-t81.pdf and https://www.w3.org/Graphics/JPEG/jfif3.pdf
// - Wikipedia has a short description of the JFIF/JPEG file format: https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format
// - the popular STB Image library includes Jon's JPEG encoder as well: https://github.com/nothings/stb/blob/master/stb_image_write.h
// - the most readable JPEG book (from a developer's perspective) is Miano's "Compressed Image File Formats" (1999, ISBN 0-201-60443-4),
// used copies are really cheap nowadays and include a CD with C++ sources as well (plus great format descriptions of GIF & PNG)
// - much more detailled is Mitchell/Pennebaker's "JPEG: Still Image Data Compression Standard" (1993, ISBN 0-442-01272-1)
// which contains the official JPEG standard, too - fun fact: I bought a signed copy in a second-hand store without noticing
namespace // anonymous namespace to hide local functions / constants / etc.
{
// ////////////////////////////////////////
// data types
using uint8_t = unsigned char;
using uint16_t = unsigned short;
using int16_t = short;
using int32_t = int; // at least four bytes
// ////////////////////////////////////////
// constants
// quantization tables from JPEG Standard, Annex K
const uint8_t DefaultQuantLuminance[8*8] =
{ 16, 11, 10, 16, 24, 40, 51, 61, // there are a few experts proposing slightly more efficient values,
12, 12, 14, 19, 26, 58, 60, 55, // e.g. https://www.imagemagick.org/discourse-server/viewtopic.php?t=20333
14, 13, 16, 24, 40, 57, 69, 56, // btw: Google's Guetzli project optimizes the quantization tables per image
14, 17, 22, 29, 51, 87, 80, 62,
18, 22, 37, 56, 68,109,103, 77,
24, 35, 55, 64, 81,104,113, 92,
49, 64, 78, 87,103,121,120,101,
72, 92, 95, 98,112,100,103, 99 };
const uint8_t DefaultQuantChrominance[8*8] =
{ 17, 18, 24, 47, 99, 99, 99, 99,
18, 21, 26, 66, 99, 99, 99, 99,
24, 26, 56, 99, 99, 99, 99, 99,
47, 66, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99,
99, 99, 99, 99, 99, 99, 99, 99 };
// 8x8 blocks are processed in zig-zag order
// most encoders use a zig-zag "forward" table, I switched to its inverse for performance reasons
// note: ZigZagInv[ZigZag[i]] = i
const uint8_t ZigZagInv[8*8] =
{ 0, 1, 8,16, 9, 2, 3,10, // ZigZag[] = 0, 1, 5, 6,14,15,27,28,
17,24,32,25,18,11, 4, 5, // 2, 4, 7,13,16,26,29,42,
12,19,26,33,40,48,41,34, // 3, 8,12,17,25,30,41,43,
27,20,13, 6, 7,14,21,28, // 9,11,18,24,31,40,44,53,
35,42,49,56,57,50,43,36, // 10,19,23,32,39,45,52,54,
29,22,15,23,30,37,44,51, // 20,22,33,38,46,51,55,60,
58,59,52,45,38,31,39,46, // 21,34,37,47,50,56,59,61,
53,60,61,54,47,55,62,63 }; // 35,36,48,49,57,58,62,63
// static Huffman code tables from JPEG standard Annex K
// - CodesPerBitsize tables define how many Huffman codes will have a certain bitsize (plus 1 because there nothing with zero bits),
// e.g. DcLuminanceCodesPerBitsize[2] = 5 because there are 5 Huffman codes being 2+1=3 bits long
// - Values tables are a list of values ordered by their Huffman code bitsize,
// e.g. AcLuminanceValues => Huffman(0x01,0x02 and 0x03) will have 2 bits, Huffman(0x00) will have 3 bits, Huffman(0x04,0x11 and 0x05) will have 4 bits, ...
// Huffman definitions for first DC/AC tables (luminance / Y channel)
const uint8_t DcLuminanceCodesPerBitsize[16] = { 0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0 }; // sum = 12
const uint8_t DcLuminanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes
const uint8_t AcLuminanceCodesPerBitsize[16] = { 0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,125 }; // sum = 162
const uint8_t AcLuminanceValues [162] = // => 162 codes
{ 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xA1,0x08, // 16*10+2 symbols because
0x23,0x42,0xB1,0xC1,0x15,0x52,0xD1,0xF0,0x24,0x33,0x62,0x72,0x82,0x09,0x0A,0x16,0x17,0x18,0x19,0x1A,0x25,0x26,0x27,0x28, // upper 4 bits can be 0..F
0x29,0x2A,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,0x59, // while lower 4 bits can be 1..A
0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x83,0x84,0x85,0x86,0x87,0x88,0x89, // plus two special codes 0x00 and 0xF0
0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,0xB5,0xB6, // order of these symbols was determined empirically by JPEG committee
0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,0xE1,0xE2,
0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF1,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
// Huffman definitions for second DC/AC tables (chrominance / Cb and Cr channels)
const uint8_t DcChrominanceCodesPerBitsize[16] = { 0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0 }; // sum = 12
const uint8_t DcChrominanceValues [12] = { 0,1,2,3,4,5,6,7,8,9,10,11 }; // => 12 codes (identical to DcLuminanceValues)
const uint8_t AcChrominanceCodesPerBitsize[16] = { 0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,119 }; // sum = 162
const uint8_t AcChrominanceValues [162] = // => 162 codes
{ 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, // same number of symbol, just different order
0xA1,0xB1,0xC1,0x09,0x23,0x33,0x52,0xF0,0x15,0x62,0x72,0xD1,0x0A,0x16,0x24,0x34,0xE1,0x25,0xF1,0x17,0x18,0x19,0x1A,0x26, // (which is more efficient for AC coding)
0x27,0x28,0x29,0x2A,0x35,0x36,0x37,0x38,0x39,0x3A,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4A,0x53,0x54,0x55,0x56,0x57,0x58,
0x59,0x5A,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7A,0x82,0x83,0x84,0x85,0x86,0x87,
0x88,0x89,0x8A,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9A,0xA2,0xA3,0xA4,0xA5,0xA6,0xA7,0xA8,0xA9,0xAA,0xB2,0xB3,0xB4,
0xB5,0xB6,0xB7,0xB8,0xB9,0xBA,0xC2,0xC3,0xC4,0xC5,0xC6,0xC7,0xC8,0xC9,0xCA,0xD2,0xD3,0xD4,0xD5,0xD6,0xD7,0xD8,0xD9,0xDA,
0xE2,0xE3,0xE4,0xE5,0xE6,0xE7,0xE8,0xE9,0xEA,0xF2,0xF3,0xF4,0xF5,0xF6,0xF7,0xF8,0xF9,0xFA };
const int16_t CodeWordLimit = 2048; // +/-2^11, maximum value after DCT
// ////////////////////////////////////////
// structs
// represent a single Huffman code
struct BitCode
{
BitCode() = default; // undefined state, must be initialized at a later time
BitCode(uint16_t code_, uint8_t numBits_)
: code(code_), numBits(numBits_) {}
uint16_t code; // JPEG's Huffman codes are limited to 16 bits
uint8_t numBits; // number of valid bits
};
// wrapper for bit output operations
struct BitWriter
{
// user-supplied callback that writes/stores one byte
TooJpeg::WRITE_ONE_BYTE output;
// initialize writer
explicit BitWriter(TooJpeg::WRITE_ONE_BYTE output_) : output(output_) {}
// store the most recently encoded bits that are not written yet
struct BitBuffer
{
int32_t data = 0; // actually only at most 24 bits are used
uint8_t numBits = 0; // number of valid bits (the right-most bits)
} buffer;
// write Huffman bits stored in BitCode, keep excess bits in BitBuffer
BitWriter& operator<<(const BitCode& data)
{
// append the new bits to those bits leftover from previous call(s)
buffer.numBits += data.numBits;
buffer.data <<= data.numBits;
buffer.data |= data.code;
// write all "full" bytes
while (buffer.numBits >= 8)
{
// extract highest 8 bits
buffer.numBits -= 8;
auto oneByte = uint8_t(buffer.data >> buffer.numBits);
output(oneByte);
if (oneByte == 0xFF) // 0xFF has a special meaning for JPEGs (it's a block marker)
output(0); // therefore pad a zero to indicate "nope, this one ain't a marker, it's just a coincidence"
// note: I don't clear those written bits, therefore buffer.bits may contain garbage in the high bits
// if you really want to "clean up" (e.g. for debugging purposes) then uncomment the following line
//buffer.bits &= (1 << buffer.numBits) - 1;
}
return *this;
}
// write all non-yet-written bits, fill gaps with 1s (that's a strange JPEG thing)
void flush()
{
// at most seven set bits needed to "fill" the last byte: 0x7F = binary 0111 1111
*this << BitCode(0x7F, 7); // I should set buffer.numBits = 0 but since there are no single bits written after flush() I can safely ignore it
}
// NOTE: all the following BitWriter functions IGNORE the BitBuffer and write straight to output !
// write a single byte
BitWriter& operator<<(uint8_t oneByte)
{
output(oneByte);
return *this;
}
// write an array of bytes
template <typename T, int Size>
BitWriter& operator<<(T (&manyBytes)[Size])
{
for (auto c : manyBytes)
output(c);
return *this;
}
// start a new JFIF block
void addMarker(uint8_t id, uint16_t length)
{
output(0xFF); output(id); // ID, always preceded by 0xFF
output(uint8_t(length >> 8)); // length of the block (big-endian, includes the 2 length bytes as well)
output(uint8_t(length & 0xFF));
}
};
// ////////////////////////////////////////
// functions / templates
// same as std::min()
template <typename Number>
Number minimum(Number value, Number maximum)
{
return value <= maximum ? value : maximum;
}
// restrict a value to the interval [minimum, maximum]
template <typename Number, typename Limit>
Number clamp(Number value, Limit minValue, Limit maxValue)
{
if (value <= minValue) return minValue; // never smaller than the minimum
if (value >= maxValue) return maxValue; // never bigger than the maximum
return value; // value was inside interval, keep it
}
// convert from RGB to YCbCr, constants are similar to ITU-R, see https://en.wikipedia.org/wiki/YCbCr#JPEG_conversion
float rgb2y (float r, float g, float b) { return +0.299f * r +0.587f * g +0.114f * b; }
float rgb2cb(float r, float g, float b) { return -0.16874f * r -0.33126f * g +0.5f * b; }
float rgb2cr(float r, float g, float b) { return +0.5f * r -0.41869f * g -0.08131f * b; }
// forward DCT computation "in one dimension" (fast AAN algorithm by Arai, Agui and Nakajima: "A fast DCT-SQ scheme for images")
void DCT(float block[8*8], uint8_t stride) // stride must be 1 (=horizontal) or 8 (=vertical)
{
const auto SqrtHalfSqrt = 1.306562965f; // sqrt((2 + sqrt(2)) / 2) = cos(pi * 1 / 8) * sqrt(2)
const auto InvSqrt = 0.707106781f; // 1 / sqrt(2) = cos(pi * 2 / 8)
const auto HalfSqrtSqrt = 0.382683432f; // sqrt(2 - sqrt(2)) / 2 = cos(pi * 3 / 8)
const auto InvSqrtSqrt = 0.541196100f; // 1 / sqrt(2 - sqrt(2)) = cos(pi * 3 / 8) * sqrt(2)
// modify in-place
auto& block0 = block[0 ];
auto& block1 = block[1 * stride];
auto& block2 = block[2 * stride];
auto& block3 = block[3 * stride];
auto& block4 = block[4 * stride];
auto& block5 = block[5 * stride];
auto& block6 = block[6 * stride];
auto& block7 = block[7 * stride];
// based on https://dev.w3.org/Amaya/libjpeg/jfdctflt.c , the original variable names can be found in my comments
auto add07 = block0 + block7; auto sub07 = block0 - block7; // tmp0, tmp7
auto add16 = block1 + block6; auto sub16 = block1 - block6; // tmp1, tmp6
auto add25 = block2 + block5; auto sub25 = block2 - block5; // tmp2, tmp5
auto add34 = block3 + block4; auto sub34 = block3 - block4; // tmp3, tmp4
auto add0347 = add07 + add34; auto sub07_34 = add07 - add34; // tmp10, tmp13 ("even part" / "phase 2")
auto add1256 = add16 + add25; auto sub16_25 = add16 - add25; // tmp11, tmp12
block0 = add0347 + add1256; block4 = add0347 - add1256; // "phase 3"
auto z1 = (sub16_25 + sub07_34) * InvSqrt; // all temporary z-variables kept their original names
block2 = sub07_34 + z1; block6 = sub07_34 - z1; // "phase 5"
auto sub23_45 = sub25 + sub34; // tmp10 ("odd part" / "phase 2")
auto sub12_56 = sub16 + sub25; // tmp11
auto sub01_67 = sub16 + sub07; // tmp12
auto z5 = (sub23_45 - sub01_67) * HalfSqrtSqrt;
auto z2 = sub23_45 * InvSqrtSqrt + z5;
auto z3 = sub12_56 * InvSqrt;
auto z4 = sub01_67 * SqrtHalfSqrt + z5;
auto z6 = sub07 + z3; // z11 ("phase 5")
auto z7 = sub07 - z3; // z13
block1 = z6 + z4; block7 = z6 - z4; // "phase 6"
block5 = z7 + z2; block3 = z7 - z2;
}
// run DCT, quantize and write Huffman bit codes
int16_t encodeBlock(BitWriter& writer, float block[8][8], const float scaled[8*8], int16_t lastDC,
const BitCode huffmanDC[256], const BitCode huffmanAC[256], const BitCode* codewords)
{
// "linearize" the 8x8 block, treat it as a flat array of 64 floats
auto block64 = (float*) block;
// DCT: rows
for (auto offset = 0; offset < 8; offset++)
DCT(block64 + offset*8, 1);
// DCT: columns
for (auto offset = 0; offset < 8; offset++)
DCT(block64 + offset*1, 8);
// scale
for (auto i = 0; i < 8*8; i++)
block64[i] *= scaled[i];
// encode DC (the first coefficient is the "average color" of the 8x8 block)
auto DC = int(block64[0] + (block64[0] >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
// quantize and zigzag the other 63 coefficients
auto posNonZero = 0; // find last coefficient which is not zero (because trailing zeros are encoded differently)
int16_t quantized[8*8];
for (auto i = 1; i < 8*8; i++) // start at 1 because block64[0]=DC was already processed
{
auto value = block64[ZigZagInv[i]];
// round to nearest integer
quantized[i] = int(value + (value >= 0 ? +0.5f : -0.5f)); // C++11's nearbyint() achieves a similar effect
// remember offset of last non-zero coefficient
if (quantized[i] != 0)
posNonZero = i;
}
// same "average color" as previous block ?
auto diff = DC - lastDC;
if (diff == 0)
writer << huffmanDC[0x00]; // yes, write a special short symbol
else
{
auto bits = codewords[diff]; // nope, encode the difference to previous block's average color
writer << huffmanDC[bits.numBits] << bits;
}
// encode ACs (quantized[1..63])
auto offset = 0; // upper 4 bits count the number of consecutive zeros
for (auto i = 1; i <= posNonZero; i++) // quantized[0] was already written, skip all trailing zeros, too
{
// zeros are encoded in a special way
while (quantized[i] == 0) // found another zero ?
{
offset += 0x10; // add 1 to the upper 4 bits
// split into blocks of at most 16 consecutive zeros
if (offset > 0xF0) // remember, the counter is in the upper 4 bits, 0xF = 15
{
writer << huffmanAC[0xF0]; // 0xF0 is a special code for "16 zeros"
offset = 0;
}
i++;
}
auto encoded = codewords[quantized[i]];
// combine number of zeros with the number of bits of the next non-zero value
writer << huffmanAC[offset + encoded.numBits] << encoded; // and the value itself
offset = 0;
}
// send end-of-block code (0x00), only needed if there are trailing zeros
if (posNonZero < 8*8 - 1) // = 63
writer << huffmanAC[0x00];
return DC;
}
// Jon's code includes the pre-generated Huffman codes
// I don't like these "magic constants" and compute them on my own :-)
void generateHuffmanTable(const uint8_t numCodes[16], const uint8_t* values, BitCode result[256])
{
// process all bitsizes 1 thru 16, no JPEG Huffman code is allowed to exceed 16 bits
auto huffmanCode = 0;
for (auto numBits = 1; numBits <= 16; numBits++)
{
// ... and each code of these bitsizes
for (auto i = 0; i < numCodes[numBits - 1]; i++) // note: numCodes array starts at zero, but smallest bitsize is 1
result[*values++] = BitCode(huffmanCode++, numBits);
// next Huffman code needs to be one bit wider
huffmanCode <<= 1;
}
}
} // end of anonymous namespace
// -------------------- externally visible code --------------------
namespace TooJpeg
{
// the only exported function ...
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels_, unsigned short width, unsigned short height,
bool isRGB, unsigned char quality_, bool downsample, const char* comment)
{
// reject invalid pointers
if (output == nullptr || pixels_ == nullptr)
return false;
// check image format
if (width == 0 || height == 0)
return false;
// number of components
const auto numComponents = isRGB ? 3 : 1;
// note: if there is just one component (=grayscale), then only luminance needs to be stored in the file
// thus everything related to chrominance need not to be written to the JPEG
// I still compute a few things, like quantization tables to avoid a complete code mess
// grayscale images can't be downsampled (because there are no Cb + Cr channels)
if (!isRGB)
downsample = false;
// wrapper for all output operations
BitWriter bitWriter(output);
// ////////////////////////////////////////
// JFIF headers
const uint8_t HeaderJfif[2+2+16] =
{ 0xFF,0xD8, // SOI marker (start of image)
0xFF,0xE0, // JFIF APP0 tag
0,16, // length: 16 bytes (14 bytes payload + 2 bytes for this length field)
'J','F','I','F',0, // JFIF identifier, zero-terminated
1,1, // JFIF version 1.1
0, // no density units specified
0,1,0,1, // density: 1 pixel "per pixel" horizontally and vertically
0,0 }; // no thumbnail (size 0 x 0)
bitWriter << HeaderJfif;
// ////////////////////////////////////////
// comment (optional)
if (comment != nullptr)
{
// look for zero terminator
auto length = 0; // = strlen(comment);
while (comment[length] != 0)
length++;
// write COM marker
bitWriter.addMarker(0xFE, 2+length); // block size is number of bytes (without zero terminator) + 2 bytes for this length field
// ... and write the comment itself
for (auto i = 0; i < length; i++)
bitWriter << comment[i];
}
// ////////////////////////////////////////
// adjust quantization tables to desired quality
// quality level must be in 1 ... 100
auto quality = clamp<uint16_t>(quality_, 1, 100);
// convert to an internal JPEG quality factor, formula taken from libjpeg
quality = quality < 50 ? 5000 / quality : 200 - quality * 2;
uint8_t quantLuminance [8*8];
uint8_t quantChrominance[8*8];
for (auto i = 0; i < 8*8; i++)
{
int luminance = (DefaultQuantLuminance [ZigZagInv[i]] * quality + 50) / 100;
int chrominance = (DefaultQuantChrominance[ZigZagInv[i]] * quality + 50) / 100;
// clamp to 1..255
quantLuminance [i] = clamp(luminance, 1, 255);
quantChrominance[i] = clamp(chrominance, 1, 255);
}
// write quantization tables
bitWriter.addMarker(0xDB, 2 + (isRGB ? 2 : 1) * (1 + 8*8)); // length: 65 bytes per table + 2 bytes for this length field
// each table has 64 entries and is preceded by an ID byte
bitWriter << 0x00 << quantLuminance; // first quantization table
if (isRGB)
bitWriter << 0x01 << quantChrominance; // second quantization table, only relevant for color images
// ////////////////////////////////////////
// write image infos (SOF0 - start of frame)
bitWriter.addMarker(0xC0, 2+6+3*numComponents); // length: 6 bytes general info + 3 per channel + 2 bytes for this length field
// 8 bits per channel
bitWriter << 0x08
// image dimensions (big-endian)
<< (height >> 8) << (height & 0xFF)
<< (width >> 8) << (width & 0xFF);
// sampling and quantization tables for each component
bitWriter << numComponents; // 1 component (grayscale, Y only) or 3 components (Y,Cb,Cr)
for (auto id = 1; id <= numComponents; id++)
bitWriter << id // component ID (Y=1, Cb=2, Cr=3)
// bitmasks for sampling: highest 4 bits: horizontal, lowest 4 bits: vertical
<< (id == 1 && downsample ? 0x22 : 0x11) // 0x11 is default YCbCr 4:4:4 and 0x22 stands for YCbCr 4:2:0
<< (id == 1 ? 0 : 1); // use quantization table 0 for Y, table 1 for Cb and Cr
// ////////////////////////////////////////
// Huffman tables
// DHT marker - define Huffman tables
bitWriter.addMarker(0xC4, isRGB ? (2+208+208) : (2+208));
// 2 bytes for the length field, store chrominance only if needed
// 1+16+12 for the DC luminance
// 1+16+162 for the AC luminance (208 = 1+16+12 + 1+16+162)
// 1+16+12 for the DC chrominance
// 1+16+162 for the AC chrominance (208 = 1+16+12 + 1+16+162, same as above)
// store luminance's DC+AC Huffman table definitions
bitWriter << 0x00 // highest 4 bits: 0 => DC, lowest 4 bits: 0 => Y (baseline)
<< DcLuminanceCodesPerBitsize
<< DcLuminanceValues;
bitWriter << 0x10 // highest 4 bits: 1 => AC, lowest 4 bits: 0 => Y (baseline)
<< AcLuminanceCodesPerBitsize
<< AcLuminanceValues;
// compute actual Huffman code tables (see Jon's code for precalculated tables)
BitCode huffmanLuminanceDC[256];
BitCode huffmanLuminanceAC[256];
generateHuffmanTable(DcLuminanceCodesPerBitsize, DcLuminanceValues, huffmanLuminanceDC);
generateHuffmanTable(AcLuminanceCodesPerBitsize, AcLuminanceValues, huffmanLuminanceAC);
// chrominance is only relevant for color images
BitCode huffmanChrominanceDC[256];
BitCode huffmanChrominanceAC[256];
if (isRGB)
{
// store luminance's DC+AC Huffman table definitions
bitWriter << 0x01 // highest 4 bits: 0 => DC, lowest 4 bits: 1 => Cr,Cb (baseline)
<< DcChrominanceCodesPerBitsize
<< DcChrominanceValues;
bitWriter << 0x11 // highest 4 bits: 1 => AC, lowest 4 bits: 1 => Cr,Cb (baseline)
<< AcChrominanceCodesPerBitsize
<< AcChrominanceValues;
// compute actual Huffman code tables (see Jon's code for precalculated tables)
generateHuffmanTable(DcChrominanceCodesPerBitsize, DcChrominanceValues, huffmanChrominanceDC);
generateHuffmanTable(AcChrominanceCodesPerBitsize, AcChrominanceValues, huffmanChrominanceAC);
}
// ////////////////////////////////////////
// start of scan (there is only a single scan for baseline JPEGs)
bitWriter.addMarker(0xDA, 2+1+2*numComponents+3); // 2 bytes for the length field, 1 byte for number of components,
// then 2 bytes for each component and 3 bytes for spectral selection
// assign Huffman tables to each component
bitWriter << numComponents;
for (auto id = 1; id <= numComponents; id++)
// highest 4 bits: DC Huffman table, lowest 4 bits: AC Huffman table
bitWriter << id << (id == 1 ? 0x00 : 0x11); // Y: tables 0 for DC and AC; Cb + Cr: tables 1 for DC and AC
// constant values for our baseline JPEGs (which have a single sequential scan)
static const uint8_t Spectral[3] = { 0, 63, 0 }; // spectral selection: must be from 0 to 63; successive approximation must be 0
bitWriter << Spectral;
// ////////////////////////////////////////
// adjust quantization tables with AAN scaling factors to simplify DCT
float scaledLuminance [8*8];
float scaledChrominance[8*8];
for (auto i = 0; i < 8*8; i++)
{
auto row = ZigZagInv[i] / 8; // same as ZigZagInv[i] >> 3
auto column = ZigZagInv[i] % 8; // same as ZigZagInv[i] & 7
// scaling constants for AAN DCT algorithm: AanScaleFactors[0] = 1, AanScaleFactors[k=1..7] = cos(k*PI/16) * sqrt(2)
static const float AanScaleFactors[8] = { 1, 1.387039845f, 1.306562965f, 1.175875602f, 1, 0.785694958f, 0.541196100f, 0.275899379f };
auto factor = 1 / (AanScaleFactors[row] * AanScaleFactors[column] * 8);
scaledLuminance [ZigZagInv[i]] = factor / quantLuminance [i];
scaledChrominance[ZigZagInv[i]] = factor / quantChrominance[i];
// if you really want JPEGs that are bitwise identical to Jon Olick's code then you need slightly different formulas (note: sqrt(8) = 2.828427125f)
//static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; // line 240 of jo_jpeg.cpp
//scaledLuminance [ZigZagInv[i]] = 1 / (quantLuminance [i] * aasf[row] * aasf[column]); // lines 266-267 of jo_jpeg.cpp
//scaledChrominance[ZigZagInv[i]] = 1 / (quantChrominance[i] * aasf[row] * aasf[column]);
}
// ////////////////////////////////////////
// precompute JPEG codewords for quantized DCT
BitCode codewordsArray[2 * CodeWordLimit]; // note: quantized[i] is found at codewordsArray[quantized[i] + CodeWordLimit]
BitCode* codewords = &codewordsArray[CodeWordLimit]; // allow negative indices, so quantized[i] is at codewords[quantized[i]]
uint8_t numBits = 1; // each codeword has at least one bit (value == 0 is undefined)
int32_t mask = 1; // mask is always 2^numBits - 1, initial value 2^1-1 = 2-1 = 1
for (int16_t value = 1; value < CodeWordLimit; value++)
{
// numBits = position of highest set bit (ignoring the sign)
// mask = (2^numBits) - 1
if (value > mask) // one more bit ?
{
numBits++;
mask = (mask << 1) | 1; // append a set bit
}
codewords[-value] = BitCode(mask - value, numBits); // note that I use a negative index => codewords[-value] = codewordsArray[CodeWordLimit value]
codewords[+value] = BitCode( value, numBits);
}
// just convert image data from void*
auto pixels = (const uint8_t*)pixels_;
// the next two variables are frequently used when checking for image borders
const auto maxWidth = width - 1; // "last row"
const auto maxHeight = height - 1; // "bottom line"
// process MCUs (minimum codes units) => image is subdivided into a grid of 8x8 or 16x16 tiles
const auto sampling = downsample ? 2 : 1; // 1x1 or 2x2 sampling
const auto mcuSize = 8 * sampling;
// average color of the previous MCU
int16_t lastYDC = 0, lastCbDC = 0, lastCrDC = 0;
// convert from RGB to YCbCr
float Y[8][8], Cb[8][8], Cr[8][8];
for (auto mcuY = 0; mcuY < height; mcuY += mcuSize) // each step is either 8 or 16 (=mcuSize)
for (auto mcuX = 0; mcuX < width; mcuX += mcuSize)
{
// YCbCr 4:4:4 format: each MCU is a 8x8 block - the same applies to grayscale images, too
// YCbCr 4:2:0 format: each MCU represents a 16x16 block, stored as 4x 8x8 Y-blocks plus 1x 8x8 Cb and 1x 8x8 Cr block)
for (auto blockY = 0; blockY < mcuSize; blockY += 8) // iterate once (YCbCr444 and grayscale) or twice (YCbCr420)
for (auto blockX = 0; blockX < mcuSize; blockX += 8)
{
// now we finally have an 8x8 block ...
for (auto deltaY = 0; deltaY < 8; deltaY++)
{
auto column = minimum(mcuX + blockX , maxWidth); // must not exceed image borders, replicate last row/column if needed
auto row = minimum(mcuY + blockY + deltaY, maxHeight);
for (auto deltaX = 0; deltaX < 8; deltaX++)
{
// find actual pixel position within the current image
auto pixelPos = row * int(width) + column; // the cast ensures that we don't run into multiplication overflows
if (column < maxWidth)
column++;
// grayscale images have solely a Y channel which can be easily derived from the input pixel by shifting it by 128
if (!isRGB)
{
Y[deltaY][deltaX] = pixels[pixelPos] - 128.f;
continue;
}
// RGB: 3 bytes per pixel (whereas grayscale images have only 1 byte per pixel)
auto r = pixels[3 * pixelPos ];
auto g = pixels[3 * pixelPos + 1];
auto b = pixels[3 * pixelPos + 2];
Y [deltaY][deltaX] = rgb2y (r, g, b) - 128; // again, the JPEG standard requires Y to be shifted by 128
// YCbCr444 is easy - the more complex YCbCr420 has to be computed about 20 lines below in a second pass
if (!downsample)
{
Cb[deltaY][deltaX] = rgb2cb(r, g, b); // standard RGB-to-YCbCr conversion
Cr[deltaY][deltaX] = rgb2cr(r, g, b);
}
}
}
// encode Y channel
lastYDC = encodeBlock(bitWriter, Y, scaledLuminance, lastYDC, huffmanLuminanceDC, huffmanLuminanceAC, codewords);
// Cb and Cr are encoded about 50 lines below
}
// grayscale images don't need any Cb and Cr information
if (!isRGB)
continue;
// ////////////////////////////////////////
// the following lines are only relevant for YCbCr420:
// average/downsample chrominance of four pixels while respecting the image borders
if (downsample)
for (short deltaY = 7; downsample && deltaY >= 0; deltaY--) // iterating loop in reverse increases cache read efficiency
{
auto row = minimum(mcuY + 2*deltaY, maxHeight); // each deltaX/Y step covers a 2x2 area
auto column = mcuX; // column is updated inside next loop
auto pixelPos = (row * int(width) + column) * 3; // numComponents = 3
// deltas (in bytes) to next row / column, must not exceed image borders
auto rowStep = (row < maxHeight) ? 3 * int(width) : 0; // always numComponents*width except for bottom line
auto columnStep = (column < maxWidth ) ? 3 : 0; // always numComponents except for rightmost pixel
for (short deltaX = 0; deltaX < 8; deltaX++)
{
// let's add all four samples (2x2 area)
auto right = pixelPos + columnStep;
auto down = pixelPos + rowStep;
auto downRight = pixelPos + columnStep + rowStep;
// note: cast from 8 bits to >8 bits to avoid overflows when adding
auto r = short(pixels[pixelPos ]) + pixels[right ] + pixels[down ] + pixels[downRight ];
auto g = short(pixels[pixelPos + 1]) + pixels[right + 1] + pixels[down + 1] + pixels[downRight + 1];
auto b = short(pixels[pixelPos + 2]) + pixels[right + 2] + pixels[down + 2] + pixels[downRight + 2];
// convert to Cb and Cr
Cb[deltaY][deltaX] = rgb2cb(r, g, b) / 4; // I still have to divide r,g,b by 4 to get their average values
Cr[deltaY][deltaX] = rgb2cr(r, g, b) / 4; // it's a bit faster if done AFTER CbCr conversion
// step forward to next 2x2 area
pixelPos += 2*3; // 2 pixels => 6 bytes (2*numComponents)
column += 2;
// reached right border ?
if (column >= maxWidth)
{
columnStep = 0;
pixelPos = ((row + 1) * int(width) - 1) * 3; // same as (row * width + maxWidth) * numComponents => current's row last pixel
}
}
} // end of YCbCr420 code for Cb and Cr
// encode Cb and Cr
lastCbDC = encodeBlock(bitWriter, Cb, scaledChrominance, lastCbDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
lastCrDC = encodeBlock(bitWriter, Cr, scaledChrominance, lastCrDC, huffmanChrominanceDC, huffmanChrominanceAC, codewords);
}
bitWriter.flush(); // now image is completely encoded, write any bits still left in the buffer
// ///////////////////////////
// EOI marker
bitWriter << 0xFF << 0xD9; // this marker has no length, therefore I can't use addMarker()
return true;
} // writeJpeg()
} // namespace TooJpeg
-62
View File
@@ -1,62 +0,0 @@
// //////////////////////////////////////////////////////////
// toojpeg.h
// written by Stephan Brumme, 2018-2019
// see https://create.stephan-brumme.com/toojpeg/
//
// This is a compact baseline JPEG/JFIF writer, written in C++ (but looks like C for the most part).
// Its interface has only one function: writeJpeg() - and that's it !
//
// basic example:
// => create an image with any content you like, e.g. 1024x768, RGB = 3 bytes per pixel
// auto pixels = new unsigned char[1024*768*3];
// => you need to define a callback that receives the compressed data byte-by-byte from my JPEG writer
// void myOutput(unsigned char oneByte) { fputc(oneByte, myFileHandle); } // save byte to file
// => let's go !
// TooJpeg::writeJpeg(myOutput, mypixels, 1024, 768);
#pragma once
namespace TooJpeg
{
// write one byte (to disk, memory, ...)
typedef void (*WRITE_ONE_BYTE)(unsigned char);
// this callback is called for every byte generated by the encoder and behaves similar to fputc
// if you prefer stylish C++11 syntax then it can be a lambda, too:
// auto myOutput = [](unsigned char oneByte) { fputc(oneByte, output); };
// output - callback that stores a single byte (writes to disk, memory, ...)
// pixels - stored in RGB format or grayscale, stored from upper-left to lower-right
// width,height - image size
// isRGB - true if RGB format (3 bytes per pixel); false if grayscale (1 byte per pixel)
// quality - between 1 (worst) and 100 (best)
// downsample - if true then YCbCr 4:2:0 format is used (smaller size, minor quality loss) instead of 4:4:4, not relevant for grayscale
// comment - optional JPEG comment (0/NULL if no comment), must not contain ASCII code 0xFF
bool writeJpeg(WRITE_ONE_BYTE output, const void* pixels, unsigned short width, unsigned short height,
bool isRGB = true, unsigned char quality = 90, bool downsample = false, const char* comment = nullptr);
} // namespace TooJpeg
// My main inspiration was Jon Olick's Minimalistic JPEG writer
// ( https://www.jonolick.com/code.html => direct link is https://www.jonolick.com/uploads/7/9/2/1/7921194/jo_jpeg.cpp ).
// However, his code documentation is quite sparse - probably because it wasn't written from scratch and is (quote:) "based on a javascript jpeg writer",
// most likely Andreas Ritter's code: https://github.com/eugeneware/jpeg-js/blob/master/lib/encoder.js
//
// Therefore I wrote the whole lib from scratch and tried hard to add tons of comments to my code, especially describing where all those magic numbers come from.
// And I managed to remove the need for any external includes ...
// yes, that's right: my library has no (!) includes at all, not even #include <stdlib.h>
// Depending on your callback WRITE_ONE_BYTE, the library writes either to disk, or in-memory, or wherever you wish.
// Moreover, no dynamic memory allocations are performed, just a few bytes on the stack.
//
// In contrast to Jon's code, compression can be significantly improved in many use cases:
// a) grayscale JPEG images need just a single Y channel, no need to save the superfluous Cb + Cr channels
// b) YCbCr 4:2:0 downsampling is often about 20% more efficient (=smaller) than the default YCbCr 4:4:4 with only little visual loss
//
// TooJpeg 1.2+ compresses about twice as fast as jo_jpeg (and about half as fast as libjpeg-turbo).
// A few benchmark numbers can be found on my website https://create.stephan-brumme.com/toojpeg/#benchmark
//
// Last but not least you can optionally add a JPEG comment.
//
// Your C++ compiler needs to support a reasonable subset of C++11 (g++ 4.7 or Visual C++ 2013 are sufficient).
// I haven't tested the code on big-endian systems or anything that smells like an apple.
//
// USE AT YOUR OWN RISK. Because you are a brave soul :-)
-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"
+150 -98
View File
@@ -33,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++;
}
@@ -47,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);
@@ -68,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;
}
@@ -107,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();
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 } };
@@ -224,9 +277,8 @@ namespace games::ccj {
static bool initialized = false;
if (initialized) {
return;
} else {
initialized = true;
}
initialized = true;
// announce
log_info("trackball", "init");
@@ -246,8 +298,6 @@ 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);
@@ -259,8 +309,9 @@ namespace games::ccj {
wndProc(hWnd, WM_INPUT, RIM_INPUT, (LPARAM)fakeHandle);
}
if (!tbThreadRunning)
if (!tbThreadRunning) {
break;
}
timer.sleep(10);
}
@@ -269,8 +320,9 @@ namespace games::ccj {
void trackball_thread_stop() {
tbThreadRunning = false;
if (tbThread)
if (tbThread) {
tbThread->join();
}
log_info("trackball", "thread stop");
+171 -171
View File
@@ -1,171 +1,171 @@
#include "asio.h"
#include <windows.h>
#include <cstring>
#include "avs/game.h"
#include "gitadora.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle,
ASIO_DRIVER.value().c_str(),
ulOptions,
samDesired,
&real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) {
log_warning(
"gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result);
log_warning(
"gitadora::asio",
"due to improper ASIO setting, audio init will fail");
}
return result;
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0';
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
HKEY target = hKey;
if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr &&
lpData != nullptr &&
lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) {
*lpType = REG_SZ;
}
return ERROR_SUCCESS;
}
// for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle;
}
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) {
return;
}
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
}
}
#include "asio.h"
#include <windows.h>
#include <cstring>
#include "avs/game.h"
#include "gitadora.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult)
{
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle,
ASIO_DRIVER.value().c_str(),
ulOptions,
samDesired,
&real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) {
log_warning(
"gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result);
log_warning(
"gitadora::asio",
"due to improper ASIO setting, audio init will fail");
}
return result;
}
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
}
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr &&
phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0)
{
*phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
}
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
}
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0';
}
return ret;
} else {
return ERROR_NO_MORE_ITEMS;
}
}
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
}
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData)
{
HKEY target = hKey;
if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr &&
lpData != nullptr &&
lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA;
}
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) {
*lpType = REG_SZ;
}
return ERROR_SUCCESS;
}
// for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle;
}
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
}
static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr;
}
return ERROR_SUCCESS;
}
return RegCloseKey_orig(hKey);
}
void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) {
return;
}
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
}
}
+12 -12
View File
@@ -1,12 +1,12 @@
#pragma once
namespace games::gitadora {
// installs IAT registry hooks in gfdm.dll that redirect the game's
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
// driver name read from games::gitadora::ASIO_DRIVER.
//
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
// forward every call straight through to advapi32.
void asio_hook_init();
}
#pragma once
namespace games::gitadora {
// installs IAT registry hooks in gfdm.dll that redirect the game's
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
// driver name read from games::gitadora::ASIO_DRIVER.
//
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks
// forward every call straight through to advapi32.
void asio_hook_init();
}
+218 -51
View File
@@ -2,17 +2,20 @@
#include "asio.h"
#include "handle.h"
#include "bi2x_hook.h"
#include <span>
#include <unordered_map>
#include "cfg/configurator.h"
#include "cfg/screen_resize.h"
#include <ks.h>
#include <ksmedia.h>
#include "cfg/configurator.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/mme.h"
#include "hooks/graphics/graphics.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "touch/native/nativetouchhook.h"
#include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h"
@@ -28,14 +31,17 @@ namespace games::gitadora {
// settings
bool TWOCHANNEL = false;
bool DISABLE_FRAME_LIMITER = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool P1_LEFTY = false;
bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
/*
* Prevent GitaDora from creating folders on F drive
@@ -61,6 +67,46 @@ namespace games::gitadora {
return CreateDirectoryA(lpPathName, lpSecurityAttributes);
}
// libshare-pj paces mainloop with separate 12 ms and 16 ms waits. these waits
// interfere with the game's normal display synchronization on modern Windows
// and can hold a nominal 60 FPS game near 58 FPS. locate the instruction
// sequences at runtime so the fix does not depend on per-version file offsets.
static void disable_mainloop_frame_limiter(HMODULE sharepj_module) {
if (!sharepj_module) {
return;
}
// both limiters have the same shape, only the millisecond target xx differs
// (0Ch for the 12 ms limiter, 10h for the 16 ms one):
//
// 48 83 F8 xx: cmp rax, xx; compare elapsed frame time with the target
// 73 10: jae +10h; skip the wait once the target has elapsed
// B9 xx 00 00 00: mov ecx, xx; load the target
// 48 2B C8: sub rcx, rax; calculate the remaining wait time
// 74 06: je +6h; skip the following six-byte Sleep call if no wait remains
//
// changing jae (73h) to jmp (EBh) makes each block always take its existing
// skip path, which bypasses only the associated Sleep call and leaves the 10h
// branch displacement and every other wait untouched.
const auto limiter_12ms_disabled = replace_pattern(
sharepj_module,
"4883F80C7310B90C000000482BC87406",
"????????EB??????????????????????", 0, 0);
const auto limiter_16ms_disabled = replace_pattern(
sharepj_module,
"4883F8107310B910000000482BC87406",
"????????EB??????????????????????", 0, 0);
if (!limiter_12ms_disabled || !limiter_16ms_disabled) {
log_fatal(
"gitadora",
"failed to disable libshare-pj mainloop frame limiter (-gdnoframelimiter), ensure patch is not already applied");
return;
}
log_info("gitadora", "successfully disabled libshare-pj mainloop frame limiter (-gdnoframelimiter)");
}
#endif
/*
@@ -270,9 +316,21 @@ namespace games::gitadora {
break;
case 2:
if (!GRAPHICS_WINDOWED) {
log_fatal(
if (D3D9_ADAPTER.has_value()) {
log_fatal(
"gitadora",
"arena model: fullscreen two-window mode cannot use -monitor; "
"use -w for borderless windows instead");
}
ARENA_TWO_HEAD_EXCLUSIVE = true;
log_info(
"gitadora",
"arena model: 2-window mode is not supported in fullscreen, choose 1 or 4");
"arena model: native two-head fullscreen adapter-group mode "
"(MAIN + SMALL; LEFT/RIGHT are virtual)");
} else {
log_info(
"gitadora",
"arena model: two-window mode uses windowed rendering");
}
log_info("gitadora", "arena model: two-window mode");
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS = true;
@@ -298,10 +356,18 @@ namespace games::gitadora {
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
// Cached real monitor paths and their source/target mode entries. modeInfoIdx values are
// renumbered so the cache is self-contained. The normal single-head emulation keeps only
// MAIN; the two-head adapter-group mode keeps MAIN and SMALL as real heads.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
static DISPLAYCONFIG_PATH_INFO real_small_path = {};
static DISPLAYCONFIG_MODE_INFO real_small_modes[2] = {}; // [0]=source, [1]=target
static bool real_small_path_available = false;
static bool is_two_head_exclusive() {
return ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED;
}
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
@@ -325,12 +391,31 @@ namespace games::gitadora {
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
// Normal single-head emulation: every non-MAIN role is fake.
static constexpr FakeMonitor FAKE_MONITORS_SINGLE_HEAD[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
// Two-head fullscreen: adapters 0/1 are the real MAIN/SMALL group heads.
// Keep only LEFT/RIGHT fake, with their adapter ids matching the D3D9 wrapper.
static constexpr FakeMonitor FAKE_MONITORS_TWO_HEAD[] = {
{ 2, 1080, 1920, -200000, -200000, 0 }, // left (DP connector instance 0)
{ 3, 1080, 1920, -300000, -300000, 1 }, // right (DP connector instance 1)
};
static std::span<const FakeMonitor> get_fake_monitors() {
if (is_two_head_exclusive()) {
return FAKE_MONITORS_TWO_HEAD;
}
return FAKE_MONITORS_SINGLE_HEAD;
}
static UINT32 real_monitor_count() {
return is_two_head_exclusive() ? 2 : 1;
}
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
@@ -368,13 +453,55 @@ namespace games::gitadora {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
if (primary->targetInfo.modeInfoIdx >= all_modes.size()) {
log_fatal("gitadora", "cache_primary_monitor_info: primary target mode is missing");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
real_small_path_available = false;
if (is_two_head_exclusive()) {
if (all_paths.size() != 2) {
log_fatal(
"gitadora",
"two-head fullscreen mode requires exactly two active display paths, found {}",
all_paths.size());
}
const auto secondary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
return &p != &*primary;
});
if (secondary == all_paths.end() ||
secondary->sourceInfo.modeInfoIdx >= all_modes.size() ||
secondary->targetInfo.modeInfoIdx >= all_modes.size()) {
log_fatal("gitadora", "could not identify the physical SMALL monitor");
}
const LUID primary_adapter = primary->sourceInfo.adapterId;
const LUID secondary_adapter = secondary->sourceInfo.adapterId;
const bool same_adapter = primary_adapter.HighPart == secondary_adapter.HighPart &&
primary_adapter.LowPart == secondary_adapter.LowPart;
if (!same_adapter) {
log_fatal(
"gitadora",
"MAIN and SMALL must use the same display adapter");
}
real_small_modes[0] = all_modes[secondary->sourceInfo.modeInfoIdx];
real_small_modes[1] = all_modes[secondary->targetInfo.modeInfoIdx];
real_small_path = *secondary;
real_small_path.sourceInfo.modeInfoIdx = 2;
real_small_path.targetInfo.modeInfoIdx = 3;
real_small_path_available = true;
log_info("gitadora", "cache_primary_monitor_info: cached real MAIN and SMALL monitors");
} else {
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
}
static
@@ -389,33 +516,36 @@ namespace games::gitadora {
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
const auto fake_monitors = get_fake_monitors();
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
const UINT32 real_count = real_monitor_count();
*pNumPathArrayElements = real_count + fake_count;
*pNumModeInfoArrayElements = (real_count + fake_count) * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
"GetDisplayConfigBufferSizes: {} real path(s) + {} fake monitor(s)",
real_count,
fake_count);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
// Write a fake monitor after the real paths. The cache is packed as [source, target]
// mode pairs, so each path consumes two mode entries.
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
const FakeMonitor &m,
UINT32 path_index,
UINT32 source_mode_index)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 src_idx = source_mode_index;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
paths[path_index] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
@@ -473,21 +603,35 @@ namespace games::gitadora {
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
// Copy cached real monitor paths into the caller buffers.
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (is_two_head_exclusive() && real_small_path_available) {
pathArray[1] = real_small_path;
modeInfoArray[2] = real_small_modes[0];
modeInfoArray[3] = real_small_modes[1];
}
const auto fake_monitors = get_fake_monitors();
const auto fake_count = static_cast<UINT32>(fake_monitors.size());
const UINT32 real_count = real_monitor_count();
*numPathArrayElements = real_count + fake_count;
*numModeInfoArrayElements = (real_count + fake_count) * 2;
if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
// Append fake monitors after the real path(s).
for (UINT32 i = 0; i < fake_count; i++) {
insert_fake_monitor(
pathArray,
modeInfoArray,
fake_monitors[i],
real_count + i,
real_count * 2 + i * 2);
}
return ERROR_SUCCESS;
@@ -518,7 +662,8 @@ namespace games::gitadora {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
const auto fake_monitors = get_fake_monitors();
for (const auto &f : fake_monitors) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
@@ -541,18 +686,27 @@ namespace games::gitadora {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
// Override MAIN to HDMI/0 and retag the real second head as the DP/2 SMALL
// display expected by the cabinet software in two-head fullscreen mode.
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
const auto target_matches = [&](const DISPLAYCONFIG_PATH_TARGET_INFO &target) {
return target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart;
};
if (target_matches(real_primary_path.targetInfo)) {
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
} else if (is_two_head_exclusive() && real_small_path_available &&
target_matches(real_small_path.targetInfo)) {
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = 2;
log_info("gitadora",
"overriding secondary monitor (id={}) to pretend to be SMALL DP/2",
targetName->header.id);
}
return ret;
}
@@ -575,6 +729,12 @@ namespace games::gitadora {
HMODULE system_module = libutils::try_module("libsystem.dll");
// patches
#ifdef SPICE64
if (DISABLE_FRAME_LIMITER && !is_arena_model()) {
disable_mainloop_frame_limiter(sharepj_module);
}
#endif
detour::inline_hook((void *) eam_network_detected_ip_change, libutils::try_proc(
sharepj_module, "eam_network_detected_ip_change"));
detour::inline_hook((void *) eam_network_settings_conflict, libutils::try_proc(
@@ -650,31 +810,38 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
}
// monitor/touch hooks (windowed or full screen)
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// enable touch hook for subscreen overlay
// touch injection drives mouse-as-touch and API touch for the subscreen,
// no matter whether it is drawn by the overlay (single-window mode) or by
// the dedicated SMALL window
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// the legacy fallback can only feed the subscreen overlay
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
#if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
if (!GRAPHICS_WINDOWED &&
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || ARENA_TWO_HEAD_EXCLUSIVE)) {
if (ARENA_TWO_HEAD_EXCLUSIVE) {
log_info(
"gitadora",
"exposing physical MAIN/SMALL and virtual LEFT/RIGHT");
}
#endif
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
// window patch
+7
View File
@@ -13,14 +13,21 @@ namespace games::gitadora {
// settings
extern bool TWOCHANNEL;
extern bool DISABLE_FRAME_LIMITER;
extern std::optional<unsigned int> CAB_TYPE;
extern bool P1_LEFTY;
extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO;
extern bool NATIVE_TOUCH;
// arena SMALL subscreen (touch panel) resolution
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
class GitaDoraGame : public games::Game {
public:
+7 -4
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -443,10 +444,12 @@ namespace games::iidx {
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
for (unsigned int i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
+10 -8
View File
@@ -534,7 +534,7 @@ namespace games::iidx {
if (mediaTypePointer && mediaTypePointer->IsString()) {
std::string mediaType = mediaTypePointer->GetString();
if (mediaType.length() > 0) {
camera->m_selectedMediaTypeDescription = mediaType;
camera->SetSelectedMediaTypeDescription(mediaType);
camera->m_useAutoMediaType = false;
} else {
camera->m_useAutoMediaType = true;
@@ -547,7 +547,7 @@ namespace games::iidx {
std::string drawModeString = drawModePointer->GetString();
for (int j = 0; j < DRAW_MODE_SIZE; j++) {
if (DRAW_MODE_LABELS[j].compare(drawModeString) == 0) {
camera->m_drawMode = (LocalCameraDrawMode) j;
camera->m_drawMode.store((LocalCameraDrawMode) j);
break;
}
}
@@ -556,12 +556,12 @@ namespace games::iidx {
// Flip
auto flipHorizontalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipHorizontal").Get(doc);
if (flipHorizontalPointer && flipHorizontalPointer->IsBool()) {
camera->m_flipHorizontal = flipHorizontalPointer->GetBool();
camera->m_flipHorizontal.store(flipHorizontalPointer->GetBool());
}
auto flipVerticalPointer = rapidjson::Pointer(root + "/" + symLink + "/FlipVertical").Get(doc);
if (flipVerticalPointer && flipVerticalPointer->IsBool()) {
camera->m_flipVertical = flipVerticalPointer->GetBool();
camera->m_flipVertical.store(flipVerticalPointer->GetBool());
}
// Allow manual control
@@ -625,15 +625,17 @@ namespace games::iidx {
if (camera->m_useAutoMediaType) {
rapidjson::Pointer(root + "MediaType").Set(doc, "");
} else {
rapidjson::Pointer(root + "MediaType").Set(doc, camera->m_selectedMediaTypeDescription);
const auto mediaTypeDescription = camera->GetSelectedMediaTypeDescription();
rapidjson::Pointer(root + "MediaType").Set(doc, mediaTypeDescription);
}
// Draw Mode
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[camera->m_drawMode]);
const auto drawMode = camera->m_drawMode.load();
rapidjson::Pointer(root + "DrawMode").Set(doc, DRAW_MODE_LABELS[drawMode]);
// Flip
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal);
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical);
rapidjson::Pointer(root + "FlipHorizontal").Set(doc, camera->m_flipHorizontal.load());
rapidjson::Pointer(root + "FlipVertical").Set(doc, camera->m_flipVertical.load());
// Manual control
rapidjson::Pointer(root + "AllowManualControl").Set(doc, camera->m_allowManualControl);
+48 -27
View File
@@ -20,9 +20,9 @@
#include "hooks/sleephook.h"
#include "launcher/options.h"
#include "touch/touch.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/detour.h"
#include "util/deferlog.h"
#include "util/fileutils.h"
@@ -41,6 +41,7 @@
#include "bi2x_hook.h"
#include "ezusb.h"
#include "io.h"
#include "poke.h"
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
@@ -57,13 +58,14 @@ namespace games::iidx {
// settings
bool FLIP_CAMS = false;
std::optional<bool> DISABLE_CAMS;
cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
bool TDJ_CAMERA = false;
bool TDJ_CAMERA_PREFER_16_9 = true;
bool TDJ_MODE = false;
bool FORCE_720P = false;
bool DISABLE_ESPEC_IO = false;
bool NATIVE_TOUCH = false;
bool NATIVE_TOUCH = true;
bool ENABLE_POKE = false;
std::optional<std::string> SOUND_OUTPUT_DEVICE = std::nullopt;
std::optional<std::string> SOUND_OUTPUT_DEVICE_IN_EFFECT = std::nullopt;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
@@ -277,6 +279,26 @@ namespace games::iidx {
return nullptr;
}
// best-effort read of a TDJ ROM file to determine if the game is running in TDJ mode
//
// these paths are not emulated - they hit whatever is actually mounted at that drive letter.
// an empty optical or removable drive raises the modal "insert a disk" error (the launcher
// clears SEM_FAILCRITICALERRORS process-wide) and a downed network drive stalls on redirector
// timeouts, so only probe what a TDJ cabinet would actually be laid out on.
// drive_path must be absolute and start with a drive letter.
static bool tdj_rom_matches(const char *drive_path, const char *expected) {
const wchar_t root[] = { (wchar_t) drive_path[0], L':', L'\\', L'\0' };
const auto drive_type = GetDriveTypeW(root);
if (drive_type != DRIVE_FIXED && drive_type != DRIVE_RAMDISK) {
log_misc("iidx", "not probing '{}' for TDJ, not a local disk (drive type {})",
drive_path, drive_type);
return false;
}
return fileutils::text_read(drive_path) == expected;
}
#endif
IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
@@ -337,8 +359,10 @@ namespace games::iidx {
HAS_LIBAIO = true;
// check TDJ mode
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA";
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ";
if (!TDJ_MODE) {
TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA")
|| tdj_rom_matches("D:\\001rom.txt", "TDJ");
}
// force TDJ mode
if (TDJ_MODE) {
@@ -350,12 +374,15 @@ namespace games::iidx {
// need to hook `avs2-core.dll` so AVS win32fs operations go through rom hook
devicehook_init(avs::core::DLL_INSTANCE);
if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else {
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("beatmania IIDX", "main", avs::game::DLL_INSTANCE);
wintouchemu::hook_title_ends(
"beatmania IIDX",
"main",
avs::game::DLL_INSTANCE);
}
}
@@ -419,10 +446,7 @@ namespace games::iidx {
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
// check if cam hook should be enabled
if (!DISABLE_CAMS.has_value()) {
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
}
if (!DISABLE_CAMS.value()) {
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
init_legacy_camera_hook(FLIP_CAMS);
}
@@ -449,19 +473,10 @@ namespace games::iidx {
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
}
// check for cab camera access for the second time (first time was in launcher.cpp)
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab
// therefore, start with cams OFF by default, and allow user to forcibly override to ON
if (!games::iidx::DISABLE_CAMS.has_value()) {
games::iidx::DISABLE_CAMS = true;
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
games::iidx::DISABLE_CAMS = false;
}
if (games::iidx::DISABLE_CAMS.value()) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// auto with iidx module means turn off camera (non-cab use)
if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
// windowed subscreen, enabled by default, unless turned off by user
@@ -533,6 +548,12 @@ namespace games::iidx {
}
}
void IIDXGame::post_attach() {
if (ENABLE_POKE) {
poke::enable();
}
}
void IIDXGame::detach() {
Game::detach();
+10 -1
View File
@@ -11,6 +11,13 @@
namespace games::iidx {
enum class cab_camera_access_mode {
automatic,
off,
on,
legacy,
};
enum class iidx_aio_emulation_state {
unknown,
bi2a_com2,
@@ -20,7 +27,7 @@ namespace games::iidx {
// settings
extern bool FLIP_CAMS;
extern std::optional<bool> DISABLE_CAMS;
extern cab_camera_access_mode CAB_CAMERA_ACCESS;
extern bool TDJ_CAMERA;
extern bool TDJ_CAMERA_PREFER_16_9;
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
@@ -29,6 +36,7 @@ namespace games::iidx {
extern bool FORCE_720P;
extern bool DISABLE_ESPEC_IO;
extern bool NATIVE_TOUCH;
extern bool ENABLE_POKE;
extern std::optional<std::string> SOUND_OUTPUT_DEVICE;
extern std::optional<std::string> ASIO_DRIVER;
extern uint8_t DIGITAL_TT_SENS;
@@ -53,6 +61,7 @@ namespace games::iidx {
virtual void attach() override;
virtual void pre_attach() override;
virtual void post_attach() override;
virtual void detach() override;
private:
File diff suppressed because it is too large Load Diff
+36 -11
View File
@@ -2,6 +2,7 @@
#if SPICE64 && !SPICE_XP
#include <atomic>
#include <d3d9.h>
#include <dxva2api.h>
#include <mutex>
@@ -66,6 +67,8 @@ extern std::string CAMERA_CONTROL_LABELS[];
extern std::string DRAW_MODE_LABELS[];
namespace games::iidx {
class IIDXCameraSourceReaderCallback;
namespace Camera {
struct PlayVideoCamera {
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
@@ -87,7 +90,7 @@ namespace games::iidx {
class IIDXLocalCamera {
protected:
virtual ~IIDXLocalCamera() {};
virtual ~IIDXLocalCamera();
LONG m_nRefCount;
CRITICAL_SECTION m_critsec;
@@ -101,9 +104,14 @@ namespace games::iidx {
// For reading frames from Camera
IMFMediaSource *m_pSource = nullptr;
IMFSourceReader *m_pSourceReader = nullptr;
IMFSourceReaderEx *m_pSourceReaderEx = nullptr;
IIDXCameraSourceReaderCallback *m_pSourceReaderCallback = nullptr;
std::mutex m_mediaTypeMutex;
IMFMediaType *m_pendingMediaType = nullptr;
int m_selectedMediaTypeIndex = 0;
std::string m_selectedMediaTypeDescription = "";
// Camera Format information
double m_frameRate = 0;
LONG m_cameraWidth;
LONG m_cameraHeight;
@@ -129,6 +137,12 @@ namespace games::iidx {
LPDIRECT3DTEXTURE9 m_conversionTexture = nullptr;
IDirect3DSurface9 *m_pConversionSurf = nullptr;
// upload surface for decoded camera frames returned in system memory
IDirect3DSurface9 *m_pDecodedSurf = nullptr;
GUID m_decodedSubtype = GUID_NULL;
GUID m_outputSubtype = GUID_NULL;
bool m_drawErrorLogged = false;
// Texture for custom transform (e.g. horizontal flip)
LPDIRECT3DTEXTURE9 m_transformTexture = nullptr;
IDirect3DSurface9 *m_pTransformSurf = nullptr;
@@ -153,21 +167,19 @@ namespace games::iidx {
BOOL m_initialized = false;
// True if all the setup steps succeeded
BOOL m_active = false;
std::atomic_bool m_active = false;
// Media type select
std::vector<MediaTypeInfo> m_mediaTypeInfos = {};
int m_selectedMediaTypeIndex = 0;
bool m_useAutoMediaType = true;
IMFMediaType *m_pAutoMediaType = nullptr;
std::string m_selectedMediaTypeDescription = "";
bool m_allowManualControl = false;
LocalCameraDrawMode m_drawMode = DrawModeCrop4_3;
std::atomic<LocalCameraDrawMode> m_drawMode = DrawModeCrop4_3;
// Render processing
bool m_flipHorizontal = false;
bool m_flipVertical = false;
std::atomic_bool m_flipHorizontal = false;
std::atomic_bool m_flipVertical = false;
IIDXLocalCamera(
std::string name,
@@ -184,11 +196,14 @@ namespace games::iidx {
HRESULT GetCameraControlProp(int index, CameraControlProp *pProp);
HRESULT SetCameraControlProp(int index, long value, long flags);
HRESULT ResetCameraControlProps();
HRESULT FlushDrawCommands();
std::string GetName();
std::string GetFriendlyName();
std::string GetSymLink();
int GetSelectedMediaTypeIndex();
std::string GetSelectedMediaTypeDescription();
void SetSelectedMediaTypeDescription(const std::string &description);
HRESULT ChangeMediaType(IMFMediaType *pType);
void RequestMediaType(IMFMediaType *pType);
HRESULT StartCapture();
void UpdateDrawRect();
@@ -197,12 +212,22 @@ namespace games::iidx {
void CreateThread();
MediaTypeInfo GetMediaTypeInfo(IMFMediaType *pType);
std::string GetVideoFormatName(GUID subtype);
HRESULT TryMediaType(IMFMediaType *pType, UINT32 *pBestWidth, double *pBestFrameRate);
static bool CompareMediaTypes(const MediaTypeInfo &a, const MediaTypeInfo &b);
bool MatchesPreferredAspect(const MediaTypeInfo &info) const;
static bool IsBetterAutoType(const MediaTypeInfo &candidate, const MediaTypeInfo &current);
IMFMediaType *FindBestNativeAutoType(bool requirePreferredAspect) const;
IMFMediaType *FindBestAutoType(const GUID &subtype, bool requirePreferredAspect) const;
HRESULT ValidateMediaType(IMFMediaType *pType);
HRESULT InitTargetTexture();
HRESULT EnsureTransformTextures();
HRESULT InitCameraControl();
void SetSelectedMediaType(int index, const std::string &description);
bool HasPendingMediaType();
HRESULT ApplyPendingMediaType();
HRESULT UploadDecodedSample(IMFMediaBuffer *pSrcBuffer);
HRESULT DrawSample(IMFMediaBuffer *pSrcBuffer);
HRESULT ReadSample();
LPDIRECT3DTEXTURE9 Render();
HRESULT Render();
};
}
+131 -115
View File
@@ -1,116 +1,132 @@
#include "mf_wrappers.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace games::iidx {
static bool INITIALIZED = false;
static HMODULE mf_dll = nullptr;
static HMODULE mfreadwrite_dll = nullptr;
static HMODULE mfplat_dll = nullptr;
typedef HRESULT (__stdcall * MFCreateAttributes_t)(
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize
);
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate
);
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader
);
typedef HRESULT (__stdcall * MFGetService_t)(
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject
);
static MFCreateAttributes_t MFCreateAttributes = nullptr;
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
static MFGetService_t MFGetService = nullptr;
void init_mf_library() {
// why was all of this needed?
//
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
//
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
if (INITIALIZED) {
return;
}
INITIALIZED = true;
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
mf_dll = libutils::load_library("mf.dll", true);
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
mfplat_dll = libutils::load_library("mfplat.dll", true);
MFCreateAttributes = (MFCreateAttributes_t)
libutils::get_proc(mfplat_dll, "MFCreateAttributes");
if (!MFCreateAttributes) {
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
}
MFEnumDeviceSources = (MFEnumDeviceSources_t)
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
if (!MFEnumDeviceSources) {
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
}
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
if (!MFCreateSourceReaderFromMediaSource) {
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
}
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
if (!MFGetService) {
log_fatal("mf_wrappers", "MFGetService failed to hook");
}
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
}
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize) {
return MFCreateAttributes(ppMFAttributes, cInitialSize);
}
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate) {
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
}
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader) {
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
}
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject) {
return MFGetService(punkObject, guidService, riid, ppvObject);
}
#include "mf_wrappers.h"
#include "util/libutils.h"
#include "util/logging.h"
namespace games::iidx {
static bool INITIALIZED = false;
static HMODULE mf_dll = nullptr;
static HMODULE mfreadwrite_dll = nullptr;
static HMODULE mfplat_dll = nullptr;
typedef HRESULT (__stdcall * MFCreateAttributes_t)(
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize
);
typedef HRESULT (__stdcall * MFCreateMediaType_t)(
_Out_ IMFMediaType** ppMFType
);
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate
);
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader
);
typedef HRESULT (__stdcall * MFGetService_t)(
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject
);
static MFCreateAttributes_t MFCreateAttributes = nullptr;
static MFCreateMediaType_t MFCreateMediaType = nullptr;
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
static MFGetService_t MFGetService = nullptr;
void init_mf_library() {
// why was all of this needed?
//
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
//
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
if (INITIALIZED) {
return;
}
INITIALIZED = true;
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
mf_dll = libutils::load_library("mf.dll", true);
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
mfplat_dll = libutils::load_library("mfplat.dll", true);
MFCreateAttributes = (MFCreateAttributes_t)
libutils::get_proc(mfplat_dll, "MFCreateAttributes");
if (!MFCreateAttributes) {
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
}
MFCreateMediaType = (MFCreateMediaType_t)
libutils::get_proc(mfplat_dll, "MFCreateMediaType");
if (!MFCreateMediaType) {
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
}
MFEnumDeviceSources = (MFEnumDeviceSources_t)
libutils::get_proc(mf_dll, "MFEnumDeviceSources");
if (!MFEnumDeviceSources) {
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
}
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
if (!MFCreateSourceReaderFromMediaSource) {
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
}
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
if (!MFGetService) {
log_fatal("mf_wrappers", "MFGetService failed to hook");
}
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
}
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize) {
return MFCreateAttributes(ppMFAttributes, cInitialSize);
}
HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType) {
return MFCreateMediaType(ppMFType);
}
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate) {
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
}
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader) {
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
}
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject) {
return MFGetService(punkObject, guidService, riid, ppvObject);
}
}
+32 -29
View File
@@ -1,30 +1,33 @@
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
#pragma once
namespace games::iidx {
void init_mf_library();
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize);
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate);
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader);
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject);
#include <mfapi.h>
#include <mfidl.h>
#include <mfreadwrite.h>
#include <mfobjects.h>
#pragma once
namespace games::iidx {
void init_mf_library();
HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize);
HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType);
HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate);
HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader);
HRESULT WrappedMFGetService (
IUnknown* punkObject,
REFGUID guidService,
REFIID riid,
_Outptr_ LPVOID* ppvObject);
}
+26 -147
View File
@@ -1,31 +1,17 @@
#include "poke.h"
#include <chrono>
#include <thread>
#include "windows.h"
#include "cfg/screen_resize.h"
#include "games/io.h"
#include "games/iidx/iidx.h"
#include "hooks/graphics/graphics.h"
#include "launcher/shutdown.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "overlay/windows/generic_sub.h"
#include "rawinput/rawinput.h"
#include "touch/native/inject.h"
#include "touch/touch.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/precise_timer.h"
#define POKE_NATIVE_TOUCH 0
#if POKE_NATIVE_TOUCH
static HINSTANCE USER32_INSTANCE = nullptr;
typedef BOOL (WINAPI *InitializeTouchInjection_t)(UINT32, DWORD);
typedef BOOL (WINAPI *InjectTouchInput_t)(UINT32, POINTER_TOUCH_INFO*);
static InitializeTouchInjection_t pInitializeTouchInjection = nullptr;
static InjectTouchInput_t pInjectTouchInput = nullptr;
#endif
namespace games::iidx::poke {
@@ -116,93 +102,29 @@ namespace games::iidx::poke {
{"1/D", 50},
};
#if POKE_NATIVE_TOUCH
void initialize_native_touch_library() {
if (USER32_INSTANCE == nullptr) {
USER32_INSTANCE = libutils::load_library("user32.dll");
}
pInitializeTouchInjection = libutils::try_proc<InitializeTouchInjection_t>(
USER32_INSTANCE, "InitializeTouchInjection");
pInjectTouchInput = libutils::try_proc<InjectTouchInput_t>(
USER32_INSTANCE, "InjectTouchInput");
}
void emulate_native_touch(TouchPoint tp, bool is_down) {
if (pInjectTouchInput == nullptr) {
static void inject_native_touch_points(const std::vector<TouchPoint> &touch_points, bool down) {
if (touch_points.empty()) {
return;
}
POINTER_TOUCH_INFO contact;
BOOL bRet = TRUE;
const int contact_offset = 2;
memset(&contact, 0, sizeof(POINTER_TOUCH_INFO));
contact.pointerInfo.pointerType = PT_TOUCH;
contact.pointerInfo.pointerId = 0;
contact.pointerInfo.ptPixelLocation.x = tp.x;
contact.pointerInfo.ptPixelLocation.y = tp.y;
if (is_down) {
contact.pointerInfo.pointerFlags = POINTER_FLAG_DOWN | POINTER_FLAG_INRANGE | POINTER_FLAG_INCONTACT;
} else {
contact.pointerInfo.pointerFlags = POINTER_FLAG_UP;
}
contact.pointerInfo.dwTime = 0;
contact.pointerInfo.PerformanceCount = 0;
contact.touchFlags = TOUCH_FLAG_NONE;
contact.touchMask = TOUCH_MASK_CONTACTAREA | TOUCH_MASK_ORIENTATION | TOUCH_MASK_PRESSURE;
contact.orientation = 0;
contact.pressure = 1024;
contact.rcContact.top = tp.y - contact_offset;
contact.rcContact.bottom = tp.y + contact_offset;
contact.rcContact.left = tp.x - contact_offset;
contact.rcContact.right = tp.x + contact_offset;
bRet = InjectTouchInput(1, &contact);
if (!bRet) {
log_warning("poke", "error injecting native touch {}: ({}, {}) error: {}", is_down ? "down" : "up", tp.x, tp.y, GetLastError());
}
const auto &touch = touch_points.front();
nativetouch::inject::inject_synthetic_touch({ touch.x, touch.y }, down);
}
void emulate_native_touch_points(std::vector<TouchPoint> *touch_points) {
int i = 0;
for (auto &touch : *touch_points) {
emulate_native_touch(touch, true);
}
}
void clear_native_touch_points(std::vector<TouchPoint> *touch_points) {
for (auto &touch : *touch_points) {
emulate_native_touch(touch, false);
}
touch_points->clear();
}
#endif
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
}
void enable() {
// check if already running
if (THREAD)
return;
if (!games::iidx::NATIVE_TOUCH) {
log_warning("poke", "keypad touch emulation requires native touch; disabled");
return;
}
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
// log
log_info("poke", "enabled");
@@ -210,35 +132,13 @@ namespace games::iidx::poke {
std::vector<TouchPoint> touch_points;
std::vector<uint16_t> last_keypad_state = {0, 0};
#if POKE_NATIVE_TOUCH
bool use_native = games::iidx::NATIVE_TOUCH;
if (use_native) {
initialize_native_touch_library();
if (pInitializeTouchInjection != nullptr) {
pInitializeTouchInjection(1, TOUCH_FEEDBACK_NONE);
}
}
#else
bool use_native = false;
#endif
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
clear_native_touch_points(&touch_points);
} else {
clear_touch_points(&touch_points);
}
#else
clear_touch_points(&touch_points);
#endif
inject_native_touch_points(touch_points, false);
touch_points.clear();
}
for (int unit = 0; unit < 2; unit++) {
@@ -263,32 +163,15 @@ namespace games::iidx::poke {
float x = x_iter->second / 1920.0;
float y = y_iter->second / 1080.0;
if (use_native) {
x *= rawinput::TOUCHSCREEN_RANGE_X;
y *= rawinput::TOUCHSCREEN_RANGE_Y;
} else if (GRAPHICS_IIDX_WSUB) {
// Scale to windowed subscreen
x *= GRAPHICS_WSUB_WIDTH;
y *= GRAPHICS_WSUB_HEIGHT;
} else if (GENERIC_SUB_WINDOW_FULLSIZE || !overlay::OVERLAY->get_active()) {
// Overlay is not present, scale to main screen
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH;
y *= SPICETOUCH_TOUCH_HEIGHT;
} else {
x *= ImGui::GetIO().DisplaySize.x;
y *= ImGui::GetIO().DisplaySize.y;
if (GRAPHICS_WINDOWED) {
if (SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
continue;
}
x = SPICETOUCH_TOUCH_X + x * SPICETOUCH_TOUCH_WIDTH;
y = SPICETOUCH_TOUCH_Y + y * SPICETOUCH_TOUCH_HEIGHT;
} else {
// Overlay subscreen
x = (GENERIC_SUB_WINDOW_X + x * GENERIC_SUB_WINDOW_WIDTH);
y = (GENERIC_SUB_WINDOW_Y + y * GENERIC_SUB_WINDOW_HEIGHT);
// Scale to window size ratio
if (GRAPHICS_WINDOWED) {
x *= SPICETOUCH_TOUCH_WIDTH / ImGui::GetIO().DisplaySize.x;
y *= SPICETOUCH_TOUCH_HEIGHT / ImGui::GetIO().DisplaySize.y;
}
x = x_iter->second;
y = y_iter->second;
}
TouchPoint tp {
@@ -309,19 +192,15 @@ namespace games::iidx::poke {
} // for all units
if (touch_points.size() > 0) {
#if POKE_NATIVE_TOUCH
if (use_native) {
emulate_native_touch_points(&touch_points);
} else {
touch_write_points(&touch_points);
}
#else
touch_write_points(&touch_points);
#endif
inject_native_touch_points(touch_points, true);
}
// slow down
timer.sleep(50);
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
if (!touch_points.empty()) {
inject_native_touch_points(touch_points, false);
}
return nullptr;
+35
View File
@@ -44,6 +44,7 @@ namespace games {
static bool IO_INITIALIZED = false;
static std::vector<std::string> games;
static robin_hood::unordered_map<std::string, std::vector<Button> &> buttons;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_modifiers;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_keypads;
static robin_hood::unordered_map<std::string, std::vector<Button>> buttons_overlay;
static robin_hood::unordered_map<std::string, std::string> buttons_help;
@@ -132,6 +133,7 @@ namespace games {
buttons_help.insert({ jb, jb::get_buttons_help() });
lights.insert({ jb, jb::get_lights() });
file_hints[jb].push_back({"jubeat.dll"});
file_hints[jb].push_back({"jubeat2019.dll"});
// mga
const std::string mga("Metal Gear");
@@ -365,6 +367,37 @@ namespace games {
return it->second;
}
static std::vector<Button> gen_buttons_modifiers(const std::string &game) {
auto modifier_buttons = GameAPI::Buttons::getButtons(game);
const std::vector<std::string> names {
"Modifier 1",
"Modifier 2",
"Modifier 3",
"Modifier 4",
};
modifier_buttons = GameAPI::Buttons::sortButtons(modifier_buttons, names);
for (auto &modifier : modifier_buttons) {
modifier.setModifierMask(0);
for (auto &alternative : modifier.getAlternatives()) {
alternative.setModifierMask(0);
}
}
return modifier_buttons;
}
std::vector<Button> *get_buttons_modifiers(const std::string &game) {
initialize();
auto it = buttons_modifiers.find(game);
if (it == buttons_modifiers.end()) {
if (game.empty()) {
return nullptr;
}
buttons_modifiers[game] = gen_buttons_modifiers(game);
return &buttons_modifiers[game];
}
return &it->second;
}
static std::vector<Button> gen_buttons_keypads(const std::string &game) {
auto buttons = GameAPI::Buttons::getButtons(game);
std::vector<std::string> names;
@@ -474,6 +507,8 @@ namespace games {
vkey_defaults.push_back(VK_F12);
names.emplace_back("Toggle Camera Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Toggle OBS Control");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 1 PIN Macro");
vkey_defaults.push_back(0xFF);
names.emplace_back("Player 2 PIN Macro");
+12
View File
@@ -5,6 +5,16 @@
namespace games {
namespace ModifierButtons {
enum {
Modifier1,
Modifier2,
Modifier3,
Modifier4,
Size,
};
}
namespace OverlayButtons {
enum {
Screenshot,
@@ -23,6 +33,7 @@ namespace games {
ToggleScreenResize,
ToggleFps,
ToggleCameraControl,
ToggleOBSControl,
TriggerPinMacroP1,
TriggerPinMacroP2,
ScreenResize,
@@ -58,6 +69,7 @@ namespace games {
const std::vector<std::string> &get_games();
std::vector<Button> *get_buttons(const std::string &game);
std::vector<Button> *get_buttons_modifiers(const std::string &game);
std::string get_buttons_help(const std::string &game);
std::string get_analogs_help(const std::string &game);
std::vector<Button> *get_buttons_keypads(const std::string &game);
+336
View File
@@ -0,0 +1,336 @@
#include "bi2x_hook.h"
#if SPICE64
#include <array>
#include <cstdint>
#include <cstring>
#include "games/io.h"
#include "games/jb/jb_touch.h"
#include "io.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/detour.h"
#include "util/logging.h"
namespace games::jb {
struct AIO_SCI_COMM {
std::array<uint8_t, 0x100> data;
};
struct AIO_NMGR_IOB2;
struct AIO_NMGR_IOB2_VTABLE {
std::array<void *, 10> unused;
void (__fastcall *begin_manage)(AIO_NMGR_IOB2 *node_mgr);
};
struct AIO_NMGR_IOB2 {
AIO_NMGR_IOB2_VTABLE *vtable;
std::array<uint8_t, 0x78> data;
};
struct AIO_IOB2_BI2X_T44 {
std::array<uint8_t, 0x80> data;
};
struct AIO_IOB2_BI2X_T44_DEVSTATUS {
std::array<uint8_t, 0x140> data;
};
struct AIO_IOB2_BI2X_WRFIRM {
uint8_t data;
};
static_assert(sizeof(AIO_NMGR_IOB2) == 0x80);
static_assert(sizeof(AIO_IOB2_BI2X_T44_DEVSTATUS) == 0x140);
using aioIob2Bi2xT44_Create_t = AIO_IOB2_BI2X_T44 *(__fastcall *)(AIO_NMGR_IOB2 *node_mgr,
uint32_t device_id);
using aioIob2Bi2xT44_GetDeviceStatus_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
AIO_IOB2_BI2X_T44_DEVSTATUS *status);
using aioIob2Bi2xT44_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t reset);
using aioIob2Bi2xT44_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint8_t count);
using aioIob2Bi2xT44_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t slot, bool open);
using aioIob2Bi2xT44_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t counter, uint32_t count);
using aioIob2Bi2xT44_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node, uint32_t color);
using aioIob2Bi2xT44_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node,
uint32_t tape, const void *data);
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t serial_number);
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(
uint32_t serial_number, uint32_t iob_mask);
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *context);
using aioIob2Bi2x_WriteFirmIsCompleted_t = bool (__fastcall *)(int32_t state);
using aioIob2Bi2x_WriteFirmIsError_t = bool (__fastcall *)(int32_t state);
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *sci, uint32_t mode);
using aioSci_Destroy_t = void (__fastcall *)(AIO_SCI_COMM *sci);
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *node_mgr);
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_T44 *node);
using aioNodeCtl_UpdateDevicesStatus_t = void (__fastcall *)();
static aioIob2Bi2xT44_Create_t aioIob2Bi2xT44_Create_orig = nullptr;
static aioIob2Bi2xT44_GetDeviceStatus_t aioIob2Bi2xT44_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xT44_IoReset_t aioIob2Bi2xT44_IoReset_orig = nullptr;
static aioIob2Bi2xT44_SetWatchDogTimer_t aioIob2Bi2xT44_SetWatchDogTimer_orig = nullptr;
static aioIob2Bi2xT44_ControlCoinBlocker_t aioIob2Bi2xT44_ControlCoinBlocker_orig = nullptr;
static aioIob2Bi2xT44_AddCounter_t aioIob2Bi2xT44_AddCounter_orig = nullptr;
static aioIob2Bi2xT44_SetIccrLed_t aioIob2Bi2xT44_SetIccrLed_orig = nullptr;
static aioIob2Bi2xT44_SetTapeLedData_t aioIob2Bi2xT44_SetTapeLedData_orig = nullptr;
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
static aioIob2Bi2x_WriteFirmIsCompleted_t aioIob2Bi2x_WriteFirmIsCompleted_orig = nullptr;
static aioIob2Bi2x_WriteFirmIsError_t aioIob2Bi2x_WriteFirmIsError_orig = nullptr;
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
static aioSci_Destroy_t aioSci_Destroy_orig = nullptr;
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
static aioNodeCtl_UpdateDevicesStatus_t aioNodeCtl_UpdateDevicesStatus_orig = nullptr;
static AIO_SCI_COMM *aio_sci_comm = nullptr;
static AIO_NMGR_IOB2 *aio_node_mgr = nullptr;
static AIO_IOB2_BI2X_T44 *aio_t44 = nullptr;
static AIO_IOB2_BI2X_WRFIRM *aio_write_firm = nullptr;
static uint8_t input_counter = 0;
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *) {
}
static AIO_NMGR_IOB2_VTABLE aio_node_mgr_vtable {
{},
aioNMgrIob_BeginManage,
};
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t) {
aio_sci_comm = new AIO_SCI_COMM {};
return aio_sci_comm;
}
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
if (sci != aio_sci_comm) {
return aioNMgrIob2_Create_orig(sci, mode);
}
aio_node_mgr = new AIO_NMGR_IOB2 {};
aio_node_mgr->vtable = &aio_node_mgr_vtable;
return aio_node_mgr;
}
static AIO_IOB2_BI2X_T44 *__fastcall aioIob2Bi2xT44_Create(
AIO_NMGR_IOB2 *node_mgr, uint32_t device_id) {
if (node_mgr != aio_node_mgr) {
return aioIob2Bi2xT44_Create_orig(node_mgr, device_id);
}
log_info("jb::bi2x", "T44 node created");
aio_t44 = new AIO_IOB2_BI2X_T44 {};
return aio_t44;
}
static void __fastcall aioIob2Bi2xT44_GetDeviceStatus(
AIO_IOB2_BI2X_T44 *node, AIO_IOB2_BI2X_T44_DEVSTATUS *status) {
if (node != aio_t44) {
return aioIob2Bi2xT44_GetDeviceStatus_orig(node, status);
}
RI_MGR->devices_flush_output();
std::memset(status, 0, sizeof(*status));
status->data[0x00] = input_counter;
status->data[0x03] = input_counter++;
status->data[0x0A] = static_cast<uint8_t>(eamuse_coin_get_stock());
games::jb::touch_update();
const auto touched = games::jb::touch_state();
auto &buttons = get_buttons();
status->data[0x04] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 0xFF : 0;
status->data[0x05] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 0xFF : 0;
status->data[0x06] = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 0xFF : 0;
static constexpr size_t PANEL_ORDER[16] = {
3, 7, 11, 15,
2, 6, 10, 14,
1, 5, 9, 13,
0, 4, 8, 12,
};
for (size_t status_index = 0; status_index < std::size(PANEL_ORDER); status_index++) {
const auto panel_index = PANEL_ORDER[status_index];
if (touched[panel_index] || GameAPI::Buttons::getState(
RI_MGR, buttons[Buttons::Button1 + panel_index])) {
status->data[0x0F + status_index] = 0xFF;
}
}
}
static void __fastcall aioIob2Bi2xT44_IoReset(AIO_IOB2_BI2X_T44 *node, uint32_t reset) {
if (node != aio_t44) {
return aioIob2Bi2xT44_IoReset_orig(node, reset);
}
}
static void __fastcall aioIob2Bi2xT44_SetWatchDogTimer(AIO_IOB2_BI2X_T44 *node, uint8_t count) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetWatchDogTimer_orig(node, count);
}
}
static void __fastcall aioIob2Bi2xT44_ControlCoinBlocker(
AIO_IOB2_BI2X_T44 *node, uint32_t slot, bool open) {
if (node != aio_t44) {
return aioIob2Bi2xT44_ControlCoinBlocker_orig(node, slot, open);
}
eamuse_coin_set_block(!open);
}
static void __fastcall aioIob2Bi2xT44_AddCounter(
AIO_IOB2_BI2X_T44 *node, uint32_t counter, uint32_t count) {
if (node != aio_t44) {
return aioIob2Bi2xT44_AddCounter_orig(node, counter, count);
}
}
static void __fastcall aioIob2Bi2xT44_SetIccrLed(AIO_IOB2_BI2X_T44 *node, uint32_t color) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetIccrLed_orig(node, color);
}
}
static void __fastcall aioIob2Bi2xT44_SetTapeLedData(
AIO_IOB2_BI2X_T44 *node, uint32_t tape, const void *data) {
if (node != aio_t44) {
return aioIob2Bi2xT44_SetTapeLedData_orig(node, tape, data);
}
}
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(uint32_t, uint32_t) {
aio_write_firm = new AIO_IOB2_BI2X_WRFIRM {};
return aio_write_firm;
}
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm) {
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
}
delete aio_write_firm;
aio_write_firm = nullptr;
}
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm) {
return aioIob2Bi2x_WriteFirmGetState_orig(context);
}
return 8;
}
static bool __fastcall aioIob2Bi2x_WriteFirmIsCompleted(int32_t state) {
if (aio_write_firm) {
return true;
}
return aioIob2Bi2x_WriteFirmIsCompleted_orig(state);
}
static bool __fastcall aioIob2Bi2x_WriteFirmIsError(int32_t state) {
if (aio_write_firm) {
return false;
}
return aioIob2Bi2x_WriteFirmIsError_orig(state);
}
static void __fastcall aioSci_Destroy(AIO_SCI_COMM *sci) {
if (sci != aio_sci_comm) {
return aioSci_Destroy_orig(sci);
}
delete aio_sci_comm;
aio_sci_comm = nullptr;
}
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_mgr) {
if (node_mgr != aio_node_mgr) {
return aioNodeMgr_Destroy_orig(node_mgr);
}
delete aio_node_mgr;
aio_node_mgr = nullptr;
}
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_T44 *node) {
if (node != aio_t44) {
return aioNodeCtl_Destroy_orig(node);
}
delete aio_t44;
aio_t44 = nullptr;
}
static void __fastcall aioNodeCtl_UpdateDevicesStatus() {
}
void bi2x_hook_init() {
static bool initialized = false;
if (initialized) {
return;
}
initialized = true;
log_info("jb::bi2x", "initializing T44 hooks");
const auto libaio_iob2_video = "libaio-iob2_video.dll";
detour::trampoline_try(libaio_iob2_video, "aioIob2Bi2xT44_Create",
aioIob2Bi2xT44_Create, &aioIob2Bi2xT44_Create_orig);
detour::trampoline_try(libaio_iob2_video,
"?GetDeviceStatus@AIO_IOB2_BI2X_T44@@QEBAXAEAUDEVSTATUS@1@@Z",
aioIob2Bi2xT44_GetDeviceStatus, &aioIob2Bi2xT44_GetDeviceStatus_orig);
detour::trampoline_try(libaio_iob2_video, "?IoReset@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
aioIob2Bi2xT44_IoReset, &aioIob2Bi2xT44_IoReset_orig);
detour::trampoline_try(libaio_iob2_video,
"?SetWatchDogTimer@AIO_IOB2_BI2X_T44@@QEAAXE@Z",
aioIob2Bi2xT44_SetWatchDogTimer, &aioIob2Bi2xT44_SetWatchDogTimer_orig);
detour::trampoline_try(libaio_iob2_video,
"?ControlCoinBlocker@AIO_IOB2_BI2X_T44@@QEAAXI_N@Z",
aioIob2Bi2xT44_ControlCoinBlocker, &aioIob2Bi2xT44_ControlCoinBlocker_orig);
detour::trampoline_try(libaio_iob2_video, "?AddCounter@AIO_IOB2_BI2X_T44@@QEAAXII@Z",
aioIob2Bi2xT44_AddCounter, &aioIob2Bi2xT44_AddCounter_orig);
detour::trampoline_try(libaio_iob2_video, "?SetIccrLed@AIO_IOB2_BI2X_T44@@QEAAXI@Z",
aioIob2Bi2xT44_SetIccrLed, &aioIob2Bi2xT44_SetIccrLed_orig);
detour::trampoline_try(libaio_iob2_video,
"?SetTapeLedData@AIO_IOB2_BI2X_T44@@QEAAXIPEBX@Z",
aioIob2Bi2xT44_SetTapeLedData, &aioIob2Bi2xT44_SetTapeLedData_orig);
const auto libaio_iob = "libaio-iob.dll";
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_OpenSciUsbCdc",
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_CreateWriteFirmContext",
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_DestroyWriteFirmContext",
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmGetState",
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsCompleted",
aioIob2Bi2x_WriteFirmIsCompleted, &aioIob2Bi2x_WriteFirmIsCompleted_orig);
detour::trampoline_try(libaio_iob, "aioIob2Bi2x_WriteFirmIsError",
aioIob2Bi2x_WriteFirmIsError, &aioIob2Bi2x_WriteFirmIsError_orig);
detour::trampoline_try(libaio_iob, "aioNMgrIob2_Create",
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
const auto libaio = "libaio.dll";
detour::trampoline_try(libaio, "aioSci_Destroy", aioSci_Destroy, &aioSci_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeMgr_Destroy",
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeCtl_Destroy",
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
detour::trampoline_try(libaio, "aioNodeCtl_UpdateDevicesStatus",
aioNodeCtl_UpdateDevicesStatus, &aioNodeCtl_UpdateDevicesStatus_orig);
}
}
#endif
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#if SPICE64
namespace games::jb {
void bi2x_hook_init();
}
#endif
+20 -207
View File
@@ -4,211 +4,27 @@
#include <filesystem>
#include "avs/game.h"
#include "bi2x_hook.h"
#include "cfg/configurator.h"
#include "hooks/graphics/graphics.h"
#include "touch/touch.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/detour.h"
#include "util/libutils.h"
#define JB_BUTTON_SIZE 160
#define JB_BUTTON_GAP 37
#define JB_BUTTON_HITBOX (JB_BUTTON_SIZE + JB_BUTTON_GAP)
namespace games::jb {
// touch stuff
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
static bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static std::vector<TouchPoint> TOUCH_POINTS;
bool TOUCH_STATE[16];
#if !SPICE64
void touch_update() {
// check if touch enabled
if (!TOUCH_ENABLE) {
return;
}
// attach touch module
if (!TOUCH_ATTACHED) {
/*
* Find the game window.
* We check the foreground window first, then fall back to searching for the window title
* All game versions seem to have their model first in the window title
*/
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), avs::game::MODEL)) {
wnd = FindWindowBeginsWith(avs::game::MODEL);
}
// check if we have a window handle
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE = false;
return;
}
// attach touch hook
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
touch_create_wnd(wnd, true);
// show cursor
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
// earlier games use a different screen orientation
if (!avs::game::is_model("L44")) {
IS_PORTRAIT = false;
}
// set attached
TOUCH_ATTACHED = true;
}
// reset touch state
memset(TOUCH_STATE, 0, sizeof(TOUCH_STATE));
// check touch points
// note that the IO code in device.cpp will correctly compensate for orientation, depending on the model.
TOUCH_POINTS.clear();
touch_get_points(TOUCH_POINTS);
if (TOUCH_ALGORITHM == Legacy) {
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : TOUCH_POINTS) {
// get grid coordinates
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
// set the corresponding state
int index = y * 4 + x;
if (index >= 0 && index < 16) {
TOUCH_STATE[index] = true;
}
}
} else {
for (auto &tp : TOUCH_POINTS) {
// check window out of bounds
if (IS_PORTRAIT) {
if (tp.x > 768 || tp.y > 1360) {
continue;
}
} else {
if (tp.x > 1360 || tp.y > 768) {
continue;
}
}
int x_relative = tp.x;
int y_relative = tp.y;
// x_relative and y_relative are relative to the top-left pixel of the first button
if (IS_PORTRAIT) {
// which is at (8, 602) in portrait:
// X: [8...759] (752 pixels wide)
// Y: [602...1353] (752 pixels high)
x_relative -= 8;
y_relative -= 602;
} else {
// and at (8, 8) in landscape
x_relative -= 8;
y_relative -= 8;
}
int x_index = -1;
int x_hitbox = 0;
int y_index = -1;
int y_hitbox = 0;
// x_hitbox and y_hitbox is relative to top-left pixel of each button
if (x_relative >= 0) {
x_index = x_relative / JB_BUTTON_HITBOX;
x_hitbox = x_relative % JB_BUTTON_HITBOX;
}
if (y_relative >= 0) {
y_index = y_relative / JB_BUTTON_HITBOX;
y_hitbox = y_relative % JB_BUTTON_HITBOX;
}
// log_info("jb", "raw={}, idx={}, hitbox={}", tp.x, x_index, x_hitbox);
if (TOUCH_ALGORITHM == Improved) {
if (IS_PORTRAIT) {
// extend to left border
if (x_relative < 0) {
x_index = 0;
}
// right and bottom borders are covered by the hit box
} else {
// extend to top border
if (y_relative < 0) {
y_index = 0;
}
// extend to left border
if (x_relative < 0) {
x_index = 0;
}
// bottom border is covered by the hit box
// rightmost edge - ignore them entirely
if (x_index == 3 && JB_BUTTON_SIZE < x_hitbox) {
continue;
}
}
}
if (x_index < 0 || y_index < 0 || x_index > 3 || y_index > 3) {
continue;
}
// check if the gap was touched
if (TOUCH_ALGORITHM == AcAccurate) {
// in ac-accurate mode, touching the gap is ignored
if (x_hitbox > JB_BUTTON_SIZE || y_hitbox > JB_BUTTON_SIZE) {
continue;
}
} else if (TOUCH_ALGORITHM == Improved) {
// in improved mode, touching the gap triggers the closest button
if (x_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < x_hitbox) {
x_index++;
}
if (y_index <= 2 && (JB_BUTTON_SIZE + JB_BUTTON_GAP / 2) < y_hitbox) {
y_index++;
}
}
// set the corresponding state
int index = y_index * 4 + x_index;
if (0 <= index && index < 16) {
TOUCH_STATE[index] = true;
}
}
}
}
/*
* to fix "IP ADDR CHANGE" errors on boot and in-game when using weird network setups such as a VPN
*/
// fixes "IP ADDR CHANGE" errors with unusual network setups (e.g. a VPN)
static BOOL __stdcall network_addr_is_changed() {
return 0;
}
/*
* to fix lag spikes when game tries to ping "eamuse.konami.fun" every few minutes
*/
// fixes lag spikes from the periodic ping to "eamuse.konami.fun"
static BOOL __stdcall network_get_network_check_info() {
return 0;
}
/*
* to fix network error on non DHCP interfaces
*/
// fixes network errors on non-DHCP interfaces
static BOOL __cdecl network_get_dhcp_result() {
return 1;
}
@@ -219,6 +35,8 @@ namespace games::jb {
return 0;
}
#endif
JBGame::JBGame() : Game("Jubeat") {
}
@@ -248,24 +66,18 @@ namespace games::jb {
void JBGame::attach() {
Game::attach();
// enable touch
TOUCH_ENABLE = true;
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo detected in touch_update, this is a bug");
break;
#if SPICE64
if (avs::game::DLL_NAME == "jubeat2019.dll") {
libutils::load_library("libaio.dll");
libutils::load_library("libaio-iob.dll");
libutils::load_library("libaio-iob2_video.dll");
bi2x_hook_init();
}
#endif
touch_attach();
#if !SPICE64
// enable debug logging of gftools
HMODULE gftools = libutils::try_module("gftools.dll");
detour::inline_hook((void *) GFDbgSetReportFunc, libutils::try_proc(
@@ -279,12 +91,13 @@ namespace games::jb {
network, "network_get_network_check_info"));
detour::inline_hook((void *) network_get_dhcp_result, libutils::try_proc(
network, "network_get_dhcp_result"));
#endif
}
void JBGame::detach() {
Game::detach();
// disable touch
TOUCH_ENABLE = false;
touch_detach();
}
}
+1 -11
View File
@@ -1,20 +1,10 @@
#pragma once
#include "games/game.h"
#include "games/jb/jb_touch.h"
namespace games::jb {
enum JubeatTouchAlgorithm {
Legacy,
Improved,
AcAccurate
};
// touch stuff
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern bool TOUCH_STATE[16];
void touch_update();
class JBGame : public games::Game {
public:
JBGame();
+485
View File
@@ -0,0 +1,485 @@
#include "jb_touch.h"
#include <windows.h>
#include <atomic>
#include <cmath>
#include <limits>
#include <vector>
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "touch/touch.h"
#include "rawinput/touch.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
// touch layout: a 4x4 grid of 160px buttons separated by 37 / 38 / 37 px gaps
// (752px across). the first button's top-left is at (8, 602) in portrait and
// (6, 8) in landscape.
#define JB_BUTTON_SIZE 160
#define JB_MAX_BUTTON_GAP 38
#define JB_T44_BUTTON_SIZE 224
#define JB_T44_BUTTON_GAP 33
// improved and plus modes use this reach around each button. must be >= the
// diagonal half of the widest gap (~27px) so the grid centre still reaches a button.
#define JB_TOUCH_RADIUS 38
namespace games::jb {
static_assert(std::atomic_bool::is_always_lock_free);
static_assert(std::atomic_uint16_t::is_always_lock_free);
static_assert(std::atomic<POINT>::is_always_lock_free);
static constexpr int JB_MAX_GAP_DISTANCE = (JB_MAX_BUTTON_GAP + 1) / 2;
static_assert(JB_TOUCH_RADIUS * JB_TOUCH_RADIUS >=
2 * JB_MAX_GAP_DISTANCE * JB_MAX_GAP_DISTANCE);
// touch state
JubeatTouchAlgorithm TOUCH_ALGORITHM = Improved;
JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY = JbTouchDebugAuto;
uint32_t TOUCH_DEBOUNCE_MS = 0;
static std::atomic_bool TOUCH_ENABLE = false;
static bool TOUCH_ATTACHED = false;
static bool IS_PORTRAIT = true;
static bool IS_T44 = false;
static std::atomic_uint16_t TOUCH_STATE = 0;
// fixed-size contact view used by the debug overlay
static const size_t JB_MAX_TOUCH_POINTS = 16;
static constexpr LONG JB_INVALID_TOUCH_COORD = std::numeric_limits<LONG>::max();
static constexpr POINT JB_INVALID_TOUCH_POINT {
JB_INVALID_TOUCH_COORD,
JB_INVALID_TOUCH_COORD
};
static std::atomic<POINT> TOUCH_POINTS[JB_MAX_TOUCH_POINTS] {};
static void clear_touch_points() {
for (auto &point : TOUCH_POINTS) {
point.store(JB_INVALID_TOUCH_POINT, std::memory_order_release);
}
}
// false when a contact is younger than the debounce window and should be ignored
static bool touch_matured(const TouchPoint &tp, double now_ms, double threshold_ms) {
return now_ms - tp.down_ms >= threshold_ms;
}
// snapshot for the debug overlay: each slot atomically holds one matured contact or
// the invalid sentinel. immature and absent contacts are published as invalid, so the
// overlay never shows a suppressed tap and a single atomic per slot cannot tear
static void publish_touch_points(const std::vector<TouchPoint> &touch_points,
double now_ms, double threshold_ms) {
size_t count = touch_points.size();
if (count > JB_MAX_TOUCH_POINTS) {
count = JB_MAX_TOUCH_POINTS;
}
for (size_t i = 0; i < JB_MAX_TOUCH_POINTS; i++) {
POINT point = JB_INVALID_TOUCH_POINT;
if (i < count && touch_matured(touch_points[i], now_ms, threshold_ms)) {
point = { touch_points[i].x, touch_points[i].y };
}
TOUCH_POINTS[i].store(point, std::memory_order_release);
}
}
// --- touch geometry ------------------------------------------------------
// gaps between the four buttons along one axis (the middle gap is 1px wider)
static const int JB_BUTTON_GAPS[3] = { 37, JB_MAX_BUTTON_GAP, 37 };
static const int JB_T44_BUTTON_GAPS[3] = {
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
JB_T44_BUTTON_GAP,
};
struct AxisGeometry {
int size;
int button[4]; // left/top edge of each button
};
// left/top edges of the four buttons along one axis, starting at `first`
static AxisGeometry axis_geometry(int first, int size, const int gaps[3]) {
AxisGeometry g {};
g.size = size;
g.button[0] = first;
for (int i = 1; i < 4; i++) {
g.button[i] = g.button[i - 1] + size + gaps[i - 1];
}
return g;
}
// button edges for the current orientation
static void touch_geometry(AxisGeometry &gx, AxisGeometry &gy) {
if (IS_T44) {
gx = axis_geometry(37, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
gy = axis_geometry(864, JB_T44_BUTTON_SIZE, JB_T44_BUTTON_GAPS);
} else if (IS_PORTRAIT) {
gx = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
gy = axis_geometry(602, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
} else {
gx = axis_geometry(6, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
gy = axis_geometry(8, JB_BUTTON_SIZE, JB_BUTTON_GAPS);
}
}
// distance from `p` to a button along one axis (0 when inside)
static int axis_distance(int p, int button, int size) {
int end = button + size - 1;
if (p < button) {
return button - p;
}
if (p > end) {
return p - end;
}
return 0;
}
// index (0..15) of the nearest button to (px, py) within `radius`, or -1 if none;
// shared by detection and the debug overlay so both agree which button a touch hits
static int nearest_button(
int px, int py, const AxisGeometry &gx, const AxisGeometry &gy, int radius) {
int best_index = -1;
int best_dist = 0;
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r], gy.size);
int dist = dx * dx + dy * dy;
if (dist <= radius * radius && (best_index < 0 || dist < best_dist)) {
best_dist = dist;
best_index = r * 4 + c;
}
}
}
return best_index;
}
// detection reach for the current algorithm (0 = register only inside a button)
static int touch_radius() {
return TOUCH_ALGORITHM == AcAccurate || (IS_T44 && TOUCH_ALGORITHM == Legacy) ?
0 : JB_TOUCH_RADIUS;
}
// mark the buttons a touch at (px, py) hits: only the nearest within `radius`, or
// every button within `radius` when `multi` (edge/gap presses trigger several)
static void mark_buttons(uint16_t &state, int px, int py,
const AxisGeometry &gx, const AxisGeometry &gy,
int radius, bool multi) {
if (!multi) {
int index = nearest_button(px, py, gx, gy, radius);
if (index >= 0) {
state |= uint16_t(1) << index;
}
return;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
int dx = axis_distance(px, gx.button[c], gx.size);
int dy = axis_distance(py, gy.button[r], gy.size);
if (dx * dx + dy * dy <= radius * radius) {
state |= uint16_t(1) << (r * 4 + c);
}
}
}
}
std::bitset<16> touch_state() {
return std::bitset<16>(TOUCH_STATE.load(std::memory_order_acquire));
}
void touch_update() {
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
// one-time touch window attach
if (!TOUCH_ATTACHED) {
IS_T44 = avs::game::is_model("T44");
IS_PORTRAIT = IS_T44 || avs::game::is_model("L44");
// find the game window: prefer the foreground window, else search by
// title (T44 uses a fixed title instead of the model prefix)
const char *window_title = IS_T44 ? "jubeat 10 main" : avs::game::MODEL;
HWND wnd = GetForegroundWindow();
if (!string_begins_with(GetActiveWindowTitle(), window_title)) {
wnd = FindWindowBeginsWith(window_title);
}
if (!wnd) {
log_warning("jubeat", "could not find window handle for touch");
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
return;
}
log_info("jubeat", "using window handle for touch: {}", fmt::ptr(wnd));
// let the rawinput stack correct the aspect ratio
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
touch_create_wnd(wnd, true);
if (GRAPHICS_SHOW_CURSOR) {
ShowCursor(TRUE);
}
TOUCH_ATTACHED = true;
}
// calculate the next state locally and publish it after processing every touch
uint16_t next_state = 0;
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
// debounce: the landing time is stamped by the touch layer, so this is
// independent of how often the game polls for input (0 = off)
double now_ms = get_performance_milliseconds();
double threshold_ms = TOUCH_DEBOUNCE_MS;
// publish every raw contact (with its maturity) for the debug overlay, then drop
// the ones still inside the debounce window so no algorithm sees them
publish_touch_points(touch_points, now_ms, threshold_ms);
std::erase_if(touch_points, [&](const TouchPoint &tp) {
return !touch_matured(tp, now_ms, threshold_ms);
});
if (TOUCH_ALGORITHM == Legacy && !IS_T44) {
// legacy: evenly divide the play area into a 4x4 grid
auto offset = IS_PORTRAIT ? 580 : 0;
for (auto &tp : touch_points) {
int x = tp.x * 4 / 768;
int y = (tp.y - offset) * 4 / (1360 - 580);
int index = y * 4 + x;
if (index >= 0 && index < 16) {
next_state |= uint16_t(1) << index;
}
}
} else {
// accurate registers only a touch inside a button; improved snaps each touch
// to the single nearest button within reach (so a gap or centre touch still
// triggers exactly one button); plus marks every button within the same reach,
// so an edge or gap touch can trigger several at once (like the mobile game)
AxisGeometry gx, gy;
touch_geometry(gx, gy);
int radius = touch_radius();
bool multi = (TOUCH_ALGORITHM == Plus);
for (auto &tp : touch_points) {
mark_buttons(next_state, tp.x, tp.y, gx, gy, radius, multi);
}
}
TOUCH_STATE.store(next_state, std::memory_order_release);
}
// true when any supported touch handler detects a touchscreen (checked once)
static bool touchscreen_detected() {
static const bool detected = is_touch_available("jubeat touch debug overlay");
return detected;
}
// auto draws the boxes only when a touch screen is present
static bool debug_show_boxes() {
switch (TOUCH_DEBUG_OVERLAY) {
case JbTouchDebugBox:
case JbTouchDebugAll:
return true;
case JbTouchDebugAuto:
return touchscreen_detected();
default:
return false;
}
}
static bool debug_show_taps() {
return TOUCH_DEBUG_OVERLAY == JbTouchDebugAll;
}
bool touch_debug_overlay_enabled() {
return TOUCH_ENABLE.load(std::memory_order_acquire) &&
(debug_show_boxes() || debug_show_taps());
}
// the 16 boundary rects: legacy divides the area evenly, others use button squares
static void debug_cells(
bool legacy, const AxisGeometry &gx, const AxisGeometry &gy, RECT cells[16]) {
if (legacy) {
int offset = IS_PORTRAIT ? 580 : 0;
int x_edges[5];
int y_edges[5];
for (int i = 0; i <= 4; i++) {
x_edges[i] = i * 768 / 4;
y_edges[i] = offset + i * (1360 - 580) / 4;
}
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = { x_edges[c], y_edges[r], x_edges[c + 1], y_edges[r + 1] };
}
}
} else {
for (int r = 0; r < 4; r++) {
for (int c = 0; c < 4; c++) {
cells[r * 4 + c] = {
gx.button[c], gy.button[r],
gx.button[c] + gx.size, gy.button[r] + gy.size
};
}
}
}
}
// hollow box outlines: grey idle, thick green when pressed (PS_INSIDEFRAME stays inside)
static void draw_debug_boxes(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
RECT cells[16];
debug_cells(legacy, gx, gy, cells);
HPEN pen_idle = CreatePen(PS_SOLID, 1, RGB(160, 160, 160));
HPEN pen_active = CreatePen(PS_INSIDEFRAME, 4, RGB(0, 200, 0));
auto state = touch_state();
for (int i = 0; i < 16; i++) {
HGDIOBJ old_pen = SelectObject(hdc, state[i] ? pen_active : pen_idle);
Rectangle(hdc, cells[i].left, cells[i].top, cells[i].right, cells[i].bottom);
SelectObject(hdc, old_pen);
}
DeleteObject(pen_idle);
DeleteObject(pen_active);
}
// whether a touch at (px, py) presses at least one button: legacy has no gaps, plus
// reaches across gaps to nearby buttons, and the others only fire inside a button square
static bool touch_presses_button(int px, int py, const AxisGeometry &gx, const AxisGeometry &gy,
bool legacy, int radius) {
if (legacy) {
return true;
}
int reach = (TOUCH_ALGORITHM == Plus) ? radius : 0;
return nearest_button(px, py, gx, gy, reach) >= 0;
}
// 90-degree arc on the circle rim facing the centre of button `index`
static void draw_tap_arc(HDC hdc, int px, int py, int index,
const AxisGeometry &gx, const AxisGeometry &gy, int arc_radius) {
int c = index % 4;
int r = index / 4;
double mid = std::atan2((gy.button[r] + gy.size / 2) - py,
(gx.button[c] + gx.size / 2) - px);
const double quarter = 3.14159265358979323846 / 2.0;
const int segments = 16;
POINT arc[segments + 1];
for (int i = 0; i <= segments; i++) {
double a = mid - quarter / 2.0 + quarter * i / segments;
arc[i].x = px + static_cast<LONG>(std::lround(arc_radius * std::cos(a)));
arc[i].y = py + static_cast<LONG>(std::lround(arc_radius * std::sin(a)));
}
Polyline(hdc, arc, segments + 1);
}
// circle at each touch: white when it presses a button, grey otherwise; in single-button
// modes a gap touch also gets a white arc facing the button it snapped to (skipped in
// plus, where a touch can trigger several buttons at once)
static void draw_debug_taps(
HDC hdc, bool legacy, const AxisGeometry &gx, const AxisGeometry &gy) {
// PS_INSIDEFRAME keeps the stroke inside the circle radius
const int stroke = 4;
HPEN pen_white = CreatePen(PS_INSIDEFRAME, stroke, RGB(255, 255, 255));
HPEN pen_gray = CreatePen(PS_INSIDEFRAME, stroke, RGB(128, 128, 128));
HGDIOBJ old_pen = SelectObject(hdc, pen_white);
// accurate has no reach, so fall back to a visible marker size when drawing the circle
int radius = touch_radius();
int draw_radius = radius > 0 ? radius : JB_TOUCH_RADIUS;
// each slot holds a matured contact or the invalid sentinel; immature and absent
// contacts were already filtered out by publish_touch_points
for (auto &touch_point : TOUCH_POINTS) {
POINT point = touch_point.load(std::memory_order_acquire);
if (point.x == JB_INVALID_TOUCH_COORD) {
continue;
}
bool pressed = touch_presses_button(point.x, point.y, gx, gy, legacy, radius);
SelectObject(hdc, pressed ? pen_white : pen_gray);
Ellipse(hdc, point.x - draw_radius, point.y - draw_radius,
point.x + draw_radius, point.y + draw_radius);
// the arc points at a single snapped-to button; plus can trigger several at
// once, so skip it there
if (pressed || TOUCH_ALGORITHM == Plus) {
continue;
}
int index = nearest_button(point.x, point.y, gx, gy, radius);
if (index >= 0) {
SelectObject(hdc, pen_white);
draw_tap_arc(hdc, point.x, point.y, index, gx, gy, draw_radius - stroke / 2);
}
}
SelectObject(hdc, old_pen);
DeleteObject(pen_white);
DeleteObject(pen_gray);
}
void touch_draw_debug_overlay(HDC hdc) {
// only draw while touch is active
if (!TOUCH_ENABLE.load(std::memory_order_acquire)) {
return;
}
bool show_boxes = debug_show_boxes();
bool show_taps = debug_show_taps();
if (!show_boxes && !show_taps) {
return;
}
// legacy divides the field evenly; the other algorithms use button squares
bool legacy = TOUCH_ALGORITHM == Legacy && !IS_T44;
AxisGeometry gx {}, gy {};
if (!legacy) {
touch_geometry(gx, gy);
}
HGDIOBJ old_brush = SelectObject(hdc, GetStockObject(NULL_BRUSH));
if (show_boxes) {
draw_debug_boxes(hdc, legacy, gx, gy);
}
if (show_taps) {
draw_debug_taps(hdc, legacy, gx, gy);
}
SelectObject(hdc, old_brush);
}
void touch_attach() {
clear_touch_points();
TOUCH_ENABLE.store(true, std::memory_order_release);
switch (TOUCH_ALGORITHM) {
case Legacy:
log_info("jubeat", "using 'legacy' touch targets");
break;
case Improved:
log_info("jubeat", "using 'improved' touch targets");
break;
case Plus:
log_info("jubeat", "using 'plus' touch targets");
break;
case AcAccurate:
log_info("jubeat", "using 'ac accurate' touch targets");
break;
default:
log_fatal("jubeat", "unknown touch algo, this is a bug");
break;
}
}
void touch_detach() {
TOUCH_ENABLE.store(false, std::memory_order_release);
TOUCH_STATE.store(0, std::memory_order_release);
}
}
+49
View File
@@ -0,0 +1,49 @@
#pragma once
#include <bitset>
#include <cstdint>
#include <windows.h>
namespace games::jb {
// touch detection algorithm
enum JubeatTouchAlgorithm {
Legacy,
Improved,
Plus,
AcAccurate
};
// debug touch overlay mode
enum JubeatTouchDebugMode {
JbTouchDebugAuto, // boundary boxes when a touch screen is detected, else nothing
JbTouchDebugNone, // draw nothing
JbTouchDebugBox, // boundary boxes
JbTouchDebugAll, // boundary boxes and touch circles
};
// selected algorithm and debug mode (set from the launcher options)
extern JubeatTouchAlgorithm TOUCH_ALGORITHM;
extern JubeatTouchDebugMode TOUCH_DEBUG_OVERLAY;
// minimum contact age in milliseconds before a touch registers (0 = off);
// filters momentary phantom touches from noisy panels
extern uint32_t TOUCH_DEBOUNCE_MS;
// atomically published panel state, read by the I/O layer
std::bitset<16> touch_state();
// read the current touch points and refresh the panel state
void touch_update();
// enable/disable touch handling on game attach/detach
void touch_attach();
void touch_detach();
// whether the debug overlay should currently draw anything
bool touch_debug_overlay_enabled();
// draw the debug touch overlay (boundary boxes and/or touch circles per the
// selected mode) onto the spice touch overlay window's device context
void touch_draw_debug_overlay(HDC hdc);
}
-1
View File
@@ -6,7 +6,6 @@
#include "util/utils.h"
#include "util/execexe.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "bi2a_hook.h"
+16 -7
View File
@@ -1,16 +1,15 @@
// enable touch functions - set version to windows 7
// mingw otherwise doesn't load touch stuff
#define _WIN32_WINNT 0x0601
#include "nost.h"
#include "poke.h"
#include "touch_mode.h"
#include "hooks/setupapihook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "avs/game.h"
namespace games::nost {
bool ENABLE_POKE = false;
bool ENABLE_TOUCH_MODE = false;
NostGame::NostGame() : Game("Nostalgia") {
}
@@ -43,9 +42,16 @@ namespace games::nost {
setupapihook_init(avs::game::DLL_INSTANCE);
setupapihook_add(touch_settings);
// custom touch
// nostalgia crashes if you inject touch events too early
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::hook("nostalgia", avs::game::DLL_INSTANCE, 20);
} else {
if (ENABLE_TOUCH_MODE) {
touch_mode::enable();
}
}
}
void NostGame::post_attach() {
@@ -55,6 +61,9 @@ namespace games::nost {
}
void NostGame::detach() {
if (ENABLE_TOUCH_MODE) {
touch_mode::disable();
}
if (ENABLE_POKE) {
poke::disable();
}
+1
View File
@@ -5,6 +5,7 @@
namespace games::nost {
extern bool ENABLE_POKE;
extern bool ENABLE_TOUCH_MODE;
class NostGame : public games::Game {
public:
+96 -80
View File
@@ -1,22 +1,25 @@
#include "poke.h"
#include <thread>
#include <atomic>
#include <chrono>
#include <optional>
#include <thread>
#include "games/nost/io.h"
#include "cfg/api.h"
#include "rawinput/rawinput.h"
#include "misc/eamuse.h"
#include "touch/touch.h"
#include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/logging.h"
#include "util/precise_timer.h"
namespace games::nost::poke {
static std::thread *THREAD = nullptr;
static volatile bool THREAD_RUNNING = false;
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_NUM {
static std::atomic<bool> THREAD_RUNNING { false };
// positions use game-client pixels, matching the legacy client-sized touch window
static const std::unordered_map<int, std::pair<LONG, LONG>> NOST_POKE_KEYPAD {
{EAM_IO_KEYPAD_0, {760, 440}},
{EAM_IO_KEYPAD_1, {760, 385}},
{EAM_IO_KEYPAD_2, {820, 385}},
@@ -31,7 +34,8 @@ namespace games::nost::poke {
{EAM_IO_KEYPAD_DECIMAL, {880, 440}} // also erase button
};
static const std::unordered_map<games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE {
static const std::unordered_map<
games::nost::Buttons::Button, std::pair<LONG, LONG>> NOST_POKE_BUTTONS {
{games::nost::Buttons::Button::PokeConfirm, {1100, 525}},
{games::nost::Buttons::Button::PokeBack, {340, 100}},
{games::nost::Buttons::Button::PokeSong1, {450, 190}},
@@ -46,13 +50,8 @@ namespace games::nost::poke {
{games::nost::Buttons::Button::PokeDifficulty4, {820, 490}},
};
void clear_touch_points(std::vector<TouchPoint> *touch_points) {
std::vector<DWORD> touch_ids;
for (auto &touch : *touch_points) {
touch_ids.emplace_back(touch.id);
}
touch_remove_points(&touch_ids);
touch_points->clear();
static bool inject_poke_position(POINT position, bool down) {
return nativetouch::inject::inject_synthetic_touch(position, down);
}
void enable() {
@@ -61,126 +60,143 @@ namespace games::nost::poke {
return;
}
if (!nativetouch::is_hooked()) {
log_warning("poke", "Nostalgia poke requires native touch; disabled");
return;
}
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
timeutils::PreciseSleepTimer timer;
const DWORD touch_id = (DWORD)(0xFFFFFFFE);
const int swipe_anim_total_frames = 6;
const int swipe_anim_y = 300;
const int swipe_next_page_x_begin = 1120;
const int swipe_next_page_x_end = 1120-120;
const int swipe_next_page_x_end = 1120 - 120;
const int swipe_prev_page_x_begin = 280-120;
const int swipe_prev_page_x_begin = 280 - 120;
const int swipe_prev_page_x_end = 280;
int next_page_anim_index = -1;
int prev_page_anim_index = -1;
std::vector<TouchPoint> touch_points;
bool touch_release = false;
bool contact_active = false;
bool release_pending = false;
POINT last_position {};
std::unordered_map<games::nost::Buttons::Button, bool> button_states;
uint16_t last_keypad_state = 0;
// log
log_info("poke", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// clean up touch from last frame
if (!touch_points.empty()) {
if (touch_release) {
clear_touch_points(&touch_points);
touch_release = false;
} else {
touch_points.clear();
if (release_pending) {
if (contact_active) {
inject_poke_position(last_position, false);
}
contact_active = false;
release_pending = false;
}
auto &buttons = games::nost::get_buttons();
std::optional<POINT> touch_position;
bool release_after_touch = false;
const auto button_triggered = [&](games::nost::Buttons::Button button) {
const auto pressed =
GameAPI::Buttons::getState(RI_MGR, buttons.at(button));
const auto triggered = pressed && !button_states[button];
button_states[button] = pressed;
return triggered;
};
std::optional<POINT> triggered_touch_position;
for (const auto& it : NOST_POKE_BUTTONS) {
if (button_triggered(it.first) && !triggered_touch_position.has_value()) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
}
}
const auto next_triggered =
button_triggered(games::nost::Buttons::PokeNextPage);
const auto prev_triggered =
button_triggered(games::nost::Buttons::PokePrevPage);
const auto keypad_state = eamuse_get_keypad_state(0);
if (!triggered_touch_position.has_value()) {
for (const auto& it : NOST_POKE_KEYPAD) {
const auto mask = static_cast<uint16_t>(1 << it.first);
if ((keypad_state & mask) && !(last_keypad_state & mask)) {
triggered_touch_position = POINT {
it.second.first,
it.second.second,
};
break;
}
}
}
last_keypad_state = keypad_state;
if (0 <= next_page_anim_index) {
const auto delta =
(swipe_next_page_x_end - swipe_next_page_x_begin)
* (swipe_anim_total_frames - next_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_next_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_next_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
next_page_anim_index--;
if (next_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else if (0 <= prev_page_anim_index) {
const auto delta =
(swipe_prev_page_x_end - swipe_prev_page_x_begin)
* (swipe_anim_total_frames - prev_page_anim_index) / swipe_anim_total_frames;
TouchPoint tp {
.id = touch_id,
.x = (LONG)swipe_prev_page_x_begin + delta,
.y = (LONG)swipe_anim_y,
.mouse = true,
touch_position = POINT {
swipe_prev_page_x_begin + delta,
swipe_anim_y,
};
touch_points.emplace_back(tp);
prev_page_anim_index--;
if (prev_page_anim_index < 0) {
touch_release = true;
release_after_touch = true;
}
} else {
for (const auto& it : NOST_POKE) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(it.first))) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
if (!touch_release) {
const auto state = eamuse_get_keypad_state(0);
if (state) {
for (const auto& it : NOST_POKE_NUM) {
if (state & (1 << it.first)) {
TouchPoint tp {
.id = touch_id,
.x = it.second.first,
.y = it.second.second,
.mouse = true,
};
touch_points.emplace_back(tp);
touch_release = true;
break;
}
}
}
}
touch_position = triggered_touch_position;
release_after_touch = touch_position.has_value();
// start animations for next frame
if (!touch_release) {
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokeNextPage))) {
if (!touch_position.has_value()) {
if (next_triggered) {
next_page_anim_index = swipe_anim_total_frames;
}
if (GameAPI::Buttons::getState(RI_MGR, buttons.at(games::nost::Buttons::PokePrevPage))) {
if (prev_triggered) {
prev_page_anim_index = swipe_anim_total_frames;
}
}
}
if (!touch_points.empty()) {
touch_write_points(&touch_points);
if (touch_position.has_value() &&
inject_poke_position(*touch_position, true)) {
contact_active = true;
last_position = *touch_position;
}
if (release_after_touch && contact_active) {
release_pending = true;
}
// slow down
timer.sleep(30);
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
if (contact_active) {
inject_poke_position(last_position, false);
}
return nullptr;
+233
View File
@@ -0,0 +1,233 @@
#include "touch_mode.h"
#include <atomic>
#include <mutex>
#include <unordered_map>
#include "touch/native/nativetouchhook.h"
#include "util/logging.h"
namespace games::nost::touch_mode {
// native contact positions feed piano input directly. native events reach the game
// in nav mode and are suppressed in piano mode. mode-button contacts are always
// suppressed and change that routing after release.
static constexpr LONG PIANO_LEFT_GAP = 11;
static constexpr LONG PIANO_RIGHT_GAP = 10;
static constexpr uint32_t PIANO_KEY_COUNT = 28;
struct TouchGeometry {
HWND window = nullptr;
RECT mode_button {};
LONG client_width = 0;
LONG client_height = 0;
bool valid() const {
return window != nullptr && client_width > 0 && client_height > 0;
}
};
struct NativeContact {
POINT position {};
// position contains game-client coordinates
bool client_position_valid = false;
// down began on the mode switch button; remains true through up
bool mode_button = false;
};
static std::atomic_bool accept_events { false };
static std::atomic<Mode> current_mode_state { Mode::Nav };
static std::mutex state_mutex;
static TouchGeometry touch_geometry;
// native contacts are kept by ID so each contact contributes exactly one position
static std::unordered_map<DWORD, NativeContact> active_contacts;
// a hardware button release requests one change after all contacts are released
static bool mode_change_pending = false;
static void reset_state_locked() {
current_mode_state.store(Mode::Nav, std::memory_order_release);
touch_geometry = {};
active_contacts.clear();
mode_change_pending = false;
}
// hardware contacts arrive in screen coordinates
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
if (!touch_geometry.valid()) {
return false;
}
POINT position { event.x, event.y };
if (!ScreenToClient(touch_geometry.window, &position)) {
return false;
}
return PtInRect(&touch_geometry.mode_button, position) != FALSE;
}
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
std::lock_guard<std::mutex> lock(state_mutex);
// first, process down / move events
if (event.down || event.move) {
auto contact = active_contacts.try_emplace(event.id).first;
// keep track of IDs that began as a down on the mode switch button
if (event.down) {
contact->second.mode_button = native_touch_in_button(event);
}
// check for valid position
POINT position { event.x, event.y };
if (touch_geometry.window != nullptr &&
ScreenToClient(touch_geometry.window, &position)) {
contact->second.position = position;
contact->second.client_position_valid = true;
}
}
const auto contact = active_contacts.find(event.id);
const bool mode_button_contact = contact != active_contacts.end() &&
contact->second.mode_button;
// process up events
if (event.up) {
active_contacts.erase(event.id);
// if a contact that began down event on the mode switch button has
// been released, a mode switch is now pending
if (mode_button_contact) {
mode_change_pending = true;
}
// apply the change on the final hardware up. switching earlier would
// split another contact's down and up events across different modes
if (mode_change_pending && active_contacts.empty()) {
mode_change_pending = false;
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
current_mode_state.store(next_mode, std::memory_order_release);
}
}
return mode_button_contact;
}
// install the Nostalgia-specific native touch interception
void enable() {
if (accept_events.exchange(true, std::memory_order_acq_rel)) {
return;
}
{
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
nativetouch::set_input_filter(filter_native_touch);
log_info("nost::touch", "enabled");
}
void disable() {
if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
return;
}
nativetouch::set_input_filter(nullptr);
std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked();
}
bool enabled() {
return accept_events.load(std::memory_order_acquire);
}
Mode current_mode() {
return current_mode_state.load(std::memory_order_acquire);
}
// publish the rendered overlay button rectangle in game-client pixels
void publish_button_bounds(HWND window, const RECT &client_bounds) {
TouchGeometry next {};
RECT client_rect {};
if (window != nullptr && GetClientRect(window, &client_rect) &&
client_rect.right > 0 && client_rect.bottom > 0) {
next.window = window;
next.mode_button = client_bounds;
next.client_width = client_rect.right;
next.client_height = client_rect.bottom;
}
std::lock_guard<std::mutex> lock(state_mutex);
touch_geometry = next;
}
// return the active 28-key piano bitfield for the PANB input update
uint32_t piano_key_state() {
if (!enabled() || current_mode() != Mode::Piano) {
return 0;
}
std::lock_guard<std::mutex> lock(state_mutex);
if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
return 0;
}
uint32_t state = 0;
for (const auto &contact : active_contacts) {
// invalid position or mode-button contact; ignore these contacts
if (!contact.second.client_position_valid || contact.second.mode_button) {
continue;
}
const auto &position = contact.second.position;
// outside the client area or on the mode button; ignore these contacts
if (position.x < 0 || position.x >= touch_geometry.client_width ||
position.y < 0 || position.y >= touch_geometry.client_height ||
PtInRect(&touch_geometry.mode_button, position)) {
continue;
}
// divide the inset width evenly; touches in either side gap clamp to
// the nearest outer key so the physical screen edges remain playable
const auto piano_width =
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
uint32_t key = 0;
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
key = PIANO_KEY_COUNT - 1;
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
key = static_cast<uint32_t>(
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
}
state |= UINT32_C(1) << key;
}
return state;
}
// update native contacts and report whether this event should be hidden from the game
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
// synthetic events are outside this hardware-only feature
if (!enabled() || event.synthetic) {
// false leaves the event visible to the game
return false;
}
// snapshot routing before an up event can commit a pending mode switch
const bool piano_mode_before_update = current_mode() == Mode::Piano;
// update the contact lifetime and commit any pending switch when safe
const bool mode_button_contact = update_touch_state(event);
// hide every event in a contact that began on the mode switch button
if (mode_button_contact) {
return true;
}
// piano mode consumes hardware events; nav mode forwards them to the game
return piano_mode_before_update;
}
}
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstdint>
#include <windows.h>
#include "touch/native/nativetouchhook.h"
namespace games::nost::touch_mode {
// nav mode forwards contacts to the game; piano mode converts them into piano keys
enum class Mode {
Nav,
Piano,
};
void enable();
void disable();
bool enabled();
Mode current_mode();
void publish_button_bounds(HWND window, const RECT &client_bounds);
uint32_t piano_key_state();
bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
}
-1
View File
@@ -12,7 +12,6 @@
#include "util/unity_player.h"
#include "util/execexe.h"
#include "acioemu/handle.h"
#include "misc/wintouchemu.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/rawinput.h"
#include "util/socd_cleaner.h"
-1
View File
@@ -2,7 +2,6 @@
#include "acio/icca/icca.h"
#include "io.h"
#include "misc/wintouchemu.h"
#include "util/detour.h"
#include "util/utils.h"
#include "util/libutils.h"
+12 -9
View File
@@ -16,16 +16,16 @@
#include "launcher/launcher.h"
#include "launcher/logger.h"
#include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "util/sysutils.h"
#include "io.h"
#include "util/deferlog.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
namespace games::popn {
bool SHOW_PIKA_MONITOR_WARNING = false;
bool NATIVE_TOUCH = false;
bool NATIVE_TOUCH = true;
#if SPICE64 && !SPICE_XP
@@ -715,13 +715,16 @@ namespace games::popn {
// 00000100 0000000B 00000001 (button 9)
// set third column to 0 and it will work with BIO2
if (!GRAPHICS_WINDOWED) {
if (NATIVE_TOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else {
wintouchemu::FORCE = true;
NATIVE_TOUCH = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!NATIVE_TOUCH) {
wintouchemu::FORCE = true;
if (!GRAPHICS_WINDOWED || GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends("", "Main Screen", avs::game::DLL_INSTANCE);
wintouchemu::hook_title_ends(
"",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
+2 -2
View File
@@ -41,7 +41,7 @@ void games::rb::RBGame::attach() {
Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) {
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
log_fatal("rb", "-rbtouchhz is set too high; cannot exceed 1000");
}
// init stuff
@@ -60,7 +60,7 @@ void games::rb::RBGame::attach() {
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
log_info("rb", "not changing touch poll rate (~125Hz by default)");
}
}
-1
View File
@@ -7,7 +7,6 @@
namespace games::rb {
extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game {
+79 -50
View File
@@ -4,7 +4,10 @@
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/touch.h"
#include "touch/touch.h"
#include "touch_debug.h"
#include "touch_defs.h"
#include "util/logging.h"
#include "util/time.h"
#include "util/utils.h"
@@ -12,28 +15,38 @@
static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb {
uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
static constexpr double TOUCH_POINT_OFFSET_DIVISOR = 3.0;
static constexpr double X_INPUT_COORDINATE_COUNT = 54.0;
uint16_t TOUCH_SCALING = TOUCH_SCALE_DEFAULT;
static void packet_set_bit(unsigned char *packet, int bit) {
packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] |=
static_cast<unsigned char>(1u << (bit % 8));
}
}
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
this->log_fps = log_fps;
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) : log_fps(log_fps) {
}
void games::rb::ReflecBeatTouchDeviceHandle::grid_insert(unsigned char *data, int cursor_x, int cursor_y) {
// scale to grid position - there are 48 columns and 76 rows of IR sensors.
// for whatever reason, the last y row (#75) results in weird input few rows above; just drop it
int grid_x = CLAMP((cursor_x * 48) / window_width, 0, 47);
int grid_y = CLAMP((cursor_y * 76) / window_height, 0, 74);
// read() contracts X uniformly so screen edges land within usable sensors 2..45
int grid_x = CLAMP(
(cursor_x * X_SENSOR_COUNT) / window_width,
0,
X_SENSOR_COUNT - 1);
int grid_y = CLAMP(
(cursor_y * Y_SENSOR_COUNT) / window_height,
0,
Y_SENSOR_COUNT - 1);
// get bit positions
int bit_x = 88 + grid_x;
int bit_y = 74 - grid_y;
int bit_x = X_SENSOR_FIRST_BIT + grid_x;
int bit_y = Y_SENSOR_FIRST_BIT - grid_y;
// insert bits
data[3 + bit_x / 8] |= (char) 1 << (bit_x % 8);
data[3 + bit_y / 8] |= (char) 1 << (bit_y % 8);
packet_set_bit(data, bit_x);
packet_set_bit(data, bit_y);
}
bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
@@ -49,6 +62,9 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
if (wnd != nullptr) {
// cache the game window so read() can size against it regardless of focus
this->game_hwnd = wnd;
// reset window process to make the game not crash
SetWindowLongPtr(wnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(DefWindowProc));
@@ -68,18 +84,19 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
// update window
SetWindowPos(wnd, nullptr, 0, 0, 0, 0,
SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER);
}
// create touch window
touch_create_wnd(wnd);
// request automatic aspect ratio fixes
::rawinput::touch::ASPECT_COMPENSATION_GAME = true;
} else {
// create touch window
touch_create_wnd(wnd);
// create touch window
touch_create_wnd(wnd);
// show game window because it lost focus
// show fullscreen game window because it lost focus
if (!GRAPHICS_WINDOWED) {
ShowWindow(wnd, SW_SHOW);
}
} else {
// fallback to dx hook
@@ -91,18 +108,21 @@ bool games::rb::ReflecBeatTouchDeviceHandle::open(LPCWSTR lpFileName) {
ShowCursor(TRUE);
}
touch_debug_attach();
return true;
}
int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
// check buffer size
if (nNumberOfBytesToRead < 20) {
if (nNumberOfBytesToRead < TOUCH_PACKET_SIZE) {
return 0;
}
// get window
HWND window = GetForegroundWindow();
// size against the game window cached in open() (not GetForegroundWindow), so
// touch stays correctly scaled and keeps working when the game loses focus
HWND window = this->game_hwnd;
if (window == nullptr) {
return 0;
}
@@ -122,17 +142,17 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// create data
unsigned char data[20] {};
unsigned char data[TOUCH_PACKET_SIZE] {};
// data header
data[0] = 0x55;
data[2] = 0x4C;
const auto SCALE_FACTOR = games::rb::TOUCH_SCALING / 1000.f;
const float scale_factor = games::rb::TOUCH_SCALING / (float) games::rb::TOUCH_SCALE_DEFAULT;
// iterate all touch points
auto offset_x = (int) (window_width / 48.0 / 3.0);
auto offset_y = (int) (window_height / 76.0 / 3.0);
int offset_x = (int) (window_width / (double) X_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
int offset_y = (int) (window_height / (double) Y_SENSOR_COUNT / TOUCH_POINT_OFFSET_DIVISOR);
std::vector<TouchPoint> touch_points;
touch_get_points(touch_points);
for (auto &point : touch_points) {
@@ -148,37 +168,44 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// apply scaling
x = x - (window_width * (1.f - SCALE_FACTOR) / 2);
x = x / SCALE_FACTOR;
y = y - (window_height * (1.f - SCALE_FACTOR) / 2);
y = y / SCALE_FACTOR;
x = x - (window_width * (1.f - scale_factor) / 2);
x = x / scale_factor;
y = y - (window_height * (1.f - scale_factor) / 2);
y = y / scale_factor;
if (x < 0 || window_width <= x || y < 0 || window_height <= y) {
continue;
}
// point scaling
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76);
// point coordinates
// keep the verified left edge fixed while adding up to one-third of a beam
// at the right edge, preventing the finger footprint from including sensor 44
const auto point_x = (int) (
(x - window_width / 2.0) * X_SENSOR_COUNT / X_INPUT_COORDINATE_COUNT +
window_width / 2.0 +
x / (X_SENSOR_COUNT * TOUCH_POINT_OFFSET_DIVISOR));
const auto point_y = (int) y;
// center
grid_insert(data, point_x, point_y);
// surrounding points
if (games::rb::TOUCH_SIZE == 3) {
grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
// model a finger as a 3x3 block of IR sensors around the touch point
// this gives better accuracy (than just 1x1) since the logic below
// can toggle anywhere from 1x1 to 2x2, and the game engine calculates
// the center point, which means by inserting up to 8 extra blocks
// we are emulating a sub-"pixel" resolution
grid_insert(data, point_x, point_y); // center
grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
touch_debug_publish(data, is_landscape);
// copy data to buffer
memcpy(lpBuffer, data, 20);
memcpy(lpBuffer, data, TOUCH_PACKET_SIZE);
// update frame logging
if (log_fps) {
@@ -193,7 +220,7 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
}
// return amount of bytes written
return 20;
return TOUCH_PACKET_SIZE;
}
int games::rb::ReflecBeatTouchDeviceHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
@@ -207,6 +234,8 @@ int games::rb::ReflecBeatTouchDeviceHandle::device_io(DWORD dwIoControlCode, LPV
bool games::rb::ReflecBeatTouchDeviceHandle::close() {
touch_debug_detach();
// detach touch module
touch_detach();
+6
View File
@@ -12,6 +12,12 @@ namespace games::rb {
int window_width = 1080;
int window_height = 1920;
// game window resolved in open(); read() sizes against this instead of the
// foreground window so touch stays correctly scaled and keeps working when
// the game is not the foreground window (rawinput delivers touch regardless
// of focus)
HWND game_hwnd = nullptr;
// logging
bool log_fps = false;
uint64_t log_time = 0;
+143
View File
@@ -0,0 +1,143 @@
#include "touch_debug.h"
#include <array>
#include <atomic>
#include <cstring>
#include <mutex>
#include "external/imgui/imgui.h"
#include "games/rb/rb.h"
#include "games/rb/touch_defs.h"
namespace games::rb {
struct TouchDebugState {
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
bool is_landscape = false;
};
std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
static std::atomic_bool TOUCH_ACTIVE = false;
static std::mutex TOUCH_DEBUG_STATE_M;
static TouchDebugState TOUCH_DEBUG_STATE;
static float touch_scale_factor() {
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
}
static void clear_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
TOUCH_DEBUG_STATE = {};
}
static TouchDebugState get_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
return TOUCH_DEBUG_STATE;
}
static bool packet_bit_active(
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
}
static int sensor_center(int sensor, int sensor_count, int extent) {
return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
}
static float sensor_span_position(int sensor, int sensor_count, int extent) {
return sensor * (extent - 1) / (float) (sensor_count - 1);
}
bool touch_debug_overlay_enabled() {
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
}
void touch_draw_debug_overlay() {
if (!touch_debug_overlay_enabled()) {
return;
}
const auto &io = ImGui::GetIO();
int width = static_cast<int>(io.DisplaySize.x);
int height = static_cast<int>(io.DisplaySize.y);
if (width <= 0 || height <= 0) {
return;
}
const float scale_factor = touch_scale_factor();
const float left = width * (1.f - scale_factor) / 2.f;
const float top = height * (1.f - scale_factor) / 2.f;
const float right = width - left;
const float bottom = height - top;
TouchDebugState state = get_touch_debug_state();
ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
auto draw_line = [&](float x1, float y1, float x2, float y2) {
draw_list->AddLine(
ImVec2(x1, y1), ImVec2(x2, y2),
IM_COL32(0, 255, 64, 255), 2.f);
};
// show the valid input area when touch scaling restricts it
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
draw_list->AddRect(
ImVec2(left, top), ImVec2(right, bottom),
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
}
// spread the usable X sensors 2..45 from edge to edge
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
continue;
}
float position = sensor_span_position(
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
state.is_landscape ? height : width);
if (state.is_landscape) {
float y = top + position * scale_factor;
draw_line(left, y, right, y);
} else {
float x = left + position * scale_factor;
draw_line(x, top, x, bottom);
}
}
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
continue;
}
int position = sensor_center(
sensor, Y_SENSOR_COUNT,
state.is_landscape ? width : height);
if (state.is_landscape) {
float x = right - position * scale_factor;
draw_line(x, top, x, bottom);
} else {
float y = top + position * scale_factor;
draw_line(left, y, right, y);
}
}
}
void touch_debug_attach() {
clear_touch_debug_state();
TOUCH_ACTIVE.store(true, std::memory_order_release);
}
void touch_debug_detach() {
TOUCH_ACTIVE.store(false, std::memory_order_release);
clear_touch_debug_state();
}
void touch_debug_publish(const unsigned char *data, bool is_landscape) {
if (!TOUCH_DEBUG_OVERLAY) {
return;
}
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
TOUCH_DEBUG_STATE.is_landscape = is_landscape;
}
}
+14
View File
@@ -0,0 +1,14 @@
#pragma once
#include <atomic>
namespace games::rb {
extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
bool touch_debug_overlay_enabled();
void touch_draw_debug_overlay();
void touch_debug_attach();
void touch_debug_detach();
void touch_debug_publish(const unsigned char *data, bool is_landscape);
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
namespace games::rb {
inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
inline constexpr int TOUCH_PACKET_SIZE = 20;
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
inline constexpr int X_SENSOR_COUNT = 48;
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
inline constexpr int X_SENSOR_ACTIVE_COUNT =
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
inline constexpr int X_SENSOR_FIRST_BIT = 88;
inline constexpr int Y_SENSOR_COUNT = 76;
inline constexpr int Y_SENSOR_FIRST_BIT = 75;
}
+13 -26
View File
@@ -3,6 +3,7 @@
#if SPICE64
#include <cstdint>
#include "api/client.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -349,43 +350,29 @@ namespace games::sdvx {
* 9 - v unit - 258 bytes - 86 colors
*
* data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
};
// check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) {
auto &map = mapping[index];
if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
auto &map = TAPELED_MAPPING[index];
const auto data_size = map.data.size();
// pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size);
const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
// program the lights into API
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
}
if (This != custom_node) {
+38 -10
View File
@@ -8,6 +8,9 @@
#include "hooks/graphics/graphics.h"
#include "hooks/devicehook.h"
#include "hooks/libraryhook.h"
#ifdef SPICE64
#include "hooks/graphics/nvapi_impl.h"
#endif
#include "hooks/graphics/nvapi_hook.h"
#include "hooks/powrprof.h"
#include "hooks/sleephook.h"
@@ -23,8 +26,8 @@
#include "util/libutils.h"
#include "util/sysutils.h"
#include "misc/eamuse.h"
#include "misc/nativetouchhook.h"
#include "misc/wintouchemu.h"
#include "touch/native/nativetouchhook.h"
#include "bi2x_hook.h"
#include "camera.h"
#include "io.h"
@@ -46,7 +49,6 @@ namespace games::sdvx {
const char *ORIGINAL_ASIO_DEVICE_NAME = "XONAR SOUND CARD(64)";
// settings
bool NATIVETOUCH = false;
uint8_t DIGITAL_KNOB_SENS = 16;
SdvxOverlayPosition OVERLAY_POS = SDVX_OVERLAY_BOTTOM;
bool ENABLE_COM_PORT_SCAN_HOOK = false;
@@ -59,6 +61,19 @@ namespace games::sdvx {
static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr;
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
};
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr &&
phkResult != nullptr &&
@@ -428,28 +443,41 @@ namespace games::sdvx {
if (aio != nullptr) {
// enable 9on12 for AMD
if (!libutils::try_library("nvapi64.dll")) {
const auto nvapi = libutils::try_library("nvapi64.dll");
if (nvapi == nullptr) {
log_info(
"sdvx",
"nvapi64.dll not found; for non-NVIDIA GPUs, requesting 9on12 to be enabled");
GRAPHICS_9_ON_12_REQUESTED_BY_GAME = true;
}
if (nvapi_hook::BYPASS_NVAPI || nvapi == nullptr) {
const uint32_t main_refresh_hz = GRAPHICS_FORCE_REFRESH > 0 ?
GRAPHICS_FORCE_REFRESH : (is_valkyrie_model() ? 120 : 60);
const uint32_t sub_refresh_hz = GRAPHICS_FORCE_REFRESH_SUB.value_or(60);
if (!nvapi_impl::initialize(
avs::game::DLL_INSTANCE,
main_refresh_hz,
sub_refresh_hz)) {
log_warning("sdvx", "failed to initialize synthetic NVAPI");
}
} else {
// don't let nvapi mess with display settings
nvapi_hook::initialize(avs::game::DLL_INSTANCE);
}
if (is_valkyrie_model()) {
if (NATIVETOUCH) {
nativetouchhook::hook(avs::game::DLL_INSTANCE);
} else if (!NATIVETOUCH && !GRAPHICS_WINDOWED) {
// hook touch window
// in windowed mode, game can accept mouse input on the second screen
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) {
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
if (!GRAPHICS_WINDOWED) {
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook_title_ends(
"SOUND VOLTEX",
"Main Screen",
avs::game::DLL_INSTANCE);
}
}
// insert BI2X hooks
+4 -1
View File
@@ -6,6 +6,7 @@
#include "avs/game.h"
#include "games/game.h"
#include "util/tapeled.h"
namespace games::sdvx {
@@ -18,7 +19,6 @@ namespace games::sdvx {
};
// settings
extern bool NATIVETOUCH;
extern uint8_t DIGITAL_KNOB_SENS;
extern std::optional<std::string> ASIO_DRIVER;
extern SdvxOverlayPosition OVERLAY_POS;
@@ -26,6 +26,9 @@ namespace games::sdvx {
// states
extern bool SHOW_VM_MONITOR_WARNING;
constexpr int SDVX_TAPELED_TOTAL = 10;
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
static inline bool is_valkyrie_model() {
return (
avs::game::is_model("KFC") &&
+72
View File
@@ -0,0 +1,72 @@
#include "sdvx_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds.
#ifdef SPICE64
#include <string>
#include "hooks/graphics/graphics.h"
#include "launcher/logger.h"
#include "util/logging.h"
namespace games::sdvx {
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
//
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy
// Live2D rendering); we watch those log lines and keep the shared flag
// the d3d9 backend reads up to date. the hook never alters the log output
// (always returns false).
static bool live2d_scene_log_hook(
void *user, const std::string &data, logger::Style style, std::string &out) {
// any of these scenes counts as in-song gameplay (different play modes)
static const char *const gameplay_scenes[] = {
"in ALTERNATIVE_GAME_SCENE",
"in MEGAMIX_GAME_SCENE",
"in MEGAMIX_BATTLE",
"in BATTLE_GAME_SCENE",
"in AUTOMATION_GAME_SCENE",
"in ARENA_GAME_SCENE",
};
bool in_gameplay_scene = false;
for (const auto *scene : gameplay_scenes) {
if (data.find(scene) != std::string::npos) {
in_gameplay_scene = true;
break;
}
}
if (!in_gameplay_scene) {
return false;
}
// note: log messages here must NOT contain any matched scene token, else
// this hook would re-enter itself when the message is pushed.
if (data.find("I:Attach: in ") != std::string::npos) {
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: entering gameplay");
}
} else if (data.find("I:Detach: in ") != std::string::npos) {
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: leaving gameplay");
}
}
return false;
}
void live2d_scene_detection_init() {
static bool installed = false;
if (installed) {
return;
}
installed = true;
// the logger's hook list is a persistent static, so registering here is
// safe even though this runs before logger::start(). we intentionally do
// NOT log a confirmation now: at this point the log file isn't open yet
// and the message would be dropped. the entering/leaving-gameplay lines
// above provide runtime confirmation once the logger is running.
logger::hook_add(live2d_scene_log_hook, nullptr);
}
}
#endif // SPICE64
+14
View File
@@ -0,0 +1,14 @@
#pragma once
namespace games::sdvx {
#ifdef SPICE64
// installs the Live2D in-game scene-detection log hook used by the
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to
// be attached, so it can be enabled purely from the launcher option.
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
// of 32-bit builds.
void live2d_scene_detection_init();
#endif
}

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