39 Commits
Author SHA1 Message Date
REDCODE 4652dc5c4a Merge remote-tracking branch 'origin/main' into r3d-spice2x
Continuous Integration / Build (push) Successful in 16m10s
2026-08-30 20:41:09 +02:00
bicarusandGitHub 357e6c86f1 gitadora: allow a landscape monitor to display subscreen (#899)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Arena Model's SMALL touch subscreen is a portrait (800x1280) panel -
however, it's common for people to set up a touch screen in landscape
orientation since IIDX/SDVX/popn all use landscape.

Add a new option, `-gdsublandscape`, which forces the subscreen to
launch at 1080p landscape. The image is rendered with black bars on the
sides. Touches will be unaligned initially; user needs to go into test
mode and recalibrate. In reality this option is just a short hand for
`-forceressub 1920,1080`.

Wire up `-forceressub` which previously didn't work for gitadora, so
that a resolution other than 1080p can be used in either orientation. In
other games `-forceressub` just forces the rendering resolution and
IIDX/SDVX/popn's game engine automatically stretches to fill... however
for gitadora it works differently since we want to keep the aspect ratio
of the original screen; it instead hijacks the back buffer and makes the
game draw to an intermediate buffer.

## Testing
2026-08-29 19:09:56 -07:00
bicarusandGitHub 67963389ef drs: hook down movement, adjust touch hooks (#898)
When Down Motion button is bound, install hooks into the game that
detects down movement via patches.

When Disable Touch option is enabled, don't install touch hooks.
2026-08-29 17:21:21 -07:00
bicarusandGitHub 4009bb8a84 sc: bone eater preliminary support (#895)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #114

## Description of change
Initial support for Silent Scope: Bone Eater, enough to be playable;
doesn't include QoS work needed to make this a smoother experience.

* Game detection
* Fix DX11 overlay to work with non-Unity games
* Implements sub monitor i/o (only really needed to get past I/O check)
* Try to make the game reasonably playable with a mouse
* Ignore the game's attempt to move windows, position them in a
reasonable place
* Fix language hook

## Testing
2026-08-28 03:04:43 -07:00
bicarusandGitHub 8fb6d94000 Include AI-Use Policy in README
Added AI-Use Policy section to README.md
2026-08-28 02:47:15 -07:00
bicarusandGitHub 8d3685415d popn: fix LED handling (#894)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix the issue with popn lights flickering.

`SetTapeLedDataPart()` updates BI3A’s internal LED buffer without
immediately sending it to hardware. High Cheers briefly clears this
buffer before writing the complete set of lights.

Stage pin-0 (button light) updates in the hook and publish only the
latest frame during `GetDeviceStatus()`, matching real BI3A behavior.
This prevents temporary clear frames from reaching HID controllers.

## Testing
Tested with my iidx controller with HID lights.
2026-08-28 01:42:56 -07:00
James LiuandGitHub 50b38b856f gitadora: fix 32bit gfdm coin button not working (#893)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix Add Coin/COIN button not actuated after pressing

## Testing
*XG2*
<img width="1594" height="458" alt="XG2"
src="https://github.com/user-attachments/assets/0d90ffaf-b0a0-421f-8986-5d87040ace93"
/>

*XG3*
<img width="1594" height="458" alt="XG3"
src="https://github.com/user-attachments/assets/e326fd91-3477-44ed-854b-4160cf0f103a"
/>
2026-08-27 09:05:36 -07:00
Jiongjia LuandGitHub dad88caf01 api: add card lookup endpoint (#891)
## Link to GitHub Issue or related Pull Request, if one exists

N/A

## Description of change

I am developing [spice.nimabe.net](https://spice.nimabe.net), a static
frontend for SpiceAPI. It already supports generating and inserting new
card IDs, but SpiceAPI does not provide a way to discover cards already
configured on the game machine.

This makes the frontend inconvenient for existing users who generated
and saved their card IDs locally. They currently have to open each card
file on the game machine and manually copy its ID into the frontend.

This change adds a `card.get_cards` API function that enumerates the
current cards for readers supported by the running game. Each entry
contains:

- reader index
- canonical 16-character card ID
- `source`, set to `file` or `override`
- `file_name` for file-backed cards

File-backed cards expose only the save-file basename. Active `-card0`
and `-card1` values are returned with `source: "override"` and no
`file_name`, instead of representing an override as a file. spicefe
derives the useful default import names `card0` and `card1` from the
reader index. Reader enumeration uses `eamuse_get_game_keypads()` so
games with only one reader do not expose a second card.

This allows spicefe and other API clients to offer one-click import for
existing cards, without requiring users to manually find and copy their
card IDs.

Because card IDs are sensitive, `card.get_cards` can only be used when
the operator has configured an API password. No full filesystem path is
exposed. Missing, unreadable, and invalid cards are omitted.

The existing `card.insert` function remains unchanged. To avoid
requiring a password for the whole card module, this change adds support
for password requirements on individual API functions while preserving
existing module-wide password behavior.

Access to configured card paths and runtime card overrides is
synchronized because API requests may read them concurrently with
configuration updates.

The Python and Dart API wrappers and README documentation have been
updated for the new function and source metadata.

## Testing

- Ran the complete `src/spice2x/build_docker.sh` build successfully.
- Built all required 32-bit and 64-bit targets.
- Built the Windows XP-compatible 32-bit targets.
- Passed static import checks.
- Passed Windows 7 and Windows XP compatibility checks.
- Completed release packaging successfully.
- Verified both updated Dart wrappers are correctly formatted.
- Ran all 106 spicefe tests successfully.
- `git diff --check` passes.

*This implementation was prepared with assistance from OpenAI Codex.*
2026-08-25 00:49:31 -07:00
bicarusandGitHub 699659d4bf cfg: update video options (#890)
Move around legacy video options.
2026-08-23 15:24:13 -07:00
bicarusandGitHub 21e7d24ed3 graphics: take stream captures off the game's present thread to avoid game perf hit (#889)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Capturing a frame for the API stream made the game wait for
`GetRenderTargetData` in the middle of its present, roughly 1270us per
frame at 1080p. A 120Hz cab visibly lost frames for as long as a viewer
was connected.

The present thread now only issues a `StretchRect` into a render target
we own, which is queued rather than waited on, and a pool thread does
the readback and the pixel conversion. That takes the present thread
cost to 1-4us. Each snapshot is read on the request after the one that
took it, so the blit and its transfer have a full frame to land and the
read does not stall on the GPU either, at the cost of one frame of
stream latency.

Only streaming takes this path, and only on a device created with
`D3DCREATE_MULTITHREADED`. Screenshots, `capture.get_jpg` and the
`THREAD_BAN` models keep the existing inline readback unchanged.

Also raises the x264 encoder from `i_threads = 1` to 4, which was
holding a 1080p60 stream to 41fps and making a keyframe cost 12.7ms
against 6.6ms for an ordinary frame. Capped rather than automatic
because this encodes on the same machine it is capturing.

## Testing
tested against iidx33, which was the most sensitive to frame drops
2026-08-23 12:24:41 -07:00
bicarusandGitHub 1cdb7be810 overlay: fix hotkey init race (#888)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #864

## Description of change
The hotkey sampler thread starts touching `games::io` the moment
`enable_raw_input()` is called, and that call sat right after `RI_MGR`
was constructed, a few lines before the main thread's own first
`games::io` calls. Both threads then hit the unsynchronized lazy
`initialize()` at once, crashing at startup with an access violation in
`memcmp`.

Moved the call down next to `enable_input()`, after the bindings are
built.

## Testing
2026-08-23 00:34:08 -07:00
bicarus-dev 09314f1fd3 fix bad merge 2026-08-22 21:53:03 -07:00
bicarusandGitHub e7b90271ca api: serve the websocket with wslay instead of headsocket (#887)
`HeadSocket` library we are currently using is full of bugs and no
longer maintained. Swap it out with `wslay`.
2026-08-22 21:47:26 -07:00
bicarusandGitHub 95f10ed733 api: get_streams (#886)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
MSE method of streaming video needs the dimensions up front. This new
api delivers that. Also provides an easier way to determine which
screens are available and a way to discover port number.

## Testing
See pending changes in substream project
2026-08-22 19:38:48 -07:00
bicarusandGitHub d51de976b1 graphics: move captures off-thread when streaming (#885)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change

Moves the API capture readback off the game's Present thread while a
video stream client is connected.

The readback is a `LockRect` plus a memcpy of the whole back buffer,
roughly 635us at 720p and 1270us at 1080p. On the Present thread that
comes out of the game's frame budget: TDJ (at 120Hz) dropped to 117fps
with a 60fps stream running, and reading on a pool thread instead gave
the full 120 back.

Only streaming takes the off-thread path, gated on a new
`capture_pump::screen_claimed()`.

Screenshots, one-off API captures, and `THREAD_BAN` games all keep the
existing inline read for compat reasons. A pool thread in `LockRect`
while the Present thread sat inside `GetRenderTargetData` deadlocks DDR
X2 for example.

`CLAIMED[]` becomes `std::atomic<bool>` so the capture path does not
take a lock on the Present thread. The read pool has a single worker so
frames cannot be enqueued out of order, and both capture pools are never
destroyed so a late read cannot queue onto a torn-down pool.

The capture pipeline itself is unchanged: `GetRenderTargetData` is still
synchronous on the Present thread.

## Testing
DDR X2
World
IIDX TDJ
SDVX VM
2026-08-22 11:11:07 -07:00
bicarusandGitHub 8acd433ec6 api: notifications for video stream (#884) 2026-08-22 00:20:41 -07:00
bicarusandGitHub b9c8afbbc1 Update CONTRIBUTING.md with DLL bundling guidelines
Added note about bundling third-party DLL binaries in contributions.
2026-08-21 16:33:28 -07:00
drmextandGitHub 7d5f528e1a overlay: tooltip static position and fix overlap (#883)
Tooltips no longer follow the mouse after initially activating, and
don't cover the main option when hovering above.
2026-08-21 15:01:46 -07:00
REDCODE 786a4254d2 Fixes build failing due to dsdmo.i686.dll being downloaded via wget command
Continuous Integration / Build (push) Successful in 11m51s
2026-08-21 14:38:32 +02:00
REDCODE 09ac50f2d5 Merge branch 'github-main' into r3d-spice2x
Continuous Integration / Build (push) Failing after 5m3s
2026-08-21 14:26:23 +02:00
bicarusandGitHub 2dc3c0cbe4 patcher: make the checkbox label toggleable (#882)
Requested by a certain sea creature. Don't know why ImGui doesn't do
this by default.

Also, use ImGui internal mixed state (tri-state) checkbox instead
rendering our own.
2026-08-20 23:48:11 -07:00
bicarusandGitHub 4c2a70bab6 build: improve dockerfile (#881)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change

`mingw-w64-libjpeg-turbo` and `mingw-w64-x264` are AUR packages, so the
deps image compiles them from source, once per mingw target arch. Stock
`makepkg.conf` leaves `MAKEFLAGS` commented out and both PKGBUILDs call
bare `make`, so all of that was building single-threaded.

Adds a `~/.makepkg.conf` for the build user setting
`MAKEFLAGS="-j$(nproc)"`. It is kept literal so it evaluates when
makepkg sources the file, rather than baking in the core count of
whichever machine built the image. Using `~/.makepkg.conf` rather than
`/etc` leaves the pacman-owned system file untouched.

Measured on 20 cores, compiling both libraries from scratch: **310s to
64s, a 4.8x improvement.**

Also merges the two `yay` invocations into a single layer.

## Testing
2026-08-20 23:25:00 -07:00
bicarusandGitHub 46f76597fc cfg: active only filter for options search (#879)
Requested by a sea creature
2026-08-20 22:38:30 -07:00
bicarusandGitHub 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
bicarusandGitHub 3f6862908e api: drop stale websocket / stream connections (#877) 2026-08-19 13:37:41 -07:00
bicarusandGitHub 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
bicarusandGitHub 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
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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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
bicarusandGitHub 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 LiuandGitHub 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
182 changed files with 37400 additions and 29143 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
+1
View File
@@ -22,6 +22,7 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
* Any additions to built-in patches (modifications / removals are OK if we have a good reason). * Any additions to built-in patches (modifications / removals are OK if we have a good reason).
* Changes to the software license terms. * Changes to the software license terms.
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers. * Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
* Bundling third party DLL binary; e.g., Discord Social SDK, PC light control integration, and so on. These should be built as hook DLLs instead, optionally with [Spice SDK](https://github.com/spice2x/spice2x.github.io/wiki/Spice-SDK).
### Avoiding regressions ### Avoiding regressions
+4
View File
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code. Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
### AI-Use Policy
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewers time.
## Additional information ## Additional information
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`. Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
+71 -8
View File
@@ -252,8 +252,60 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL)
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL) add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
add_subdirectory(external/imgui EXCLUDE_FROM_ALL) add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
add_subdirectory(external/minhook EXCLUDE_FROM_ALL) add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
add_subdirectory(external/wslay EXCLUDE_FROM_ALL)
add_subdirectory(external/cpu_features 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 # set link time optimizations (disabled for Debug builds for speed, disabled
# for RelWithDebInfo builds due to "lto1: error: two or more sections for" # for RelWithDebInfo builds due to "lto1: error: two or more sections for"
# errors) # errors)
@@ -328,6 +380,10 @@ set(SOURCE_FILES ${SOURCE_FILES}
# api # api
api/controller.cpp api/controller.cpp
api/websocket.cpp api/websocket.cpp
api/capture_pump.cpp
api/h264_stream.cpp
api/stream_format.cpp
api/stream_server.cpp
api/request.cpp api/request.cpp
api/response.cpp api/response.cpp
api/module.cpp api/module.cpp
@@ -391,7 +447,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
external/tinyxml2/tinyxml2.cpp external/tinyxml2/tinyxml2.cpp
external/http-parser/http_parser.c external/http-parser/http_parser.c
external/usbhidusage/usb-hid-usage.c external/usbhidusage/usb-hid-usage.c
external/toojpeg/toojpeg.cpp external/fpng/fpng.cpp
external/scard/scard.cpp external/scard/scard.cpp
# games # games
@@ -475,6 +531,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/scotto/io.cpp games/scotto/io.cpp
games/drs/drs.cpp games/drs/drs.cpp
games/drs/io.cpp games/drs/io.cpp
games/drs/motion_cam.cpp
games/drs/rgb_cam.cpp games/drs/rgb_cam.cpp
games/we/we.cpp games/we/we.cpp
games/we/io.cpp games/we/io.cpp
@@ -486,6 +543,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/otoca/p4io.cpp games/otoca/p4io.cpp
games/silentscope/silentscope.cpp games/silentscope/silentscope.cpp
games/silentscope/io.cpp games/silentscope/io.cpp
games/silentscope/projector.cpp
games/pcm/pcm.cpp games/pcm/pcm.cpp
games/pcm/io.cpp games/pcm/io.cpp
games/onpara/onpara.cpp games/onpara/onpara.cpp
@@ -548,10 +606,12 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/devicehook.cpp hooks/devicehook.cpp
hooks/graphics/graphics.cpp hooks/graphics/graphics.cpp
hooks/graphics/graphics_windowed.cpp hooks/graphics/graphics_windowed.cpp
hooks/graphics/jpeg_encoder.cpp
hooks/graphics/nvapi_impl.cpp hooks/graphics/nvapi_impl.cpp
hooks/graphics/nvapi_hook.cpp hooks/graphics/nvapi_hook.cpp
hooks/graphics/nvenc_hook.cpp hooks/graphics/nvenc_hook.cpp
hooks/graphics/backends/d3d9/d3d9_backend.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_screenshot.cpp
hooks/graphics/backends/d3d9/d3d9_device.cpp hooks/graphics/backends/d3d9/d3d9_device.cpp
hooks/graphics/backends/d3d9/d3d9_gfdm.cpp hooks/graphics/backends/d3d9/d3d9_gfdm.cpp
@@ -594,6 +654,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
misc/device.cpp misc/device.cpp
misc/eamuse.cpp misc/eamuse.cpp
misc/extdev.cpp misc/extdev.cpp
misc/hotkeys.cpp
misc/sciunit.cpp misc/sciunit.cpp
misc/sde.cpp misc/sde.cpp
misc/wintouchemu.cpp misc/wintouchemu.cpp
@@ -755,7 +816,7 @@ endfunction()
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES}) add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
target_link_libraries(spicetools_spice_objs target_link_libraries(spicetools_spice_objs
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp 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 wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
target_link_libraries(spicetools_spice_objs PUBLIC winscard) target_link_libraries(spicetools_spice_objs PUBLIC winscard)
if(NOT MSVC) if(NOT MSVC)
@@ -795,7 +856,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_linux target_link_libraries(spicetools_spice_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp 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 wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux") 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) target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
@@ -813,8 +874,8 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64 target_link_libraries(spicetools_spice64
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 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 wslay 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 PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64") set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64 PRIVATE SPICE64=1)
@@ -836,7 +897,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break # do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64_linux target_link_libraries(spicetools_spice64_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2 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 wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "") set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux") set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1) target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
@@ -855,9 +916,11 @@ endif()
set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp) set(SOURCE_FILES ${SOURCE_FILES} launcher/options.h launcher/options.cpp)
set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc) set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.rc)
add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
# the configurator serves neither the API nor the video stream, so it needs no codecs
target_link_libraries(spicetools_cfg target_link_libraries(spicetools_cfg
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "") set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg") set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
target_compile_definitions(spicetools_cfg PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1) target_compile_definitions(spicetools_cfg PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
@@ -874,7 +937,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES}) add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg_linux target_link_libraries(spicetools_cfg_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features) PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "") set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux") set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1) target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
+64
View File
@@ -127,7 +127,30 @@ doesn't matter since the TCP protocol doesn't allow for out of order data,
however this may change when/if support for UDP is being introduced. The only however this may change when/if support for UDP is being introduced. The only
restriction is that the ID has to be a valid 64-bit unsigned integer. restriction is that the ID has to be a valid 64-bit unsigned integer.
#### Capture
- get_screens()
- returns the screen numbers the game has registered for capture
- get_jpg(screen: uint, quality: uint, divide: uint)
- returns the timestamp, width, height and base64 encoded JPEG of one screen
- all parameters are optional and default to screen 0, quality 70, divide 1
- divide shrinks the image by that factor before encoding
- get_streams()
- returns a dict describing the HTTP video stream, or no data at all when
`-apistream` is not enabled and there is nothing to describe
- `port` is the stream server port
- `formats` lists the wire formats this build serves, each with a `name`
(`h264` or `mjpeg`) and the `path` to request them on
- `screens` lists every capturable screen with its `width`, `height`, and
`busy`
#### Card #### Card
- get_cards()
- returns the current card ID and source for each active card reader
- each entry contains `index`, `card_id`, and `source`
- `source` is `file` or `override`
- file entries also contain `file_name`, without the full configured path
- `-card0` and `-card1` overrides do not contain `file_name`
- this function only works when an API password is configured
- insert(index: uint, card_id: hex) - insert(index: uint, card_id: hex)
- inserts a card which gets read by the emulated card readers for the game - inserts a card which gets read by the emulated card readers for the game
- index has to be either 0 (for P1) or 1 (for P2) - index has to be either 0 (for P1) or 1 (for P2)
@@ -291,6 +314,47 @@ which also means that your hex edits are applicable directly.
- image_resize_set_scene(scene: int) - image_resize_set_scene(scene: int)
- sets the active scene for image resize state; set to 0 to disable resize - 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.
Rather than working the port out, clients should ask the JSON API for it with
`capture.get_streams()`, which also reports which of the formats below this
build serves, the size of each screen and whether one is already taken.
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 nothing
listens on the stream port even with `-apistream`, `capture.get_streams()`
returns no data, and the JSON API's JPEG screen capture is unavailable for the
same reason.
## Native wrapper libraries ## Native wrapper libraries
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python. Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
Python is the only one that is fully spec compliant. Python is the only one that is fully spec compliant.
+31 -5
View File
@@ -1,5 +1,7 @@
#include "bmpu.h" #include "bmpu.h"
#include <algorithm>
#include "acio/icca/icca.h" #include "acio/icca/icca.h"
#include "avs/game.h" #include "avs/game.h"
#include "cfg/api.h" #include "cfg/api.h"
@@ -9,6 +11,7 @@
#include "games/ftt/io.h" #include "games/ftt/io.h"
#include "games/museca/io.h" #include "games/museca/io.h"
#include "games/silentscope/io.h" #include "games/silentscope/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
STATUS_BUFFER[4] |= 0x20; STATUS_BUFFER[4] |= 0x20;
} }
// joy stick raw input // gun position, sent big endian; the game keeps the top 12 bits of each pair
auto &analogs = games::silentscope::get_analogs(); auto &analogs = games::silentscope::get_analogs();
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
unsigned short joy_x = 0x7FFF; unsigned short joy_x = 0x7FFF;
unsigned short joy_y = 0x7FFF; unsigned short joy_y = 0x7FFF;
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX); if (gun_x.isSet() || gun_y.isSet()) {
if (gun_x.isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
}
if (gun_y.isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
}
} else {
// the gun reports where it is aimed, which a relative pointer cannot express - the
// calibration screen asks for the screen centre and two corners, so read the cursor
POINT cursor {};
RECT client {};
POINT origin {};
if (NDD_MAIN_WINDOW != nullptr &&
GetCursorPos(&cursor) &&
GetClientRect(NDD_MAIN_WINDOW, &client) &&
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
client.right > 1 && client.bottom > 1)
{
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
} }
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
} }
// invert X axis // invert X axis
+59
View File
@@ -0,0 +1,59 @@
#include "capture_pump.h"
#include <array>
#include <atomic>
#include <mutex>
#include "hooks/graphics/graphics.h"
namespace api::capture_pump {
namespace {
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
// read once per capture from the present thread, so it stays lock free
std::atomic<bool> CLAIMED[GRAPHICS_CAPTURE_SCREEN_NO] {};
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;
}
return !CLAIMED[screen].exchange(true);
}
void release_screen(int screen) {
if (!valid_screen(screen)) {
return;
}
CLAIMED[screen] = false;
}
bool screen_claimed(int screen) {
if (!valid_screen(screen)) {
return false;
}
return CLAIMED[screen];
}
}
+27
View File
@@ -0,0 +1,27 @@
#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);
// true while a video stream client holds this screen
bool screen_claimed(int screen);
}
+5 -3
View File
@@ -337,9 +337,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
if (module->name == request.module) { if (module->name == request.module) {
module_found = true; module_found = true;
// check password force // check password requirement
if (module->password_force && this->password.empty() && request.function != "session_refresh") { if (module->requires_password(request.function)
Value err("Module requires the password to be set."); && this->password.empty()
&& request.function != "session_refresh") {
Value err("Function requires the password to be set.");
response.add_error(err); response.add_error(err);
break; break;
} }
+230
View File
@@ -0,0 +1,230 @@
#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;
// sliced threading, which zerolatency already selected, so a frame is split
// across workers rather than held back to be reordered. deliberately not the
// automatic count: this shares a machine with the game it is capturing, and
// taking every core to encode would win back frames at the game's expense
param.i_threads = 4;
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);
}
+8
View File
@@ -16,6 +16,14 @@ namespace api {
this->password_force = password_force; this->password_force = password_force;
} }
void Module::require_password(const std::string &function) {
this->password_force_functions.emplace(function);
}
bool Module::requires_password(const std::string &function) const {
return this->password_force || this->password_force_functions.contains(function);
}
void Module::handle(Request &req, Response &res) { void Module::handle(Request &req, Response &res) {
// log module access // log module access
+10
View File
@@ -4,6 +4,7 @@
#include <map> #include <map>
#include <string> #include <string>
#include <sstream> #include <sstream>
#include <unordered_set>
#include <external/robin_hood.h> #include <external/robin_hood.h>
#include "response.h" #include "response.h"
@@ -26,6 +27,13 @@ namespace api {
// default constructor // default constructor
explicit Module(std::string name, bool password_force=false); explicit Module(std::string name, bool password_force=false);
void require_password(const std::string &function);
private:
// functions which expose sensitive data or actions
std::unordered_set<std::string> password_force_functions;
public: public:
// virtual deconstructor // virtual deconstructor
@@ -35,6 +43,8 @@ namespace api {
std::string name; std::string name;
bool password_force; bool password_force;
bool requires_password(const std::string &function) const;
// the magic // the magic
void handle(Request &req, Response &res); void handle(Request &req, Response &res);
+110 -6
View File
@@ -1,9 +1,14 @@
#include "capture.h" #include "capture.h"
#include <atomic>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include "api/capture_pump.h"
#include "api/stream_format.h"
#include "api/stream_server.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "hooks/graphics/jpeg_encoder.h"
#include "util/crypt.h" #include "util/crypt.h"
using namespace std::placeholders; using namespace std::placeholders;
@@ -69,6 +74,7 @@ namespace api::modules {
Capture::Capture() : Module("capture") { Capture::Capture() : Module("capture") {
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2); functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2); functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2);
} }
/** /**
@@ -93,8 +99,6 @@ namespace api::modules {
* reduce: uint for dividing image size * reduce: uint for dividing image size
*/ */
void Capture::get_jpg(Request &req, Response &res) { void Capture::get_jpg(Request &req, Response &res) {
CAPTURE_BUFFER.clear();
CAPTURE_BUFFER.reserve(1024 * 128);
// settings // settings
int screen = 0; int screen = 0;
@@ -120,10 +124,16 @@ namespace api::modules {
uint64_t timestamp = 0; uint64_t timestamp = 0;
int width = 0; int width = 0;
int height = 0; int height = 0;
graphics_capture_trigger(screen);
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) { std::shared_ptr<uint8_t[]> pixels;
CAPTURE_BUFFER.push_back(byte); bool success = capture_pump::capture_direct(
}, true, quality, true, divide, &timestamp, &width, &height); screen, pixels, divide, &timestamp, &width, &height);
if (success) {
CAPTURE_BUFFER.clear();
success = jpeg_encoder::encode(
CAPTURE_BUFFER, pixels.get(), width, height, quality);
}
if (success) { if (success) {
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res); add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
@@ -135,4 +145,98 @@ namespace api::modules {
CAPTURE_BUFFER.clear(); CAPTURE_BUFFER.clear();
try_cached_response(screen, res); try_cached_response(screen, res);
} }
/**
* get_streams()
*/
void Capture::get_streams(Request &req, Response &res) {
auto &alloc = res.doc()->GetAllocator();
// nothing is listening without -apistream, so there is no stream to describe
const unsigned short port = stream_server_port();
if (port == 0) {
return;
}
Value formats(kArrayType);
for (const auto &[name, path] : stream_formats()) {
Value entry(kObjectType);
entry.AddMember("name", Value(name.c_str(), alloc), alloc);
entry.AddMember("path", Value(path.c_str(), alloc), alloc);
formats.PushBack(entry, alloc);
}
std::vector<int> screen_numbers;
graphics_screens_get(screen_numbers);
// measuring a screen nobody has captured yet waits for the game to present, which can
// take as long as the whole request is allowed, so only one screen is measured per
// call and the rest are reported null until a later one settles them. which screen
// gets the attempt rotates, otherwise one that never presents would take every
// request and the screens behind it would stay unmeasured forever
int probe_screen = -1;
{
std::vector<int> unmeasured;
for (const auto screen : screen_numbers) {
if (screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
&& !graphics_capture_last_size(screen, nullptr, nullptr)
&& !capture_pump::screen_claimed(screen)) {
unmeasured.push_back(screen);
}
}
if (!unmeasured.empty()) {
static std::atomic<unsigned> probe_cursor { 0 };
probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()];
}
}
Value screens(kArrayType);
for (const auto screen : screen_numbers) {
if (screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
continue;
}
int width = 0;
int height = 0;
bool known = graphics_capture_last_size(screen, &width, &height);
// a probe holds the screen for as long as it waits, so a second caller arriving
// during one would queue behind it and then take a wait of its own; let it report
// the screen as unmeasured instead and pick the size up once the first is done
static std::atomic<bool> probe_running { false };
if (!known && screen == probe_screen && !probe_running.exchange(true)) {
std::shared_ptr<uint8_t[]> pixels;
known = capture_pump::capture_direct(
screen, pixels, 1, nullptr, &width, &height);
probe_running = false;
}
// a screen of unknown size cannot be described, and a client told about it could
// not size its decoder anyway; leaving it out until it has been measured beats
// handing over an entry that has to be treated as absent
if (!known) {
continue;
}
Value entry(kObjectType);
entry.AddMember("screen", screen, alloc);
entry.AddMember("width", width, alloc);
entry.AddMember("height", height, alloc);
// a screen carries one viewer at a time, so this is what decides whether a client
// can connect at all; still racy by the time it does, only more honest than not
entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc);
screens.PushBack(entry, alloc);
}
Value info(kObjectType);
info.AddMember("port", port, alloc);
info.AddMember("formats", formats, alloc);
info.AddMember("screens", screens, alloc);
res.add_data(info);
}
} }
+1
View File
@@ -19,5 +19,6 @@ namespace api::modules {
// function definitions // function definitions
void get_screens(Request &req, Response &res); void get_screens(Request &req, Response &res);
void get_jpg(Request &req, Response &res); void get_jpg(Request &req, Response &res);
void get_streams(Request &req, Response &res);
}; };
} }
+64
View File
@@ -1,4 +1,5 @@
#include "card.h" #include "card.h"
#include <fstream>
#include <functional> #include <functional>
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "util/logging.h" #include "util/logging.h"
@@ -11,8 +12,71 @@ using namespace rapidjson;
namespace api::modules { namespace api::modules {
static bool normalize_card_id(const std::string &value, std::string &card_id) {
if (value.size() != 16) {
return false;
}
uint8_t card_bin[8] {};
if (!hex2bin(value.c_str(), card_bin)) {
return false;
}
card_id = bin2hex(card_bin, std::size(card_bin));
return true;
}
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
std::ifstream file(path);
char buffer[16] {};
if (!file.read(buffer, std::size(buffer))) {
return false;
}
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
}
Card::Card() : Module("card") { Card::Card() : Module("card") {
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
functions["insert"] = std::bind(&Card::insert, this, _1, _2); functions["insert"] = std::bind(&Card::insert, this, _1, _2);
require_password("get_cards");
}
/**
* get_cards()
*/
void Card::get_cards(Request &req, Response &res) {
auto &alloc = res.doc()->GetAllocator();
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
std::string card_id;
std::string filename;
const auto card_override = eamuse_get_card_override(index);
const bool has_override = !card_override.empty();
if (has_override) {
if (!normalize_card_id(card_override, card_id)) {
continue;
}
} else {
const auto path = eamuse_get_card_path(index);
if (!read_card_id(path, card_id)) {
continue;
}
const auto filename_u8 = path.filename().u8string();
filename.assign(filename_u8.begin(), filename_u8.end());
}
Value card(kObjectType);
card.AddMember("index", index, alloc);
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
if (!has_override) {
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
}
res.add_data(card);
}
} }
/** /**
+1
View File
@@ -12,6 +12,7 @@ namespace api::modules {
private: private:
// function definitions // function definitions
void get_cards(Request &req, Response &res);
void insert(Request &req, Response &res); void insert(Request &req, Response &res);
}; };
} }
+41 -11
View File
@@ -12,7 +12,9 @@
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "touch/native/transform.h"
#include "util/utils.h" #include "util/utils.h"
#include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
using namespace std::placeholders; using namespace std::placeholders;
@@ -40,6 +42,35 @@ namespace api::modules {
return nativetouch::inject::inject_synthetic_touch(position, 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") { Touch::Touch() : Module("touch") {
is_sdvx = avs::game::is_model("KFC"); is_sdvx = avs::game::is_model("KFC");
@@ -55,8 +86,8 @@ namespace api::modules {
native_canvas_w = 0; native_canvas_w = 0;
native_canvas_h = 0; native_canvas_h = 0;
if (is_sdvx) { if (is_sdvx) {
// windowed and landscape API coordinates already match the primary screen orientation; // windowed API coordinates land on the sub screen window as-is; fullscreen
// fullscreen portrait coordinates are rotated by apply_touch_errata // coordinates are rotated into the game's touch space by apply_touch_errata
const bool landscape_coordinates = const bool landscape_coordinates =
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP; GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
native_canvas_w = landscape_coordinates ? 1920 : 1080; native_canvas_w = landscape_coordinates ? 1920 : 1080;
@@ -69,6 +100,10 @@ namespace api::modules {
// pop'n music API touch surface // pop'n music API touch surface
native_canvas_w = 1280; native_canvas_w = 1280;
native_canvas_h = 800; 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["read"] = std::bind(&Touch::read, this, _1, _2);
@@ -212,19 +247,14 @@ namespace api::modules {
} }
void Touch::apply_touch_errata(int &x, int &y) { void Touch::apply_touch_errata(int &x, int &y) {
int x_raw = x;
int y_raw = y;
if (is_tdj_fhd) { if (is_tdj_fhd) {
// deal with TDJ FHD resolution mismatch (upgrade 720p to 1080p) // 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 // 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 // the target of the touch events so just assume it's the sub screen
x = x_raw * 1920 / 1280; x = x * 1920 / 1280;
y = y_raw * 1080 / 720; y = y * 1080 / 720;
} else if (is_sdvx && !GRAPHICS_WINDOWED && !GRAPHICS_FS_ORIENTATION_SWAP) { } else if (is_sdvx) {
// rotate API coordinates into SDVX's portrait touch space sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
x = 1080 - y_raw;
y = x_raw;
} }
} }
} }
@@ -11,6 +11,7 @@ part "src/exceptions.dart";
part "src/rc4.dart"; part "src/rc4.dart";
part "src/wrappers/analogs.dart"; part "src/wrappers/analogs.dart";
part "src/wrappers/buttons.dart"; part "src/wrappers/buttons.dart";
part "src/wrappers/capture.dart";
part "src/wrappers/card.dart"; part "src/wrappers/card.dart";
part "src/wrappers/coin.dart"; part "src/wrappers/coin.dart";
part "src/wrappers/control.dart"; part "src/wrappers/control.dart";
@@ -4,8 +4,8 @@ part of spiceapi;
class Connection { class Connection {
// settings // settings
static const _TIMEOUT = Duration(seconds: 2); static const _TIMEOUT = Duration(seconds: 3);
static const _BUFFER_SIZE = 1024 * 8; static const _BUFFER_SIZE = 1024 * 1024 * 8;
// state // state
final String host, pass; final String host, pass;
@@ -0,0 +1,47 @@
part of spiceapi;
class CaptureData {
int timestamp;
int width, height;
Uint8List data;
}
var _base64DecoderInstance = Base64Decoder();
Future<List> captureGetScreens(Connection con) {
var req = Request("capture", "get_screens");
return con.request(req).then((res) {
return res.getData();
});
}
Future<CaptureData> captureGetJPG(Connection con, {
int screen = 0,
int quality = 70,
int divide = 1,
}) {
var req = Request("capture", "get_jpg");
req.addParam(screen);
req.addParam(quality);
req.addParam(divide);
return con.request(req).then((res) {
var captureData = CaptureData();
var data = res.getData();
if (data.length > 0) captureData.timestamp = data[0];
if (data.length > 1) captureData.width = data[1];
if (data.length > 2) captureData.height = data[2];
if (data.length > 3) {
captureData.data = _base64DecoderInstance.convert(data[3]);
}
return captureData;
});
}
/// Describes the HTTP video stream, or null when this spice2x serves none.
Future<Map> captureGetStreams(Connection con) {
var req = Request("capture", "get_streams");
return con.request(req).then((res) {
var data = res.getData();
return data.length > 0 ? data[0] : null;
});
}
@@ -1,5 +1,32 @@
part of spiceapi; part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) { Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert"); var req = Request("card", "insert");
req.addParam(unit); req.addParam(unit);
@@ -17,7 +17,7 @@ Future<List> captureGetScreens(Connection con) {
Future<CaptureData> captureGetJPG(Connection con, { Future<CaptureData> captureGetJPG(Connection con, {
int screen = 0, int screen = 0,
int quality = 60, int quality = 70,
int divide = 1, int divide = 1,
}) { }) {
var req = Request("capture", "get_jpg"); var req = Request("capture", "get_jpg");
@@ -36,3 +36,12 @@ Future<CaptureData> captureGetJPG(Connection con, {
return captureData; return captureData;
}); });
} }
/// Describes the HTTP video stream, or null when this spice2x serves none.
Future<Map> captureGetStreams(Connection con) {
var req = Request("capture", "get_streams");
return con.request(req).then((res) {
var data = res.getData();
return data.length > 0 ? data[0] : null;
});
}
@@ -1,5 +1,32 @@
part of spiceapi; part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) { Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert"); var req = Request("card", "insert");
req.addParam(unit); req.addParam(unit);
@@ -2,6 +2,7 @@ from .connection import Connection
from .request import Request from .request import Request
from .analogs import * from .analogs import *
from .buttons import * from .buttons import *
from .capture import *
from .card import * from .card import *
from .coin import * from .coin import *
from .control import * from .control import *
@@ -0,0 +1,33 @@
import base64
from .connection import Connection
from .request import Request
def capture_get_screens(con: Connection):
res = con.request(Request("capture", "get_screens"))
return res.get_data()
def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1):
req = Request("capture", "get_jpg")
req.add_param(screen)
req.add_param(quality)
req.add_param(divide)
data = con.request(req).get_data()
if len(data) < 4:
return None
return {
"timestamp": data[0],
"width": data[1],
"height": data[2],
"data": base64.b64decode(data[3]),
}
def capture_get_streams(con: Connection):
"""Describes the HTTP video stream, or None when this spice2x serves none."""
data = con.request(Request("capture", "get_streams")).get_data()
return data[0] if data else None
@@ -2,6 +2,10 @@ from .connection import Connection
from .request import Request from .request import Request
def card_get_cards(con: Connection):
return con.request(Request("card", "get_cards")).get_data()
def card_insert(con: Connection, unit: int, card_id: str): def card_insert(con: Connection, unit: int, card_id: str):
req = Request("card", "insert") req = Request("card", "insert")
req.add_param(unit) req.add_param(unit)
+84
View File
@@ -0,0 +1,84 @@
#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;
}
std::vector<std::pair<std::string, std::string>> stream_formats() {
std::vector<std::pair<std::string, std::string>> formats;
#ifdef SPICE_JPEG
formats.emplace_back("mjpeg", "/stream.mjpg");
#endif
#ifdef SPICE_H264
formats.emplace_back("h264", "/stream.h264");
#endif
return formats;
}
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#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);
// name and path of every format compiled into this build, for clients to pick from
std::vector<std::pair<std::string, std::string>> stream_formats();
}
+525
View File
@@ -0,0 +1,525 @@
#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 "overlay/notifications.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";
// the port is unauthenticated, so a scanner hammering a busy/missing screen could
// otherwise flood the overlay; throttle failure toasts per distinct cause. kept under
// a second so it only swallows that, not a legitimate reconnect - substream itself
// switches screens with a 300ms gap, and only backs off to a full second once a
// retry has actually failed
constexpr double notification_throttle_seconds = 0.5;
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);
}
std::atomic<unsigned short> LISTENING_PORT { 0 };
}
unsigned short stream_server_port() {
return LISTENING_PORT.load();
}
StreamServer::StreamServer(unsigned short port)
: port(port)
{
// WinXP builds compile in neither encoder, so there would be nothing to serve and
// every request would 404; taking the port instead only invites confused clients
if (stream_formats().empty()) {
log_warning("api::stream",
"this build has no video encoders, the video stream is unavailable");
return;
}
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();
});
LISTENING_PORT = this->port;
// 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;
LISTENING_PORT = 0;
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);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Warning,
"api::stream.client_limit",
notification_throttle_seconds,
fmt::format("Video stream refused: client limit reached ({})", address));
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);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Warning,
fmt::format("api::stream.screen_unavailable.{}", screen),
notification_throttle_seconds,
fmt::format("Video stream refused: screen {} not available ({})",
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);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Warning,
fmt::format("api::stream.screen_claimed.{}", screen),
notification_throttle_seconds,
fmt::format("Video stream refused: screen {} already streaming ({})",
screen, address));
send_error(socket, "503 Service Unavailable");
} else {
log_info("api::stream",
"client connected: {} ({}, screen={}, fps={}, quality={})",
address, request.path, screen, fps, quality);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Video stream client connected ({}, screen {})",
address, screen));
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);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("Video 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);
}
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <array>
#include <atomic>
#include <cstdint>
#include <mutex>
#include <string>
#include <thread>
#include <winsock2.h>
namespace api {
// 0 while no stream server is listening, so the API can tell clients not to look for one
unsigned short stream_server_port();
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;
};
}
+547 -144
View File
@@ -1,176 +1,579 @@
#define HEADSOCKET_IMPLEMENTATION #include <winsock2.h>
#include "external/headsocket.h" #include <ws2tcpip.h>
#include "websocket.h" #include "websocket.h"
#include "util/utils.h"
#include "util/rc4.h"
#include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h"
using namespace headsocket; #include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "external/wslay/msvc_compat.h"
#include <wslay/wslay.h>
#include "controller.h"
#include "external/hash-library/sha1.h"
#include "overlay/notifications.h"
#include "util/crypt.h"
#include "util/logging.h"
namespace api { namespace api {
/* namespace {
* Client class declaration
*/
class WebSocketClient : public web_socket_client {
// required class header constexpr int server_backlog = 4;
HEADSOCKET_CLIENT(WebSocketClient, web_socket_client); constexpr size_t client_limit = 8;
private: // a peer that connects and then says nothing must not hold a slot forever
constexpr int handshake_timeout_ms = 5000;
constexpr size_t request_size_limit = 8 * 1024;
constexpr uint64_t message_size_limit = 64 * 1024;
// how long a quiet connection waits before the loop rechecks whether we are stopping
constexpr int idle_poll_ms = 500;
// RFC 6455 appends this to the client key before hashing
constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
constexpr double notification_throttle_seconds = 0.5;
std::string trim(const std::string &text) {
const auto begin = text.find_first_not_of(" \t");
if (begin == std::string::npos) {
return "";
}
return text.substr(begin, text.find_last_not_of(" \t") - begin + 1);
}
std::string to_lower(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return text;
}
bool contains_token(const std::string &value, const std::string &expected) {
size_t pos = 0;
while (pos < value.size()) {
const size_t end = value.find(',', pos);
if (to_lower(trim(value.substr(pos, end - pos))) == expected) {
return true;
}
if (end == std::string::npos) {
break;
}
pos = end + 1;
}
return false;
}
bool valid_websocket_key(const std::string &key) {
if (key.size() != 24 || key[22] != '=' || key[23] != '=') {
return false;
}
const std::string alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (size_t i = 0; i < 22; i++) {
if (alphabet.find(key[i]) == std::string::npos) {
return false;
}
}
return true;
}
bool send_all(SOCKET socket, const std::string &text) {
size_t remaining = text.size();
const char *cursor = text.data();
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;
}
void set_recv_timeout(SOCKET socket, int milliseconds) {
const DWORD timeout = static_cast<DWORD>(milliseconds);
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
}
std::string accept_key(const std::string &client_key) {
SHA1 sha1;
sha1.add(client_key.data(), client_key.size());
sha1.add(websocket_guid, strlen(websocket_guid));
unsigned char digest[SHA1::HashBytes] {};
sha1.getHash(digest);
return crypt::base64_encode(digest, sizeof(digest));
}
bool handshake(SOCKET socket) {
std::string request;
char byte = 0;
bool complete = false;
while (request.size() < request_size_limit) {
const int read = recv(socket, &byte, 1, 0);
if (read <= 0) {
return false;
}
request.push_back(byte);
if (request.size() >= 4
&& request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) {
complete = true;
break;
}
}
std::string key;
std::string version;
bool connection_upgrade = false;
bool upgrade = false;
const size_t request_line_end = request.find("\r\n");
const std::string request_line = request.substr(0, request_line_end);
const size_t target_end = request_line.find(' ', 4);
const bool valid_request_line = request_line_end != std::string::npos
&& request_line.compare(0, 4, "GET ") == 0
&& target_end != std::string::npos
&& target_end > 4
&& target_end == request_line.rfind(' ')
&& request_line.substr(target_end + 1) == "HTTP/1.1";
size_t pos = request_line_end;
while (pos != std::string::npos) {
const size_t end = request.find("\r\n", pos + 2);
if (end == std::string::npos || end == pos + 2) {
break;
}
const std::string line = request.substr(pos + 2, end - pos - 2);
const size_t colon = line.find(':');
if (colon != std::string::npos) {
const std::string name = to_lower(trim(line.substr(0, colon)));
const std::string value = trim(line.substr(colon + 1));
if (name == "sec-websocket-key") {
key = value;
} else if (name == "sec-websocket-version") {
version = value;
} else if (name == "connection") {
connection_upgrade = connection_upgrade
|| contains_token(value, "upgrade");
} else if (name == "upgrade") {
upgrade = upgrade || contains_token(value, "websocket");
}
}
pos = end;
}
if (!complete || !valid_request_line || !connection_upgrade || !upgrade
|| version != "13" || !valid_websocket_key(key)) {
send_all(socket,
"HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n"
"Content-Length: 0\r\n"
"\r\n");
return false;
}
// deliberately echoes back no extension or subprotocol: naming one the client did
// not offer is a handshake failure, and none of them are wanted here
return send_all(socket,
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: " + accept_key(key) + "\r\n"
"\r\n");
}
// everything one connection needs; wslay hands this back to the callbacks
struct Session {
SOCKET socket = INVALID_SOCKET;
Controller *controller = nullptr;
ClientState *state = nullptr; ClientState *state = nullptr;
bool failed = false;
protected:
bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override;
void on_accept() override;
void on_disconnect() override;
}; };
/* ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length,
* Server class declaration int flags, void *user_data) {
*/
class WebSocketServer : public web_socket_server<WebSocketClient> {
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
public:
WebSocketController *websocket;
};
void api::WebSocketServer::init() {} (void) flags;
auto *session = static_cast<Session *>(user_data);
const int read = recv(session->socket, reinterpret_cast<char *>(buffer),
static_cast<int>(length), 0);
if (read > 0) {
return read;
}
// the socket is non-blocking, so an empty one has to read as "nothing yet"
// rather than as a dead peer
if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
return -1;
}
wslay_event_set_error(ctx,
read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE);
return -1;
}
ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length,
int flags, void *user_data) {
(void) flags;
auto *session = static_cast<Session *>(user_data);
const int sent = send(session->socket, reinterpret_cast<const char *>(data),
static_cast<int>(length), 0);
if (sent > 0) {
return sent;
}
if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
return -1;
}
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
return -1;
}
void on_msg_recv(wslay_event_context_ptr ctx,
const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
auto *session = static_cast<Session *>(user_data);
// pings and closes are wslay's business, it answers them itself
if (wslay_is_ctrl_frame(arg->opcode)) {
return;
}
if (arg->opcode != WSLAY_BINARY_FRAME) {
log_warning("api::websocket", "ignoring a non-binary message");
return;
}
std::vector<char> in(arg->msg, arg->msg + arg->msg_length);
std::vector<char> out;
if (session->state->cipher) {
session->state->cipher->crypt(
reinterpret_cast<uint8_t *>(in.data()), in.size());
}
session->controller->process_request(session->state, &in, &out);
if (session->state->cipher) {
session->state->cipher->crypt(
reinterpret_cast<uint8_t *>(out.data()), out.size());
}
wslay_event_msg reply {};
reply.opcode = WSLAY_BINARY_FRAME;
reply.msg = reinterpret_cast<const uint8_t *>(out.data());
reply.msg_length = out.size();
if (wslay_event_queue_msg(ctx, &reply) != 0) {
session->failed = true;
return;
}
Controller::process_password_change(session->state);
}
}
/*
* Controller state so we don't have to import headsocket stuff in our header
*/
struct WebSocketControllerState { struct WebSocketControllerState {
std::shared_ptr<WebSocketServer> server; struct Client {
std::thread thread;
SOCKET socket = INVALID_SOCKET;
bool active = false;
}; };
Controller *controller = nullptr;
unsigned short port = 0;
SOCKET listener = INVALID_SOCKET;
bool wsa_started = false;
std::atomic_bool running { false };
std::thread acceptor;
std::mutex clients_m;
std::array<Client, client_limit> clients;
bool open_listener();
void accept_worker();
void client_worker(int slot, SOCKET socket, std::string address);
void stop();
};
bool WebSocketControllerState::open_listener() {
WSADATA wsa_data;
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
if (error != 0) {
log_warning("api::websocket", "WSAStartup() returned {}", error);
return false;
}
this->wsa_started = true;
this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (this->listener == INVALID_SOCKET) {
log_warning("api::websocket", "socket() returned {}", WSAGetLastError());
return false;
}
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(this->port);
if (bind(this->listener, reinterpret_cast<sockaddr *>(&address), sizeof(address))
== SOCKET_ERROR) {
log_warning("api::websocket", "bind() returned {}", WSAGetLastError());
return false;
}
if (listen(this->listener, server_backlog) == SOCKET_ERROR) {
log_warning("api::websocket", "listen() returned {}", WSAGetLastError());
return false;
}
return true;
}
void WebSocketControllerState::accept_worker() {
while (this->running) {
sockaddr_in client_address {};
int client_address_size = sizeof(sockaddr_in);
const SOCKET client = accept(
this->listener, reinterpret_cast<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;
}
// formatted by hand rather than with inet_ntop, which needs a newer Windows than
// the XP toolchain targets, or inet_ntoa, which answers from a shared buffer
const uint32_t raw = ntohl(client_address.sin_addr.s_addr);
const std::string address = fmt::format("{}.{}.{}.{}",
(raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
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::websocket", "client limit of {} hit", client_limit);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Warning,
"api::websocket.client_limit",
notification_throttle_seconds,
fmt::format("API websocket refused: client limit reached ({})", address));
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();
}
// the handshake runs on the client thread on purpose: doing it here would put
// every later connection behind whatever this one is waiting for
this->clients[slot].thread = std::thread([this, slot, client, address] {
this->client_worker(slot, client, address);
});
}
}
void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) {
// wslay writes a frame header and its payload as separate sends, so leaving Nagle on
// holds the payload back until the peer acknowledges the header, costing a delayed
// ack per message; requests here are small and latency sensitive
int nodelay = 1;
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<const char *>(&nodelay), sizeof(nodelay));
set_recv_timeout(socket, handshake_timeout_ms);
if (handshake(socket)) {
// wslay reads until the socket would block, so leaving it blocking would make
// every reply wait out a receive timeout before the send got a turn
u_long non_blocking = 1;
ioctlsocket(socket, FIONBIO, &non_blocking);
Session session;
session.socket = socket;
session.controller = this->controller;
session.state = new ClientState();
this->controller->init_state(session.state);
log_info("api::websocket", "client connected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("API websocket client connected ({})", address));
wslay_event_callbacks callbacks {};
callbacks.recv_callback = recv_callback;
callbacks.send_callback = send_callback;
callbacks.on_msg_recv_callback = on_msg_recv;
wslay_event_context_ptr ctx = nullptr;
if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) {
wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit);
while (this->running && !session.failed
&& (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) {
fd_set read_set;
fd_set write_set;
FD_ZERO(&read_set);
FD_ZERO(&write_set);
if (wslay_event_want_read(ctx)) {
FD_SET(socket, &read_set);
}
if (wslay_event_want_write(ctx)) {
FD_SET(socket, &write_set);
}
// bounded so a silent connection still notices us shutting down
timeval timeout {};
timeout.tv_usec = idle_poll_ms * 1000;
if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) {
break;
}
if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) {
break;
}
// unconditional, so a reply queued by the read above goes out now
if (wslay_event_send(ctx) != 0) {
break;
}
}
wslay_event_context_free(ctx);
}
Controller::free_state(session.state);
delete session.state;
log_info("api::websocket", "client disconnected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("API websocket client disconnected ({})", address));
}
closesocket(socket);
std::lock_guard<std::mutex> lock(this->clients_m);
this->clients[slot].socket = INVALID_SOCKET;
this->clients[slot].active = false;
}
void WebSocketControllerState::stop() {
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();
this->wsa_started = false;
}
}
WebSocketController::WebSocketController(Controller *controller, uint16_t port) { WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
this->controller = controller; this->controller = controller;
// create state
this->state = new WebSocketControllerState(); this->state = new WebSocketControllerState();
this->state->controller = controller;
this->state->port = port;
// start server if (!this->state->open_listener()) {
this->state->server = WebSocketServer::create(port);
this->state->server->websocket = this;
if (this->state->server->is_running()) {
log_info("api::websocket", "server listening on port: {}", port);
} else {
log_warning("api::websocket", "server failed to listen on port: {}", port); log_warning("api::websocket", "server failed to listen on port: {}", port);
return;
} }
this->state->running = true;
this->state->acceptor = std::thread([this] {
this->state->accept_worker();
});
log_info("api::websocket", "server listening on port: {}", port);
} }
WebSocketController::~WebSocketController() { WebSocketController::~WebSocketController() {
this->state->stop();
// stop server
this->state->server->stop();
// delete state
delete this->state; delete this->state;
this->state = nullptr;
} }
void WebSocketController::free_socket() { void WebSocketController::free_socket() {
this->state->server->stop(); this->state->stop();
}
void WebSocketClient::on_accept() {
web_socket_client::on_accept();
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "on_accept has no server");
}
// check for init
state = new ClientState();
srv->websocket->controller->init_state(state);
// log connection
log_info("api::websocket", "client connected");
overlay::notifications::add(
overlay::notifications::Severity::Success,
"API websocket client connected");
}
void WebSocketClient::on_disconnect() {
// log disconnection
log_info("api::websocket", "client disconnected");
overlay::notifications::add(
overlay::notifications::Severity::Info,
"API websocket client disconnected");
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "on_disconnect has no server");
}
// clean up state
srv->websocket->controller->free_state(state);
delete state;
state = nullptr;
// call super
web_socket_client::on_disconnect();
}
/*
* This is where business actually happens, gets called on every datablock receive
*/
bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) {
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "received datablock without server");
}
// check state
if (!state) {
log_fatal("api::websocket", "client with no state received datablock");
}
// check datablock type
switch (db.op) {
case opcode::binary: {
// allocate buffers
std::vector<char> in(ptr, ptr + length);
std::vector<char> out;
// crypt in-data
if (state->cipher) {
state->cipher->crypt(reinterpret_cast<uint8_t *>(in.data()), in.size());
}
// process request
srv->websocket->controller->process_request(state, &in, &out);
// crypt out-data
if (state->cipher) {
state->cipher->crypt(reinterpret_cast<uint8_t *>(out.data()), out.size());
}
// send answer
push(out.data(), out.size());
// check for password change
srv->websocket->controller->process_password_change(state);
break;
}
default:
log_warning("api::websocket", "datablock received with non-binary type");
break;
}
// always consume the datablock, nomnom
return true;
} }
} }
+10
View File
@@ -7,6 +7,7 @@ RUN pacman --noconfirm -Syu git \
ninja \ ninja \
cmake \ cmake \
unzip \ unzip \
nasm \
wget \ wget \
mingw-w64-crt \ mingw-w64-crt \
mingw-w64-winpthreads \ mingw-w64-winpthreads \
@@ -24,3 +25,12 @@ 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" \ 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 | tar -xJ -C /usr/local/bin
# Stock makepkg.conf builds serially; this makes the AUR compiles below parallel.
RUN printf '%s\n' 'MAKEFLAGS="-j$(nproc)"' > /home/user/.makepkg.conf \
&& chown user: /home/user/.makepkg.conf
# libjpeg-turbo for JPEG encoding, x264 for the API H.264 video stream. Only the
# mingw-w64 toolchains get these; the WinXP targets build without JPEG support
# and without the stream encoder.
RUN su user -c "yay --noconfirm -S mingw-w64-libjpeg-turbo mingw-w64-x264"
+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
-2192
View File
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 :-)
+33
View File
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.12)
project(wslay C)
set(WSLAY_HEADERS
msvc_compat.h
wslay_event.h
wslay_frame.h
wslay_macro.h
wslay_net.h
wslay_queue.h
includes/wslay/wslay.h
includes/wslay/wslayver.h
)
set(WSLAY_SOURCES
wslay_event.c
wslay_frame.c
wslay_net.c
wslay_queue.c
)
add_library(wslay STATIC ${WSLAY_HEADERS} ${WSLAY_SOURCES})
# config.h here is checked in rather than generated, see the note in it
target_compile_definitions(wslay PRIVATE HAVE_CONFIG_H)
target_include_directories(wslay PRIVATE ${PROJECT_SOURCE_DIR})
target_include_directories(wslay PUBLIC ${PROJECT_SOURCE_DIR}/includes)
if(MSVC)
target_compile_options(wslay PRIVATE "/FI${PROJECT_SOURCE_DIR}/msvc_compat.h")
else()
# upstream code, and not ours to keep warning clean
target_compile_options(wslay PRIVATE -w)
endif()
+22
View File
@@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
/* Hand written stand-in for the autotools/CMake generated config.h. Spice only
builds this for Windows on x86, so the probes have single known answers. */
#ifndef CONFIG_H
#define CONFIG_H
#define HAVE_WINSOCK2_H
/* x86 and x86_64 are little endian, so WORDS_BIGENDIAN stays undefined */
#endif /* CONFIG_H */
+841
View File
@@ -0,0 +1,841 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_H
#define WSLAY_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
/*
* wslay/wslayver.h is generated from wslay/wslayver.h.in by
* configure. The projects which do not use autotools can set
* WSLAY_VERSION macro from outside to avoid to generating wslayver.h
*/
#ifndef WSLAY_VERSION
# include <wslay/wslayver.h>
#endif /* WSLAY_VERSION */
enum wslay_error {
WSLAY_ERR_WANT_READ = -100,
WSLAY_ERR_WANT_WRITE = -101,
WSLAY_ERR_PROTO = -200,
WSLAY_ERR_INVALID_ARGUMENT = -300,
WSLAY_ERR_INVALID_CALLBACK = -301,
WSLAY_ERR_NO_MORE_MSG = -302,
WSLAY_ERR_CALLBACK_FAILURE = -400,
WSLAY_ERR_WOULDBLOCK = -401,
WSLAY_ERR_NOMEM = -500
};
/*
* Status codes defined in RFC6455
*/
enum wslay_status_code {
WSLAY_CODE_NORMAL_CLOSURE = 1000,
WSLAY_CODE_GOING_AWAY = 1001,
WSLAY_CODE_PROTOCOL_ERROR = 1002,
WSLAY_CODE_UNSUPPORTED_DATA = 1003,
WSLAY_CODE_NO_STATUS_RCVD = 1005,
WSLAY_CODE_ABNORMAL_CLOSURE = 1006,
WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA = 1007,
WSLAY_CODE_POLICY_VIOLATION = 1008,
WSLAY_CODE_MESSAGE_TOO_BIG = 1009,
WSLAY_CODE_MANDATORY_EXT = 1010,
WSLAY_CODE_INTERNAL_SERVER_ERROR = 1011,
WSLAY_CODE_TLS_HANDSHAKE = 1015
};
enum wslay_io_flags {
/*
* There is more data to send.
*/
WSLAY_MSG_MORE = 1
};
/*
* Callback function used by wslay_frame_send() function when it needs
* to send data. The implementation of this function must send at most
* len bytes of data in data. flags is the bitwise OR of zero or more
* of the following flag:
*
* WSLAY_MSG_MORE
* There is more data to send
*
* It provides some hints to tune performance and behaviour. user_data
* is one given in wslay_frame_context_init() function. The
* implementation of this function must return the number of bytes
* sent. If there is an error, return -1. The return value 0 is also
* treated an error by the library.
*/
typedef ssize_t (*wslay_frame_send_callback)(const uint8_t *data, size_t len,
int flags, void *user_data);
/*
* Callback function used by wslay_frame_recv() function when it needs
* more data. The implementation of this function must fill at most
* len bytes of data into buf. The memory area of buf is allocated by
* library and not be freed by the application code. flags is always 0
* in this version. user_data is one given in
* wslay_frame_context_init() function. The implementation of this
* function must return the number of bytes filled. If there is an
* error, return -1. The return value 0 is also treated an error by
* the library.
*/
typedef ssize_t (*wslay_frame_recv_callback)(uint8_t *buf, size_t len,
int flags, void *user_data);
/*
* Callback function used by wslay_frame_send() function when it needs
* new mask key. The implementation of this function must write
* exactly len bytes of mask key to buf. user_data is one given in
* wslay_frame_context_init() function. The implementation of this
* function return 0 on success. If there is an error, return -1.
*/
typedef int (*wslay_frame_genmask_callback)(uint8_t *buf, size_t len,
void *user_data);
struct wslay_frame_callbacks {
wslay_frame_send_callback send_callback;
wslay_frame_recv_callback recv_callback;
wslay_frame_genmask_callback genmask_callback;
};
/*
* The opcode defined in RFC6455.
*/
enum wslay_opcode {
WSLAY_CONTINUATION_FRAME = 0x0u,
WSLAY_TEXT_FRAME = 0x1u,
WSLAY_BINARY_FRAME = 0x2u,
WSLAY_CONNECTION_CLOSE = 0x8u,
WSLAY_PING = 0x9u,
WSLAY_PONG = 0xau
};
/*
* Macro that returns 1 if opcode is control frame opcode, otherwise
* returns 0.
*/
#define wslay_is_ctrl_frame(opcode) ((opcode >> 3) & 1)
/*
* Macros that represent and return reserved bits: RSV1, RSV2, RSV3.
* These macros assume that rsv is constructed by ((RSV1 << 2) |
* (RSV2 << 1) | RSV3)
*/
#define WSLAY_RSV_NONE ((uint8_t)0)
#define WSLAY_RSV1_BIT (((uint8_t)1) << 2)
#define WSLAY_RSV2_BIT (((uint8_t)1) << 1)
#define WSLAY_RSV3_BIT (((uint8_t)1) << 0)
#define wslay_get_rsv1(rsv) ((rsv >> 2) & 1)
#define wslay_get_rsv2(rsv) ((rsv >> 1) & 1)
#define wslay_get_rsv3(rsv) (rsv & 1)
struct wslay_frame_iocb {
/* 1 for fragmented final frame, 0 for otherwise */
uint8_t fin;
/*
* reserved 3 bits. rsv = ((RSV1 << 2) | (RSV << 1) | RSV3).
* RFC6455 requires 0 unless extensions are negotiated.
*/
uint8_t rsv;
/* 4 bit opcode */
uint8_t opcode;
/* payload length [0, 2**63-1] */
uint64_t payload_length;
/* 1 for masked frame, 0 for unmasked */
uint8_t mask;
/* part of payload data */
const uint8_t *data;
/* bytes of data defined above */
size_t data_length;
};
struct wslay_frame_context;
typedef struct wslay_frame_context *wslay_frame_context_ptr;
/*
* Initializes ctx using given callbacks and user_data. This function
* allocates memory for struct wslay_frame_context and stores the
* result to *ctx. The callback functions specified in callbacks are
* copied to ctx. user_data is stored in ctx and it will be passed to
* callback functions. When the user code finished using ctx, it must
* call wslay_frame_context_free to deallocate memory.
*/
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
const struct wslay_frame_callbacks *callbacks,
void *user_data);
/*
* Deallocates memory pointed by ctx.
*/
void wslay_frame_context_free(wslay_frame_context_ptr ctx);
/*
* Send WebSocket frame specified in iocb. ctx must be initialized
* using wslay_frame_context_init() function. iocb->fin must be 1 if
* this is a fin frame, otherwise 0. iocb->rsv is reserved bits.
* iocb->opcode must be the opcode of this frame. iocb->mask must be
* 1 if this is masked frame, otherwise 0. iocb->payload_length is
* the payload_length of this frame. iocb->data must point to the
* payload data to be sent. iocb->data_length must be the length of
* the data. This function calls send_callback function if it needs
* to send bytes. This function calls gen_mask_callback function if
* it needs new mask key. This function returns the number of payload
* bytes sent. Please note that it does not include any number of
* header bytes. If it cannot send any single bytes of payload, it
* returns WSLAY_ERR_WANT_WRITE. If the library detects error in iocb,
* this function returns WSLAY_ERR_INVALID_ARGUMENT. If callback
* functions report a failure, this function returns
* WSLAY_ERR_INVALID_CALLBACK. This function does not always send all
* given data in iocb. If there are remaining data to be sent, adjust
* data and data_length in iocb accordingly and call this function
* again.
*/
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb);
/*
* Write WebSocket frame specified in iocb to buf of length
* buflen. ctx must be initialized using wslay_frame_context_init()
* function. iocb->fin must be 1 if this is a fin frame, otherwise 0.
* iocb->rsv is reserved bits. iocb->opcode must be the opcode of
* this frame. iocb->mask must be 1 if this is masked frame,
* otherwise 0. iocb->payload_length is the payload_length of this
* frame. iocb->data must point to the payload data to be
* sent. iocb->data_length must be the length of the data. Unlike
* wslay_frame_send, this function does not call send_callback
* function. This function calls gen_mask_callback function if it
* needs new mask key. This function returns the number of bytes
* written to a buffer. Unlike wslay_frame_send, it includes the
* number of header bytes. Instead, the number of payload bytes
* written is assigned to *pwpayloadlen if this function succeeds. If
* there is not enough space left in a buffer, it returns 0. If the
* library detects error in iocb, this function returns
* WSLAY_ERR_INVALID_ARGUMENT. If callback functions report a
* failure, this function returns WSLAY_ERR_INVALID_CALLBACK. This
* function does not always send all given data in iocb. If there are
* remaining data to be sent, adjust data and data_length in iocb
* accordingly and call this function again.
*/
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb, uint8_t *buf,
size_t buflen, size_t *pwpayloadlen);
/*
* Receives WebSocket frame and stores it in iocb. This function
* returns the number of payload bytes received. This does not
* include header bytes. In this case, iocb will be populated as
* follows: iocb->fin is 1 if received frame is fin frame, otherwise
* 0. iocb->rsv is reserved bits of received frame. iocb->opcode is
* opcode of received frame. iocb->mask is 1 if received frame is
* masked, otherwise 0. iocb->payload_length is the payload length of
* received frame. iocb->data is pointed to the buffer containing
* received payload data. This buffer is allocated by the library and
* must be read-only. iocb->data_length is the number of payload
* bytes recieved. This function calls recv_callback if it needs to
* receive additional bytes. If it cannot receive any single bytes of
* payload, it returns WSLAY_ERR_WANT_READ. If the library detects
* protocol violation in a received frame, this function returns
* WSLAY_ERR_PROTO. If callback functions report a failure, this
* function returns WSLAY_ERR_INVALID_CALLBACK. This function does
* not always receive whole frame in a single call. If there are
* remaining data to be received, call this function again. This
* function ensures frame alignment.
*/
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb);
struct wslay_event_context;
/* Pointer to the event-based API context */
typedef struct wslay_event_context *wslay_event_context_ptr;
struct wslay_event_on_msg_recv_arg {
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
uint8_t rsv;
/* opcode */
uint8_t opcode;
/* received message */
const uint8_t *msg;
/* message length */
size_t msg_length;
/*
* Status code iff opcode == WSLAY_CONNECTION_CLOSE. If no status
* code is included in the close control frame, it is set to 0.
*/
uint16_t status_code;
};
/*
* Callback function invoked by wslay_event_recv() when a message is
* completely received.
*/
typedef void (*wslay_event_on_msg_recv_callback)(
wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg,
void *user_data);
struct wslay_event_on_frame_recv_start_arg {
/* fin bit; 1 for final frame, or 0. */
uint8_t fin;
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
uint8_t rsv;
/* opcode of the frame */
uint8_t opcode;
/* payload length of ths frame */
uint64_t payload_length;
};
/*
* Callback function invoked by wslay_event_recv() when a new frame
* starts to be received. This callback function is only invoked once
* for each frame.
*/
typedef void (*wslay_event_on_frame_recv_start_callback)(
wslay_event_context_ptr ctx,
const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data);
struct wslay_event_on_frame_recv_chunk_arg {
/* chunk of payload data */
const uint8_t *data;
/* length of data */
size_t data_length;
};
/*
* Callback function invoked by wslay_event_recv() when a chunk of
* frame payload is received.
*/
typedef void (*wslay_event_on_frame_recv_chunk_callback)(
wslay_event_context_ptr ctx,
const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data);
/*
* Callback function invoked by wslay_event_recv() when a frame is
* completely received.
*/
typedef void (*wslay_event_on_frame_recv_end_callback)(
wslay_event_context_ptr ctx, void *user_data);
/*
* Callback function invoked by wslay_event_recv() when it wants to
* receive more data from peer. The implementation of this callback
* function must read data at most len bytes from peer and store them
* in buf and return the number of bytes read. flags is always 0 in
* this version.
*
* If there is an error, return -1 and set error code
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
* event-based API on the whole assumes non-blocking I/O. If the cause
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
* instead. This is important because it tells wslay_event_recv() to
* stop receiving further data and return.
*/
typedef ssize_t (*wslay_event_recv_callback)(wslay_event_context_ptr ctx,
uint8_t *buf, size_t len,
int flags, void *user_data);
/*
* Callback function invoked by wslay_event_send() when it wants to
* send more data to peer. The implementation of this callback
* function must send data at most len bytes to peer and return the
* number of bytes sent. flags is the bitwise OR of zero or more of
* the following flag:
*
* WSLAY_MSG_MORE
* There is more data to send
*
* It provides some hints to tune performance and behaviour.
*
* If there is an error, return -1 and set error code
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
* event-based API on the whole assumes non-blocking I/O. If the cause
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
* instead. This is important because it tells wslay_event_send() to
* stop sending data and return.
*/
typedef ssize_t (*wslay_event_send_callback)(wslay_event_context_ptr ctx,
const uint8_t *data, size_t len,
int flags, void *user_data);
/*
* Callback function invoked by wslay_event_send() when it wants new
* mask key. As described in RFC6455, only the traffic from WebSocket
* client is masked, so this callback function is only needed if an
* event-based API is initialized for WebSocket client use.
*/
typedef int (*wslay_event_genmask_callback)(wslay_event_context_ptr ctx,
uint8_t *buf, size_t len,
void *user_data);
struct wslay_event_callbacks {
wslay_event_recv_callback recv_callback;
wslay_event_send_callback send_callback;
wslay_event_genmask_callback genmask_callback;
wslay_event_on_frame_recv_start_callback on_frame_recv_start_callback;
wslay_event_on_frame_recv_chunk_callback on_frame_recv_chunk_callback;
wslay_event_on_frame_recv_end_callback on_frame_recv_end_callback;
wslay_event_on_msg_recv_callback on_msg_recv_callback;
};
/*
* Initializes ctx as WebSocket Server. user_data is an arbitrary
* pointer, which is directly passed to each callback functions as
* user_data argument.
*
* On success, returns 0. On error, returns one of following negative
* values:
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_context_server_init(
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
void *user_data);
/*
* Initializes ctx as WebSocket client. user_data is an arbitrary
* pointer, which is directly passed to each callback functions as
* user_data argument.
*
* On success, returns 0. On error, returns one of following negative
* values:
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_context_client_init(
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
void *user_data);
/*
* Releases allocated resources for ctx.
*/
void wslay_event_context_free(wslay_event_context_ptr ctx);
/*
* Sets a bit mask of allowed reserved bits.
* Currently only permitted values are WSLAY_RSV1_BIT to allow PMCE
* extension (see RFC-7692) or WSLAY_RSV_NONE to disable.
*
* Default: WSLAY_RSV_NONE
*/
void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx,
uint8_t rsv);
/*
* Enables or disables buffering of an entire message for non-control
* frames. If val is 0, buffering is enabled. Otherwise, buffering is
* disabled. If wslay_event_on_msg_recv_callback is invoked when
* buffering is disabled, the msg_length member of struct
* wslay_event_on_msg_recv_arg is set to 0.
*
* The control frames are always buffered regardless of this function call.
*
* This function must not be used after the first invocation of
* wslay_event_recv() function.
*/
void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val);
/*
* Sets maximum length of a message that can be received. The length
* of message is checked by wslay_event_recv() function. If the length
* of a message is larger than this value, reading operation is
* disabled (same effect with wslay_event_shutdown_read() call) and
* close control frame with WSLAY_CODE_MESSAGE_TOO_BIG is queued. If
* buffering for non-control frames is disabled, the library checks
* each frame payload length and does not check length of entire
* message.
*
* The default value is (1u << 31)-1.
*/
void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx,
uint64_t val);
/*
* Sets callbacks to ctx. The callbacks previously set by this function
* or wslay_event_context_server_init() or
* wslay_event_context_client_init() are replaced with callbacks.
*/
void wslay_event_config_set_callbacks(
wslay_event_context_ptr ctx, const struct wslay_event_callbacks *callbacks);
/*
* Receives messages from peer. When receiving
* messages, it uses wslay_event_recv_callback function. Single call
* of this function receives multiple messages until
* wslay_event_recv_callback function sets error code
* WSLAY_ERR_WOULDBLOCK.
*
* When close control frame is received, this function automatically
* queues close control frame. Also this function calls
* wslay_event_set_read_enabled() with second argument 0 to disable
* further read from peer.
*
* When ping control frame is received, this function automatically
* queues pong control frame.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_read_enabled() with second argument
* 0 to disable further read from peer.
*
* wslay_event_recv() returns 0 if it succeeds, or one of the
* following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_recv() and must close WebSocket
* connection.
*/
int wslay_event_recv(wslay_event_context_ptr ctx);
/*
* Sends queued messages to peer. When sending a
* message, it uses wslay_event_send_callback function. Single call of
* wslay_event_send() sends multiple messages until
* wslay_event_send_callback sets error code WSLAY_ERR_WOULDBLOCK.
*
* If ctx is initialized for WebSocket client use, wslay_event_send()
* uses wslay_event_genmask_callback to get new mask key.
*
* When a message queued using wslay_event_queue_fragmented_msg() is
* sent, wslay_event_send() invokes
* wslay_event_fragmented_msg_callback for that message.
*
* After close control frame is sent, this function calls
* wslay_event_set_write_enabled() with second argument 0 to disable
* further transmission to peer.
*
* If there are any pending messages, wslay_event_want_write() returns
* 1, otherwise returns 0.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_write_enabled() with second argument
* 0 to disable further transmission to peer.
*
* wslay_event_send() returns 0 if it succeeds, or one of the
* following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_send() and must close WebSocket
* connection.
*/
int wslay_event_send(wslay_event_context_ptr ctx);
/*
* Writes queued messages to a buffer. Unlike wslay_event_send(), this
* function writes messages into the given buffer. It does not use
* wslay_event_send_callback function. Single call of
* wslay_event_write() writes multiple messages until there is not
* enough space left in a buffer.
*
* If ctx is initialized for WebSocket client use, wslay_event_write()
* uses wslay_event_genmask_callback to get new mask key.
*
* buf is a pointer to buffer and its capacity is given in buflen. It
* should have at least 14 bytes.
*
* When a message queued using wslay_event_queue_fragmented_msg() is
* sent, wslay_event_write() invokes
* wslay_event_fragmented_msg_callback for that message.
*
* After close control frame is sent, this function calls
* wslay_event_set_write_enabled() with second argument 0 to disable
* further transmission to peer.
*
* If there are any pending messages, wslay_event_want_write() returns
* 1, otherwise returns 0.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_write_enabled() with second argument
* 0 to disable further transmission to peer.
*
* wslay_event_write() returns the number of bytes written to a buffer
* if it succeeds, or one of the following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_write() and must close WebSocket
* connection.
*/
ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf,
size_t buflen);
struct wslay_event_msg {
uint8_t opcode;
const uint8_t *msg;
size_t msg_length;
};
/*
* Queues message specified in arg.
*
* This function supports both control and non-control messages and
* the given message is sent without fragmentation. If fragmentation
* is needed, use wslay_event_queue_fragmented_msg() function instead.
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_msg() returns 0 if it succeeds, or returns the
* following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_msg(wslay_event_context_ptr ctx,
const struct wslay_event_msg *arg);
/*
* Extended version of wslay_event_queue_msg which allows to set reserved bits.
*/
int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx,
const struct wslay_event_msg *arg, uint8_t rsv);
/*
* Specify "source" to generate message.
*/
union wslay_event_msg_source {
int fd;
void *data;
};
/*
* Callback function called by wslay_event_send() to read message data
* from source. The implementation of
* wslay_event_fragmented_msg_callback must store at most len bytes of
* data to buf and return the number of stored bytes. If all data is
* read (i.e., EOF), set *eof to 1. If no data can be generated at the
* moment, return 0. If there is an error, return -1 and set error
* code WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error().
*/
typedef ssize_t (*wslay_event_fragmented_msg_callback)(
wslay_event_context_ptr ctx, uint8_t *buf, size_t len,
const union wslay_event_msg_source *source, int *eof, void *user_data);
struct wslay_event_fragmented_msg {
/* opcode */
uint8_t opcode;
/* "source" to generate message data */
union wslay_event_msg_source source;
/* Callback function to read message data from source. */
wslay_event_fragmented_msg_callback read_callback;
};
/*
* Queues a fragmented message specified in arg.
*
* This function supports non-control messages only. For control frames,
* use wslay_event_queue_msg() or wslay_event_queue_close().
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_fragmented_msg() returns 0 if it succeeds, or
* returns the following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_fragmented_msg(
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg);
/*
* Extended version of wslay_event_queue_fragmented_msg which allows to set
* reserved bits.
*/
int wslay_event_queue_fragmented_msg_ex(
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg,
uint8_t rsv);
/*
* Queues close control frame. This function is provided just for
* convenience. wslay_event_queue_msg() can queue a close control
* frame as well. status_code is the status code of close control
* frame. reason is the close reason encoded in UTF-8. reason_length
* is the length of reason in bytes. reason_length must be less than
* 123 bytes.
*
* If status_code is 0, reason and reason_length is not used and close
* control frame with zero-length payload will be queued.
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_close() returns 0 if it succeeds, or returns the
* following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code,
const uint8_t *reason, size_t reason_length);
/*
* Sets error code to tell the library there is an error. This
* function is typically used in user defined callback functions. See
* the description of callback function to know which error code
* should be used.
*/
void wslay_event_set_error(wslay_event_context_ptr ctx, int val);
/*
* Query whehter the library want to read more data from peer.
*
* wslay_event_want_read() returns 1 if the library want to read more
* data from peer, or returns 0.
*/
int wslay_event_want_read(wslay_event_context_ptr ctx);
/*
* Query whehter the library want to send more data to peer.
*
* wslay_event_want_write() returns 1 if the library want to send more
* data to peer, or returns 0.
*/
int wslay_event_want_write(wslay_event_context_ptr ctx);
/*
* Prevents the event-based API context from reading any further data
* from peer.
*
* This function may be used with wslay_event_queue_close() if the
* application detects error in the data received and wants to fail
* WebSocket connection.
*/
void wslay_event_shutdown_read(wslay_event_context_ptr ctx);
/*
* Prevents the event-based API context from sending any further data
* to peer.
*/
void wslay_event_shutdown_write(wslay_event_context_ptr ctx);
/*
* Returns 1 if the event-based API context allows read operation, or
* return 0.
*
* After wslay_event_shutdown_read() is called,
* wslay_event_get_read_enabled() returns 0.
*/
int wslay_event_get_read_enabled(wslay_event_context_ptr ctx);
/*
* Returns 1 if the event-based API context allows write operation, or
* return 0.
*
* After wslay_event_shutdown_write() is called,
* wslay_event_get_write_enabled() returns 0.
*/
int wslay_event_get_write_enabled(wslay_event_context_ptr ctx);
/*
* Returns 1 if a close control frame has been received from peer, or
* returns 0.
*/
int wslay_event_get_close_received(wslay_event_context_ptr ctx);
/*
* Returns 1 if a close control frame has been sent to peer, or
* returns 0.
*/
int wslay_event_get_close_sent(wslay_event_context_ptr ctx);
/*
* Returns status code received in close control frame. If no close
* control frame has not been received, returns
* WSLAY_CODE_ABNORMAL_CLOSURE. If received close control frame has no
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
*/
uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx);
/*
* Returns status code sent in close control frame. If no close
* control frame has not been sent, returns
* WSLAY_CODE_ABNORMAL_CLOSURE. If sent close control frame has no
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
*/
uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx);
/*
* Returns the number of queued messages.
*/
size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx);
/*
* Returns the sum of queued message length. It only counts the
* message length queued using wslay_event_queue_msg() or
* wslay_event_queue_close().
*/
size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx);
#ifdef __cplusplus
}
#endif
#endif /* WSLAY_H */
+31
View File
@@ -0,0 +1,31 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAYVER_H
#define WSLAYVER_H
/* normally substituted by the build system; pinned to the vendored release */
#define WSLAY_VERSION "1.1.1"
#endif /* WSLAYVER_H */
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#ifdef _MSC_VER
#include <BaseTsd.h>
#ifndef _SSIZE_T_DEFINED
#define _SSIZE_T_DEFINED
typedef SSIZE_T ssize_t;
#endif
#endif
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_EVENT_H
#define WSLAY_EVENT_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#include "wslay_queue.h"
struct wslay_event_byte_chunk {
struct wslay_queue_entry qe;
uint8_t *data;
size_t data_length;
};
struct wslay_event_imsg {
uint8_t fin;
uint8_t rsv;
uint8_t opcode;
uint32_t utf8state;
struct wslay_queue chunks;
size_t msg_length;
};
enum wslay_event_msg_type { WSLAY_NON_FRAGMENTED, WSLAY_FRAGMENTED };
struct wslay_event_omsg {
struct wslay_queue_entry qe;
uint8_t fin;
uint8_t opcode;
uint8_t rsv;
enum wslay_event_msg_type type;
uint8_t *data;
size_t data_length;
union wslay_event_msg_source source;
wslay_event_fragmented_msg_callback read_callback;
};
struct wslay_event_frame_user_data {
wslay_event_context_ptr ctx;
void *user_data;
};
enum wslay_event_close_status {
WSLAY_CLOSE_RECEIVED = 1 << 0,
WSLAY_CLOSE_QUEUED = 1 << 1,
WSLAY_CLOSE_SENT = 1 << 2
};
enum wslay_event_config { WSLAY_CONFIG_NO_BUFFERING = 1 << 0 };
struct wslay_event_context {
/* config status, bitwise OR of enum wslay_event_config values*/
uint32_t config;
/* maximum message length that can be received */
uint64_t max_recv_msg_length;
/* 1 if initialized for server, otherwise 0 */
uint8_t server;
/* bitwise OR of enum wslay_event_close_status values */
uint8_t close_status;
/* status code in received close control frame */
uint16_t status_code_recv;
/* status code in sent close control frame */
uint16_t status_code_sent;
wslay_frame_context_ptr frame_ctx;
/* 1 if reading is enabled, otherwise 0. Upon receiving close
control frame this value set to 0. If any errors in read
operation will also set this value to 0. */
uint8_t read_enabled;
/* 1 if writing is enabled, otherwise 0 Upon completing sending
close control frame, this value set to 0. If any errors in write
opration will also set this value to 0. */
uint8_t write_enabled;
/* imsg buffer to allow interleaved control frame between
non-control frames. */
struct wslay_event_imsg imsgs[2];
/* Pointer to imsgs to indicate current used buffer. */
struct wslay_event_imsg *imsg;
/* payload length of frame currently being received. */
uint64_t ipayloadlen;
/* next byte offset of payload currently being received. */
uint64_t ipayloadoff;
/* error value set by user callback */
int error;
/* Pointer to the message currently being sent. NULL if no message
is currently sent. */
struct wslay_event_omsg *omsg;
/* Queue for non-control frames */
struct wslay_queue /*<wslay_omsg*>*/ send_queue;
/* Queue for control frames */
struct wslay_queue /*<wslay_omsg*>*/ send_ctrl_queue;
/* Size of send_queue + size of send_ctrl_queue */
size_t queued_msg_count;
/* The sum of message length in send_queue */
size_t queued_msg_length;
/* Buffer used for fragmented messages */
uint8_t obuf[4096];
uint8_t *obuflimit;
uint8_t *obufmark;
/* payload length of frame currently being sent. */
uint64_t opayloadlen;
/* next byte offset of payload currently being sent. */
uint64_t opayloadoff;
struct wslay_event_callbacks callbacks;
struct wslay_event_frame_user_data frame_user_data;
void *user_data;
uint8_t allowed_rsv_bits;
};
#endif /* WSLAY_EVENT_H */
+438
View File
@@ -0,0 +1,438 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_frame.h"
#include <stddef.h>
#include <string.h>
#include <assert.h>
#include "wslay_net.h"
#define wslay_min(A, B) (((A) < (B)) ? (A) : (B))
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
const struct wslay_frame_callbacks *callbacks,
void *user_data) {
*ctx = malloc(sizeof(struct wslay_frame_context));
if (*ctx == NULL) {
return -1;
}
memset(*ctx, 0, sizeof(struct wslay_frame_context));
(*ctx)->istate = RECV_HEADER1;
(*ctx)->ireqread = 2;
(*ctx)->ostate = PREP_HEADER;
(*ctx)->user_data = user_data;
(*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf;
(*ctx)->callbacks = *callbacks;
return 0;
}
void wslay_frame_context_free(wslay_frame_context_ptr ctx) { free(ctx); }
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb) {
if (iocb->data_length > iocb->payload_length) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (ctx->ostate == PREP_HEADER) {
uint8_t *hdptr = ctx->oheader;
memset(ctx->oheader, 0, sizeof(ctx->oheader));
*hdptr |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
*hdptr |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
/* Suppress stubborn gcc-10 warning */
*hdptr |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
++hdptr;
*hdptr |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->payload_length < 126) {
*hdptr |= (uint8_t)iocb->payload_length;
++hdptr;
} else if (iocb->payload_length < (1 << 16)) {
uint16_t len = htons((uint16_t)iocb->payload_length);
*hdptr |= 126;
++hdptr;
memcpy(hdptr, &len, 2);
hdptr += 2;
} else if (iocb->payload_length < (1ull << 63)) {
uint64_t len = hton64(iocb->payload_length);
*hdptr |= 127;
++hdptr;
memcpy(hdptr, &len, 8);
hdptr += 8;
} else {
/* Too large payload length */
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->mask) {
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
0) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->omask = 1;
memcpy(hdptr, ctx->omaskkey, 4);
hdptr += 4;
}
}
ctx->ostate = SEND_HEADER;
ctx->oheadermark = ctx->oheader;
ctx->oheaderlimit = hdptr;
ctx->opayloadlen = iocb->payload_length;
ctx->opayloadoff = 0;
}
if (ctx->ostate == SEND_HEADER) {
ptrdiff_t len = ctx->oheaderlimit - ctx->oheadermark;
ssize_t r;
int flags = 0;
if (iocb->data_length > 0) {
flags |= WSLAY_MSG_MORE;
}
r = ctx->callbacks.send_callback(ctx->oheadermark, (size_t)len, flags,
ctx->user_data);
if (r > 0) {
if (r > len) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->oheadermark += r;
if (ctx->oheadermark == ctx->oheaderlimit) {
ctx->ostate = SEND_PAYLOAD;
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
if (ctx->ostate == SEND_PAYLOAD) {
size_t totallen = 0;
if (iocb->data_length > 0) {
if (ctx->omask) {
uint8_t temp[4096];
const uint8_t *datamark = iocb->data,
*datalimit = iocb->data + iocb->data_length;
while (datamark < datalimit) {
size_t datalen = (size_t)(datalimit - datamark);
const uint8_t *writelimit =
datamark + wslay_min(sizeof(temp), datalen);
size_t writelen = (size_t)(writelimit - datamark);
ssize_t r;
size_t i;
for (i = 0; i < writelen; ++i) {
temp[i] = datamark[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
}
r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data);
if (r > 0) {
if ((size_t)r > writelen) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
datamark += r;
ctx->opayloadoff += (uint64_t)r;
totallen += (size_t)r;
}
} else {
if (totallen > 0) {
break;
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
}
} else {
ssize_t r;
r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0,
ctx->user_data);
if (r > 0) {
if ((size_t)r > iocb->data_length) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->opayloadoff += (uint64_t)r;
totallen = (size_t)r;
}
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
}
if (ctx->opayloadoff == ctx->opayloadlen) {
ctx->ostate = PREP_HEADER;
}
return (ssize_t)totallen;
}
return WSLAY_ERR_INVALID_ARGUMENT;
}
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb, uint8_t *buf,
size_t buflen, size_t *pwpayloadlen) {
uint8_t *buf_last = buf;
size_t i;
size_t hdlen;
*pwpayloadlen = 0;
if (iocb->data_length > iocb->payload_length) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
switch (ctx->ostate) {
case PREP_HEADER:
case PREP_HEADER_NOBUF:
hdlen = 2;
if (iocb->payload_length < 126) {
/* nothing to do */
} else if (iocb->payload_length < (1 << 16)) {
hdlen += 2;
} else if (iocb->payload_length < (1ull << 63)) {
hdlen += 8;
}
if (iocb->mask) {
hdlen += 4;
}
if (buflen < hdlen) {
ctx->ostate = PREP_HEADER_NOBUF;
return 0;
}
memset(buf_last, 0, hdlen);
*buf_last |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
*buf_last |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
/* Suppress stubborn gcc-10 warning */
*buf_last |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
++buf_last;
*buf_last |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->payload_length < 126) {
*buf_last |= (uint8_t)iocb->payload_length;
++buf_last;
} else if (iocb->payload_length < (1 << 16)) {
uint16_t len = htons((uint16_t)iocb->payload_length);
*buf_last |= 126;
++buf_last;
memcpy(buf_last, &len, 2);
buf_last += 2;
} else if (iocb->payload_length < (1ull << 63)) {
uint64_t len = hton64(iocb->payload_length);
*buf_last |= 127;
++buf_last;
memcpy(buf_last, &len, 8);
buf_last += 8;
} else {
/* Too large payload length */
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->mask) {
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
0) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->omask = 1;
memcpy(buf_last, ctx->omaskkey, 4);
buf_last += 4;
}
}
ctx->ostate = SEND_PAYLOAD;
ctx->opayloadlen = iocb->payload_length;
ctx->opayloadoff = 0;
buflen -= (size_t)(buf_last - buf);
/* fall through */
case SEND_PAYLOAD:
if (iocb->data_length > 0) {
size_t writelen = wslay_min(buflen, iocb->data_length);
if (ctx->omask) {
for (i = 0; i < writelen; ++i) {
*buf_last++ =
iocb->data[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
}
} else {
memcpy(buf_last, iocb->data, writelen);
buf_last += writelen;
}
ctx->opayloadoff += writelen;
*pwpayloadlen = writelen;
}
if (ctx->opayloadoff == ctx->opayloadlen) {
ctx->ostate = PREP_HEADER;
}
return buf_last - buf;
default:
return WSLAY_ERR_INVALID_ARGUMENT;
}
}
static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) {
ptrdiff_t len = ctx->ibuflimit - ctx->ibufmark;
memmove(ctx->ibuf, ctx->ibufmark, (size_t)len);
ctx->ibuflimit = ctx->ibuf + len;
ctx->ibufmark = ctx->ibuf;
}
static ssize_t wslay_recv(wslay_frame_context_ptr ctx) {
ssize_t r;
if (ctx->ibufmark != ctx->ibuf) {
wslay_shift_ibuf(ctx);
}
r = ctx->callbacks.recv_callback(
ctx->ibuflimit, (size_t)(ctx->ibuf + sizeof(ctx->ibuf) - ctx->ibuflimit),
0, ctx->user_data);
if (r > 0) {
ctx->ibuflimit += r;
} else {
r = WSLAY_ERR_WANT_READ;
}
return r;
}
#define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark))
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb) {
ssize_t r;
if (ctx->istate == RECV_HEADER1) {
uint8_t fin, opcode, rsv, payloadlen;
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
fin = (ctx->ibufmark[0] >> 7) & 1;
rsv = (ctx->ibufmark[0] >> 4) & 7;
opcode = ctx->ibufmark[0] & 0xfu;
ctx->iom.opcode = opcode;
ctx->iom.fin = fin;
ctx->iom.rsv = rsv;
++ctx->ibufmark;
ctx->imask = (ctx->ibufmark[0] >> 7) & 1;
payloadlen = ctx->ibufmark[0] & 0x7fu;
++ctx->ibufmark;
if (wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) {
return WSLAY_ERR_PROTO;
}
if (payloadlen == 126) {
ctx->istate = RECV_EXT_PAYLOADLEN;
ctx->ireqread = 2;
} else if (payloadlen == 127) {
ctx->istate = RECV_EXT_PAYLOADLEN;
ctx->ireqread = 8;
} else {
ctx->ipayloadlen = payloadlen;
ctx->ipayloadoff = 0;
if (ctx->imask) {
ctx->istate = RECV_MASKKEY;
ctx->ireqread = 4;
} else {
ctx->istate = RECV_PAYLOAD;
}
}
}
if (ctx->istate == RECV_EXT_PAYLOADLEN) {
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
}
ctx->ipayloadlen = 0;
ctx->ipayloadoff = 0;
memcpy((uint8_t *)&ctx->ipayloadlen + (8 - ctx->ireqread), ctx->ibufmark,
ctx->ireqread);
ctx->ipayloadlen = ntoh64(ctx->ipayloadlen);
ctx->ibufmark += ctx->ireqread;
if (ctx->ireqread == 8) {
if (ctx->ipayloadlen < (1 << 16) || ctx->ipayloadlen & (1ull << 63)) {
return WSLAY_ERR_PROTO;
}
} else if (ctx->ipayloadlen < 126) {
return WSLAY_ERR_PROTO;
}
if (ctx->imask) {
ctx->istate = RECV_MASKKEY;
ctx->ireqread = 4;
} else {
ctx->istate = RECV_PAYLOAD;
}
}
if (ctx->istate == RECV_MASKKEY) {
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
}
memcpy(ctx->imaskkey, ctx->ibufmark, 4);
ctx->ibufmark += 4;
ctx->istate = RECV_PAYLOAD;
}
if (ctx->istate == RECV_PAYLOAD) {
uint8_t *readlimit, *readmark;
uint64_t rempayloadlen = ctx->ipayloadlen - ctx->ipayloadoff;
if (WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
}
readmark = ctx->ibufmark;
readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen
? ctx->ibuflimit
: ctx->ibufmark + rempayloadlen;
if (ctx->imask) {
for (; ctx->ibufmark != readlimit; ++ctx->ibufmark, ++ctx->ipayloadoff) {
ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4];
}
} else {
ctx->ibufmark = readlimit;
ctx->ipayloadoff += (uint64_t)(readlimit - readmark);
}
iocb->fin = ctx->iom.fin;
iocb->rsv = ctx->iom.rsv;
iocb->opcode = ctx->iom.opcode;
iocb->payload_length = ctx->ipayloadlen;
iocb->mask = ctx->imask;
iocb->data = readmark;
iocb->data_length = (size_t)(ctx->ibufmark - readmark);
if (ctx->ipayloadlen == ctx->ipayloadoff) {
ctx->istate = RECV_HEADER1;
ctx->ireqread = 2;
}
return (ssize_t)iocb->data_length;
}
return WSLAY_ERR_INVALID_ARGUMENT;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_FRAME_H
#define WSLAY_FRAME_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
enum wslay_frame_state {
PREP_HEADER,
PREP_HEADER_NOBUF,
SEND_HEADER,
SEND_PAYLOAD,
RECV_HEADER1,
RECV_PAYLOADLEN,
RECV_EXT_PAYLOADLEN,
RECV_MASKKEY,
RECV_PAYLOAD
};
struct wslay_frame_opcode_memo {
uint8_t fin;
uint8_t opcode;
uint8_t rsv;
};
struct wslay_frame_context {
uint8_t ibuf[4096];
uint8_t *ibufmark;
uint8_t *ibuflimit;
struct wslay_frame_opcode_memo iom;
uint64_t ipayloadlen;
uint64_t ipayloadoff;
uint8_t imask;
uint8_t imaskkey[4];
enum wslay_frame_state istate;
size_t ireqread;
uint8_t oheader[14];
uint8_t *oheadermark;
uint8_t *oheaderlimit;
uint64_t opayloadlen;
uint64_t opayloadoff;
uint8_t omask;
uint8_t omaskkey[4];
enum wslay_frame_state ostate;
struct wslay_frame_callbacks callbacks;
void *user_data;
};
#endif /* WSLAY_FRAME_H */
+39
View File
@@ -0,0 +1,39 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2020 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_MACRO_H
#define WSLAY_MACRO_H
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#include <stddef.h>
#define wslay_struct_of(ptr, type, member) \
((type *)(void *)((char *)(ptr)-offsetof(type, member)))
#endif /* WSLAY_MACRO_H */
+35
View File
@@ -0,0 +1,35 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_net.h"
#ifndef WORDS_BIGENDIAN
uint64_t wslay_byteswap64(uint64_t x) {
uint64_t u = ntohl(x & 0xffffffffllu);
uint64_t l = ntohl((uint32_t)(x >> 32));
return (u << 32) | l;
}
#endif /* !WORDS_BIGENDIAN */
+54
View File
@@ -0,0 +1,54 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_NET_H
#define WSLAY_NET_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif /* HAVE_ARPA_INET_H */
#ifdef HAVE_NETINET_IN_H
# include <netinet/in.h>
#endif /* HAVE_NETINET_IN_H */
/* For Mingw build */
#ifdef HAVE_WINSOCK2_H
# include <winsock2.h>
#endif /* HAVE_WINSOCK2_H */
#ifdef WORDS_BIGENDIAN
# define ntoh64(x) (x)
# define hton64(x) (x)
#else /* !WORDS_BIGENDIAN */
uint64_t wslay_byteswap64(uint64_t x);
# define ntoh64(x) wslay_byteswap64(x)
# define hton64(x) wslay_byteswap64(x)
#endif /* !WORDS_BIGENDIAN */
#endif /* WSLAY_NET_H */
+77
View File
@@ -0,0 +1,77 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_queue.h"
#include <string.h>
#include <assert.h>
#include "wslay_macro.h"
void wslay_queue_init(struct wslay_queue *queue) {
queue->top = NULL;
queue->tail = &queue->top;
}
void wslay_queue_deinit(struct wslay_queue *queue) { (void)queue; }
void wslay_queue_push(struct wslay_queue *queue,
struct wslay_queue_entry *ent) {
ent->next = NULL;
*queue->tail = ent;
queue->tail = &ent->next;
}
void wslay_queue_push_front(struct wslay_queue *queue,
struct wslay_queue_entry *ent) {
ent->next = queue->top;
queue->top = ent;
if (ent->next == NULL) {
queue->tail = &ent->next;
}
}
void wslay_queue_pop(struct wslay_queue *queue) {
assert(queue->top);
queue->top = queue->top->next;
if (queue->top == NULL) {
queue->tail = &queue->top;
}
}
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue) {
assert(queue->top);
return queue->top;
}
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue) {
assert(queue->top);
return wslay_struct_of(queue->tail, struct wslay_queue_entry, next);
}
int wslay_queue_empty(struct wslay_queue *queue) {
assert(queue->top || queue->tail == &queue->top);
return queue->top == NULL;
}
+53
View File
@@ -0,0 +1,53 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_QUEUE_H
#define WSLAY_QUEUE_H
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
struct wslay_queue_entry {
struct wslay_queue_entry *next;
};
struct wslay_queue {
struct wslay_queue_entry *top;
struct wslay_queue_entry **tail;
};
void wslay_queue_init(struct wslay_queue *queue);
void wslay_queue_deinit(struct wslay_queue *queue);
void wslay_queue_push(struct wslay_queue *queue, struct wslay_queue_entry *ent);
void wslay_queue_push_front(struct wslay_queue *queue,
struct wslay_queue_entry *ent);
void wslay_queue_pop(struct wslay_queue *queue);
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue);
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue);
int wslay_queue_empty(struct wslay_queue *queue);
#endif /* WSLAY_QUEUE_H */
+6 -2
View File
@@ -9,6 +9,8 @@
#include "util/logging.h" #include "util/logging.h"
#include "util/precise_timer.h" #include "util/precise_timer.h"
#include "util/memutils.h" #include "util/memutils.h"
#include "io.h"
#include "motion_cam.h"
#include "rgb_cam.h" #include "rgb_cam.h"
#pragma pack(push) #pragma pack(push)
@@ -276,6 +278,7 @@ namespace games::drs {
void DRSGame::attach() { void DRSGame::attach() {
Game::attach(); Game::attach();
if (!DISABLE_TOUCH) {
// TouchSDK hooks // TouchSDK hooks
detour::iat("??0TouchSDK@@QEAA@XZ", detour::iat("??0TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE); (void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
@@ -312,12 +315,13 @@ namespace games::drs {
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z", detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE); (void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
if (!DISABLE_TOUCH) {
start_touch(); start_touch();
} else { } else {
log_info("drs", "no native input method detected"); log_info("drs", "touch input for dance floor disabled");
} }
init_down_motion_hook();
if (RGB_CAMERA_HOOK) { if (RGB_CAMERA_HOOK) {
init_rgb_camera_hook(); init_rgb_camera_hook();
} }
+4 -3
View File
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
"P2 Up", "P2 Up",
"P2 Down", "P2 Down",
"P2 Left", "P2 Left",
"P2 Right" "P2 Right",
"Down Motion"
); );
} }
@@ -30,8 +31,8 @@ std::vector<Button> &games::drs::get_buttons() {
std::string games::drs::get_buttons_help() { std::string games::drs::get_buttons_help() {
// keep to max 100 characters wide // keep to max 100 characters wide
return return
"Motion camera required for DOWN movement.\n" "Touchscreen supported for dance floor.\n"
"Touchscreen supported for dance floor." "Down Motion applies to both players."
; ;
} }
+2 -1
View File
@@ -20,7 +20,8 @@ namespace games::drs {
P2_Up, P2_Up,
P2_Down, P2_Down,
P2_Left, P2_Left,
P2_Right P2_Right,
DownMotion
}; };
} }
+109
View File
@@ -0,0 +1,109 @@
#include "games/drs/motion_cam.h"
#include <cstdint>
#include "avs/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "io.h"
namespace games::drs {
// the game reads down movement from a depth camera whose driver is compiled into the game DLL,
// so there is no import to replace.
//
// with no camera the tracked height never changes, so the motion state never leaves "still"
// and DOWN notes can never be hit.
//
// rather than answer CInputManager::IsDown, the button drives the same state pair the camera
// would; that leaves the game's own edge test untouched and keeps every note in a frame seeing
// one answer.
static int32_t *MOTION_STATE = nullptr;
static int32_t *MOTION_STATE_PREV = nullptr;
static constexpr int32_t MOTION_STATE_STILL = 1;
static constexpr int32_t MOTION_STATE_DOWN = 2;
static void (*UpdateMotion_orig)() = nullptr;
static bool DOWN_MOTION_HELD = false;
static void InputManager_UpdateMotion() {
UpdateMotion_orig();
auto &buttons = get_buttons();
const bool held = GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::DownMotion));
// a state of DOWN only counts as movement when the frame before it was something else
if (held) {
*MOTION_STATE_PREV = DOWN_MOTION_HELD ? MOTION_STATE_DOWN : MOTION_STATE_STILL;
*MOTION_STATE = MOTION_STATE_DOWN;
}
DOWN_MOTION_HELD = held;
}
void init_down_motion_hook() {
// only hook motion camera if the user has down motion button bound to something
auto &buttons = get_buttons();
if (!buttons.at(Buttons::DownMotion).isSet()) {
return;
}
// CInputManager::IsDown - read the state pair out of its two operands.
// Identical in game versions 2020121400 and 2022121400 / 2024120300:
//
// 83 3D xx xx xx xx 02 cmp dword [rip+prev], 2 ; state one frame ago
// 74 0C je FALSE ; already down, so no new edge
// 83 3D xx xx xx xx 02 cmp dword [rip+cur], 2 ; state this frame
// 75 03 jne FALSE ; not down
// B0 01 mov al, 1 ; went down on this frame
// C3 ret
// 32 C0 FALSE: xor al, al
// C3 ret
auto is_down = reinterpret_cast<uint8_t *>(find_pattern(
avs::game::DLL_INSTANCE,
"833D0000000002740C833D00000000027503B001C332C0C3",
"XX????XXXXX????XXXXXXXXX",
0, 0));
if (is_down == nullptr) {
log_warning("drs", "motion sensor state not found, DOWN motion button unavailable");
return;
}
// rip-relative displacements resolve against the end of their own instruction, and each
// cmp above is 7 bytes with its displacement 4 bytes in: the first ends at +7 with its
// displacement at +2, the second starts at +9 so it ends at +16 with its own at +11.
// read signed, since a target sitting below the instruction encodes as negative.
MOTION_STATE_PREV = (int32_t *) (is_down + 7 + *(int32_t *) (is_down + 2));
MOTION_STATE = (int32_t *) (is_down + 16 + *(int32_t *) (is_down + 11));
// CInputManager::UpdateMotion is hooked for the sole purpose of reading the Down Motion
// button mapping frequently enough so that the rising edge can be detected.
//
// Matched over the prologue up to the camera read; the wildcards are a float and a callee:
//
// 48 83 EC 38 sub rsp, 0x38
// 0F 29 74 24 20 movaps [rsp+0x20], xmm6
// F3 0F 10 35 xx xx xx xx movss xmm6, [rip+height] ; reading as of last frame
// E8 xx xx xx xx call GetPlayVideoProcess
// 48 8B C8 mov rcx, rax
// 48 8B 10 mov rdx, [rax]
// FF 52 60 call [rdx+0x60] ; reading off the newest frame
auto update = reinterpret_cast<void *>(find_pattern(
avs::game::DLL_INSTANCE,
"4883EC380F29742420F30F103500000000E800000000488BC8488B10FF5260",
"XXXXXXXXXXXXX????X????XXXXXXXXX",
0, 0));
if (update == nullptr ||
!detour::trampoline_try(
update, (void *) InputManager_UpdateMotion, (void **) &UpdateMotion_orig)) {
log_warning("drs", "motion update not hooked, DOWN motion button unavailable");
return;
}
log_info("drs", "hooked DOWN motion at +{:#x}",
(uintptr_t) ((uint8_t *) update - (uint8_t *) avs::game::DLL_INSTANCE));
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::drs {
void init_down_motion_hook();
}
+52 -7
View File
@@ -39,8 +39,37 @@ namespace games::gitadora {
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt; std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false; bool ARENA_TWO_HEAD_EXCLUSIVE = false;
bool ARENA_SUBSCREEN_LANDSCAPE = false;
std::optional<std::string> ASIO_DRIVER = std::nullopt; std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false; bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
std::pair<UINT, UINT> arena_subscreen_host_size() {
if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value();
}
if (ARENA_SUBSCREEN_LANDSCAPE) {
return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT };
}
return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT };
}
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height) {
if (host_width <= 0 || host_height <= 0) {
return RECT {};
}
LONG width = MulDiv(host_height, ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT);
LONG height = host_height;
if (width > host_width) {
width = host_width;
height = MulDiv(host_width, ARENA_SUBSCREEN_HEIGHT, ARENA_SUBSCREEN_WIDTH);
}
const LONG left = (host_width - width) / 2;
const LONG top = (host_height - height) / 2;
return RECT { left, top, left + width, top + height };
}
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -342,6 +371,23 @@ namespace games::gitadora {
"gitadora", "gitadora",
"arena model: unsupported window count: {}", count); "arena model: unsupported window count: {}", count);
} }
if (ARENA_SUBSCREEN_LANDSCAPE && !ARENA_TWO_HEAD_EXCLUSIVE) {
log_warning(
"gitadora",
"arena model: landscape subscreen needs full screen two-window mode, ignoring");
ARENA_SUBSCREEN_LANDSCAPE = false;
}
if (ARENA_TWO_HEAD_EXCLUSIVE) {
const auto [host_width, host_height] = arena_subscreen_host_size();
if (host_width != ARENA_SUBSCREEN_WIDTH
|| host_height != ARENA_SUBSCREEN_HEIGHT) {
log_info(
"gitadora",
"arena model: SMALL head runs at {}x{}, subscreen is scaled to fit",
host_width, host_height);
}
}
} }
#endif #endif
@@ -809,18 +855,17 @@ namespace games::gitadora {
hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true; hooks::audio::INJECT_FAKE_REALTEK_AUDIO = true;
} }
// Single-window mode needs touch injection for its overlay. Two-head fullscreen // touch injection drives mouse-as-touch and API touch for the subscreen,
// mode uses the real SMALL output, so it only needs the display-topology shim. // no matter whether it is drawn by the overlay (single-window mode) or by
if (GRAPHICS_PREVENT_SECONDARY_WINDOWS) { // the dedicated SMALL window
// enable touch hook for subscreen overlay NATIVE_TOUCH = !wintouchemu::FORCE &&
const auto native_touch_ready = !wintouchemu::FORCE &&
nativetouch::hook(avs::game::DLL_INSTANCE); nativetouch::hook(avs::game::DLL_INSTANCE);
if (!native_touch_ready) { if (!NATIVE_TOUCH && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
// the legacy fallback can only feed the subscreen overlay
wintouchemu::FORCE = true; wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true; wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE); wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
} }
}
#if !SPICE_XP #if !SPICE_XP
+19
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <utility>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
@@ -21,8 +22,26 @@ namespace games::gitadora {
extern std::optional<socd::SocdAlgorithm> PICK_ALGO; extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT; extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE; extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern bool ARENA_SUBSCREEN_LANDSCAPE;
extern std::optional<std::string> ASIO_DRIVER; extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO; 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;
// used when a landscape monitor drives the SMALL head and -forceressub is unset
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_WIDTH = 1920;
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_HEIGHT = 1080;
// resolution the SMALL head actually runs at; the portrait panel size unless
// -forceressub or the landscape option overrides it
std::pair<UINT, UINT> arena_subscreen_host_size();
// area of the host the portrait subscreen occupies, centered and aspect-preserved;
// whatever is left over on either side stays black
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height);
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
+24 -2
View File
@@ -279,6 +279,26 @@ namespace games::iidx {
return nullptr; 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 #endif
IIDXGame::IIDXGame() : Game("Beatmania IIDX") { IIDXGame::IIDXGame() : Game("Beatmania IIDX") {
@@ -339,8 +359,10 @@ namespace games::iidx {
HAS_LIBAIO = true; HAS_LIBAIO = true;
// check TDJ mode // check TDJ mode
TDJ_MODE |= fileutils::text_read("C:\\000rom.txt") == "TDJ-JA"; if (!TDJ_MODE) {
TDJ_MODE |= fileutils::text_read("D:\\001rom.txt") == "TDJ"; TDJ_MODE = tdj_rom_matches("C:\\000rom.txt", "TDJ-JA")
|| tdj_rom_matches("D:\\001rom.txt", "TDJ");
}
// force TDJ mode // force TDJ mode
if (TDJ_MODE) { if (TDJ_MODE) {
+110 -55
View File
@@ -2,6 +2,8 @@
#if SPICE64 && !SPICE_XP #if SPICE64 && !SPICE_XP
#include <array>
#include <mutex>
#include <optional> #include <optional>
#include <cstdint> #include <cstdint>
#include "util/detour.h" #include "util/detour.h"
@@ -141,10 +143,100 @@ namespace games::popn {
static AIO_SCI_COMM *aioSciComm; static AIO_SCI_COMM *aioSciComm;
static AIO_IOB5_BI3A *aioIob5Bi3a; static AIO_IOB5_BI3A *aioIob5Bi3a;
static constexpr size_t PIKA_BUTTON_COUNT = 9;
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
static bool pika_button_staging_dirty = false;
static std::mutex pika_button_staging_mutex;
/* /*
* implementations * implementations
*/ */
static void publish_pika_button_staging() {
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
if (!pika_button_staging_dirty) {
return;
}
data = pika_button_staging;
pika_button_staging_dirty = false;
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R,
Lights::popn_lights_t::PikaButton1_G,
Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R,
Lights::popn_lights_t::PikaButton2_G,
Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R,
Lights::popn_lights_t::PikaButton3_G,
Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R,
Lights::popn_lights_t::PikaButton4_G,
Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R,
Lights::popn_lights_t::PikaButton5_G,
Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R,
Lights::popn_lights_t::PikaButton6_G,
Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R,
Lights::popn_lights_t::PikaButton7_G,
Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R,
Lights::popn_lights_t::PikaButton8_G,
Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R,
Lights::popn_lights_t::PikaButton9_G,
Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
auto &lights = get_lights();
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
const auto data_offset = button * 3;
// special handling converting RGB data to non-RGB lights
// take the max(R, G, B) and use it to write the light value
const auto light_value = std::max({
data[data_offset],
data[data_offset + 1],
data[data_offset + 2]
});
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
// at all times, even when "off". When translating to legacy on/off lights,
// treat only values above about 60% brightness as fully on to preserve the
// dim "off" state.
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[button]),
light_value > 150 ? 1.f : 0.f);
}
for (size_t light = 0; light < data.size(); light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
data[light] / 255.f);
}
}
// libaio.dll // libaio.dll
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) { static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
@@ -260,6 +352,11 @@ namespace games::popn {
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p)); log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
if (i_pNodeMgr == aioNmgrIob5) { if (i_pNodeMgr == aioNmgrIob5) {
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
pika_button_staging.fill(0);
pika_button_staging_dirty = false;
}
aioIob5Bi3a = new AIO_IOB5_BI3A; aioIob5Bi3a = new AIO_IOB5_BI3A;
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a)); log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
return aioIob5Bi3a; return aioIob5Bi3a;
@@ -269,11 +366,15 @@ namespace games::popn {
} }
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) { static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
RI_MGR->devices_flush_output();
if (i_pNodeCtl != aioIob5Bi3a) { if (i_pNodeCtl != aioIob5Bi3a) {
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus); return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
} }
// snapshot the current state of lights and flush the output
// this closely mirrors what the game does with real I/O
publish_pika_button_staging();
RI_MGR->devices_flush_output();
memset(o_DevStatus, 0, sizeof(*o_DevStatus)); memset(o_DevStatus, 0, sizeof(*o_DevStatus));
auto &buttons = get_buttons(); auto &buttons = get_buttons();
@@ -413,61 +514,15 @@ namespace games::popn {
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data); i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
} }
auto &lights = get_lights();
if (i_CnPin == 0 && number_of_leds == 9 * 3) { if (i_CnPin == 0 && number_of_leds == 9 * 3) {
// special handling converting to non-RGB lights // SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
// take the max(R, G, B) and use it to write the light value // clears the buffer (set to 0 / lights off) between animations but they are not meant
uint8_t light_value[9] = { 0 }; // to be seen by the I/O
for (uint32_t i = 0; i < number_of_leds; i += 3) { // therefore, instead of writing the lights out directly here, we stage them in a buffer
light_value[i / 3] = // and wait for GetDeviceStatus to be called
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] }); std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
} memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
pika_button_staging_dirty = true;
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
static_assert(std::size(legacy_button_lights) == 9);
for (size_t light = 0; light < 9; light++) {
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
// otherwise off, to avoid dimly lit "off" state
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[light]),
light_value[light] > 150 ? 1.f : 0.f);
}
// color buttons
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(button_lights) == 9 * 3);
for (size_t light = 0; light < number_of_leds; light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
((uint8_t *)i_pData)[light] / 255.f);
}
} }
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) { if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
+150
View File
@@ -0,0 +1,150 @@
#include "projector.h"
#include <algorithm>
#include <cstring>
#include <cwchar>
#include "util/logging.h"
namespace {
// request/response layout: identifier, command, two zero bytes, payload length,
// the payload itself and a checksum over everything before it
constexpr size_t HEADER_SIZE = 5;
constexpr uint8_t RESPONSE_ID = 0x23;
constexpr uint8_t COMMAND_COMMON_DATA = 0x8A;
constexpr uint8_t COMMAND_TEMPERATURE = 0x99;
constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B;
// shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT
constexpr uint32_t LAMP_SECONDS = 0;
constexpr uint32_t TEMPERATURE_INTAKE = 25;
constexpr uint32_t TEMPERATURE_EXHAUST = 35;
// anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact
// value was already recorded in /projecter/current on an earlier boot
constexpr uint32_t LAMP_CURRENT = 1000;
void put32(uint8_t *dest, uint32_t value) {
dest[0] = (uint8_t) (value & 0xFF);
dest[1] = (uint8_t) ((value >> 8) & 0xFF);
dest[2] = (uint8_t) ((value >> 16) & 0xFF);
dest[3] = (uint8_t) ((value >> 24) & 0xFF);
}
}
bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) {
return false;
}
log_info("silentscope", "Opened COM2 (projector)");
return true;
}
int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) {
buffer[bytes_read++] = this->response.front();
this->response.pop_front();
}
return (int) bytes_read;
}
int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite);
while (this->request.size() >= HEADER_SIZE) {
const size_t packet_size = HEADER_SIZE + this->request[4] + 1;
if (this->request.size() < packet_size) {
break;
}
this->process_request(this->request.data());
this->request.erase(this->request.begin(), this->request.begin() + packet_size);
}
return (int) nNumberOfBytesToWrite;
}
size_t games::silentscope::ProjectorHandle::bytes_available() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->response.size();
}
bool games::silentscope::ProjectorHandle::close() {
std::lock_guard<std::mutex> lock(this->mutex);
this->request.clear();
this->response.clear();
log_info("silentscope", "Closed COM2 (projector)");
return true;
}
void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) {
const uint8_t command = packet[1];
const uint8_t length = packet[4];
const uint8_t *data = packet + HEADER_SIZE;
switch (command) {
case COMMAND_COMMON_DATA: {
// the game only looks at the lamp usage time near the end of the block
std::vector<uint8_t> payload(98, 0);
put32(&payload[94], LAMP_SECONDS);
this->reply(command, payload);
break;
}
case COMMAND_TEMPERATURE: {
// the requested sensor is echoed back along with its reading
const uint8_t sensor = length > 0 ? data[0] : 0;
std::vector<uint8_t> payload(5, 0);
payload[0] = sensor;
put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST);
this->reply(command, payload);
break;
}
case COMMAND_LAMP_CURRENT: {
// the three byte item selector is echoed back along with the measurement
std::vector<uint8_t> payload(7, 0);
memcpy(payload.data(), data, std::min<size_t>(length, 3));
put32(&payload[3], LAMP_CURRENT);
this->reply(command, payload);
break;
}
default:
log_misc("silentscope", "unknown projector command {:#04x}", command);
this->reply(command, {});
break;
}
}
void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector<uint8_t> &data) {
std::vector<uint8_t> packet {
RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size()
};
packet.insert(packet.end(), data.begin(), data.end());
uint8_t checksum = 0;
for (auto byte : packet) {
checksum += byte;
}
packet.push_back(checksum);
this->response.insert(this->response.end(), packet.begin(), packet.end());
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
#include "hooks/devicehook.h"
namespace games::silentscope {
// The cabinet talks to its projector over COM2. Without an answer the game stops at
// I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly").
class ProjectorHandle : public CustomHandle {
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
size_t bytes_available() override;
bool close() override;
private:
std::mutex mutex;
std::vector<uint8_t> request;
std::deque<uint8_t> response;
void process_request(const uint8_t *packet);
void reply(uint8_t command, const std::vector<uint8_t> &data);
};
}
@@ -1,10 +1,11 @@
#include "silentscope.h" #include "silentscope.h"
#include "acioemu/handle.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "hooks/devicehook.h" #include "hooks/devicehook.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "projector.h"
namespace games::silentscope { namespace games::silentscope {
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") { SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
@@ -16,9 +17,8 @@ namespace games::silentscope {
// load the game DLL so hooks apply // load the game DLL so hooks apply
libutils::try_library("gamendd.dll"); libutils::try_library("gamendd.dll");
// TODO(felix): implement stuff on this port
devicehook_init(); devicehook_init();
devicehook_add(new acioemu::ACIOHandle(L"COM2")); devicehook_add(new ProjectorHandle());
} }
void SilentScopeGame::detach() { void SilentScopeGame::detach() {
+1 -1
View File
@@ -574,7 +574,7 @@ void devicehook_init(HMODULE module) {
STORE(EscapeCommFunction_orig, detour::iat_try("EscapeCommFunction", EscapeCommFunction_hook, module)); STORE(EscapeCommFunction_orig, detour::iat_try("EscapeCommFunction", EscapeCommFunction_hook, module));
STORE(GetCommState_orig, detour::iat_try("GetCommState", GetCommState_hook, module)); STORE(GetCommState_orig, detour::iat_try("GetCommState", GetCommState_hook, module));
STORE(GetFileSize_orig, detour::iat_try("GetFileSize", GetFileSize_hook, module)); STORE(GetFileSize_orig, detour::iat_try("GetFileSize", GetFileSize_hook, module));
STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSize", GetFileSizeEx_hook, module)); STORE(GetFileSizeEx_orig, detour::iat_try("GetFileSizeEx", GetFileSizeEx_hook, module));
STORE(GetFileInformationByHandle_orig, detour::iat_try( STORE(GetFileInformationByHandle_orig, detour::iat_try(
"GetFileInformationByHandle", GetFileInformationByHandle_hook, module)); "GetFileInformationByHandle", GetFileInformationByHandle_hook, module));
STORE(PurgeComm_orig, detour::iat_try("PurgeComm", PurgeComm_hook, module)); STORE(PurgeComm_orig, detour::iat_try("PurgeComm", PurgeComm_hook, module));
@@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {}
#include <dxgi.h> #include <dxgi.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include "avs/game.h"
#include "d3d11_internal.h" #include "d3d11_internal.h"
#include "util/nt_loader.h" #include "util/nt_loader.h"
@@ -237,10 +238,11 @@ bool d3dcompiler_available() {
} // namespace } // namespace
void graphics_d3d11_init() { void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps // dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska
// their startup path completely untouched (no exports patched, no poll // engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their
// thread, no LDR callback). // startup path completely untouched (no exports patched, no poll thread, no
if (!GetModuleHandleW(L"execexe.dll")) { // LDR callback).
if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) {
return; return;
} }
@@ -23,10 +23,7 @@
#include "external/imgui/backends/imgui_impl_dx11.h" #include "external/imgui/backends/imgui_impl_dx11.h"
#include "overlay/imgui/impl_spice.h" #include "overlay/imgui/impl_spice.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
#include "util/utils.h" #include "util/utils.h"
// -------------------------------------------------------------------------- // --------------------------------------------------------------------------
@@ -108,8 +105,62 @@ Present1_t Present1_orig = nullptr;
bool g_swapchain_hooked = false; bool g_swapchain_hooked = false;
bool g_swapchain1_hooked = false; bool g_swapchain1_hooked = false;
// sub-screens / IME helpers are usually child or zero-sized windows.
// visibility isn't checked - the game may present before showing the window.
bool looks_like_game_window(HWND hwnd) {
RECT client {};
return GetAncestor(hwnd, GA_ROOT) == hwnd
&& GetClientRect(hwnd, &client)
&& client.right > client.left
&& client.bottom > client.top;
}
// only the main game window; ignore sub-screens / IME helpers.
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
// checks are ordered cheapest first, since this runs on every present
void try_create_overlay(IDXGISwapChain *swapchain) { void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain || overlay::OVERLAY) { if (!swapchain) {
return;
}
// overlay is disabled by user
if (!overlay::ENABLED) {
return;
}
// overlay is already enabled and attached
if (overlay::OVERLAY) {
return;
}
// ignore sub windows
if (!is_main_game_swapchain(swapchain)) {
return; return;
} }
@@ -118,12 +169,6 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
return; return;
} }
// only attach to the main game window; ignore sub-screens / IME helpers.
HWND main = d3d11_hooks::main_hwnd();
if (main && desc.OutputWindow != main) {
return;
}
// theme the native title bar; first present is the only reliable point for // theme the native title bar; first present is the only reliable point for
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll) // windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
set_window_dark_titlebar(desc.OutputWindow); set_window_dark_titlebar(desc.OutputWindow);
@@ -149,26 +194,22 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
device->Release(); device->Release();
} }
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour). // screenshots have to keep working with the overlay disabled, so they are not gated on it
void poll_screenshot_hotkey() { void pump_frame(IDXGISwapChain *swapchain) {
static bool s_down = false; const bool has_overlay =
auto buttons = games::get_buttons_overlay(eamuse_get_game()); overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
const bool pressed = buttons if (!has_overlay && !is_main_game_swapchain(swapchain)) {
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered())
&& GameAPI::Buttons::getState(RI_MGR,
buttons->at(games::OverlayButtons::Screenshot));
if (pressed && !s_down) {
graphics_screenshot_trigger();
}
s_down = pressed;
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
return; return;
} }
poll_screenshot_hotkey(); graphics_poll_screenshot_hotkey();
// before the overlay render so the screenshot excludes it
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
d3d11_hooks::try_screenshot(swapchain);
}
if (has_overlay) {
// size imgui to the backbuffer (not window client). dxgi may upscale // size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override // a small backbuffer into a larger client rect; without this override
@@ -183,10 +224,13 @@ void pump_overlay(IDXGISwapChain *swapchain) {
overlay::OVERLAY->update(); overlay::OVERLAY->update();
overlay::OVERLAY->new_frame(); overlay::OVERLAY->new_frame();
overlay::OVERLAY->render(); overlay::OVERLAY->render();
}
// after overlay render so toasts/menus end up in the saved image. // after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
d3d11_hooks::try_screenshot(swapchain); d3d11_hooks::try_screenshot(swapchain);
} }
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// swapchain method hooks // swapchain method hooks
@@ -194,8 +238,11 @@ void pump_overlay(IDXGISwapChain *swapchain) {
HRESULT STDMETHODCALLTYPE Present_hook( HRESULT STDMETHODCALLTYPE Present_hook(
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags) IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
{ {
// a test present doesn't display anything; don't pick a window or take a screenshot off it
if (!(Flags & DXGI_PRESENT_TEST)) {
try_create_overlay(swapchain); try_create_overlay(swapchain);
pump_overlay(swapchain); pump_frame(swapchain);
}
return Present_orig(swapchain, SyncInterval, Flags); return Present_orig(swapchain, SyncInterval, Flags);
} }
@@ -203,8 +250,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags, IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
const DXGI_PRESENT_PARAMETERS *pParams) const DXGI_PRESENT_PARAMETERS *pParams)
{ {
if (!(Flags & DXGI_PRESENT_TEST)) {
try_create_overlay(swapchain); try_create_overlay(swapchain);
pump_overlay(swapchain); pump_frame(swapchain);
}
return Present1_orig(swapchain, SyncInterval, Flags, pParams); return Present1_orig(swapchain, SyncInterval, Flags, pParams);
} }
@@ -944,9 +944,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
} else if (!D3D9_DEVICE_HOOK_DISABLE) { } else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters); graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( auto *wrapped = new WrappedIDirect3DDevice9(
hFocusWindow, hFocusWindow,
*ppReturnedDeviceInterface); *ppReturnedDeviceInterface);
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
*ppReturnedDeviceInterface = wrapped;
} }
// return result // return result
@@ -1030,14 +1033,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
return D3DERR_INVALIDCALL; return D3DERR_INVALIDCALL;
} }
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_warning(
"graphics::d3d9",
"-forceressub is unavailable; SMALL must remain {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT);
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags; DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1; size_t num_adapters = 1;
@@ -1093,7 +1088,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
params->BackBufferHeight, params->BackBufferWidth); params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight); std::swap(params->BackBufferWidth, params->BackBufferHeight);
} }
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) { } else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_misc( log_misc(
"graphics::d3d9", "graphics::d3d9",
"use custom sub resolution {}x{} => {}x{}", "use custom sub resolution {}x{} => {}x{}",
@@ -1137,7 +1133,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (GRAPHICS_FS_ORIENTATION_SWAP) { } else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height); std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
} }
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) { } else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first; fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second; fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
} }
@@ -1287,14 +1284,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
ppReturnedDeviceInterface); ppReturnedDeviceInterface);
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) { if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0]; gfdm_publish_two_head_parameters(
pPresentationParameters[gfdm_parameters.logical_small_swapchain] = pPresentationParameters,
gfdm_parameters.presentation_parameters[1]; pFullscreenDisplayMode,
if (pFullscreenDisplayMode != nullptr) { gfdm_parameters);
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
} }
// check for error // check for error
@@ -1307,13 +1300,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (!D3D9_DEVICE_HOOK_DISABLE) { } else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters); graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9( auto *wrapped = new WrappedIDirect3DDevice9(
hFocusWindow, hFocusWindow,
*ppReturnedDeviceInterface, *ppReturnedDeviceInterface,
gfdm_parameters.logical_small_swapchain, gfdm_parameters.logical_small_swapchain,
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr, gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr); gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
*ppReturnedDeviceInterface = wrapped;
// initialize sub screen if the game requested a multi-head context // initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) && if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
@@ -1478,7 +1474,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
void graphics_d3d9_on_present( void graphics_d3d9_on_present(
HWND hFocusWindow, HWND hFocusWindow,
IDirect3DDevice9 *device, IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device) { WrappedIDirect3DDevice9 *wrapped_device) {
// image resize / orientation swap. run here (the present path) rather than from `EndScene`, // image resize / orientation swap. run here (the present path) rather than from `EndScene`,
// which may fire several times per frame on multi-pass / render-to-texture games. this is the // which may fire several times per frame on multi-pass / render-to-texture games. this is the
@@ -1489,6 +1485,13 @@ void graphics_d3d9_on_present(
SurfaceHook(device); SurfaceHook(device);
} }
graphics_poll_screenshot_hotkey();
// before the overlay render so the screenshot excludes it
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
}
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never // Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with // call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
// `dx9osd.dll` for pfreepanic. // `dx9osd.dll` for pfreepanic.
@@ -1508,6 +1511,15 @@ void graphics_d3d9_on_present(
device->EndScene(); device->EndScene();
} }
// after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
}
// API capture always includes the overlay; it must run before the subscreen present
// below, which leaves the arena SMALL back buffer black
graphics_d3d9_process_capture(device, wrapped_device);
// for IIDX TDJ / SDVX UFC, handle subscreen // for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model(); const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE; const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
@@ -1521,9 +1533,6 @@ void graphics_d3d9_on_present(
if (is_mfc) { if (is_mfc) {
wintouchemu::update(); wintouchemu::update();
} }
graphics_d3d9_poll_screenshot_hotkey();
graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN);
} }
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
@@ -4,6 +4,8 @@
#include "d3d9_gfdm.h" #include "d3d9_gfdm.h"
struct WrappedIDirect3DDevice9;
// {EEE9CCF6-53D6-4326-9AE5-60921B3DB394} // {EEE9CCF6-53D6-4326-9AE5-60921B3DB394}
static const GUID IID_WrappedIDirect3D9 = { static const GUID IID_WrappedIDirect3D9 = {
0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 } 0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 }
@@ -13,7 +15,7 @@ void graphics_d3d9_init();
void graphics_d3d9_on_present( void graphics_d3d9_on_present(
HWND hFocusWindow, HWND hFocusWindow,
IDirect3DDevice9 *device, IDirect3DDevice9 *device,
IDirect3DDevice9 *wrapped_device); WrappedIDirect3DDevice9 *wrapped_device);
void graphics_d3d9_notify_subscreen_present(); void graphics_d3d9_notify_subscreen_present();
@@ -17,12 +17,18 @@
#include "d3d9_backend.h" #include "d3d9_backend.h"
#include "d3d9_live2d.h" #include "d3d9_live2d.h"
#include "d3d9_readback.h"
#include "d3d9_texture.h" #include "d3d9_texture.h"
#ifndef SPICE64 #ifndef SPICE64
#include "shaders/vertex_shader.h" #include "shaders/vertex_shader.h"
#endif #endif
// maps arena's cached additional swap chains (SMALL, LEFT, RIGHT) to screen numbers.
// MAIN is the implicit swap chain, is not in those slots, and is always screen 0.
// screen 1 is the subscreen for every other game, so SMALL takes that number here too.
static constexpr int GFDM_ARENA_SLOT_SCREENS[] { 1, 2, 3 };
#define CHECK_RESULT_FMT(x, fmt, ...) \ #define CHECK_RESULT_FMT(x, fmt, ...) \
HRESULT __ret = (x); \ HRESULT __ret = (x); \
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \ if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
@@ -151,6 +157,11 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
} }
} }
// holds a reference on the device, so it has to go before the counts are compared
this->gfdm_small_head.release();
d3d9_readback::release_device_resources(this->pReal);
if (overlay::ENABLED) { if (overlay::ENABLED) {
const std::lock_guard<std::mutex> lock(overlay::OVERLAY_MUTEX); const std::lock_guard<std::mutex> lock(overlay::OVERLAY_MUTEX);
@@ -336,20 +347,25 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
int index = 0; int index = 0;
bool create_swap_chain = false; bool create_swap_chain = false;
bool create_fake_swap_chain = false; bool create_fake_swap_chain = false;
bool arena_slot = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) { if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true; create_swap_chain = true;
} else if (games::gitadora::is_arena_model() && } else if (games::gitadora::is_arena_model() &&
(GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) { (GRAPHICS_SCREENSHOT_SUBSCREENS ||
GRAPHICS_PREVENT_SECONDARY_WINDOWS ||
GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) {
if (pPresentationParameters->BackBufferWidth == 800) { if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen) // SMALL (subscreen)
create_swap_chain = true; create_swap_chain = true;
arena_slot = true;
index = 0; index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) { } else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT // LEFT/RIGHT
create_swap_chain = true; create_swap_chain = true;
arena_slot = true;
index = 1; index = 1;
if (sub_swapchain[index] || fake_sub_swapchain[index]) { if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2; index = 2;
@@ -361,6 +377,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
} }
} }
// the api lists screens from this registry, so arena heads need their logical numbers in it
if (arena_slot) {
graphics_screens_register(GFDM_ARENA_SLOT_SCREENS[index]);
}
if (create_fake_swap_chain) { if (create_fake_swap_chain) {
if (!fake_sub_swapchain[index]) { if (!fake_sub_swapchain[index]) {
log_info( log_info(
@@ -539,6 +560,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
return n; return n;
} }
void WrappedIDirect3DDevice9::get_screenshot_screens(std::vector<int> &screens) const {
if (games::gitadora::is_arena_model()) {
screens.push_back(0);
// every head the game renders into, whether or not it reaches a display
for (int slot = 0; slot < 3; slot++) {
if (sub_swapchain[slot] != nullptr || fake_sub_swapchain[slot] != nullptr) {
screens.push_back(GFDM_ARENA_SLOT_SCREENS[slot]);
}
}
return;
}
graphics_screens_get(screens);
// the sub screen is only registered once the game asks for it by index
if (sub_swapchain[0] != nullptr &&
avs::game::is_model({"LDJ", "KFC", "M39"}) &&
std::find(screens.begin(), screens.end(), 1) == screens.end())
{
screens.push_back(1);
}
}
HRESULT WrappedIDirect3DDevice9::get_screenshot_swap_chain(
UINT iSwapChain,
IDirect3DSwapChain9 **ppSwapChain)
{
if (ppSwapChain == nullptr) {
return D3DERR_INVALIDCALL;
}
// the game numbers the two-head SMALL head itself; keep screen 1 meaning SMALL
if (games::gitadora::is_arena_model() && is_gfdm_two_head_exclusive() && iSwapChain == 1) {
return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain);
}
if (games::gitadora::is_arena_model()) {
for (int slot = 0; slot < 3; slot++) {
if (GFDM_ARENA_SLOT_SCREENS[slot] != (int) iSwapChain) {
continue;
}
if (sub_swapchain[slot] != nullptr) {
sub_swapchain[slot]->AddRef();
*ppSwapChain = sub_swapchain[slot];
return D3D_OK;
}
if (fake_sub_swapchain[slot] != nullptr) {
fake_sub_swapchain[slot]->AddRef();
*ppSwapChain = fake_sub_swapchain[slot];
return D3D_OK;
}
}
}
return GetSwapChain(iSwapChain, ppSwapChain);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset( HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
D3DPRESENT_PARAMETERS *pPresentationParameters) D3DPRESENT_PARAMETERS *pPresentationParameters)
{ {
@@ -558,6 +637,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
overlay::OVERLAY->reset_invalidate(); overlay::OVERLAY->reset_invalidate();
} }
gfdm_small_head.release();
// Reset refuses to run while any default pool resource is outstanding
d3d9_readback::discard_snapshot_targets(pReal);
HRESULT res = pReal->Reset(pPresentationParameters); HRESULT res = pReal->Reset(pPresentationParameters);
// recreate overlay // recreate overlay
@@ -583,6 +667,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
graphics_d3d9_on_present(hFocusWindow, pReal, this); graphics_d3d9_on_present(hFocusWindow, pReal, this);
// an adapter group device presents every head at once, so the SMALL head has to be
// composed here as well as in its own swap chain
if (gfdm_small_head.scaled()) {
gfdm_small_head.compose(pReal);
}
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion)); CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
} }
@@ -604,6 +694,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
if (is_gfdm_two_head_exclusive() if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain)) && is_gfdm_logical_small_swapchain(iSwapChain))
{ {
if (gfdm_small_head.scaled()
&& iBackBuffer == 0
&& Type == D3DBACKBUFFER_TYPE_MONO)
{
CHECK_RESULT(gfdm_small_head.backbuffer(pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer( CHECK_RESULT(pReal->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN, GFDM_NATIVE_SMALL_SWAPCHAIN,
iBackBuffer, iBackBuffer,
@@ -2245,6 +2341,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
overlay::OVERLAY->reset_invalidate(); overlay::OVERLAY->reset_invalidate();
} }
gfdm_small_head.release();
// ResetEx refuses to run while any default pool resource is outstanding
d3d9_readback::discard_snapshot_targets(pReal);
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx( HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
gfdm_parameters.presentation_parameters, gfdm_parameters.presentation_parameters,
gfdm_parameters.fullscreen_display_modes); gfdm_parameters.fullscreen_display_modes);
@@ -2259,14 +2360,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
} }
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) { if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain; gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0]; gfdm_publish_two_head_parameters(
pPresentationParameters[gfdm_parameters.logical_small_swapchain] = pPresentationParameters,
gfdm_parameters.presentation_parameters[1]; pFullscreenDisplayMode,
if (pFullscreenDisplayMode != nullptr) { gfdm_parameters);
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
} }
// recreate overlay // recreate overlay
@@ -3,6 +3,7 @@
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
#include <vector>
#include <initguid.h> #include <initguid.h>
#include <d3d9.h> #include <d3d9.h>
@@ -10,6 +11,7 @@
#include "util/logging.h" #include "util/logging.h"
#include "d3d9_fake_swapchain.h" #include "d3d9_fake_swapchain.h"
#include "d3d9_gfdm.h"
#include "d3d9_swapchain.h" #include "d3d9_swapchain.h"
/* /*
@@ -234,6 +236,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override; virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override;
#pragma endregion #pragma endregion
// logical screens the game draws, and the swap chain each one lives on
void get_screenshot_screens(std::vector<int> &screens) const;
HRESULT get_screenshot_swap_chain(UINT iSwapChain, IDirect3DSwapChain9 **ppSwapChain);
bool is_gfdm_two_head_exclusive() const; bool is_gfdm_two_head_exclusive() const;
bool is_gfdm_logical_small_swapchain(UINT swapchain) const; bool is_gfdm_logical_small_swapchain(UINT swapchain) const;
bool is_gfdm_logical_side_swapchain(UINT swapchain) const; bool is_gfdm_logical_side_swapchain(UINT swapchain) const;
@@ -259,6 +265,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
IDirect3DDevice9 *pReal; IDirect3DDevice9 *pReal;
bool is_d3d9ex = false; bool is_d3d9ex = false;
// set from the creation flags; without it the runtime does no locking of its own, so
// nothing but the present thread may touch the device
bool device_multithreaded = false;
std::atomic_ulong refs = 1; std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
@@ -270,6 +280,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {}; std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
bool gfdm_logical_group_parameters_valid = false; bool gfdm_logical_group_parameters_valid = false;
IDirect3D9 *gfdm_parent_d3d = nullptr; IDirect3D9 *gfdm_parent_d3d = nullptr;
GfdmSmallHead gfdm_small_head;
std::mutex gfdm_recovery_mutex; std::mutex gfdm_recovery_mutex;
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {}; std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
@@ -16,6 +16,54 @@ bool gfdm_two_head_exclusive() {
&& !GRAPHICS_WINDOWED; && !GRAPHICS_WINDOWED;
} }
static std::pair<UINT, UINT> gfdm_small_head_size() {
if (!gfdm_two_head_exclusive()) {
return { GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT };
}
return games::gitadora::arena_subscreen_host_size();
}
// scaling is only needed when the head is not the panel size the game draws into
static bool gfdm_small_head_scaled() {
const auto [width, height] = gfdm_small_head_size();
return width != GFDM_SMALL_WIDTH || height != GFDM_SMALL_HEIGHT;
}
// Hand the resolved MAIN/SMALL heads back to the game's own array. The SMALL entry keeps
// the portrait size the game asked for: that is what it renders, and the next reset finds
// the SMALL head by matching it.
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state)
{
if (logical_presentation_parameters == nullptr) {
return;
}
const UINT small = state.logical_small_swapchain;
const bool publish_modes = logical_fullscreen_display_modes != nullptr
&& state.fullscreen_display_modes != nullptr;
logical_presentation_parameters[0] = state.presentation_parameters[0];
logical_presentation_parameters[small] = state.presentation_parameters[1];
if (publish_modes) {
logical_fullscreen_display_modes[0] = state.fullscreen_display_modes[0];
logical_fullscreen_display_modes[small] = state.fullscreen_display_modes[1];
}
if (!gfdm_small_head_scaled()) {
return;
}
logical_presentation_parameters[small].BackBufferWidth = GFDM_SMALL_WIDTH;
logical_presentation_parameters[small].BackBufferHeight = GFDM_SMALL_HEIGHT;
if (publish_modes) {
logical_fullscreen_display_modes[small].Width = GFDM_SMALL_WIDTH;
logical_fullscreen_display_modes[small].Height = GFDM_SMALL_HEIGHT;
}
}
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters( HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
const D3DPRESENT_PARAMETERS *logical_presentation_parameters, const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes, const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
@@ -186,6 +234,26 @@ HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
return D3DERR_INVALIDCALL; return D3DERR_INVALIDCALL;
} }
// the game keeps rendering the portrait subscreen; only the head it is scanned out
// on changes size, and the scaling happens when the head is presented
if (gfdm_small_head_scaled()) {
const auto [host_width, host_height] = gfdm_small_head_size();
log_info(
"graphics::d3d9",
"two-head exclusive: {} SMALL head {}x{} -> {}x{}",
operation,
secondary.BackBufferWidth,
secondary.BackBufferHeight,
host_width,
host_height);
secondary.BackBufferWidth = host_width;
secondary.BackBufferHeight = host_height;
if (fullscreen_display_modes != nullptr) {
fullscreen_display_modes[1].Width = host_width;
fullscreen_display_modes[1].Height = host_height;
}
}
return D3D_OK; return D3D_OK;
} }
@@ -318,20 +386,21 @@ HRESULT validate_gfdm_two_head_exclusive(
const auto &main = presentation_parameters[0]; const auto &main = presentation_parameters[0];
const auto &small_params = presentation_parameters[1]; const auto &small_params = presentation_parameters[1];
const auto [expected_small_width, expected_small_height] = gfdm_small_head_size();
if (main.Windowed || small_params.Windowed) { if (main.Windowed || small_params.Windowed) {
log_warning( log_warning(
"graphics::d3d9", "graphics::d3d9",
"two-head exclusive mode requires both group heads to be fullscreen"); "two-head exclusive mode requires both group heads to be fullscreen");
return D3DERR_INVALIDCALL; return D3DERR_INVALIDCALL;
} }
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH if (small_params.BackBufferWidth != expected_small_width
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT) || small_params.BackBufferHeight != expected_small_height)
{ {
log_warning( log_warning(
"graphics::d3d9", "graphics::d3d9",
"SMALL head must be {}x{}; got {}x{}", "SMALL head must be {}x{}; got {}x{}",
GFDM_SMALL_WIDTH, expected_small_width,
GFDM_SMALL_HEIGHT, expected_small_height,
small_params.BackBufferWidth, small_params.BackBufferWidth,
small_params.BackBufferHeight); small_params.BackBufferHeight);
return D3DERR_INVALIDCALL; return D3DERR_INVALIDCALL;
@@ -547,6 +616,7 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
{ {
if (presentation_parameters == nullptr) { if (presentation_parameters == nullptr) {
gfdm_logical_group_parameters_valid = false; gfdm_logical_group_parameters_valid = false;
gfdm_small_head.resolve(nullptr);
return; return;
} }
std::copy_n( std::copy_n(
@@ -554,6 +624,8 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
gfdm_logical_group_parameters.size(), gfdm_logical_group_parameters.size(),
gfdm_logical_group_parameters.begin()); gfdm_logical_group_parameters.begin());
gfdm_logical_group_parameters_valid = true; gfdm_logical_group_parameters_valid = true;
gfdm_small_head.resolve(
&gfdm_logical_group_parameters[gfdm_logical_small_swapchain]);
} }
FakeIDirect3DSwapChain9 * FakeIDirect3DSwapChain9 *
@@ -599,6 +671,110 @@ void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
} }
} }
void GfdmSmallHead::resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters) {
release();
active = gfdm_small_head_scaled();
format = D3DFMT_X8R8G8B8;
multisample = D3DMULTISAMPLE_NONE;
multisample_quality = 0;
if (!active || logical_small_parameters == nullptr) {
return;
}
if (logical_small_parameters->BackBufferFormat != D3DFMT_UNKNOWN) {
format = logical_small_parameters->BackBufferFormat;
}
multisample = logical_small_parameters->MultiSampleType;
multisample_quality = logical_small_parameters->MultiSampleQuality;
}
void GfdmSmallHead::apply_logical_size(UINT *width, UINT *height) const {
if (!active) {
return;
}
if (width != nullptr) {
*width = GFDM_SMALL_WIDTH;
}
if (height != nullptr) {
*height = GFDM_SMALL_HEIGHT;
}
}
HRESULT GfdmSmallHead::backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out) {
if (device == nullptr || out == nullptr || !active) {
return D3DERR_INVALIDCALL;
}
if (surface == nullptr) {
const HRESULT result = device->CreateRenderTarget(
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT,
format,
multisample,
multisample_quality,
FALSE,
&surface,
nullptr);
if (FAILED(result)) {
log_warning(
"graphics::d3d9",
"two-head exclusive: could not create the portrait SMALL surface, hr={}",
FMT_HRESULT(result));
surface = nullptr;
return result;
}
device->ColorFill(surface, nullptr, D3DCOLOR_XRGB(0, 0, 0));
}
surface->AddRef();
*out = surface;
return D3D_OK;
}
void GfdmSmallHead::release() {
if (surface != nullptr) {
surface->Release();
surface = nullptr;
}
}
HRESULT GfdmSmallHead::compose(IDirect3DDevice9 *device) {
IDirect3DSurface9 *proxy = nullptr;
HRESULT result = backbuffer(device, &proxy);
if (FAILED(result)) {
return result;
}
IDirect3DSurface9 *head = nullptr;
result = device->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
0,
D3DBACKBUFFER_TYPE_MONO,
&head);
if (FAILED(result)) {
proxy->Release();
return result;
}
D3DSURFACE_DESC desc {};
result = head->GetDesc(&desc);
if (SUCCEEDED(result)) {
const RECT content = games::gitadora::arena_subscreen_content_rect(
static_cast<LONG>(desc.Width),
static_cast<LONG>(desc.Height));
// discard swap effect leaves the whole head undefined every frame, so the bars
// have to be repainted along with the image
device->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0));
result = device->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR);
}
head->Release();
proxy->Release();
return result;
}
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() { void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex); std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
gfdm_recovery_armed = false; gfdm_recovery_armed = false;
@@ -4,13 +4,43 @@
#include <d3d9.h> #include <d3d9.h>
#include "games/gitadora/gitadora.h"
inline constexpr UINT GFDM_SIDE_WIDTH = 1080; inline constexpr UINT GFDM_SIDE_WIDTH = 1080;
inline constexpr UINT GFDM_SIDE_HEIGHT = 1920; inline constexpr UINT GFDM_SIDE_HEIGHT = 1920;
inline constexpr UINT GFDM_SMALL_WIDTH = 800; inline constexpr UINT GFDM_SMALL_WIDTH = games::gitadora::ARENA_SUBSCREEN_WIDTH;
inline constexpr UINT GFDM_SMALL_HEIGHT = 1280; inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGHT;
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4; inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1; inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
// Owns the portrait surface the game draws into while the SMALL head is scanned out at a
// different resolution, and composes it onto the real head. Allocates nothing and reports
// nothing until resolve() finds a head that is not the panel size.
struct GfdmSmallHead {
GfdmSmallHead() = default;
~GfdmSmallHead() { release(); }
GfdmSmallHead(const GfdmSmallHead &) = delete;
GfdmSmallHead &operator=(const GfdmSmallHead &) = delete;
// settled with the device, so it cannot change under the game mid-session
void resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters);
bool scaled() const { return active; }
HRESULT backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out);
HRESULT compose(IDirect3DDevice9 *device);
void release();
// the game only ever sees the portrait size it asked for
void apply_logical_size(UINT *width, UINT *height) const;
private:
bool active = false;
D3DFORMAT format = D3DFMT_X8R8G8B8;
D3DMULTISAMPLE_TYPE multisample = D3DMULTISAMPLE_NONE;
DWORD multisample_quality = 0;
IDirect3DSurface9 *surface = nullptr;
};
struct GfdmTwoHeadDeviceState { struct GfdmTwoHeadDeviceState {
explicit GfdmTwoHeadDeviceState( explicit GfdmTwoHeadDeviceState(
D3DPRESENT_PARAMETERS *presentation_parameters, D3DPRESENT_PARAMETERS *presentation_parameters,
@@ -39,6 +69,10 @@ struct GfdmTwoHeadDeviceState {
}; };
bool gfdm_two_head_exclusive(); bool gfdm_two_head_exclusive();
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state);
bool is_fake_subscreen_adapter(UINT adapter); bool is_fake_subscreen_adapter(UINT adapter);
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode); void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode); void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
@@ -0,0 +1,628 @@
#include "d3d9_readback.h"
#include <array>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <vector>
#include "hooks/graphics/graphics.h"
#include "util/logging.h"
namespace d3d9_readback {
namespace {
// the snapshot path stamps frames with this to recognise one left behind by a break in the
// request stream
uint64_t now_us() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *surface = nullptr;
const HRESULT hr = device->CreateOffscreenPlainSurface(
desc.Width, desc.Height, desc.Format, D3DPOOL_SYSTEMMEM, &surface, nullptr);
if (FAILED(hr) || surface == nullptr) {
log_warning("graphics::d3d9",
"failed to create readback surface, hr={}",
FMT_HRESULT(hr));
return nullptr;
}
return SurfacePtr(surface);
}
size_t surface_bytes(const D3DSURFACE_DESC &desc) {
size_t bytes_per_pixel = 4;
switch (desc.Format) {
case D3DFMT_R5G6B5:
case D3DFMT_X1R5G5B5:
case D3DFMT_A1R5G5B5:
bytes_per_pixel = 2;
break;
default:
break;
}
return static_cast<size_t>(desc.Width) * desc.Height * bytes_per_pixel;
}
// idle surfaces are kept between captures, bucketed by layout so that screens of
// differing resolution do not evict each other. a new device drops everything,
// since system memory surfaces outlive Reset but not the device itself
class ReadbackPool {
public:
SurfacePtr acquire(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
{
std::lock_guard<std::mutex> lock(this->mutex);
if (this->device != device) {
this->drop();
this->device = device;
}
auto *bucket = this->find(desc);
if (bucket && !bucket->idle.empty()) {
auto surface = std::move(bucket->idle.back());
bucket->idle.pop_back();
const size_t bytes = surface_bytes(desc);
this->idle_bytes = this->idle_bytes > bytes ? this->idle_bytes - bytes : 0;
return surface;
}
}
return create_readback_surface(device, desc);
}
void release(IDirect3DDevice9 *device, SurfacePtr surface) {
if (!surface) {
return;
}
D3DSURFACE_DESC desc {};
if (FAILED(surface->GetDesc(&desc))) {
return;
}
const size_t bytes = surface_bytes(desc);
std::lock_guard<std::mutex> lock(this->mutex);
if (this->device != device || this->idle_bytes + bytes > MAX_IDLE_BYTES) {
return;
}
auto *bucket = this->find(desc);
if (bucket == nullptr) {
if (this->buckets.size() >= MAX_BUCKETS) {
return;
}
this->buckets.push_back(Bucket { desc.Width, desc.Height, desc.Format, {} });
bucket = &this->buckets.back();
}
if (bucket->idle.size() < MAX_IDLE_PER_BUCKET) {
bucket->idle.push_back(std::move(surface));
this->idle_bytes += bytes;
}
}
// every cached surface holds a reference on the device, so they have to go
// before it does or the device never reaches a zero reference count
void clear_device(IDirect3DDevice9 *device) {
std::lock_guard<std::mutex> lock(this->mutex);
if (this->device != device) {
return;
}
this->drop();
this->device = nullptr;
}
private:
struct Bucket {
UINT width;
UINT height;
D3DFORMAT format;
std::vector<SurfacePtr> idle;
};
static constexpr size_t MAX_BUCKETS = GRAPHICS_CAPTURE_SCREEN_NO;
// one returning surface plus one for the next capture; a full screen surface
// is several megabytes, so the cap matters
static constexpr size_t MAX_IDLE_PER_BUCKET = 2;
// a 4K surface is 33MB, so the per bucket count alone does not bound this
static constexpr size_t MAX_IDLE_BYTES = 64u * 1024 * 1024;
void drop() {
this->buckets.clear();
this->idle_bytes = 0;
}
Bucket *find(const D3DSURFACE_DESC &desc) {
for (auto &bucket : this->buckets) {
if (bucket.width == desc.Width
&& bucket.height == desc.Height
&& bucket.format == desc.Format) {
return &bucket;
}
}
return nullptr;
}
std::mutex mutex;
std::vector<Bucket> buckets;
IDirect3DDevice9 *device = nullptr;
size_t idle_bytes = 0;
};
// deliberately never destroyed: releasing D3D surfaces during static destruction
// would run after d3d9 may already be unloaded
ReadbackPool &pool() {
static ReadbackPool *instance = new ReadbackPool();
return *instance;
}
// the back buffer plus the checks every caller has to make before copying out of it
SurfacePtr open_backbuffer(IDirect3DSwapChain9 *swap_chain, int screen, D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *buffer = nullptr;
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
if (FAILED(hr) || buffer == nullptr) {
log_warning("graphics::d3d9",
"failed to get back buffer for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
return nullptr;
}
SurfacePtr surface(buffer);
hr = surface->GetDesc(&desc);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr));
return nullptr;
}
// GetRenderTargetData rejects multisampled sources. no supported game has been
// seen presenting one, so resolving is left unimplemented rather than untested
if (desc.MultiSampleType != D3DMULTISAMPLE_NONE) {
static std::once_flag warned;
std::call_once(warned, [&desc] {
log_warning("graphics::d3d9",
"back buffer is multisampled ({}), screenshots and capture are unsupported",
static_cast<uint32_t>(desc.MultiSampleType));
});
return nullptr;
}
return surface;
}
SurfacePtr create_snapshot_target(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *surface = nullptr;
// matching the back buffer keeps the blit a straight copy and leaves the pixels in the
// format the conversion step would have seen without the detour
const HRESULT hr = device->CreateRenderTarget(
desc.Width, desc.Height, desc.Format,
D3DMULTISAMPLE_NONE, 0, FALSE, &surface, nullptr);
if (FAILED(hr) || surface == nullptr) {
log_warning("graphics::d3d9",
"failed to create snapshot target, hr={}",
FMT_HRESULT(hr));
return nullptr;
}
return SurfacePtr(surface);
}
// two render targets per screen, reused across frames. two because one holds the frame waiting
// out its deferral while the other takes the next one; a read only holds its target until the
// pixels are in system memory, which is far shorter than the gap between requests.
//
// unlike the readback surfaces these live in the default pool, so they have to be gone before a
// Reset and not merely before the device is released
class SnapshotTargets {
public:
SurfacePtr acquire(
IDirect3DDevice9 *device,
int screen,
const D3DSURFACE_DESC &desc,
uint64_t *out_generation) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return nullptr;
}
std::lock_guard<std::mutex> lock(this->mutex);
if (this->device != device) {
this->drop();
this->device = device;
this->usable = true;
}
auto &entry = this->screens[screen];
Slot *free_slot = nullptr;
for (size_t i = 0; i < SLOTS_PER_SCREEN; i++) {
auto &candidate = entry.slots[(entry.next + i) % SLOTS_PER_SCREEN];
if (!candidate.busy) {
free_slot = &candidate;
entry.next = (entry.next + i + 1) % SLOTS_PER_SCREEN;
break;
}
}
if (free_slot == nullptr) {
return nullptr;
}
auto &slot = *free_slot;
if (slot.target
&& (slot.width != desc.Width
|| slot.height != desc.Height
|| slot.format != desc.Format)) {
slot.target.reset();
}
if (!slot.target) {
slot.target = create_snapshot_target(device, desc);
if (!slot.target) {
// a back buffer format the device will not give us a render target for. the
// inline path can still read it, so stop trying rather than lose the stream
this->usable = false;
return nullptr;
}
slot.width = desc.Width;
slot.height = desc.Height;
slot.format = desc.Format;
}
slot.busy = true;
*out_generation = this->current_generation;
slot.target->AddRef();
return SurfacePtr(slot.target.get());
}
// holds the fresh snapshot back and returns the one from the previous request, which by now
// has had a full frame for its blit and transfer to land
std::optional<Snapshot> rotate(int screen, std::optional<Snapshot> fresh) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return std::nullopt;
}
const uint64_t now = now_us();
std::optional<Snapshot> previous;
{
std::lock_guard<std::mutex> lock(this->mutex);
previous = std::move(this->pending[screen]);
this->pending[screen] = std::move(fresh);
}
// a frame waits here for the next request rather than for a deadline, so a gap in
// requests, a client reconnecting most obviously, would otherwise hand the new session
// a frame from before the gap. showing a stale frame is worse than showing none
if (previous.has_value() && now - previous->issued_us > MAX_DEFERRAL_US) {
return std::nullopt;
}
return previous;
}
// a reader whose target was already thrown away must not free the slot, or it would free
// the snapshot that replaced it while that one is still being read
void finish(int screen, uint64_t generation, IDirect3DSurface9 *target) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return;
}
{
std::lock_guard<std::mutex> lock(this->mutex);
if (generation != this->current_generation) {
return;
}
this->free_slot(screen, target);
}
this->idle.notify_all();
}
bool still_current(uint64_t generation) {
std::lock_guard<std::mutex> lock(this->mutex);
return generation == this->current_generation;
}
bool is_usable() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->usable;
}
// Reset fails outright while the device still owns default pool resources, so dropping our
// own references is not enough and any read in flight has to finish first. the wait is
// bounded because a failed Reset is something games retry and a stalled present thread is
// not something they survive
void discard(IDirect3DDevice9 *device) {
// destroyed after the lock is released, since dropping a snapshot calls back in here
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> stale;
{
std::unique_lock<std::mutex> lock(this->mutex);
if (this->device != nullptr && this->device != device) {
return;
}
// deferred frames are abandoned rather than waited for; only a read that is already
// running has to be allowed to finish
for (auto &held : this->pending) {
if (held.has_value() && held->surface) {
this->free_slot(held->screen, held->surface.get());
}
}
stale = std::move(this->pending);
this->pending = {};
const bool drained = this->idle.wait_for(
lock,
std::chrono::milliseconds(100),
[this] { return !this->any_busy(); });
if (!drained) {
log_warning("graphics::d3d9",
"capture snapshot still in flight, discarding its target anyway");
}
this->drop();
this->device = nullptr;
}
}
private:
static constexpr size_t SLOTS_PER_SCREEN = 2;
// generous next to the frame interval this is meant to bridge, so that ordinary jitter
// never trips it and only a real break in the request stream does
static constexpr uint64_t MAX_DEFERRAL_US = 250'000;
struct Slot {
SurfacePtr target;
UINT width = 0;
UINT height = 0;
D3DFORMAT format = D3DFMT_UNKNOWN;
bool busy = false;
};
struct Screen {
std::array<Slot, SLOTS_PER_SCREEN> slots;
size_t next = 0;
};
void free_slot(int screen, IDirect3DSurface9 *target) {
for (auto &slot : this->screens[screen].slots) {
if (slot.target.get() == target) {
slot.busy = false;
return;
}
}
}
bool any_busy() const {
for (const auto &entry : this->screens) {
for (const auto &slot : entry.slots) {
if (slot.busy) {
return true;
}
}
}
return false;
}
void drop() {
for (auto &entry : this->screens) {
for (auto &slot : entry.slots) {
slot.target.reset();
slot.busy = false;
}
entry.next = 0;
}
this->current_generation++;
}
std::mutex mutex;
std::condition_variable idle;
std::array<Screen, GRAPHICS_CAPTURE_SCREEN_NO> screens;
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> pending;
IDirect3DDevice9 *device = nullptr;
uint64_t current_generation = 1;
bool usable = true;
};
// never destroyed, for the same reason the readback pool is not
SnapshotTargets &targets() {
static SnapshotTargets *instance = new SnapshotTargets();
return *instance;
}
} // namespace
void release_device_resources(IDirect3DDevice9 *device) {
targets().discard(device);
pool().clear_device(device);
}
void discard_snapshot_targets(IDirect3DDevice9 *device) {
targets().discard(device);
}
bool snapshots_supported() {
return targets().is_usable();
}
BackbufferCopy::~BackbufferCopy() {
if (this->pooled && this->surface) {
pool().release(this->device, std::move(this->surface));
}
}
Snapshot::~Snapshot() {
// still holding the target means the read never ran, and the slot would otherwise stay
// marked busy and take the screen out of capture permanently
if (this->surface) {
targets().finish(this->screen, this->generation, this->surface.get());
}
}
namespace {
std::optional<Snapshot> take_snapshot(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
const uint64_t started_us = now_us();
D3DSURFACE_DESC desc {};
auto buffer = open_backbuffer(swap_chain, screen, desc);
if (!buffer) {
return std::nullopt;
}
uint64_t generation = 0;
auto target = targets().acquire(device, screen, desc, &generation);
if (!target) {
return std::nullopt;
}
// built before the blit so that a failure below hands the slot back through the destructor
Snapshot snapshot;
snapshot.screen = screen;
snapshot.desc = desc;
snapshot.device = device;
snapshot.surface = std::move(target);
snapshot.generation = generation;
snapshot.issued_us = started_us;
// the point of the whole exercise: this is queued rather than waited on, so the game pays
// for issuing the copy and not for it completing. identical size and format, so there is
// no filtering to ask for
const HRESULT hr = device->StretchRect(
buffer.get(), nullptr, snapshot.surface.get(), nullptr, D3DTEXF_NONE);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to snapshot back buffer for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
return std::nullopt;
}
return snapshot;
}
} // namespace
std::optional<Snapshot> snapshot_backbuffer(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
return targets().rotate(screen, take_snapshot(device, swap_chain, screen));
}
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot) {
if (!snapshot.surface) {
return std::nullopt;
}
// a Reset between the blit and now means the target no longer holds the captured frame
if (!targets().still_current(snapshot.generation)) {
return std::nullopt;
}
auto destination = pool().acquire(snapshot.device, snapshot.desc);
if (!destination) {
return std::nullopt;
}
const HRESULT hr = snapshot.device->GetRenderTargetData(
snapshot.surface.get(), destination.get());
// the target is reusable as soon as the pixels are in system memory. dropping the
// reference before freeing the slot keeps the destructor from freeing it twice
const int screen = snapshot.screen;
const uint64_t generation = snapshot.generation;
IDirect3DSurface9 *target = snapshot.surface.get();
snapshot.surface.reset();
targets().finish(screen, generation, target);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to read snapshot contents, hr={}",
FMT_HRESULT(hr));
pool().release(snapshot.device, std::move(destination));
return std::nullopt;
}
BackbufferCopy copy;
copy.screen = screen;
copy.desc = snapshot.desc;
copy.device = snapshot.device;
copy.surface = std::move(destination);
copy.pooled = true;
return copy;
}
std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
D3DSURFACE_DESC desc {};
auto buffer = open_backbuffer(swap_chain, screen, desc);
if (!buffer) {
return std::nullopt;
}
auto destination = pooled
? pool().acquire(device, desc)
: create_readback_surface(device, desc);
if (!destination) {
return std::nullopt;
}
const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get());
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to copy back buffer contents, hr={}",
FMT_HRESULT(hr));
if (pooled) {
pool().release(device, std::move(destination));
}
return std::nullopt;
}
BackbufferCopy copy;
copy.screen = screen;
copy.desc = desc;
copy.device = device;
copy.surface = std::move(destination);
copy.pooled = pooled;
return copy;
}
}
@@ -0,0 +1,90 @@
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
#include <d3d9.h>
namespace d3d9_readback {
struct SurfaceReleaser {
void operator()(IDirect3DSurface9 *surface) const {
surface->Release();
}
};
using SurfacePtr = std::unique_ptr<IDirect3DSurface9, SurfaceReleaser>;
// system memory copy of a back buffer; locking it neither stalls the GPU nor reads over PCIe
struct BackbufferCopy {
int screen {};
D3DSURFACE_DESC desc {};
IDirect3DDevice9 *device = nullptr;
SurfacePtr surface;
bool pooled = false;
BackbufferCopy() = default;
BackbufferCopy(BackbufferCopy &&) noexcept = default;
BackbufferCopy &operator=(BackbufferCopy &&) noexcept = default;
BackbufferCopy(const BackbufferCopy &) = delete;
BackbufferCopy &operator=(const BackbufferCopy &) = delete;
~BackbufferCopy();
};
// pooled copies reuse surfaces across calls and return them once the copy is destroyed,
// so the caller must keep it alive for as long as the pixels are being read
std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *swap_chain,
int screen,
bool pooled);
// GPU side copy of a back buffer, taken while the contents are still the frame that was
// presented, so that reading them into system memory no longer has to happen before it
struct Snapshot {
int screen {};
D3DSURFACE_DESC desc {};
IDirect3DDevice9 *device = nullptr;
SurfacePtr surface;
uint64_t generation {};
// when the blit was issued, so a frame left behind by a break in the request stream can
// be recognised as stale rather than handed over
uint64_t issued_us {};
Snapshot() = default;
Snapshot(Snapshot &&) noexcept = default;
Snapshot &operator=(Snapshot &&) noexcept = default;
Snapshot(const Snapshot &) = delete;
Snapshot &operator=(const Snapshot &) = delete;
~Snapshot();
};
// for the present thread, between the last EndScene and Present. blits the current frame,
// then returns the snapshot taken on the *previous* call: waiting a frame before reading
// means the blit and its system memory transfer have already happened, so the read does not
// stall on the GPU. costs the stream one frame of latency.
//
// returns nothing on the first call of a stream, and whenever the frame could not be taken,
// which is the caller's cue to skip rather than to wait
std::optional<Snapshot> snapshot_backbuffer(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *swap_chain,
int screen);
// the expensive half, for a thread that is not the present thread. only legal on a device
// created with D3DCREATE_MULTITHREADED
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot);
// false once a device has refused to give up a render target matching its back buffer,
// which leaves reading the back buffer directly as the only way to capture it
bool snapshots_supported();
// snapshot targets live in the default pool, so unlike the readback surfaces they have to
// be gone before a Reset and not merely before the device is released
void discard_snapshot_targets(IDirect3DDevice9 *device);
// pooled surfaces hold references on the device; call this before releasing it
void release_device_resources(IDirect3DDevice9 *device);
}
File diff suppressed because it is too large Load Diff
@@ -2,8 +2,12 @@
#include <d3d9.h> #include <d3d9.h>
void graphics_d3d9_poll_screenshot_hotkey(); struct WrappedIDirect3DDevice9;
void graphics_d3d9_process_screenshot_and_capture( void graphics_d3d9_process_screenshot(
IDirect3DDevice9 *device, IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain); WrappedIDirect3DDevice9 *wrapped_device);
void graphics_d3d9_process_capture(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device);
@@ -21,6 +21,12 @@
} \ } \
return ret return ret
// inert unless this is the SMALL head and it is being scaled onto a differently sized monitor
static bool scales_small_head(const WrappedIDirect3DSwapChain9 *chain) {
return chain->native_group_head == WrappedIDirect3DSwapChain9::NativeGroupHead::Small
&& chain->pDev->gfdm_small_head.scaled();
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) { HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) { if (ppvObj == nullptr) {
return E_POINTER; return E_POINTER;
@@ -89,6 +95,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev); graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
} }
if (scales_small_head(this)) {
pDev->gfdm_small_head.compose(pDev->pReal);
}
HRESULT result = pReal->Present( HRESULT result = pReal->Present(
pSourceRect, pSourceRect,
pDestRect, pDestRect,
@@ -138,6 +148,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type, HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
IDirect3DSurface9 **ppBackBuffer) IDirect3DSurface9 **ppBackBuffer)
{ {
if (scales_small_head(this) && iBackBuffer == 0 && Type == D3DBACKBUFFER_TYPE_MONO) {
CHECK_RESULT(pDev->gfdm_small_head.backbuffer(pDev->pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer)); CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
} }
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) { HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
@@ -159,7 +173,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetDevice(IDirect3DDevice9
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters( HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
D3DPRESENT_PARAMETERS *pPresentationParameters) D3DPRESENT_PARAMETERS *pPresentationParameters)
{ {
CHECK_RESULT(pReal->GetPresentParameters(pPresentationParameters)); HRESULT result = pReal->GetPresentParameters(pPresentationParameters);
if (SUCCEEDED(result) && pPresentationParameters != nullptr && scales_small_head(this)) {
pDev->gfdm_small_head.apply_logical_size(
&pPresentationParameters->BackBufferWidth,
&pPresentationParameters->BackBufferHeight);
}
CHECK_RESULT(result);
} }
/* /*
+178 -13
View File
@@ -17,11 +17,14 @@
#include "games/ddr/ddr.h" #include "games/ddr/ddr.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/io.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "hooks/graphics/jpeg_encoder.h"
#include "hooks/graphics/backends/d3d9/d3d9_backend.h" #include "hooks/graphics/backends/d3d9/d3d9_backend.h"
#include "hooks/graphics/backends/d3d11/d3d11_backend.h" #include "hooks/graphics/backends/d3d11/d3d11_backend.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "misc/hotkeys.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "touch/touch_gestures.h" #include "touch/touch_gestures.h"
@@ -31,6 +34,7 @@
#include "util/utils.h" #include "util/utils.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "touch/native/inject.h" #include "touch/native/inject.h"
#include "touch/native/nativetouchhook.h"
#include "util/time.h" #include "util/time.h"
#include "rawinput/rawinput.h" #include "rawinput/rawinput.h"
@@ -48,6 +52,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr; static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr; static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr; HWND POPN_SUBSCREEN_WINDOW = nullptr;
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
HWND NDD_MAIN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false; bool FAKE_SUBSCREEN_ADAPTER = false;
// icon // icon
@@ -61,7 +67,6 @@ static bool GRAPHICS_SCREENSHOT_TRIGGER = false;
static std::set<int> GRAPHICS_SCREENS { 0 }; static std::set<int> GRAPHICS_SCREENS { 0 };
static std::mutex GRAPHICS_SCREENS_M {}; static std::mutex GRAPHICS_SCREENS_M {};
static std::vector<int> GRAPHICS_CAPTURE_SCREENS; static std::vector<int> GRAPHICS_CAPTURE_SCREENS;
static const size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
static std::mutex GRAPHICS_CAPTURE_SCREENS_M {}; static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {}; static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {}; static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
@@ -116,6 +121,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
// settings // settings
std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146"; std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146";
std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots"; std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots";
bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = false;
bool GRAPHICS_SCREENSHOT_SUBSCREENS = false;
static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr; static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr;
static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr; static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr;
@@ -212,6 +219,18 @@ static void gitadora_remember_window(HWND hWnd, const std::string &window_name)
} else if (window_name == "SMALL") { } else if (window_name == "SMALL") {
GFDM_SUBSCREEN_WINDOW = hWnd; GFDM_SUBSCREEN_WINDOW = hWnd;
} }
// touch belongs to the SMALL panel when it exists, otherwise to the main window
// that draws the subscreen overlay
const bool hosts_touch = window_name == "SMALL" ||
(window_name == "GITADORA" && !graphics_gitadora_has_dedicated_subscreen());
if (nativetouch::is_hooked() && hWnd != nullptr && hosts_touch) {
nativetouch::inject::set_preferred_injection_window(hWnd);
}
}
bool graphics_gitadora_has_dedicated_subscreen() {
return GFDM_SUBSCREEN_WINDOW != nullptr;
} }
bool graphics_gitadora_prepare_two_head_device_window( bool graphics_gitadora_prepare_two_head_device_window(
@@ -612,9 +631,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
const bool is_sdvx = avs::game::is_model("KFC");
bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub"); bool is_tdj_sub_window = avs::game::is_model("LDJ") && window_name.ends_with(" sub");
bool is_sdvx_sub_window = avs::game::is_model("KFC") && window_name.ends_with(" Sub Screen"); bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen"); bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
const bool is_ndd = avs::game::is_model("NDD");
bool is_ndd_sub_window = is_ndd && window_name.starts_with("Aska MultiDisplay");
bool is_ndd_main_window = is_ndd && window_name == "ASKA";
const std::string gfdm_window_name = games::gitadora::is_arena_model() const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name) ? gitadora_canonical_window_name(effective_window_name)
: ""; : "";
@@ -716,6 +740,20 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(SDVX_SUBSCREEN_WINDOW);
} }
// SDVX registers touch on both windows, so name the one synthetic touches must land on
// instead of letting window creation order decide: the sub screen window when windowed,
// the main window in fullscreen since the game reads it in primary-display coordinates
if (nativetouch::is_hooked() &&
result != nullptr &&
(GRAPHICS_WINDOWED ? is_sdvx_sub_window : is_sdvx_main_window)) {
log_misc(
"graphics",
"SDVX touch surface is {}, {}",
fmt::ptr(result),
window_name);
nativetouch::inject::set_preferred_injection_window(result);
}
// only hook touch window if multiple windows are allowed // only hook touch window if multiple windows are allowed
if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") { if (gfdm_window_name == "LEFT" || gfdm_window_name == "RIGHT") {
gitadora_remember_window(result, gfdm_window_name); gitadora_remember_window(result, gfdm_window_name);
@@ -727,6 +765,11 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW); graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
} }
// the dedicated SMALL window is the touch panel; mouse and API touch target it
if (nativetouch::is_hooked() && result != nullptr) {
nativetouch::inject::register_and_attach_window(result);
}
} }
if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) { if (is_gfdm_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
gitadora_force_window_style(result); gitadora_force_window_style(result);
@@ -739,6 +782,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
} }
} }
if (is_ndd_sub_window) {
NDD_SUBSCREEN_WINDOW = result;
}
if (is_ndd_main_window) {
NDD_MAIN_WINDOW = result;
}
disable_touch_gestures(result); disable_touch_gestures(result);
log_misc( log_misc(
"graphics", "graphics",
@@ -864,6 +915,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
return value; return value;
} }
// the sub screen renders into a fixed 800x480 buffer, but the game's saved layout asks for rects
// that do not match it, and dxgi stretches the buffer to fill whatever the client area ends up as
static void ndd_subscreen_size(HWND hWnd, int &width, int &height) {
RECT rect {};
SetRect(&rect, 0, 0, 800, 480);
AdjustWindowRect(&rect, GetWindowLongA(hWnd, GWL_STYLE), 0);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
// the saved layout drops the sub window wherever it sat on the machine that wrote the file
static void ndd_subscreen_position(int &x, int &y) {
RECT main {};
if (NDD_MAIN_WINDOW != nullptr && GetWindowRect(NDD_MAIN_WINDOW, &main)) {
x = main.right;
y = main.top;
}
}
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) { static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})", log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
fmt::ptr(hWnd), fmt::ptr(hWnd),
@@ -893,6 +964,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nHeight = rect.bottom - rect.top; nHeight = rect.bottom - rect.top;
} }
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
ndd_subscreen_size(hWnd, nWidth, nHeight);
ndd_subscreen_position(X, Y);
}
// iidx windowed TDJ mode // iidx windowed TDJ mode
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) { if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
if (GRAPHICS_IIDX_WSUB) { if (GRAPHICS_IIDX_WSUB) {
@@ -1035,6 +1111,15 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter, static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) { int X, int Y, int cx, int cy, UINT uFlags) {
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
if (!(uFlags & SWP_NOSIZE)) {
ndd_subscreen_size(hWnd, cx, cy);
}
if (!(uFlags & SWP_NOMOVE)) {
ndd_subscreen_position(X, Y);
}
}
if (is_gfdm_two_head_small_window(hWnd) && if (is_gfdm_two_head_small_window(hWnd) &&
((uFlags & SWP_HIDEWINDOW) || ((uFlags & SWP_HIDEWINDOW) ||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) { (uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
@@ -1100,6 +1185,15 @@ static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
return true; return true;
} }
// fullscreen SDVX keeps two adapters so the subscreen overlay can draw, so the game still
// creates the sub window even when the user asked for it to be gone
if (avs::game::is_model("KFC") &&
GRAPHICS_PREVENT_SECONDARY_WINDOWS &&
hWnd == SDVX_SUBSCREEN_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true;
}
// call original // call original
return ShowWindow_orig(hWnd, nCmdShow); return ShowWindow_orig(hWnd, nCmdShow);
} }
@@ -1316,7 +1410,9 @@ void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParamet
const bool native_touch_overlay = const bool native_touch_overlay =
(games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) || (games::iidx::NATIVE_TOUCH && games::iidx::TDJ_MODE && !GRAPHICS_IIDX_WSUB) ||
(games::popn::NATIVE_TOUCH && (games::popn::NATIVE_TOUCH &&
games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS); games::popn::is_pikapika_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS) ||
(games::gitadora::NATIVE_TOUCH &&
games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOWS);
if (native_touch_overlay) { if (native_touch_overlay) {
nativetouch::inject::register_and_attach_window(hWnd); nativetouch::inject::register_and_attach_window(hWnd);
} }
@@ -1374,6 +1470,12 @@ void graphics_screens_get(std::vector<int> &screens) {
screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end()); screens.insert(screens.end(), GRAPHICS_SCREENS.begin(), GRAPHICS_SCREENS.end());
} }
void graphics_poll_screenshot_hotkey() {
if (hotkeys::consume_screenshot()) {
graphics_screenshot_trigger();
}
}
void graphics_screenshot_trigger() { void graphics_screenshot_trigger() {
GRAPHICS_SCREENSHOT_TRIGGER = true; GRAPHICS_SCREENSHOT_TRIGGER = true;
} }
@@ -1426,10 +1528,33 @@ void graphics_capture_skip(int screen) {
GRAPHICS_CAPTURE_CV[screen].notify_one(); GRAPHICS_CAPTURE_CV[screen].notify_one();
} }
bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, bool graphics_capture_last_size(int screen, int *width, int *height) {
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp, if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return false;
}
// consuming a frame clears the pixels but leaves the size, so this survives the read
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
const auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
if (!capture.width || !capture.height) {
return false;
}
if (width != nullptr) {
*width = capture.width;
}
if (height != nullptr) {
*height = capture.height;
}
return true;
}
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide, uint64_t *timestamp,
int *width, int *height) { int *width, int *height) {
out = nullptr;
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) { if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return false; return false;
} }
@@ -1501,11 +1626,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
capture_height = height_new; capture_height = height_new;
} }
// compress out = std::move(capture_data);
auto success = TooJpeg::writeJpeg(
receiver, capture_data.get(),
capture_width, capture_height,
rgb, quality, downsample);
// status // status
if (timestamp) { if (timestamp) {
@@ -1518,11 +1639,45 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
*height = capture_height; *height = capture_height;
} }
return true;
}
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
int quality, int divide, uint64_t *timestamp,
int *width, int *height) {
out.clear();
std::shared_ptr<uint8_t[]> pixels;
int capture_width = 0;
int capture_height = 0;
if (!graphics_capture_receive_raw(
screen, pixels, divide, timestamp, &capture_width, &capture_height)) {
return false;
}
// compress
const bool success = jpeg_encoder::encode(
out, pixels.get(),
capture_width, capture_height, quality);
if (!success) {
out.clear();
}
// status
if (width) {
*width = capture_width;
}
if (height) {
*height = capture_height;
}
// clean up // clean up
return success; return success;
} }
std::string graphics_screenshot_genpath() { std::string graphics_screenshot_genpath(const std::vector<int> &screens) {
// verify dir path // verify dir path
if (GRAPHICS_SCREENSHOT_DIR.empty()) { if (GRAPHICS_SCREENSHOT_DIR.empty()) {
@@ -1547,11 +1702,21 @@ std::string graphics_screenshot_genpath() {
auto tm_now = *std::gmtime(&t_now); auto tm_now = *std::gmtime(&t_now);
auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d")); auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d"));
// find next filename // find next filename; the whole set has to be free so one shot stays numbered together
size_t id = 0; size_t id = 0;
while (true) { while (true) {
auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id); auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id);
if (!fileutils::file_exists(filepath)) { bool available = !fileutils::file_exists(filepath);
for (const auto screen : screens) {
if (!available) {
break;
}
if (screen != 0) {
available = !fileutils::file_exists(fmt::format(
"{}\\{}_{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id, screen));
}
}
if (available) {
return filepath; return filepath;
} }
+22 -5
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include <optional> #include <optional>
@@ -12,8 +13,6 @@
#include <dwmapi.h> #include <dwmapi.h>
#endif #endif
#include "external/toojpeg/toojpeg.h"
// order must match spice2x_AutoOrientation UI enum order // order must match spice2x_AutoOrientation UI enum order
enum graphics_orientation { enum graphics_orientation {
ORIENTATION_CW = 0, ORIENTATION_CW = 0,
@@ -104,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
extern HWND SDVX_SUBSCREEN_WINDOW; extern HWND SDVX_SUBSCREEN_WINDOW;
extern HWND POPN_SUBSCREEN_WINDOW; extern HWND POPN_SUBSCREEN_WINDOW;
extern HWND GFDM_SUBSCREEN_WINDOW; extern HWND GFDM_SUBSCREEN_WINDOW;
extern HWND NDD_MAIN_WINDOW;
extern bool SUBSCREEN_FORCE_REDRAW; extern bool SUBSCREEN_FORCE_REDRAW;
extern bool FAKE_SUBSCREEN_ADAPTER; extern bool FAKE_SUBSCREEN_ADAPTER;
@@ -111,6 +111,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER;
// settings // settings
extern std::string GRAPHICS_DEVICEID; extern std::string GRAPHICS_DEVICEID;
extern std::string GRAPHICS_SCREENSHOT_DIR; extern std::string GRAPHICS_SCREENSHOT_DIR;
extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY;
extern bool GRAPHICS_SCREENSHOT_SUBSCREENS;
// Direct3D 9 settings // Direct3D 9 settings
extern std::optional<UINT> D3D9_ADAPTER; extern std::optional<UINT> D3D9_ADAPTER;
@@ -119,6 +121,7 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
void graphics_init(); void graphics_init();
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
bool graphics_gitadora_has_dedicated_subscreen();
// The native GITADORA two-head D3D9 group uses the game's named SMALL // The native GITADORA two-head D3D9 group uses the game's named SMALL
// device window for the native physical SMALL head. The game requests // device window for the native physical SMALL head. The game requests
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized // D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
@@ -134,17 +137,31 @@ void graphics_hook_subscreen_window(HWND hWnd);
void graphics_screens_register(int screen); void graphics_screens_register(int screen);
void graphics_screens_unregister(int screen); void graphics_screens_unregister(int screen);
void graphics_screens_get(std::vector<int> &screens); void graphics_screens_get(std::vector<int> &screens);
void graphics_poll_screenshot_hotkey();
void graphics_screenshot_trigger(); void graphics_screenshot_trigger();
bool graphics_screenshot_consume(); bool graphics_screenshot_consume();
inline constexpr size_t GRAPHICS_CAPTURE_SCREEN_NO = 4;
void graphics_capture_trigger(int screen); void graphics_capture_trigger(int screen);
bool graphics_capture_consume(int *screen); bool graphics_capture_consume(int *screen);
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height); void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
void graphics_capture_skip(int screen); void graphics_capture_skip(int screen);
bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, // size of the last frame captured off this screen, before any caller side downscale; false
bool rgb = true, int quality = 80, bool downsample = true, int divide = 0, // until one has been captured, so it cannot report a size for a screen the game never drew
bool graphics_capture_last_size(int screen, int *width, int *height);
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide = 0,
uint64_t *timestamp = nullptr, uint64_t *timestamp = nullptr,
int *width = nullptr, int *height = nullptr); int *width = nullptr, int *height = nullptr);
std::string graphics_screenshot_genpath(); // on success `out` holds the encoded JPEG; its storage is reused across calls
bool graphics_capture_receive_jpeg(int screen, std::vector<uint8_t> &out,
int quality = 80, int divide = 0,
uint64_t *timestamp = nullptr,
int *width = nullptr, int *height = nullptr);
// the returned path is for screen 0; any extra screens only reserve their suffixed names
std::string graphics_screenshot_genpath(const std::vector<int> &screens = {});
// graphics_windowed.cpp // graphics_windowed.cpp
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+156
View File
@@ -0,0 +1,156 @@
#include "jpeg_encoder.h"
#ifdef SPICE_JPEG
#include <csetjmp>
#include <cstdio>
#include <jpeglib.h>
namespace jpeg_encoder {
namespace {
constexpr size_t CHUNK_SIZE = 16 * 1024;
// libjpeg writes through a destination manager; this one appends straight into
// the caller's vector so the encoded frame is never copied
struct VectorDestination {
jpeg_destination_mgr mgr {};
std::vector<uint8_t> *out = nullptr;
uint8_t chunk[CHUNK_SIZE] {};
};
void dest_init(j_compress_ptr cinfo) {
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
dest->mgr.next_output_byte = dest->chunk;
dest->mgr.free_in_buffer = CHUNK_SIZE;
}
// libjpeg cannot unwind a C++ exception out of its own frames, so growing the
// output has to fail by value and be turned into an error_exit by the caller
bool dest_append(VectorDestination *dest, size_t size) {
try {
dest->out->insert(dest->out->end(), dest->chunk, dest->chunk + size);
return true;
} catch (...) {
return false;
}
}
boolean dest_empty(j_compress_ptr cinfo) {
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
// returning FALSE would mean suspension to libjpeg, not failure
if (!dest_append(dest, CHUNK_SIZE)) {
(*cinfo->err->error_exit)(reinterpret_cast<j_common_ptr>(cinfo));
}
dest->mgr.next_output_byte = dest->chunk;
dest->mgr.free_in_buffer = CHUNK_SIZE;
return TRUE;
}
void dest_term(j_compress_ptr cinfo) {
auto dest = reinterpret_cast<VectorDestination *>(cinfo->dest);
if (!dest_append(dest, CHUNK_SIZE - dest->mgr.free_in_buffer)) {
(*cinfo->err->error_exit)(reinterpret_cast<j_common_ptr>(cinfo));
}
}
// the default handler calls exit(), which is not an option inside a game process
struct ErrorManager {
jpeg_error_mgr mgr {};
jmp_buf escape {};
};
void on_error(j_common_ptr cinfo) {
longjmp(reinterpret_cast<ErrorManager *>(cinfo->err)->escape, 1);
}
void on_message(j_common_ptr) {
}
} // namespace
bool encode(
std::vector<uint8_t> &out,
const uint8_t *pixels,
int width,
int height,
int quality) {
if (!pixels || width <= 0 || height <= 0) {
return false;
}
if (quality < 1) {
quality = 1;
} else if (quality > 100) {
quality = 100;
}
// zero init is load bearing: the trap below is armed before the struct is
// created, and jpeg_destroy_compress only tolerates that on a zeroed struct
jpeg_compress_struct cinfo {};
ErrorManager err;
VectorDestination dest;
cinfo.err = jpeg_std_error(&err.mgr);
err.mgr.error_exit = on_error;
err.mgr.output_message = on_message;
if (setjmp(err.escape)) {
jpeg_destroy_compress(&cinfo);
return false;
}
jpeg_create_compress(&cinfo);
dest.out = &out;
dest.mgr.init_destination = dest_init;
dest.mgr.empty_output_buffer = dest_empty;
dest.mgr.term_destination = dest_term;
cinfo.dest = &dest.mgr;
cinfo.image_width = static_cast<JDIMENSION>(width);
cinfo.image_height = static_cast<JDIMENSION>(height);
cinfo.in_color_space = JCS_RGB;
cinfo.input_components = 3;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
// 4:2:0, matching what the capture path asked the previous encoder for
cinfo.comp_info[0].h_samp_factor = 2;
cinfo.comp_info[0].v_samp_factor = 2;
jpeg_start_compress(&cinfo, TRUE);
const size_t pitch = static_cast<size_t>(width) * 3;
while (cinfo.next_scanline < cinfo.image_height) {
auto row = const_cast<uint8_t *>(pixels + cinfo.next_scanline * pitch);
JSAMPROW rows[1] = { row };
jpeg_write_scanlines(&cinfo, rows, 1);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
return true;
}
}
#else // SPICE_JPEG
namespace jpeg_encoder {
// builds without libjpeg-turbo (the WinXP toolchains) simply cannot encode;
// callers already treat a false return as "no frame available"
bool encode(std::vector<uint8_t> &out, const uint8_t *, int, int, int) {
out.clear();
return false;
}
}
#endif // SPICE_JPEG
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
#include <vector>
namespace jpeg_encoder {
// appends a baseline 4:2:0 JPEG of packed 24bpp RGB pixels to `out`
bool encode(
std::vector<uint8_t> &out,
const uint8_t *pixels,
int width,
int height,
int quality);
}
+27 -12
View File
@@ -26,13 +26,13 @@ constexpr UINT CODEPAGE_SHIFT_JIS = 932;
static decltype(GetACP) *GetACP_orig = nullptr; static decltype(GetACP) *GetACP_orig = nullptr;
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr; static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr; static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr; static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;
#ifdef SPICE64 #ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr; static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr; static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr; static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr; static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr; static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
#endif #endif
@@ -209,6 +209,8 @@ static BOOL WINAPI IsDBCSLeadByteEx_hook(
return IsDBCSLeadByteEx_orig(CodePage, TestChar); return IsDBCSLeadByteEx_orig(CodePage, TestChar);
} }
#endif
static static
int int
WINAPI WINAPI
@@ -244,6 +246,8 @@ WideCharToMultiByte_hook(
lpUsedDefaultChar); lpUsedDefaultChar);
} }
#ifdef SPICE64
int int
WINAPI WINAPI
GetLocaleInfoA_hook( GetLocaleInfoA_hook(
@@ -332,9 +336,11 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig); &GetLocaleInfoA_orig);
} }
// for TDJ subscreen search keyboard and T44 narrow-string handling // for TDJ subscreen search keyboard
// T44 narrow-string handling
// NDD text measuring
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) || if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) { avs::game::is_model({ "T44", "NDD" })) {
log_info("hooks::lang", "hooking IsDBCSLeadByte"); log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try( detour::trampoline_try(
"kernel32.dll", "kernel32.dll",
@@ -343,15 +349,6 @@ void hooks::lang::early_init() {
&IsDBCSLeadByte_orig); &IsDBCSLeadByte_orig);
} }
if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) {
log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try(
"kernel32.dll",
"WideCharToMultiByte",
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) { if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
detour::trampoline_try( detour::trampoline_try(
"kernel32.dll", "kernel32.dll",
@@ -362,6 +359,24 @@ void hooks::lang::early_init() {
#endif #endif
#ifdef SPICE64
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" });
#else
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
#endif
if (hook_wide_char_to_multi_byte) {
log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try(
"kernel32.dll",
"WideCharToMultiByte",
WideCharToMultiByte_hook,
&WideCharToMultiByte_orig);
}
} }
void hooks::lang::init() { void hooks::lang::init() {
+63 -12
View File
@@ -14,6 +14,7 @@
#include "acio/icca/icca.h" #include "acio/icca/icca.h"
#include "acio/mdxf/mdxf.h" #include "acio/mdxf/mdxf.h"
#include "api/controller.h" #include "api/controller.h"
#include "api/stream_server.h"
#include "avs/automap.h" #include "avs/automap.h"
#include "avs/core.h" #include "avs/core.h"
#include "avs/ea3.h" #include "avs/ea3.h"
@@ -90,13 +91,13 @@
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "launcher/logger.h" #include "launcher/logger.h"
#include "launcher/signal.h" #include "launcher/signal.h"
#include "launcher/superexit.h"
#include "launcher/richpresence.h" #include "launcher/richpresence.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "launcher/options.h" #include "launcher/options.h"
#include "misc/bt5api.h" #include "misc/bt5api.h"
#include "misc/device.h" #include "misc/device.h"
#include "misc/eamuse.h" #include "misc/eamuse.h"
#include "misc/hotkeys.h"
#include "misc/extdev.h" #include "misc/extdev.h"
#include "misc/ami2000.h" #include "misc/ami2000.h"
#include "misc/sciunit.h" #include "misc/sciunit.h"
@@ -153,6 +154,7 @@ std::string CARD_OVERRIDES[2];
// sub-systems // sub-systems
std::unique_ptr<api::Controller> API_CONTROLLER; std::unique_ptr<api::Controller> API_CONTROLLER;
std::unique_ptr<api::StreamServer> API_STREAM_SERVER;
std::unique_ptr<rawinput::RawInputManager> RI_MGR; std::unique_ptr<rawinput::RawInputManager> RI_MGR;
// trigger NVIDIA Optimus & AMD Enduro High Performance Graphics // trigger NVIDIA Optimus & AMD Enduro High Performance Graphics
@@ -201,6 +203,7 @@ int main_implementation(int argc, char *argv[]) {
bool api_pretty = false; bool api_pretty = false;
bool api_debug = false; bool api_debug = false;
unsigned short api_port = 1337; unsigned short api_port = 1337;
bool api_stream_enable = false;
std::string api_pass = ""; std::string api_pass = "";
std::vector<std::string> api_serial_port; std::vector<std::string> api_serial_port;
std::vector<DWORD> api_serial_baud; std::vector<DWORD> api_serial_baud;
@@ -693,6 +696,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) { if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) {
games::gitadora::ALLOW_REALTEK_AUDIO = true; games::gitadora::ALLOW_REALTEK_AUDIO = true;
} }
if (options[launcher::Options::GitaDoraSubscreenLandscape].value_bool()) {
games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = true;
}
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) { if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
attach_nostalgia = true; attach_nostalgia = true;
} }
@@ -1035,6 +1041,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::APIScreenMirrorDivide].is_active()) { if (options[launcher::Options::APIScreenMirrorDivide].is_active()) {
api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32(); api::modules::CAPTURE_DIVIDE = options[launcher::Options::APIScreenMirrorDivide].value_uint32();
} }
if (options[launcher::Options::APIStreamEnable].value_bool() && !cfg::CONFIGURATOR_STANDALONE) {
api_stream_enable = true;
}
if (options[launcher::Options::DisableDebugHooks].value_bool()) { if (options[launcher::Options::DisableDebugHooks].value_bool()) {
debughook::DEBUGHOOK_LOGGING = false; debughook::DEBUGHOOK_LOGGING = false;
@@ -1106,6 +1115,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::ScreenshotFolder].is_active()) { if (options[launcher::Options::ScreenshotFolder].is_active()) {
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text(); GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
} }
if (options[launcher::Options::ScreenshotIncludeOverlay].value_bool()) {
GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = true;
}
if (options[launcher::Options::ScreenshotSubscreens].value_bool()) {
GRAPHICS_SCREENSHOT_SUBSCREENS = true;
}
if (options[launcher::Options::DisableColoredOutput].value_bool()) { if (options[launcher::Options::DisableColoredOutput].value_bool()) {
logger::COLOR = false; logger::COLOR = false;
} }
@@ -1680,6 +1695,26 @@ int main_implementation(int argc, char *argv[]) {
}); });
} }
if (options[launcher::Options::PathToModules].is_active() && !cfg::CONFIGURATOR_STANDALONE) {
log_warning(
"launcher",
"WARNING - user specified -modules option\n\n\n"
"!!! !!!\n"
"!!! Using -modules changes which game DLLs get loaded! !!!\n"
"!!! Unless you know exactly what you are doing, clear -modules !!!\n"
"!!! and try again; usually this is accidentally set by users !!!\n"
"!!! without understanding the implications. !!!\n"
"!!! !!!\n"
);
deferredlogs::defer_error_messages({
"-modules option specified by user",
" game DLLs and patches are loaded from that folder instead of the spice folder,",
" and it is also prepended to the DLL search path, so dependencies may resolve to",
" unexpected copies; instead, clear -modules option and place spice binaries in",
" the intended game directory",
});
}
if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) { if (launcher::signal::DISABLE && !cfg::CONFIGURATOR_STANDALONE) {
log_warning( log_warning(
"launcher", "launcher",
@@ -1780,9 +1815,8 @@ int main_implementation(int argc, char *argv[]) {
nvapi::initialize(); nvapi::initialize();
// add application profile to nvcp // add application profile to nvcp
nvapi::set_profile_settings(); nvapi::set_profile_settings();
// enable super exit // keep ALT+F4 available during lengthy non-standalone boot
superexit::enable(); hotkeys::start();
// enable subscreen touch emulation // enable subscreen touch emulation
if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) { if (options[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch].is_active()) {
games::iidx::ENABLE_POKE = true; games::iidx::ENABLE_POKE = true;
@@ -2478,6 +2512,10 @@ int main_implementation(int argc, char *argv[]) {
log_misc("rawinput", "Analog mappings:"); log_misc("rawinput", "Analog mappings:");
dump_analog_bindings(); dump_analog_bindings();
// mappings are ready; begin screenshot and coin polling during late startup
hotkeys::enable_raw_input();
hotkeys::enable_input();
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available) // for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) { if (show_cursor_if_no_touch && !is_touch_available("launcher::main_implementation")) {
GRAPHICS_SHOW_CURSOR = true; GRAPHICS_SHOW_CURSOR = true;
@@ -2713,9 +2751,20 @@ int main_implementation(int argc, char *argv[]) {
for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) { for (size_t i = 0; i < std::min(api_serial_port.size(), api_serial_baud.size()); i++) {
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]); API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
} }
// the websocket already sits on the API port plus one, so the stream takes plus two
// start coin input thread if (api_stream_enable) {
eamuse_coin_start_thread(); if (!api_enable) {
log_fatal("launcher", "video stream requires API port to be set (-api)");
} else if (api_port + 2 > 65535) {
log_fatal(
"launcher",
"ignoring the video stream, API port {} leaves no room for port plus two",
api_port);
} else {
API_STREAM_SERVER = std::make_unique<api::StreamServer>(
static_cast<unsigned short>(api_port + 2));
}
}
// pin macro // pin macro
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) { if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
@@ -2765,6 +2814,9 @@ int main_implementation(int argc, char *argv[]) {
// cleanup procedure // cleanup procedure
launcher::on_shutdown = [&]() { launcher::on_shutdown = [&]() {
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
hotkeys::disable_input();
// clear presence // clear presence
richpresence::shutdown(); richpresence::shutdown();
@@ -2803,11 +2855,9 @@ int main_implementation(int argc, char *argv[]) {
} }
// free api controller // free api controller
API_STREAM_SERVER.reset();
API_CONTROLLER.reset(); API_CONTROLLER.reset();
// stop coin input thread
eamuse_coin_stop_thread();
eamuse_pin_macro_stop_thread(); eamuse_pin_macro_stop_thread();
// BT5API // BT5API
@@ -2818,6 +2868,7 @@ int main_implementation(int argc, char *argv[]) {
sdk::fini_sdk_modules(); sdk::fini_sdk_modules();
// stop raw input // stop raw input
hotkeys::disable_raw_input();
RI_MGR.reset(); RI_MGR.reset();
// debug hook // debug hook
@@ -2850,8 +2901,8 @@ int main_implementation(int argc, char *argv[]) {
// dispose crypt // dispose crypt
crypt::dispose(); crypt::dispose();
// disable super exit // end early/late ALT+F4 monitoring at the same teardown point as legacy SuperExit
superexit::disable(); hotkeys::stop();
// disable poke // disable poke
games::iidx::poke::disable(); games::iidx::poke::disable();
+63 -13
View File
@@ -1,6 +1,7 @@
#include "logger.h" #include "logger.h"
#include <algorithm> #include <algorithm>
#include <atomic>
#include <condition_variable> #include <condition_variable>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -25,13 +26,16 @@ namespace logger {
bool COLOR = true; bool COLOR = true;
// state // state
static bool RUNNING = false; static std::atomic<bool> RUNNING = false;
static WORD DEFAULT_ATTRIBUTES = 0; static WORD DEFAULT_ATTRIBUTES = 0;
static std::mutex EVENT_MUTEX; static std::mutex EVENT_MUTEX;
static std::condition_variable EVENT_CV; static std::condition_variable EVENT_CV;
static std::thread *THREAD = nullptr; static std::thread *THREAD = nullptr;
static HANDLE THREAD_FINISHED = nullptr;
static std::atomic<bool> THREAD_ABANDONED = false;
static std::mutex OUTPUT_MUTEX; static std::mutex OUTPUT_MUTEX;
static bool OUTPUT_BUFFER_HOT = false; static std::mutex FLUSH_MUTEX;
static std::atomic<bool> OUTPUT_BUFFER_HOT = false;
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER1; static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER1;
static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER2; static std::vector<std::pair<std::string, Style>> OUTPUT_BUFFER2;
static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER = &OUTPUT_BUFFER1; static std::vector<std::pair<std::string, Style>> *OUTPUT_BUFFER = &OUTPUT_BUFFER1;
@@ -71,7 +75,9 @@ namespace logger {
SetConsoleTextAttribute(hTerminal, info.wAttributes); SetConsoleTextAttribute(hTerminal, info.wAttributes);
} }
static void output_buffer_flush() { // the buffer is swapped under OUTPUT_MUTEX but drained outside of it, so two concurrent
// drains would leave one of them iterating a buffer that push() has started appending to
static void output_buffer_flush_locked() {
// get buffer and swap // get buffer and swap
auto buffer = output_buffer_swap(); auto buffer = output_buffer_swap();
@@ -142,6 +148,23 @@ namespace logger {
} }
} }
static void output_buffer_flush() {
// a detached logging thread can hold FLUSH_MUTEX forever, so never wait on it
if (THREAD_ABANDONED) {
std::unique_lock<std::mutex> guard(FLUSH_MUTEX, std::try_to_lock);
if (guard.owns_lock()) {
output_buffer_flush_locked();
}
return;
}
std::lock_guard<std::mutex> guard(FLUSH_MUTEX);
output_buffer_flush_locked();
}
void start() { void start() {
// don't start if blocking // don't start if blocking
@@ -151,6 +174,7 @@ namespace logger {
// start logging thread // start logging thread
RUNNING = true; RUNNING = true;
THREAD_FINISHED = CreateEvent(nullptr, TRUE, FALSE, nullptr);
THREAD = new std::thread([] { THREAD = new std::thread([] {
std::unique_lock<std::mutex> lock(EVENT_MUTEX); std::unique_lock<std::mutex> lock(EVENT_MUTEX);
@@ -160,7 +184,7 @@ namespace logger {
while (RUNNING) { while (RUNNING) {
// wait for hot buffer // wait for hot buffer
EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT; }); EVENT_CV.wait(lock, [] { return OUTPUT_BUFFER_HOT.load(); });
OUTPUT_BUFFER_HOT = false; OUTPUT_BUFFER_HOT = false;
// flush buffer // flush buffer
@@ -180,22 +204,51 @@ namespace logger {
HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE); HANDLE hTerminal = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES); SetConsoleTextAttribute(hTerminal, DEFAULT_ATTRIBUTES);
} }
if (THREAD_FINISHED) {
SetEvent(THREAD_FINISHED);
}
}); });
} }
void stop() { void stop() {
log_info("logger", "stop");
// NOTE: don't log to the logger here!
RUNNING = false;
// clean up thread if required // clean up thread if required
RUNNING = false;
if (THREAD) { if (THREAD) {
// fake notify to exit wait loop // fake notify to exit wait loop
OUTPUT_BUFFER_HOT = true; OUTPUT_BUFFER_HOT = true;
EVENT_CV.notify_all(); EVENT_CV.notify_all();
// join and clean up // never block forever - this also runs on the fatal/crash path, where the logging
// thread may be suspended or wedged and would take the whole process down with it
const bool finished = THREAD_FINISHED != nullptr &&
WaitForSingleObject(THREAD_FINISHED, 1000) == WAIT_OBJECT_0;
if (finished) {
THREAD->join(); THREAD->join();
CloseHandle(THREAD_FINISHED);
THREAD_FINISHED = nullptr;
} else {
THREAD->detach();
THREAD_ABANDONED = true;
// THREAD_FINISHED is leaked on purpose: the detached thread can still wake up and
// signal it, and closing it here risks signaling an unrelated recycled handle
// write out whatever the logging thread never got to
output_buffer_flush();
if (LOG_FILE && LOG_FILE != INVALID_HANDLE_VALUE) {
FlushFileBuffers(LOG_FILE);
}
}
delete THREAD; delete THREAD;
THREAD = nullptr; THREAD = nullptr;
} }
@@ -229,17 +282,14 @@ namespace logger {
// check if blocking or the logging thread is not running // check if blocking or the logging thread is not running
if (BLOCKING || !RUNNING) { if (BLOCKING || !RUNNING) {
// blocking guard
static std::mutex blocking_lock;
std::lock_guard<std::mutex> blocking_guard(blocking_lock);
// immediately process logs // immediately process logs
output_buffer_flush(); output_buffer_flush();
} else { } else {
// mark buffer as hot // never block here - the logging thread can be suspended while holding EVENT_MUTEX,
std::unique_lock<std::mutex> lock(EVENT_MUTEX); // and it re-checks OUTPUT_BUFFER_HOT before waiting again
std::unique_lock<std::mutex> lock(EVENT_MUTEX, std::try_to_lock);
OUTPUT_BUFFER_HOT = true; OUTPUT_BUFFER_HOT = true;
EVENT_CV.notify_one(); EVENT_CV.notify_one();
} }
+42 -1
View File
@@ -1729,7 +1729,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.", "Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.",
.type = OptionType::Integer, .type = OptionType::Integer,
.setting_name = "(0-100)", .setting_name = "(0-100)",
.category = "Companion & API", .category = "API Dev",
}, },
{ {
// APIScreenMirrorDivide // APIScreenMirrorDivide
@@ -1741,6 +1741,18 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"Value must be 1 or greater. Default: 1.", "Value must be 1 or greater. Default: 1.",
.type = OptionType::Integer, .type = OptionType::Integer,
.setting_name = "1", .setting_name = "1",
.category = "API Dev",
},
{
// APIStreamEnable
.title = "API Video Stream Server Enable (EXPERIMENTAL)",
.name = "apistream",
.desc = "Allows companion apps to receive compressed video streams over the API. "
"Companion app must support this feature to take advantage of it.\n\n"
"Video is served unencrypted over the network. "
"May cause older games to hang and crash.\n\n"
"Developers: see the wiki page for more details.",
.type = OptionType::Bool,
.category = "Companion & API", .category = "Companion & API",
}, },
{ {
@@ -3399,6 +3411,35 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool, .type = OptionType::Bool,
.category = "OBS Control", .category = "OBS Control",
}, },
{
// ScreenshotIncludeOverlay
.title = "Include Overlay in Screenshots",
.name = "screenshotoverlay",
.desc = "Includes Spice overlay in screenshots.",
.type = OptionType::Bool,
.category = "General Overlay",
},
{
// ScreenshotSubscreens
.title = "Include Subscreens in Screenshots",
.name = "screenshotsub",
.desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.",
.type = OptionType::Bool,
.category = "General Overlay",
},
{
// GitaDoraSubscreenLandscape
.title = "GitaDora Arena Landscape Subscreen (EXPERIMENTAL)",
.name = "gdsublandscape",
.desc = "For Arena Model full screen two-window mode: drive the SMALL touch subscreen "
"with a landscape monitor instead of a portrait one.\n\n"
"The portrait image is centered and black bars fill the space on either side.\n\n"
"Launches at 1920x1080; use -forceressub if you need a different resolution.",
.type = OptionType::Bool,
.game_name = "GitaDora",
.category = "Game Options",
.quick_setting_category = "Game",
},
}; };
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) { const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+5 -1
View File
@@ -170,6 +170,7 @@ namespace launcher {
APIDebugMode, APIDebugMode,
APIScreenMirrorQuality, APIScreenMirrorQuality,
APIScreenMirrorDivide, APIScreenMirrorDivide,
APIStreamEnable,
EnableAllIOModules, EnableAllIOModules,
EnableACIOModule, EnableACIOModule,
EnableICCAModule, EnableICCAModule,
@@ -320,7 +321,10 @@ namespace launcher {
OBSWebSocketHost, OBSWebSocketHost,
OBSWebSocketPort, OBSWebSocketPort,
OBSWebSocketPassword, OBSWebSocketPassword,
OBSWebSocketDebug OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens,
GitaDoraSubscreenLandscape
}; };
enum class OptionsCategory { enum class OptionsCategory {
+11 -88
View File
@@ -1,21 +1,14 @@
#include "superexit.h" #include "superexit.h"
#include <thread>
#include "windows.h" #include "windows.h"
#include "cfg/configurator.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
#include "touch/touch.h" #include "touch/touch.h"
#include "misc/eamuse.h" #include "util/logging.h"
#include "games/io.h"
#include "overlay/overlay.h"
namespace superexit { namespace superexit {
static std::thread *THREAD = nullptr;
static bool THREAD_RUNNING = false;
bool has_focus() { bool has_focus() {
HWND fg_wnd = GetForegroundWindow(); HWND fg_wnd = GetForegroundWindow();
if (fg_wnd == NULL) { if (fg_wnd == NULL) {
@@ -29,92 +22,22 @@ namespace superexit {
return fg_pid == GetCurrentProcessId(); return fg_pid == GetCurrentProcessId();
} }
void enable() { void handle_hotkeys(bool alt_f4, bool mapped_exit) {
if (!alt_f4 && !mapped_exit) {
// check if already running
if (THREAD)
return; return;
// create new thread
THREAD_RUNNING = true;
THREAD = new std::thread([] {
// log
log_info("superexit", "enabled");
// set variable to false to stop
while (THREAD_RUNNING) {
// check rawinput for ALT+F4
bool rawinput_exit = false;
if (RI_MGR != nullptr) {
auto devices = RI_MGR->devices_get();
for (auto &device : devices) {
switch (device.type) {
case rawinput::KEYBOARD: {
auto &key_states = device.keyboardInfo->key_states;
for (int page_index = 0; page_index < 1024; page_index += 256) {
if (key_states[page_index + VK_MENU]
&& key_states[page_index + VK_F4]) {
rawinput_exit = true;
} }
if (cfg::CONFIGURATOR_STANDALONE) {
return;
} }
break; if (!has_focus()) {
return;
} }
default: if (alt_f4) {
break;
}
}
}
bool async_key_exit =
(GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
bool overlay_exit = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
if (buttons &&
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::SuperExit))) {
overlay_exit = true;
}
// check for exit
if (rawinput_exit || async_key_exit) {
if (has_focus()) {
log_info("superexit", "detected ALT+F4, exiting..."); log_info("superexit", "detected ALT+F4, exiting...");
launcher::shutdown(); launcher::shutdown();
return;
} }
}
if (overlay_exit) {
if (has_focus()) {
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting..."); log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown(); launcher::shutdown();
} }
} }
// slow down
Sleep(100);
}
return nullptr;
});
}
void disable() {
if (!THREAD) {
return;
}
// stop old thread
THREAD_RUNNING = false;
THREAD->join();
// delete thread
delete THREAD;
THREAD = nullptr;
// log
log_info("superexit", "disabled");
}
}
+1 -2
View File
@@ -2,6 +2,5 @@
namespace superexit { namespace superexit {
bool has_focus(); bool has_focus();
void enable(); void handle_hotkeys(bool alt_f4, bool mapped_exit);
void disable();
} }
+134 -16
View File
@@ -527,25 +527,78 @@ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABI
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
toojpeg (zlib) fpng (Unlicense)
------------------------------------------- -------------------------------------------
zlib License Copyright (c) 2021 Richard Geldreich, Jr.
Incorporates public domain code by Alex Evans, the original miniz by
Richard Geldreich, Jr., and Huffman code size work by Alistair Moffat and
Jyrki Katajainen.
Copyright (c) 2011-2016 Stephan Brumme This is free and unencumbered software released into the public domain.
This software is provided 'as-is', without any express or implied warranty. In Anyone is free to copy, modify, publish, use, compile, sell, or distribute
no event will the authors be held liable for any damages arising from the use this software, either in source code form or as a compiled binary, for any
of this software. purpose, commercial or non-commercial, and by any means.
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 In jurisdictions that recognize copyright laws, the author or authors of this
the following restrictions: software dedicate any and all copyright interest in the software to the public
1. The origin of this software must not be misrepresented; you must not claim domain. We make this dedication for the benefit of the public at large and to
that you wrote the original software. If you use this software in a product, the detriment of our heirs and successors. We intend this dedication to be an
an acknowledgment in the product documentation would be appreciated but is overt act of relinquishment in perpetuity of all present and future rights to
not required. this software under copyright law.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
3. This notice may not be removed or altered from any source distribution. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
libjpeg-turbo (IJG)
-------------------------------------------
This software is based in part on the work of the Independent JPEG Group.
Only the libjpeg API library is used here. Per the libjpeg-turbo licensing
terms that portion is covered by the IJG License, reproduced below; the SIMD
sources bear the zlib License, whose terms are subsumed by the IJG License in
the context of the overall libjpeg API library. The Modified (3-clause) BSD
License covers the TurboJPEG API library and build system, neither of which is
distributed here.
The authors make NO WARRANTY or representation, either express or implied,
with respect to this software, its quality, accuracy, merchantability, or
fitness for a particular purpose. This software is provided "AS IS", and you,
its user, assume the entire risk as to its quality and accuracy.
This software is copyright (C) 1991-2020, Thomas G. Lane, Guido Vollbeding.
All Rights Reserved except as specified below.
Permission is hereby granted to use, copy, modify, and distribute this
software (or portions thereof) for any purpose, without fee, subject to these
conditions:
(1) If any part of the source code for this software is distributed, then this
README file must be included, with this copyright and no-warranty notice
unaltered; and any additions, deletions, or changes to the original files
must be clearly indicated in accompanying documentation.
(2) If only executable code is distributed, then the accompanying
documentation must state that "this software is based in part on the work of
the Independent JPEG Group".
(3) Permission for use of this software is granted only if the user accepts
full responsibility for any undesirable consequences; the authors accept
NO LIABILITY for damages of any kind.
These conditions apply to any software derived from or based on the IJG code,
not just to the unmodified library. If you use our work, you ought to
acknowledge us.
Permission is NOT granted for the use of any IJG author's name or company name
in advertising or publicity relating to this software or products derived from
it. This software may be referred to only as "the Independent JPEG Group's
software".
We specifically permit and encourage the use of this software as the basis of
commercial products, provided that all warranty or liability claims are
assumed by the product vendor.
robin_hood.h (MIT) robin_hood.h (MIT)
------------------------------------------- -------------------------------------------
@@ -1349,6 +1402,71 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
Wslay (MIT)
-------------------------------------------
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Wslay UTF-8 decoder (MIT)
-------------------------------------------
Copyright (c) 2008-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
x264 (GPL-2.0-or-later)
-------------------------------------------
https://www.videolan.org/developers/x264.html
Statically linked for the API video stream H.264 encoder.
Copyright (C) 2003-2024 x264 project
This program is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation; either version 2 of the License, or (at your option) any later
version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Contributions Contributions
------------------------------------------- -------------------------------------------
cardio - Felix - MIT License cardio - Felix - MIT License
+5 -4
View File
@@ -58,17 +58,18 @@ namespace clipboard {
return; return;
} }
// If the screenshot button is print screen, the OpenClipboard call seems to fail often if we only // print screen key leaves the OS briefly holding the clipboard, so retry
// call it once, probably due to a race condition. So, we can try calling a lot until we can open it. // spinning without yielding starves the thread we are waiting on and looks like a hang
bool clipboard_open = false; bool clipboard_open = false;
for (int i = 0; i < 1000000; i++) { for (int i = 0; i < 100; i++) {
if (OpenClipboard(nullptr)) { if (OpenClipboard(nullptr)) {
clipboard_open = true; clipboard_open = true;
break; break;
} }
Sleep(5);
} }
if (!clipboard_open) { if (!clipboard_open) {
log_warning("clipboard", "Failed to open clipboard"); log_warning("clipboard", "failed to open clipboard");
return; return;
} }
+7 -3
View File
@@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) {
return 1; return 1;
} }
static int __cdecl device_set_coinblocker_open(char number, char open) { static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) {
return 0; // libdevice exposes one open flag per coin slot. Since spice has a shared
// software coin stock, consider it blocked only when both slots are closed.
eamuse_coin_set_block(!slot1_open && !slot2_open);
} }
static void __cdecl device_set_coincounter_merge() { static void __cdecl device_set_coincounter_merge() {
@@ -617,7 +619,9 @@ void spicedevice_attach() {
detour::inline_hook((void *) device_finalize, libutils::try_proc_list( detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"})); DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list( detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock", "?device_get_coinstock@@YAXPEAG0@Z"})); DEVICE_INSTANCE, {"device_get_coinstock",
"?device_get_coinstock@@YAXPEAG0@Z",
"?device_get_coinstock@@YAXPAG0@Z"}));
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list( detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock_all", DEVICE_INSTANCE, {"device_get_coinstock_all",
"?device_get_coinstock_all@@YAXPEAG0@Z", "?device_get_coinstock_all@@YAXPEAG0@Z",
+47 -64
View File
@@ -25,10 +25,8 @@ static double CARD_INSERT_TIME[2] = {0, 0};
static double CARD_INSERT_TIMEOUT = 2.0; static double CARD_INSERT_TIMEOUT = 2.0;
static char CARD_INSERT_UID[2][8] = {{0}, {0}}; static char CARD_INSERT_UID[2][8] = {{0}, {0}};
static char CARD_INSERT_UID_ENABLE[2] = {false, false}; static char CARD_INSERT_UID_ENABLE[2] = {false, false};
static int COIN_STOCK = 0; static std::atomic_int COIN_STOCK {0};
static bool COIN_BLOCK = false; static std::atomic_bool COIN_BLOCK {false};
static std::thread *COIN_INPUT_THREAD;
static bool COIN_INPUT_THREAD_ACTIVE = false;
static uint16_t KEYPAD_STATE[] = {0, 0}; static uint16_t KEYPAD_STATE[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES_BT5[] = {0, 0};
@@ -36,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0}; static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
static std::string EAMUSE_GAME_NAME; static std::string EAMUSE_GAME_NAME;
static ConfigKeypadBindings KEYPAD_BINDINGS {}; static ConfigKeypadBindings KEYPAD_BINDINGS {};
static std::mutex KEYPAD_BINDINGS_LOCK;
// auto card // auto card
bool AUTO_INSERT_CARD[2] = {false, false}; bool AUTO_INSERT_CARD[2] = {false, false};
@@ -89,23 +88,23 @@ bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
return true; return true;
} }
// get file path // call the next function
std::filesystem::path path; return eamuse_get_card(eamuse_get_card_path(index), card, index);
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
path = KEYPAD_BINDINGS.card_paths[index];
} else {
path = index > 0 ? "card1.txt" : "card0.txt";
} }
// call the next function std::filesystem::path eamuse_get_card_path(size_t index) {
return eamuse_get_card(path, card, index); std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
if (index >= std::size(KEYPAD_BINDINGS.card_paths)) {
return {};
}
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
return KEYPAD_BINDINGS.card_paths[index];
}
return index > 0 ? "card1.txt" : "card0.txt";
} }
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) { bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
// do a quick copy under lock const auto card_override = eamuse_get_card_override(index);
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
const auto card_override = CARD_OVERRIDES[index];
lock.unlock();
// Check if card overrides are present // Check if card overrides are present
if (!card_override.empty()) { if (!card_override.empty()) {
@@ -151,6 +150,14 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
return eamuse_get_card_from_file(path, card, index); return eamuse_get_card_from_file(path, card, index);
} }
std::string eamuse_get_card_override(size_t index) {
std::lock_guard<std::mutex> lock(CARD_OVERRIDES_LOCK);
if (index >= std::size(CARD_OVERRIDES)) {
return {};
}
return CARD_OVERRIDES[index];
}
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) { bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
// open file // open file
@@ -292,78 +299,49 @@ bool eamuse_card_insert_consume(int active_count, int unit_id) {
} }
bool eamuse_coin_get_block() { bool eamuse_coin_get_block() {
return COIN_BLOCK; return COIN_BLOCK.load(std::memory_order_relaxed);
} }
void eamuse_coin_set_block(bool block) { void eamuse_coin_set_block(bool block) {
COIN_BLOCK = block; COIN_BLOCK.store(block, std::memory_order_relaxed);
} }
int eamuse_coin_get_stock() { int eamuse_coin_get_stock() {
return COIN_STOCK; return COIN_STOCK.load(std::memory_order_relaxed);
} }
void eamuse_coin_set_stock(int amount) { void eamuse_coin_set_stock(int amount) {
COIN_STOCK = amount; COIN_STOCK.store(amount, std::memory_order_relaxed);
} }
bool eamuse_coin_consume(int amount) { bool eamuse_coin_consume(int amount) {
if (COIN_STOCK < amount) { auto stock = COIN_STOCK.load(std::memory_order_relaxed);
return false; while (stock >= amount) {
} else { if (COIN_STOCK.compare_exchange_weak(
COIN_STOCK -= amount; stock,
stock - amount,
std::memory_order_relaxed)) {
return true; return true;
} }
} }
return false;
}
int eamuse_coin_consume_stock() { int eamuse_coin_consume_stock() {
int stock = COIN_STOCK; return COIN_STOCK.exchange(0, std::memory_order_relaxed);
COIN_STOCK = 0;
return stock;
} }
int eamuse_coin_add() { int eamuse_coin_add() {
return ++COIN_STOCK; return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
} }
void eamuse_coin_start_thread() { void eamuse_coin_insert() {
if (COIN_BLOCK.load(std::memory_order_relaxed)) {
// set active
COIN_INPUT_THREAD_ACTIVE = true;
// create thread
COIN_INPUT_THREAD = new std::thread([]() {
auto overlay_buttons = games::get_buttons_overlay(eamuse_get_game());
static bool COIN_INPUT_KEY_STATE = false;
while (COIN_INPUT_THREAD_ACTIVE) {
// check input key
if (overlay_buttons && GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(
games::OverlayButtons::InsertCoin))) {
if (!COIN_INPUT_KEY_STATE) {
if (COIN_BLOCK)
log_info("eamuse", "coin inserted while blocked"); log_info("eamuse", "coin inserted while blocked");
else {
log_info("eamuse", "coin insert");
COIN_STOCK++;
}
}
COIN_INPUT_KEY_STATE = true;
} else { } else {
COIN_INPUT_KEY_STATE = false; log_info("eamuse", "coin insert");
COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
} }
// once every two frames
std::this_thread::sleep_for(std::chrono::milliseconds(1000 / 30));
}
});
}
void eamuse_coin_stop_thread() {
COIN_INPUT_THREAD_ACTIVE = false;
COIN_INPUT_THREAD->join();
delete COIN_INPUT_THREAD;
COIN_INPUT_THREAD = nullptr;
} }
void eamuse_pin_macro_start_thread() { void eamuse_pin_macro_start_thread() {
@@ -615,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
} }
bool eamuse_keypad_state_naive() { bool eamuse_keypad_state_naive() {
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty(); return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
} }
@@ -626,7 +605,9 @@ void eamuse_set_game(std::string game) {
} }
void eamuse_update_keypad_bindings() { void eamuse_update_keypad_bindings() {
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME); auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
KEYPAD_BINDINGS = std::move(bindings);
} }
const std::string &eamuse_get_game() { const std::string &eamuse_get_game() {
@@ -749,6 +730,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("Mahjong Fight Girl"); eamuse_set_game("Mahjong Fight Girl");
else if (avs::game::is_model("XIF")) else if (avs::game::is_model("XIF"))
eamuse_set_game("Polaris Chord"); eamuse_set_game("Polaris Chord");
else if (avs::game::is_model("NDD"))
eamuse_set_game("Silent Scope: Bone Eater");
else { else {
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL); log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
eamuse_set_game("unknown"); eamuse_set_game("unknown");
+3 -3
View File
@@ -43,6 +43,8 @@ extern std::string AUTO_PIN_MACRO_TRIGGER[2];
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card); bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id); bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index); bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index);
std::filesystem::path eamuse_get_card_path(size_t index);
std::string eamuse_get_card_override(size_t index);
void eamuse_card_insert(int unit); void eamuse_card_insert(int unit);
void eamuse_card_insert(int unit, const uint8_t *card); void eamuse_card_insert(int unit, const uint8_t *card);
@@ -58,9 +60,7 @@ bool eamuse_coin_consume(int amount);
int eamuse_coin_consume_stock(); int eamuse_coin_consume_stock();
int eamuse_coin_add(); int eamuse_coin_add();
void eamuse_coin_insert();
void eamuse_coin_start_thread();
void eamuse_coin_stop_thread();
void eamuse_pin_macro_start_thread(); void eamuse_pin_macro_start_thread();
void eamuse_pin_macro_stop_thread(); void eamuse_pin_macro_stop_thread();
+154
View File
@@ -0,0 +1,154 @@
#include "hotkeys.h"
#include <atomic>
#include <chrono>
#include <mutex>
#include <stop_token>
#include <thread>
#include <vector>
#include <windows.h>
#include "games/io.h"
#include "launcher/superexit.h"
#include "misc/eamuse.h"
#include "overlay/overlay.h"
#include "rawinput/rawinput.h"
#include "util/logging.h"
namespace hotkeys {
namespace {
// 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
// interval during early boot or when process timer adjustments are disabled
constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
std::atomic_bool SCREENSHOT_PENDING {false};
std::mutex INPUT_MUTEX;
bool INPUT_ENABLED = false;
bool RAW_INPUT_ENABLED = false;
std::jthread WORKER;
bool read_button(std::vector<Button> *buttons, size_t index) {
// getState retains each binding's focus, modifier, inversion, and debounce policy
return RI_MGR && buttons && index < buttons->size() &&
GameAPI::Buttons::getState(RI_MGR, buttons->at(index));
}
bool read_alt_f4() {
// preserve both legacy detection paths whenever raw input is available
bool pressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
if (!RAW_INPUT_ENABLED || !RI_MGR) {
return pressed;
}
return pressed || RI_MGR->keyboard_combo_pressed(VK_MENU, VK_F4);
}
bool rising_edge(bool current, bool &previous) {
const bool edge = current && !previous;
previous = current;
return edge;
}
void run(std::stop_token stop_token) {
bool screenshot_previous = false;
bool coin_previous = false;
while (!stop_token.stop_requested()) {
bool coin_edge = false;
bool super_exit_current = false;
bool alt_f4_current = false;
{
// lifecycle functions hold this mutex until raw-input polling is complete
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
if (INPUT_ENABLED || RAW_INPUT_ENABLED) {
auto *buttons = games::get_buttons_overlay(eamuse_get_game());
const bool screenshot_down = INPUT_ENABLED && read_button(
buttons, games::OverlayButtons::Screenshot);
const bool coin_current = INPUT_ENABLED && read_button(
buttons, games::OverlayButtons::InsertCoin);
const bool super_exit_down = RAW_INPUT_ENABLED && read_button(
buttons, games::OverlayButtons::SuperExit);
// global_hotkeys_triggered takes OVERLAY_MUTEX, then the overlay's
// hotkeys_mutex; its button reads may then take device mutexes. polling
// every mapped-input tick is intentional so HotkeyToggle releases cannot
// be missed when the render thread stalls.
const bool gate_active = overlay::global_hotkeys_triggered();
const bool screenshot_current = screenshot_down && gate_active;
super_exit_current = super_exit_down && gate_active;
if (rising_edge(screenshot_current, screenshot_previous)) {
SCREENSHOT_PENDING.store(true, std::memory_order_relaxed);
}
coin_edge = rising_edge(coin_current, coin_previous);
} else {
screenshot_previous = false;
coin_previous = false;
}
alt_f4_current = read_alt_f4();
}
if (coin_edge) {
eamuse_coin_insert();
}
// pass held state so returning focus can exit without another key press
superexit::handle_hotkeys(alt_f4_current, super_exit_current);
std::this_thread::sleep_for(MIN_SAMPLE_INTERVAL);
}
}
}
void start() {
if (WORKER.joinable()) {
return;
}
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
WORKER = std::jthread(run);
log_info("hotkeys", "sampler started");
}
void enable_raw_input() {
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
RAW_INPUT_ENABLED = true;
}
void enable_input() {
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
INPUT_ENABLED = true;
log_info("hotkeys", "configured input enabled");
}
void disable_input() {
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
INPUT_ENABLED = false;
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
log_info("hotkeys", "configured input disabled");
}
void disable_raw_input() {
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
RAW_INPUT_ENABLED = false;
}
void stop() {
if (!WORKER.joinable()) {
return;
}
WORKER.request_stop();
WORKER.join();
log_info("hotkeys", "sampler stopped");
}
bool consume_screenshot() {
return SCREENSHOT_PENDING.exchange(false, std::memory_order_relaxed);
}
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
namespace hotkeys {
// ALT+F4 monitoring spans boot and teardown; configured actions are enabled separately
void start();
void enable_raw_input();
void enable_input();
void disable_input();
void disable_raw_input();
void stop();
bool consume_screenshot();
}
+25
View File
@@ -21,12 +21,36 @@ namespace ImGui {
base.y + overlay::apply_scaling(1.0f)); base.y + overlay::apply_scaling(1.0f));
} }
// Tooltips follow the mouse and only avoid a small box around the cursor, so
// they can cover the option they describe. Anchor to the hovered item and let
// ImGui's popup placer pick a side that clears it and stays on-screen. The
// size is taken from the tooltip window, so it covers any content.
static void SetNextTooltipPos() {
ImGuiContext& g = *GImGui;
char name[32];
ImFormatString(name, IM_COUNTOF(name), "##Tooltip_%02d", g.TooltipOverrideCount);
ImGuiWindow *tooltip = ImGui::FindWindowByName(name);
if (tooltip == nullptr || !tooltip->WasActive) {
// Reopened tooltips are hidden for one frame while measuring size.
return;
}
const ImRect item(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
ImGui::SetNextWindowPos(ImGui::FindBestWindowPosForPopupEx(
item.Min,
ImGui::CalcWindowNextAutoFitSize(tooltip),
&tooltip->AutoPosLastDirection,
ImGui::GetPopupAllowedExtentRect(tooltip),
item,
ImGuiPopupPositionPolicy_Tooltip));
}
void HelpTooltip(const char* desc) { void HelpTooltip(const char* desc) {
ImGui::PushStyleColor(ImGuiCol_Border, bg); ImGui::PushStyleColor(ImGuiCol_Border, bg);
ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg); ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg);
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg); ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
ImGui::PushStyleColor(ImGuiCol_Text, fg); ImGui::PushStyleColor(ImGuiCol_Text, fg);
SetNextTooltipPos();
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
ImGui::TextUnformatted(desc); ImGui::TextUnformatted(desc);
@@ -49,6 +73,7 @@ namespace ImGui {
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg); ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
ImGui::PushStyleColor(ImGuiCol_Text, fg); ImGui::PushStyleColor(ImGuiCol_Text, fg);
SetNextTooltipPos();
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f); ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
if (desc && desc[0]) { if (desc && desc[0]) {
+3 -1
View File
@@ -172,7 +172,9 @@ namespace overlay::notifications {
// small gutter past the accent bar, then wrapped text // small gutter past the accent bar, then wrapped text
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f)); ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
ImGui::SameLine(); ImGui::SameLine();
ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X)); // wrap pos is in window-local space, not screen space - adding win_pos.x here
// pushed the boundary far past the window's own width, so it never wrapped
ImGui::PushTextWrapPos(win_size.x - apply_scaling(TOAST_PAD_X));
ImGui::TextUnformatted(n.text.c_str()); ImGui::TextUnformatted(n.text.c_str());
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();

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