Compare commits

...

17 Commits

Author SHA1 Message Date
bicarus-dev 94574c485a exlude line ending enforcement for external dir 2026-08-16 21:27:03 -07:00
bicarus-dev f857926ec3 fix line endings 2026-08-16 21:23:41 -07:00
bicarus adf4cccd4a overlay: fix subscreen overlay rendering for popn (#869)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

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

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

## Description of change

Three SDVX (Valkyrie model) fixes:

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

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

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

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


## Testing
Test:

gitadora 1/2/4 windowed mode, fullscreen

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

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

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

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

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

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

## Description of change

Adds the `-screenshotoverlay` option.

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

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

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

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

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

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

## Testing
2026-08-12 20:04:59 -07:00
James Liu 3b29227dbc gitadora: fix XG2 gibberish text showing (#863)
> [!NOTE]
> Before submitting code changes... 
> * Please do note that this is a GPL v3.0 open source project.
> * Please read the
[CONTRIBUTING](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md)
guide.
> * Maintainers reserve the right to reject or modify your submission
without reason.
> * No new compiler warnings must be introduced. Check the CI build
results.
> 
> Feel free to remove this section after you have read it.

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

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

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

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

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

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

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

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

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

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

This PR only affects windowed mode SDVX.

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

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

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

This fixex both native touch and wintouchemu paths.

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

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

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

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

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

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

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

New truth table for `-iidxcabcams` :

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

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

## Description of change

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

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

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

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

## Testing
2026-08-04 09:04:29 -07:00
121 changed files with 26773 additions and 25832 deletions
+46
View File
@@ -0,0 +1,46 @@
# Normalize every text file to LF in the repository.
# The build runs under Linux/MinGW containers, so LF is also used in the
# working tree; Windows editors and toolchains handle LF fine.
* text=auto eol=lf
# Windows-only files that must keep CRLF in the working tree.
*.bat text eol=crlf
*.cmd text eol=crlf
*.sln text eol=crlf
*.vcproj text eol=crlf
*.vcxproj text eol=crlf
*.props text eol=crlf
*.filters text eol=crlf
# Files that must keep LF even if a Windows editor rewrites them.
*.sh text eol=lf
*.in text eol=lf
*.cmake text eol=lf
*.mk text eol=lf
Makefile text eol=lf
Dockerfile text eol=lf
# Binary files - never touch the contents.
*.bin binary
*.ico binary
*.ttf binary
*.otf binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.bmp binary
*.zip binary
*.7z binary
*.gz binary
*.dll binary
*.exe binary
*.lib binary
*.a binary
*.o binary
*.obj binary
*.pdb binary
# Vendored code is stored and checked out byte-for-byte as upstream ships it,
# so re-importing a library never produces line-ending-only diffs.
src/spice2x/external/** -text
+31 -31
View File
@@ -1,32 +1,32 @@
on: [push, pull_request] on: [push, pull_request]
name: Continuous Integration name: Continuous Integration
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
fw-ci: fw-ci:
name: Build name: Build
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
working-directory: ./src/spice2x working-directory: ./src/spice2x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- name: Set ccache environment variables - name: Set ccache environment variables
run: | run: |
echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV echo "CCACHE_DIR=${{ github.workspace }}/src/spice2x/.ccache" >> $GITHUB_ENV
- name: Install ccache - name: Install ccache
uses: hendrikmuhs/ccache-action@v1.2.23 uses: hendrikmuhs/ccache-action@v1.2.23
- name: Calculate commit SHA - name: Calculate commit SHA
id: vars id: vars
run: | run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }}) calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV
- name: Compile - name: Compile
run: ./build_docker.sh run: ./build_docker.sh
- uses: actions/upload-artifact@v6 - uses: actions/upload-artifact@v6
with: with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }} name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin path: src/spice2x/bin
if-no-files-found: error if-no-files-found: error
+49 -49
View File
@@ -1,49 +1,49 @@
name: Draft Release name: Draft Release
on: on:
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: write contents: write
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
release: release:
name: Build and Draft Release name: Build and Draft Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
run: run:
working-directory: ./src/spice2x working-directory: ./src/spice2x
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
with: with:
ref: main ref: main
fetch-depth: 0 fetch-depth: 0
- name: Clean leftover build artifacts - name: Clean leftover build artifacts
run: | run: |
rm -rf .ccache dist bin cmake-build-* rm -rf .ccache dist bin cmake-build-*
- name: Compile - name: Compile
run: ./build_docker.sh run: ./build_docker.sh
- name: Determine release name from dist filename - name: Determine release name from dist filename
run: | run: |
dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')") dist=$(basename "$(ls dist/spice2x-*.zip | grep -v -- '-full.zip')")
# strip the ".zip" to get the base name, e.g. spice2x-26-06-28 # strip the ".zip" to get the base name, e.g. spice2x-26-06-28
name="${dist%.zip}" name="${dist%.zip}"
# the tag is the date portion, e.g. 26-06-28 # the tag is the date portion, e.g. 26-06-28
tag="${name#spice2x-}" tag="${name#spice2x-}"
echo "RELEASE_NAME=$name" >> $GITHUB_ENV echo "RELEASE_NAME=$name" >> $GITHUB_ENV
echo "RELEASE_TAG=$tag" >> $GITHUB_ENV echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
- name: Create draft release - name: Create draft release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
with: with:
draft: true draft: true
prerelease: true prerelease: true
tag_name: ${{ env.RELEASE_TAG }} tag_name: ${{ env.RELEASE_TAG }}
name: ${{ env.RELEASE_NAME }} name: ${{ env.RELEASE_NAME }}
target_commitish: main target_commitish: main
generate_release_notes: true generate_release_notes: true
files: | files: |
src/spice2x/dist/spice2x-*.zip src/spice2x/dist/spice2x-*.zip
+3
View File
@@ -332,6 +332,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
api/modules/control.cpp api/modules/control.cpp
api/modules/touch.cpp api/modules/touch.cpp
api/modules/iidx.cpp api/modules/iidx.cpp
api/modules/sdvx.cpp
api/serial.cpp api/serial.cpp
api/modules/drs.cpp api/modules/drs.cpp
api/modules/lcd.cpp api/modules/lcd.cpp
@@ -535,6 +536,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
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_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
hooks/graphics/backends/d3d9/d3d9_live2d.cpp hooks/graphics/backends/d3d9/d3d9_live2d.cpp
@@ -576,6 +578,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
+14
View File
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
- `Side Panel Right Inner` - `Side Panel Right Inner`
- `Side Panel Right` - `Side Panel Right`
#### SDVX
- tapeled_get(name: str, ...)
- returns a list containing a dict of the current tape LED states. The dict keys are:
- `Title`
- `Upper Left Speaker`
- `Upper Right Speaker`
- `Left Wing`
- `Right Wing`
- `Control Panel`
- `Lower Left Speaker`
- `Lower Right Speaker`
- `Woofer`
- `V Unit`
#### LCD #### LCD
- info() - info()
- returns information about the serial LCD controller some games use - returns information about the serial LCD controller some games use
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace api {
extern std::atomic_uint32_t CLIENT_COUNT;
inline bool has_clients() {
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
}
}
+9
View File
@@ -5,6 +5,7 @@
#include <utility> #include <utility>
#include "client.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "util/crypt.h" #include "util/crypt.h"
@@ -28,6 +29,7 @@
#include "modules/lcd.h" #include "modules/lcd.h"
#include "modules/lights.h" #include "modules/lights.h"
#include "modules/memory.h" #include "modules/memory.h"
#include "modules/sdvx.h"
#include "modules/touch.h" #include "modules/touch.h"
#include "modules/resize.h" #include "modules/resize.h"
#include "request.h" #include "request.h"
@@ -36,6 +38,8 @@
using namespace rapidjson; using namespace rapidjson;
using namespace api; using namespace api;
std::atomic_uint32_t api::CLIENT_COUNT = 0;
Controller::Controller(unsigned short port, std::string password, bool pretty) Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty) : port(port), password(std::move(password)), pretty(pretty)
{ {
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
state->modules.push_back(new modules::LCD()); state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights()); state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory()); state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::SDVX());
state->modules.push_back(new modules::Touch()); state->modules.push_back(new modules::Touch());
state->modules.push_back(new modules::Resize()); state->modules.push_back(new modules::Resize());
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
} }
void Controller::free_state(api::ClientState *state) { void Controller::free_state(api::ClientState *state) {
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
// free cipher // free cipher
delete state->cipher; delete state->cipher;
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
} }
void Controller::free_socket() { void Controller::free_socket() {
+54 -54
View File
@@ -1,54 +1,54 @@
#include "ddr.h" #include "ddr.h"
#include <functional> #include <functional>
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "games/ddr/ddr.h" #include "games/ddr/ddr.h"
using namespace std::placeholders; using namespace std::placeholders;
using namespace rapidjson; using namespace rapidjson;
namespace api::modules { namespace api::modules {
DDR::DDR() : Module("ddr") { DDR::DDR() : Module("ddr") {
functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2); functions["tapeled_get"] = std::bind(&DDR::tapeled_get, this, _1, _2);
} }
/** /**
* Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI * Allows fetching of the RGB LED strips that are gold cabinets, via SpiceAPI
*/ */
void DDR::tapeled_get(Request &req, Response &res) { void DDR::tapeled_get(Request &req, Response &res) {
static const char* device_names[11] = { static const char* device_names[11] = {
"p1_foot_up", "p1_foot_up",
"p1_foot_right", "p1_foot_right",
"p1_foot_left", "p1_foot_left",
"p1_foot_down", "p1_foot_down",
"p2_foot_up", "p2_foot_up",
"p2_foot_right", "p2_foot_right",
"p2_foot_left", "p2_foot_left",
"p2_foot_down", "p2_foot_down",
"top_panel", "top_panel",
"monitor_left", "monitor_left",
"monitor_right" "monitor_right"
}; };
Value response_object(kObjectType); Value response_object(kObjectType);
// Iterate through each device and dump its lights data into the response // Iterate through each device and dump its lights data into the response
for (size_t device = 0; device < 11; device++) { for (size_t device = 0; device < 11; device++) {
size_t num_leds = 25; size_t num_leds = 25;
if (device > 7) if (device > 7)
num_leds = 50; num_leds = 50;
Value light_state(kArrayType); Value light_state(kArrayType);
light_state.Reserve(num_leds * 3, res.doc()->GetAllocator()); light_state.Reserve(num_leds * 3, res.doc()->GetAllocator());
for (size_t led = 0; led < num_leds; led++) { for (size_t led = 0; led < num_leds; led++) {
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator()); light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][0], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator()); light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][1], res.doc()->GetAllocator());
light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator()); light_state.PushBack(games::ddr::DDR_TAPELEDS[device][led][2], res.doc()->GetAllocator());
} }
response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator()); response_object.AddMember(StringRef(device_names[device]), light_state, res.doc()->GetAllocator());
} }
res.add_data(response_object); res.add_data(response_object);
} }
} }
+17 -17
View File
@@ -1,17 +1,17 @@
#pragma once #pragma once
#include <vector> #include <vector>
#include "api/module.h" #include "api/module.h"
#include "api/request.h" #include "api/request.h"
namespace api::modules { namespace api::modules {
class DDR : public Module { class DDR : public Module {
public: public:
DDR(); DDR();
private: private:
// function definitions // function definitions
void tapeled_get(Request &req, Response &res); void tapeled_get(Request &req, Response &res);
}; };
} }
+4 -2
View File
@@ -106,14 +106,16 @@ namespace api::modules {
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) { void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
// Create an array for the light state // Create an array for the light state
Value light_state(kArrayType); Value light_state(kArrayType);
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator()); light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) { for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator()); light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator()); light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator()); light_state.PushBack(b, res.doc()->GetAllocator());
} }
// Can't use StringRef here, turns some strings partially into null bytes for some reason // can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator()); Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator()); response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
} }
+67
View File
@@ -0,0 +1,67 @@
#include "sdvx.h"
#include <functional>
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
SDVX::SDVX() : Module("sdvx") {
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
for (auto &light : games::sdvx::TAPELED_MAPPING) {
lights_by_names.emplace(light.lightName, light);
}
}
/**
* tapeled_get()
* tapeled_get(name: str, ...)
*/
void SDVX::tapeled_get(Request &req, Response &res) {
Value response_object(kObjectType);
// all tape leds
if (req.params.Size() == 0) {
// iterate through each device and dump its lights data into the response
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
copy_tapeled_data(res, response_object, mapping);
}
} else {
// specified light names
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsString()) {
error_type(res, "name", "string");
return;
}
const auto name = param.GetString();
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
copy_tapeled_data(res, response_object, it->second.get());
}
}
}
res.add_data(response_object);
}
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
const tapeledutils::tape_led &mapping)
{
Value light_state(kArrayType);
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <string>
#include "api/module.h"
#include "api/request.h"
#include "external/robin_hood.h"
#include "games/sdvx/sdvx.h"
namespace api::modules {
class SDVX : public Module {
public:
SDVX();
private:
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
void tapeled_get(Request &req, Response &res);
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
const tapeledutils::tape_led &mapping);
};
}
+44 -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,9 +86,12 @@ 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) {
// exceed gear subscreen, portrait after the rotation applied in apply_touch_errata // windowed API coordinates land on the sub screen window as-is; fullscreen
native_canvas_w = 1080; // coordinates are rotated into the game's touch space by apply_touch_errata
native_canvas_h = 1920; const bool landscape_coordinates =
GRAPHICS_WINDOWED || GRAPHICS_FS_ORIENTATION_SWAP;
native_canvas_w = landscape_coordinates ? 1920 : 1080;
native_canvas_h = landscape_coordinates ? 1080 : 1920;
} else if (avs::game::is_model("LDJ")) { } else if (avs::game::is_model("LDJ")) {
// TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata // TDJ subscreen; FHD models are upscaled to 1080p by apply_touch_errata
native_canvas_w = is_tdj_fhd ? 1920 : 1280; native_canvas_w = is_tdj_fhd ? 1920 : 1280;
@@ -66,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);
@@ -209,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) { } else if (is_sdvx) {
// for exceed gear, they are both 1080p screens, but need to apply transformation sdvx_touch_errata(x, y, use_native, native_canvas_w, native_canvas_h);
x = 1080 - y_raw;
y = x_raw;
} }
} }
} }
@@ -7,6 +7,7 @@ from .coin import *
from .control import * from .control import *
from .exceptions import * from .exceptions import *
from .iidx import * from .iidx import *
from .sdvx import *
from .info import * from .info import *
from .keypads import * from .keypads import *
from .lights import * from .lights import *
@@ -0,0 +1,12 @@
from .connection import Connection
from .request import Request
def sdvx_tapeled_get(con: Connection, *light_names):
req = Request("sdvx", "tapeled_get")
for light_name in light_names:
req.add_param(light_name)
res = con.request(req)
return res.get_data()
+1
View File
@@ -6,6 +6,7 @@
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness> <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
<dpiAware>true</dpiAware> <dpiAware>true</dpiAware>
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">Legacy</activeCodePage>
</asmv3:windowsSettings> </asmv3:windowsSettings>
</asmv3:application> </asmv3:application>
<dependency> <dependency>
@@ -1,72 +1,72 @@
# fails the build if a PE binary statically imports a forbidden DLL. # fails the build if a PE binary statically imports a forbidden DLL.
# #
# some DLLs must never end up in spice's static import table, for two reasons: # some DLLs must never end up in spice's static import table, for two reasons:
# #
# 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy # 1. user-overridable DLLs (e.g. DXVK's d3d9.dll): users drop their own copy
# into the modules directory to replace the system one. a static import # into the modules directory to replace the system one. a static import
# forces the loader to load the SYSTEM copy at process startup - before the # forces the loader to load the SYSTEM copy at process startup - before the
# modules directory is added to the DLL search path and before the game DLL # modules directory is added to the DLL search path and before the game DLL
# loads - so the user-supplied override never takes effect (see issue #779). # loads - so the user-supplied override never takes effect (see issue #779).
# #
# 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/ # 2. DLLs that break games when present (e.g. Media Foundation: mf/mfplat/
# mfreadwrite): a static import loads them eagerly and breaks Unity games. # mfreadwrite): a static import loads them eagerly and breaks Unity games.
# #
# in both cases the DLL must instead be loaded dynamically (libutils::try_library # in both cases the DLL must instead be loaded dynamically (libutils::try_library
# / GetProcAddress / delay load) so it is only pulled in when actually needed. # / GetProcAddress / delay load) so it is only pulled in when actually needed.
# #
# invoked via `cmake -P` from a POST_BUILD step. required -D variables: # invoked via `cmake -P` from a POST_BUILD step. required -D variables:
# OBJDUMP - path to objdump (CMAKE_OBJDUMP) # OBJDUMP - path to objdump (CMAKE_OBJDUMP)
# TARGET_FILE - path to the PE binary to inspect # TARGET_FILE - path to the PE binary to inspect
# FORBIDDEN - semicolon-separated list of lowercase DLL names to reject # FORBIDDEN - semicolon-separated list of lowercase DLL names to reject
if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}") if(NOT OBJDUMP OR NOT EXISTS "${OBJDUMP}")
message(WARNING message(WARNING
"check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}") "check_no_static_dll_imports: objdump not found, skipping import check for ${TARGET_FILE}")
return() return()
endif() endif()
execute_process( execute_process(
COMMAND "${OBJDUMP}" -p "${TARGET_FILE}" COMMAND "${OBJDUMP}" -p "${TARGET_FILE}"
OUTPUT_VARIABLE dump_output OUTPUT_VARIABLE dump_output
RESULT_VARIABLE dump_result RESULT_VARIABLE dump_result
ERROR_VARIABLE dump_error) ERROR_VARIABLE dump_error)
if(NOT dump_result EQUAL 0) if(NOT dump_result EQUAL 0)
message(WARNING message(WARNING
"check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}") "check_no_static_dll_imports: objdump failed for ${TARGET_FILE}: ${dump_error}")
return() return()
endif() endif()
# both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per # both GNU objdump and llvm-objdump print one "DLL Name: <name>" line per
# statically imported DLL in their PE private-header dump. # statically imported DLL in their PE private-header dump.
string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}") string(REGEX MATCHALL "DLL Name:[ \t]*[^\n\r]+" dll_lines "${dump_output}")
set(violations "") set(violations "")
foreach(line IN LISTS dll_lines) foreach(line IN LISTS dll_lines)
string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}") string(REGEX REPLACE "DLL Name:[ \t]*" "" dll_name "${line}")
string(STRIP "${dll_name}" dll_name) string(STRIP "${dll_name}" dll_name)
string(TOLOWER "${dll_name}" dll_name_lower) string(TOLOWER "${dll_name}" dll_name_lower)
if(dll_name_lower IN_LIST FORBIDDEN) if(dll_name_lower IN_LIST FORBIDDEN)
list(APPEND violations "${dll_name}") list(APPEND violations "${dll_name}")
endif() endif()
endforeach() endforeach()
if(violations) if(violations)
list(REMOVE_DUPLICATES violations) list(REMOVE_DUPLICATES violations)
string(REPLACE ";" ", " violations_str "${violations}") string(REPLACE ";" ", " violations_str "${violations}")
message(FATAL_ERROR message(FATAL_ERROR
"static DLL import check FAILED for ${TARGET_FILE}\n" "static DLL import check FAILED for ${TARGET_FILE}\n"
" forbidden static imports found: ${violations_str}\n" " forbidden static imports found: ${violations_str}\n"
"\n" "\n"
" these DLLs must never be statically imported by spice:\n" " these DLLs must never be statically imported by spice:\n"
" * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n" " * user-overridable DLLs (e.g. DXVK d3d9.dll) - a static import loads the\n"
" system copy at startup and preempts the modules override (issue #779).\n" " system copy at startup and preempts the modules override (issue #779).\n"
" * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n" " * Media Foundation DLLs (mf/mfplat/mfreadwrite) - a static import breaks\n"
" Unity games.\n" " Unity games.\n"
"\n" "\n"
" fix: load the DLL dynamically instead - replace the direct API call with a\n" " fix: load the DLL dynamically instead - replace the direct API call with a\n"
" libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n" " libutils::try_library() + libutils::try_proc() lookup (or a delay load), then\n"
" call through the resolved function pointer.") " call through the resolved function pointer.")
endif() endif()
message(STATUS "static DLL import check passed for ${TARGET_FILE}") message(STATUS "static DLL import check passed for ${TARGET_FILE}")
+95 -95
View File
@@ -1,95 +1,95 @@
Copyright (c) 2017, keshikan (http://www.keshikan.net), Copyright (c) 2017, keshikan (http://www.keshikan.net),
with Reserved Font Name "DSEG". with Reserved Font Name "DSEG".
This Font Software is licensed under the SIL Open Font License, Version 1.1. This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL http://scripts.sil.org/OFL
----------------------------------------------------------- -----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
----------------------------------------------------------- -----------------------------------------------------------
PREAMBLE PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership open framework in which fonts may be shared and improved in partnership
with others. with others.
The OFL allows the licensed fonts to be used, studied, modified and The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded, fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives, names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives. to any document created using the fonts or their derivatives.
DEFINITIONS DEFINITIONS
"Font Software" refers to the set of files released by the Copyright "Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation. include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the "Reserved Font Name" refers to any names specified as such after the
copyright statement(s). copyright statement(s).
"Original Version" refers to the collection of Font Software components as "Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s). distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, "Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a Original Version, by changing formats or by porting the Font Software to a
new environment. new environment.
"Author" refers to any designer, engineer, programmer, technical "Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software. writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify, a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions: Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, 1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself. in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, 2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user. binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font 3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as Copyright Holder. This restriction only applies to the primary font name as
presented to the users. presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written Copyright Holder(s) and the Author(s) or with their explicit written
permission. permission.
5) The Font Software, modified or unmodified, in part or in whole, 5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created remain under this license does not apply to any document created
using the Font Software. using the Font Software.
TERMINATION TERMINATION
This license becomes null and void if any of the above conditions are This license becomes null and void if any of the above conditions are
not met. not met.
DISCLAIMER DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE. OTHER DEALINGS IN THE FONT SOFTWARE.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+73 -73
View File
@@ -1,73 +1,73 @@
#ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD #ifndef EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
#define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD #define EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD
// This code comes from: // This code comes from:
// https://github.com/dhbaird/easywsclient // https://github.com/dhbaird/easywsclient
// //
// To get the latest version: // To get the latest version:
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp // wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.hpp
// wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp // wget https://raw.github.com/dhbaird/easywsclient/master/easywsclient.cpp
#include <string> #include <string>
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
namespace easywsclient { namespace easywsclient {
struct Callback_Imp { virtual void operator()(const std::string& message) = 0; }; struct Callback_Imp { virtual void operator()(const std::string& message) = 0; };
struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; }; struct BytesCallback_Imp { virtual void operator()(const std::vector<uint8_t>& message) = 0; };
class WebSocket { class WebSocket {
public: public:
typedef WebSocket * pointer; typedef WebSocket * pointer;
typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues; typedef enum readyStateValues { CLOSING, CLOSED, CONNECTING, OPEN } readyStateValues;
// Factories: // Factories:
static pointer create_dummy(); static pointer create_dummy();
static pointer from_url(const std::string& url, const std::string& origin = std::string()); static pointer from_url(const std::string& url, const std::string& origin = std::string());
static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string()); static pointer from_url_no_mask(const std::string& url, const std::string& origin = std::string());
// Interfaces: // Interfaces:
virtual ~WebSocket() { } virtual ~WebSocket() { }
virtual void poll(int timeout = 0) = 0; // timeout in milliseconds virtual void poll(int timeout = 0) = 0; // timeout in milliseconds
virtual void send(const std::string& message) = 0; virtual void send(const std::string& message) = 0;
virtual void sendBinary(const std::string& message) = 0; virtual void sendBinary(const std::string& message) = 0;
virtual void sendBinary(const std::vector<uint8_t>& message) = 0; virtual void sendBinary(const std::vector<uint8_t>& message) = 0;
virtual void sendPing() = 0; virtual void sendPing() = 0;
virtual void close() = 0; virtual void close() = 0;
virtual readyStateValues getReadyState() const = 0; virtual readyStateValues getReadyState() const = 0;
template<class Callable> template<class Callable>
void dispatch(Callable callable) void dispatch(Callable callable)
// For callbacks that accept a string argument. // For callbacks that accept a string argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public Callback_Imp { struct _Callback : public Callback_Imp {
Callable& callable; Callable& callable;
_Callback(Callable& callable) : callable(callable) { } _Callback(Callable& callable) : callable(callable) { }
void operator()(const std::string& message) { callable(message); } void operator()(const std::string& message) { callable(message); }
}; };
_Callback callback(callable); _Callback callback(callable);
_dispatch(callback); _dispatch(callback);
} }
template<class Callable> template<class Callable>
void dispatchBinary(Callable callable) void dispatchBinary(Callable callable)
// For callbacks that accept a std::vector<uint8_t> argument. // For callbacks that accept a std::vector<uint8_t> argument.
{ // N.B. this is compatible with both C++11 lambdas, functors and C function pointers { // N.B. this is compatible with both C++11 lambdas, functors and C function pointers
struct _Callback : public BytesCallback_Imp { struct _Callback : public BytesCallback_Imp {
Callable& callable; Callable& callable;
_Callback(Callable& callable) : callable(callable) { } _Callback(Callable& callable) : callable(callable) { }
void operator()(const std::vector<uint8_t>& message) { callable(message); } void operator()(const std::vector<uint8_t>& message) { callable(message); }
}; };
_Callback callback(callable); _Callback callback(callable);
_dispatchBinary(callback); _dispatchBinary(callback);
} }
protected: protected:
virtual void _dispatch(Callback_Imp& callable) = 0; virtual void _dispatch(Callback_Imp& callable) = 0;
virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0; virtual void _dispatchBinary(BytesCallback_Imp& callable) = 0;
}; };
} // namespace easywsclient } // namespace easywsclient
#endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */ #endif /* EASYWSCLIENT_HPP_20120819_MIOFVASDTNUASZDQPLFD */
File diff suppressed because it is too large Load Diff
+171 -171
View File
@@ -1,171 +1,171 @@
#include "asio.h" #include "asio.h"
#include <windows.h> #include <windows.h>
#include <cstring> #include <cstring>
#include "avs/game.h" #include "avs/game.h"
#include "gitadora.h" #include "gitadora.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
namespace games::gitadora { namespace games::gitadora {
// Redirects the game's hard-coded "XONAR" ASIO driver lookup to the // Redirects the game's hard-coded "XONAR" ASIO driver lookup to the
// driver name in ASIO_DRIVER by intercepting registry calls to // driver name in ASIO_DRIVER by intercepting registry calls to
// HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles // HKLM\SOFTWARE\ASIO. Sentinel HKEY values mark the redirected handles
// so we can recognise them on subsequent reg* calls. // so we can recognise them on subsequent reg* calls.
static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001); static const HKEY PARENT_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4001);
static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002); static const HKEY DEVICE_ASIO_REG_HANDLE = reinterpret_cast<HKEY>(0x4002);
static const char *FAKE_ASIO_DEVICE_NAME = "XONAR"; static const char *FAKE_ASIO_DEVICE_NAME = "XONAR";
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr; static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr; static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr; static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr; static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr; static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
static HKEY real_asio_reg_handle = nullptr; static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr; static HKEY real_asio_device_reg_handle = nullptr;
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
PHKEY phkResult) PHKEY phkResult)
{ {
if (ASIO_DRIVER.has_value() && if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr && lpSubKey != nullptr &&
phkResult != nullptr && phkResult != nullptr &&
hKey == PARENT_ASIO_REG_HANDLE && hKey == PARENT_ASIO_REG_HANDLE &&
_stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) { _stricmp(lpSubKey, FAKE_ASIO_DEVICE_NAME) == 0) {
*phkResult = DEVICE_ASIO_REG_HANDLE; *phkResult = DEVICE_ASIO_REG_HANDLE;
log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value()); log_info("gitadora::asio", "replacing '{}' with '{}'", lpSubKey, ASIO_DRIVER.value());
const auto result = RegOpenKeyExA_orig( const auto result = RegOpenKeyExA_orig(
real_asio_reg_handle, real_asio_reg_handle,
ASIO_DRIVER.value().c_str(), ASIO_DRIVER.value().c_str(),
ulOptions, ulOptions,
samDesired, samDesired,
&real_asio_device_reg_handle); &real_asio_device_reg_handle);
if (result != ERROR_SUCCESS) { if (result != ERROR_SUCCESS) {
log_warning( log_warning(
"gitadora::asio", "gitadora::asio",
"failed to open registry subkey '{}', error=0x{:x}", "failed to open registry subkey '{}', error=0x{:x}",
ASIO_DRIVER.value(), result); ASIO_DRIVER.value(), result);
log_warning( log_warning(
"gitadora::asio", "gitadora::asio",
"due to improper ASIO setting, audio init will fail"); "due to improper ASIO setting, audio init will fail");
} }
return result; return result;
} }
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult); return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
} }
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (ASIO_DRIVER.has_value() && if (ASIO_DRIVER.has_value() &&
lpSubKey != nullptr && lpSubKey != nullptr &&
phkResult != nullptr && phkResult != nullptr &&
hKey == HKEY_LOCAL_MACHINE && hKey == HKEY_LOCAL_MACHINE &&
_stricmp(lpSubKey, "software\\asio") == 0) _stricmp(lpSubKey, "software\\asio") == 0)
{ {
*phkResult = PARENT_ASIO_REG_HANDLE; *phkResult = PARENT_ASIO_REG_HANDLE;
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle); return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
} }
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult); return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
} }
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) { static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) { if (hKey == PARENT_ASIO_REG_HANDLE && ASIO_DRIVER.has_value()) {
if (dwIndex == 0) { if (dwIndex == 0) {
// forward to real handle just to verify the key exists; we // forward to real handle just to verify the key exists; we
// overwrite the name with our fake driver string regardless // overwrite the name with our fake driver string regardless
auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName); auto ret = RegEnumKeyA_orig(real_asio_reg_handle, dwIndex, lpName, cchName);
if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) { if (ret == ERROR_SUCCESS && lpName != nullptr && cchName > 0) {
log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME); log_info("gitadora::asio", "stubbing '{}' with '{}'", lpName, FAKE_ASIO_DEVICE_NAME);
strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName); strncpy(lpName, FAKE_ASIO_DEVICE_NAME, cchName);
lpName[cchName - 1] = '\0'; lpName[cchName - 1] = '\0';
} }
return ret; return ret;
} else { } else {
return ERROR_NO_MORE_ITEMS; return ERROR_NO_MORE_ITEMS;
} }
} }
return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName); return RegEnumKeyA_orig(hKey, dwIndex, lpName, cchName);
} }
static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType, static LONG WINAPI RegQueryValueExA_hook(HKEY hKey, LPCSTR lpValueName, LPDWORD lpReserved, LPDWORD lpType,
LPBYTE lpData, LPDWORD lpcbData) LPBYTE lpData, LPDWORD lpcbData)
{ {
HKEY target = hKey; HKEY target = hKey;
if (ASIO_DRIVER.has_value() && if (ASIO_DRIVER.has_value() &&
lpValueName != nullptr && lpValueName != nullptr &&
lpData != nullptr && lpData != nullptr &&
lpcbData != nullptr && lpcbData != nullptr &&
hKey == DEVICE_ASIO_REG_HANDLE) { hKey == DEVICE_ASIO_REG_HANDLE) {
if (_stricmp(lpValueName, "Description") == 0) { if (_stricmp(lpValueName, "Description") == 0) {
// engine may verify the driver name after open; ensure it still // engine may verify the driver name after open; ensure it still
// sees something containing "XONAR" so the substring check passes // sees something containing "XONAR" so the substring check passes
const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1; const size_t len = strlen(FAKE_ASIO_DEVICE_NAME) + 1;
if (*lpcbData < len) { if (*lpcbData < len) {
*lpcbData = static_cast<DWORD>(len); *lpcbData = static_cast<DWORD>(len);
return ERROR_MORE_DATA; return ERROR_MORE_DATA;
} }
memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len); memcpy(lpData, FAKE_ASIO_DEVICE_NAME, len);
*lpcbData = static_cast<DWORD>(len); *lpcbData = static_cast<DWORD>(len);
if (lpType != nullptr) { if (lpType != nullptr) {
*lpType = REG_SZ; *lpType = REG_SZ;
} }
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
// for everything else (CLSID etc.) defer to the real driver subkey // for everything else (CLSID etc.) defer to the real driver subkey
target = real_asio_device_reg_handle; target = real_asio_device_reg_handle;
} }
return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData); return RegQueryValueExA_orig(target, lpValueName, lpReserved, lpType, lpData, lpcbData);
} }
static LONG WINAPI RegCloseKey_hook(HKEY hKey) { static LONG WINAPI RegCloseKey_hook(HKEY hKey) {
if (hKey == PARENT_ASIO_REG_HANDLE) { if (hKey == PARENT_ASIO_REG_HANDLE) {
if (real_asio_reg_handle != nullptr) { if (real_asio_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_reg_handle); RegCloseKey_orig(real_asio_reg_handle);
real_asio_reg_handle = nullptr; real_asio_reg_handle = nullptr;
} }
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
if (hKey == DEVICE_ASIO_REG_HANDLE) { if (hKey == DEVICE_ASIO_REG_HANDLE) {
if (real_asio_device_reg_handle != nullptr) { if (real_asio_device_reg_handle != nullptr) {
RegCloseKey_orig(real_asio_device_reg_handle); RegCloseKey_orig(real_asio_device_reg_handle);
real_asio_device_reg_handle = nullptr; real_asio_device_reg_handle = nullptr;
} }
return ERROR_SUCCESS; return ERROR_SUCCESS;
} }
return RegCloseKey_orig(hKey); return RegCloseKey_orig(hKey);
} }
void asio_hook_init() { void asio_hook_init() {
if (!ASIO_DRIVER.has_value()) { if (!ASIO_DRIVER.has_value()) {
return; return;
} }
log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value()); log_info("gitadora::asio", "installing ASIO driver redirect: XONAR -> {}", ASIO_DRIVER.value());
RegCloseKey_orig = detour::iat_try( RegCloseKey_orig = detour::iat_try(
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE); "RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
RegEnumKeyA_orig = detour::iat_try( RegEnumKeyA_orig = detour::iat_try(
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE); "RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyA_orig = detour::iat_try( RegOpenKeyA_orig = detour::iat_try(
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE); "RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
RegOpenKeyExA_orig = detour::iat_try( RegOpenKeyExA_orig = detour::iat_try(
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE); "RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
RegQueryValueExA_orig = detour::iat_try( RegQueryValueExA_orig = detour::iat_try(
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE); "RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
} }
} }
+12 -12
View File
@@ -1,12 +1,12 @@
#pragma once #pragma once
namespace games::gitadora { namespace games::gitadora {
// installs IAT registry hooks in gfdm.dll that redirect the game's // installs IAT registry hooks in gfdm.dll that redirect the game's
// ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen // ASIO driver lookup (hard-coded "XONAR" substring) to a user-chosen
// driver name read from games::gitadora::ASIO_DRIVER. // driver name read from games::gitadora::ASIO_DRIVER.
// //
// safe to call unconditionally; if ASIO_DRIVER is unset the hooks // safe to call unconditionally; if ASIO_DRIVER is unset the hooks
// forward every call straight through to advapi32. // forward every call straight through to advapi32.
void asio_hook_init(); void asio_hook_init();
} }
+11 -11
View File
@@ -41,6 +41,7 @@ namespace games::gitadora {
bool ARENA_TWO_HEAD_EXCLUSIVE = false; bool ARENA_TWO_HEAD_EXCLUSIVE = 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;
/* /*
* Prevent GitaDora from creating folders on F drive * Prevent GitaDora from creating folders on F drive
@@ -809,17 +810,16 @@ 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 && GRAPHICS_PREVENT_SECONDARY_WINDOWS) {
if (!native_touch_ready) { // 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
+5
View File
@@ -23,6 +23,11 @@ namespace games::gitadora {
extern bool ARENA_TWO_HEAD_EXCLUSIVE; extern bool ARENA_TWO_HEAD_EXCLUSIVE;
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;
class GitaDoraGame : public games::Game { class GitaDoraGame : public games::Game {
public: public:
+7 -4
View File
@@ -3,6 +3,7 @@
#if SPICE64 #if SPICE64
#include <cstdint> #include <cstdint>
#include "api/client.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -443,10 +444,12 @@ namespace games::iidx {
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
for (unsigned int i = 0; i < data_size; ++i) { if (api::has_clients()) {
map.data[i].r = data[i * 3]; for (size_t i = 0; i < data_size; ++i) {
map.data[i].g = data[i * 3 + 1]; map.data[i].r = data[i * 3];
map.data[i].b = data[i * 3 + 2]; map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
} }
} }
+6 -18
View File
@@ -58,7 +58,7 @@ namespace games::iidx {
// settings // settings
bool FLIP_CAMS = false; bool FLIP_CAMS = false;
std::optional<bool> DISABLE_CAMS; cab_camera_access_mode CAB_CAMERA_ACCESS = cab_camera_access_mode::automatic;
bool TDJ_CAMERA = false; bool TDJ_CAMERA = false;
bool TDJ_CAMERA_PREFER_16_9 = true; bool TDJ_CAMERA_PREFER_16_9 = true;
bool TDJ_MODE = false; bool TDJ_MODE = false;
@@ -424,10 +424,7 @@ namespace games::iidx {
"RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE); "RegQueryValueExA", RegQueryValueExA_hook, avs::game::DLL_INSTANCE);
// check if cam hook should be enabled // check if cam hook should be enabled
if (!DISABLE_CAMS.has_value()) { if (CAB_CAMERA_ACCESS == cab_camera_access_mode::legacy) {
log_fatal("iidx", "assertion failure - DISABLE_CAMS not set during attach");
}
if (!DISABLE_CAMS.value()) {
init_legacy_camera_hook(FLIP_CAMS); init_legacy_camera_hook(FLIP_CAMS);
} }
@@ -454,19 +451,10 @@ namespace games::iidx {
SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str()); SetEnvironmentVariable("SCREEN_MODE", SCREEN_MODE.value().c_str());
} }
// check for cab camera access for the second time (first time was in launcher.cpp) // auto with iidx module means turn off camera (non-cab use)
// this time, we are inside -iidx module hook, which means the user is likely NOT on a cab if (CAB_CAMERA_ACCESS == cab_camera_access_mode::automatic) {
// therefore, start with cams OFF by default, and allow user to forcibly override to ON log_misc("iidx", "CONNECT_CAMERA env var set to 0");
if (!games::iidx::DISABLE_CAMS.has_value()) { SetEnvironmentVariable("CONNECT_CAMERA", "0");
games::iidx::DISABLE_CAMS = true;
if (options->at(launcher::Options::IIDXCabCamAccess).is_active() &&
options->at(launcher::Options::IIDXCabCamAccess).value_text() == "on") {
games::iidx::DISABLE_CAMS = false;
}
if (games::iidx::DISABLE_CAMS.value()) {
log_misc("iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0");
}
} }
// windowed subscreen, enabled by default, unless turned off by user // windowed subscreen, enabled by default, unless turned off by user
+8 -1
View File
@@ -11,6 +11,13 @@
namespace games::iidx { namespace games::iidx {
enum class cab_camera_access_mode {
automatic,
off,
on,
legacy,
};
enum class iidx_aio_emulation_state { enum class iidx_aio_emulation_state {
unknown, unknown,
bi2a_com2, bi2a_com2,
@@ -20,7 +27,7 @@ namespace games::iidx {
// settings // settings
extern bool FLIP_CAMS; extern bool FLIP_CAMS;
extern std::optional<bool> DISABLE_CAMS; extern cab_camera_access_mode CAB_CAMERA_ACCESS;
extern bool TDJ_CAMERA; extern bool TDJ_CAMERA;
extern bool TDJ_CAMERA_PREFER_16_9; extern bool TDJ_CAMERA_PREFER_16_9;
extern std::optional<std::string> TDJ_CAMERA_OVERRIDE; extern std::optional<std::string> TDJ_CAMERA_OVERRIDE;
+131 -131
View File
@@ -1,132 +1,132 @@
#include "mf_wrappers.h" #include "mf_wrappers.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
namespace games::iidx { namespace games::iidx {
static bool INITIALIZED = false; static bool INITIALIZED = false;
static HMODULE mf_dll = nullptr; static HMODULE mf_dll = nullptr;
static HMODULE mfreadwrite_dll = nullptr; static HMODULE mfreadwrite_dll = nullptr;
static HMODULE mfplat_dll = nullptr; static HMODULE mfplat_dll = nullptr;
typedef HRESULT (__stdcall * MFCreateAttributes_t)( typedef HRESULT (__stdcall * MFCreateAttributes_t)(
_Out_ IMFAttributes** ppMFAttributes, _Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize _In_ UINT32 cInitialSize
); );
typedef HRESULT (__stdcall * MFCreateMediaType_t)( typedef HRESULT (__stdcall * MFCreateMediaType_t)(
_Out_ IMFMediaType** ppMFType _Out_ IMFMediaType** ppMFType
); );
typedef HRESULT (__stdcall * MFEnumDeviceSources_t)( typedef HRESULT (__stdcall * MFEnumDeviceSources_t)(
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate _Out_ UINT32* pcSourceActivate
); );
typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)( typedef HRESULT (__stdcall * MFCreateSourceReaderFromMediaSource_t)(
_In_ IMFMediaSource *pMediaSource, _In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes, _In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader _Out_ IMFSourceReader **ppSourceReader
); );
typedef HRESULT (__stdcall * MFGetService_t)( typedef HRESULT (__stdcall * MFGetService_t)(
IUnknown* punkObject, IUnknown* punkObject,
REFGUID guidService, REFGUID guidService,
REFIID riid, REFIID riid,
_Outptr_ LPVOID* ppvObject _Outptr_ LPVOID* ppvObject
); );
static MFCreateAttributes_t MFCreateAttributes = nullptr; static MFCreateAttributes_t MFCreateAttributes = nullptr;
static MFCreateMediaType_t MFCreateMediaType = nullptr; static MFCreateMediaType_t MFCreateMediaType = nullptr;
static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr; static MFEnumDeviceSources_t MFEnumDeviceSources = nullptr;
static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr; static MFCreateSourceReaderFromMediaSource_t MFCreateSourceReaderFromMediaSource = nullptr;
static MFGetService_t MFGetService = nullptr; static MFGetService_t MFGetService = nullptr;
void init_mf_library() { void init_mf_library() {
// why was all of this needed? // why was all of this needed?
// //
// when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib // when iidx camhook was initially implemented, we linked to mf.lib, mfreadwrite.lib, and mfplat.lib
// this made Unity-based really unhappy, causing them to skip over the logic that loads mf library // this made Unity-based really unhappy, causing them to skip over the logic that loads mf library
// causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt) // causing videos to not play ("Initializing Microsoft Media Foundation failed." in the cmd prompt)
// //
// as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file // as a result, the static linking to mf libs were removed, and we are now doing the mess that is this file
if (INITIALIZED) { if (INITIALIZED) {
return; return;
} }
INITIALIZED = true; INITIALIZED = true;
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN"); log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - BEGIN");
mf_dll = libutils::load_library("mf.dll", true); mf_dll = libutils::load_library("mf.dll", true);
mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true); mfreadwrite_dll = libutils::load_library("mfreadwrite.dll", true);
mfplat_dll = libutils::load_library("mfplat.dll", true); mfplat_dll = libutils::load_library("mfplat.dll", true);
MFCreateAttributes = (MFCreateAttributes_t) MFCreateAttributes = (MFCreateAttributes_t)
libutils::get_proc(mfplat_dll, "MFCreateAttributes"); libutils::get_proc(mfplat_dll, "MFCreateAttributes");
if (!MFCreateAttributes) { if (!MFCreateAttributes) {
log_fatal("mf_wrappers", "MFCreateAttributes failed to hook"); log_fatal("mf_wrappers", "MFCreateAttributes failed to hook");
} }
MFCreateMediaType = (MFCreateMediaType_t) MFCreateMediaType = (MFCreateMediaType_t)
libutils::get_proc(mfplat_dll, "MFCreateMediaType"); libutils::get_proc(mfplat_dll, "MFCreateMediaType");
if (!MFCreateMediaType) { if (!MFCreateMediaType) {
log_fatal("mf_wrappers", "MFCreateMediaType failed to hook"); log_fatal("mf_wrappers", "MFCreateMediaType failed to hook");
} }
MFEnumDeviceSources = (MFEnumDeviceSources_t) MFEnumDeviceSources = (MFEnumDeviceSources_t)
libutils::get_proc(mf_dll, "MFEnumDeviceSources"); libutils::get_proc(mf_dll, "MFEnumDeviceSources");
if (!MFEnumDeviceSources) { if (!MFEnumDeviceSources) {
log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook"); log_fatal("mf_wrappers", "MFEnumDeviceSources failed to hook");
} }
MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t) MFCreateSourceReaderFromMediaSource = (MFCreateSourceReaderFromMediaSource_t)
libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource"); libutils::get_proc(mfreadwrite_dll, "MFCreateSourceReaderFromMediaSource");
if (!MFCreateSourceReaderFromMediaSource) { if (!MFCreateSourceReaderFromMediaSource) {
log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook"); log_fatal("mf_wrappers", "MFCreateSourceReaderFromMediaSource failed to hook");
} }
MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService"); MFGetService = (MFGetService_t)libutils::get_proc(mf_dll, "MFGetService");
if (!MFGetService) { if (!MFGetService) {
log_fatal("mf_wrappers", "MFGetService failed to hook"); log_fatal("mf_wrappers", "MFGetService failed to hook");
} }
log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE"); log_misc("mf_wrappers", "creating delay-loaded wrappers for MF routines - DONE");
} }
HRESULT WrappedMFCreateAttributes ( HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes, _Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize) { _In_ UINT32 cInitialSize) {
return MFCreateAttributes(ppMFAttributes, cInitialSize); return MFCreateAttributes(ppMFAttributes, cInitialSize);
} }
HRESULT WrappedMFCreateMediaType ( HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType) { _Out_ IMFMediaType** ppMFType) {
return MFCreateMediaType(ppMFType); return MFCreateMediaType(ppMFType);
} }
HRESULT WrappedMFEnumDeviceSources ( HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate) { _Out_ UINT32* pcSourceActivate) {
return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate); return MFEnumDeviceSources(pAttributes, pppSourceActivate, pcSourceActivate);
} }
HRESULT WrappedMFCreateSourceReaderFromMediaSource ( HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource, _In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes, _In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader) { _Out_ IMFSourceReader **ppSourceReader) {
return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader); return MFCreateSourceReaderFromMediaSource(pMediaSource, pAttributes, ppSourceReader);
} }
HRESULT WrappedMFGetService ( HRESULT WrappedMFGetService (
IUnknown* punkObject, IUnknown* punkObject,
REFGUID guidService, REFGUID guidService,
REFIID riid, REFIID riid,
_Outptr_ LPVOID* ppvObject) { _Outptr_ LPVOID* ppvObject) {
return MFGetService(punkObject, guidService, riid, ppvObject); return MFGetService(punkObject, guidService, riid, ppvObject);
} }
} }
+32 -32
View File
@@ -1,33 +1,33 @@
#include <mfapi.h> #include <mfapi.h>
#include <mfidl.h> #include <mfidl.h>
#include <mfreadwrite.h> #include <mfreadwrite.h>
#include <mfobjects.h> #include <mfobjects.h>
#pragma once #pragma once
namespace games::iidx { namespace games::iidx {
void init_mf_library(); void init_mf_library();
HRESULT WrappedMFCreateAttributes ( HRESULT WrappedMFCreateAttributes (
_Out_ IMFAttributes** ppMFAttributes, _Out_ IMFAttributes** ppMFAttributes,
_In_ UINT32 cInitialSize); _In_ UINT32 cInitialSize);
HRESULT WrappedMFCreateMediaType ( HRESULT WrappedMFCreateMediaType (
_Out_ IMFMediaType** ppMFType); _Out_ IMFMediaType** ppMFType);
HRESULT WrappedMFEnumDeviceSources ( HRESULT WrappedMFEnumDeviceSources (
_In_ IMFAttributes* pAttributes, _In_ IMFAttributes* pAttributes,
_Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate, _Outptr_result_buffer_(*pcSourceActivate) IMFActivate*** pppSourceActivate,
_Out_ UINT32* pcSourceActivate); _Out_ UINT32* pcSourceActivate);
HRESULT WrappedMFCreateSourceReaderFromMediaSource ( HRESULT WrappedMFCreateSourceReaderFromMediaSource (
_In_ IMFMediaSource *pMediaSource, _In_ IMFMediaSource *pMediaSource,
_In_opt_ IMFAttributes *pAttributes, _In_opt_ IMFAttributes *pAttributes,
_Out_ IMFSourceReader **ppSourceReader); _Out_ IMFSourceReader **ppSourceReader);
HRESULT WrappedMFGetService ( HRESULT WrappedMFGetService (
IUnknown* punkObject, IUnknown* punkObject,
REFGUID guidService, REFGUID guidService,
REFIID riid, REFIID riid,
_Outptr_ LPVOID* ppvObject); _Outptr_ LPVOID* ppvObject);
} }
+233 -233
View File
@@ -1,233 +1,233 @@
#include "touch_mode.h" #include "touch_mode.h"
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
#include "util/logging.h" #include "util/logging.h"
namespace games::nost::touch_mode { namespace games::nost::touch_mode {
// native contact positions feed piano input directly. native events reach the game // native contact positions feed piano input directly. native events reach the game
// in nav mode and are suppressed in piano mode. mode-button contacts are always // in nav mode and are suppressed in piano mode. mode-button contacts are always
// suppressed and change that routing after release. // suppressed and change that routing after release.
static constexpr LONG PIANO_LEFT_GAP = 11; static constexpr LONG PIANO_LEFT_GAP = 11;
static constexpr LONG PIANO_RIGHT_GAP = 10; static constexpr LONG PIANO_RIGHT_GAP = 10;
static constexpr uint32_t PIANO_KEY_COUNT = 28; static constexpr uint32_t PIANO_KEY_COUNT = 28;
struct TouchGeometry { struct TouchGeometry {
HWND window = nullptr; HWND window = nullptr;
RECT mode_button {}; RECT mode_button {};
LONG client_width = 0; LONG client_width = 0;
LONG client_height = 0; LONG client_height = 0;
bool valid() const { bool valid() const {
return window != nullptr && client_width > 0 && client_height > 0; return window != nullptr && client_width > 0 && client_height > 0;
} }
}; };
struct NativeContact { struct NativeContact {
POINT position {}; POINT position {};
// position contains game-client coordinates // position contains game-client coordinates
bool client_position_valid = false; bool client_position_valid = false;
// down began on the mode switch button; remains true through up // down began on the mode switch button; remains true through up
bool mode_button = false; bool mode_button = false;
}; };
static std::atomic_bool accept_events { false }; static std::atomic_bool accept_events { false };
static std::atomic<Mode> current_mode_state { Mode::Nav }; static std::atomic<Mode> current_mode_state { Mode::Nav };
static std::mutex state_mutex; static std::mutex state_mutex;
static TouchGeometry touch_geometry; static TouchGeometry touch_geometry;
// native contacts are kept by ID so each contact contributes exactly one position // native contacts are kept by ID so each contact contributes exactly one position
static std::unordered_map<DWORD, NativeContact> active_contacts; static std::unordered_map<DWORD, NativeContact> active_contacts;
// a hardware button release requests one change after all contacts are released // a hardware button release requests one change after all contacts are released
static bool mode_change_pending = false; static bool mode_change_pending = false;
static void reset_state_locked() { static void reset_state_locked() {
current_mode_state.store(Mode::Nav, std::memory_order_release); current_mode_state.store(Mode::Nav, std::memory_order_release);
touch_geometry = {}; touch_geometry = {};
active_contacts.clear(); active_contacts.clear();
mode_change_pending = false; mode_change_pending = false;
} }
// hardware contacts arrive in screen coordinates // hardware contacts arrive in screen coordinates
static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) { static bool native_touch_in_button(const nativetouch::NativeTouchEvent &event) {
if (!touch_geometry.valid()) { if (!touch_geometry.valid()) {
return false; return false;
} }
POINT position { event.x, event.y }; POINT position { event.x, event.y };
if (!ScreenToClient(touch_geometry.window, &position)) { if (!ScreenToClient(touch_geometry.window, &position)) {
return false; return false;
} }
return PtInRect(&touch_geometry.mode_button, position) != FALSE; return PtInRect(&touch_geometry.mode_button, position) != FALSE;
} }
static bool update_touch_state(const nativetouch::NativeTouchEvent &event) { static bool update_touch_state(const nativetouch::NativeTouchEvent &event) {
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
// first, process down / move events // first, process down / move events
if (event.down || event.move) { if (event.down || event.move) {
auto contact = active_contacts.try_emplace(event.id).first; auto contact = active_contacts.try_emplace(event.id).first;
// keep track of IDs that began as a down on the mode switch button // keep track of IDs that began as a down on the mode switch button
if (event.down) { if (event.down) {
contact->second.mode_button = native_touch_in_button(event); contact->second.mode_button = native_touch_in_button(event);
} }
// check for valid position // check for valid position
POINT position { event.x, event.y }; POINT position { event.x, event.y };
if (touch_geometry.window != nullptr && if (touch_geometry.window != nullptr &&
ScreenToClient(touch_geometry.window, &position)) { ScreenToClient(touch_geometry.window, &position)) {
contact->second.position = position; contact->second.position = position;
contact->second.client_position_valid = true; contact->second.client_position_valid = true;
} }
} }
const auto contact = active_contacts.find(event.id); const auto contact = active_contacts.find(event.id);
const bool mode_button_contact = contact != active_contacts.end() && const bool mode_button_contact = contact != active_contacts.end() &&
contact->second.mode_button; contact->second.mode_button;
// process up events // process up events
if (event.up) { if (event.up) {
active_contacts.erase(event.id); active_contacts.erase(event.id);
// if a contact that began down event on the mode switch button has // if a contact that began down event on the mode switch button has
// been released, a mode switch is now pending // been released, a mode switch is now pending
if (mode_button_contact) { if (mode_button_contact) {
mode_change_pending = true; mode_change_pending = true;
} }
// apply the change on the final hardware up. switching earlier would // apply the change on the final hardware up. switching earlier would
// split another contact's down and up events across different modes // split another contact's down and up events across different modes
if (mode_change_pending && active_contacts.empty()) { if (mode_change_pending && active_contacts.empty()) {
mode_change_pending = false; mode_change_pending = false;
const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav; const auto next_mode = current_mode() == Mode::Nav ? Mode::Piano : Mode::Nav;
current_mode_state.store(next_mode, std::memory_order_release); current_mode_state.store(next_mode, std::memory_order_release);
} }
} }
return mode_button_contact; return mode_button_contact;
} }
// install the Nostalgia-specific native touch interception // install the Nostalgia-specific native touch interception
void enable() { void enable() {
if (accept_events.exchange(true, std::memory_order_acq_rel)) { if (accept_events.exchange(true, std::memory_order_acq_rel)) {
return; return;
} }
{ {
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked(); reset_state_locked();
} }
nativetouch::set_input_filter(filter_native_touch); nativetouch::set_input_filter(filter_native_touch);
log_info("nost::touch", "enabled"); log_info("nost::touch", "enabled");
} }
void disable() { void disable() {
if (!accept_events.exchange(false, std::memory_order_acq_rel)) { if (!accept_events.exchange(false, std::memory_order_acq_rel)) {
return; return;
} }
nativetouch::set_input_filter(nullptr); nativetouch::set_input_filter(nullptr);
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
reset_state_locked(); reset_state_locked();
} }
bool enabled() { bool enabled() {
return accept_events.load(std::memory_order_acquire); return accept_events.load(std::memory_order_acquire);
} }
Mode current_mode() { Mode current_mode() {
return current_mode_state.load(std::memory_order_acquire); return current_mode_state.load(std::memory_order_acquire);
} }
// publish the rendered overlay button rectangle in game-client pixels // publish the rendered overlay button rectangle in game-client pixels
void publish_button_bounds(HWND window, const RECT &client_bounds) { void publish_button_bounds(HWND window, const RECT &client_bounds) {
TouchGeometry next {}; TouchGeometry next {};
RECT client_rect {}; RECT client_rect {};
if (window != nullptr && GetClientRect(window, &client_rect) && if (window != nullptr && GetClientRect(window, &client_rect) &&
client_rect.right > 0 && client_rect.bottom > 0) { client_rect.right > 0 && client_rect.bottom > 0) {
next.window = window; next.window = window;
next.mode_button = client_bounds; next.mode_button = client_bounds;
next.client_width = client_rect.right; next.client_width = client_rect.right;
next.client_height = client_rect.bottom; next.client_height = client_rect.bottom;
} }
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
touch_geometry = next; touch_geometry = next;
} }
// return the active 28-key piano bitfield for the PANB input update // return the active 28-key piano bitfield for the PANB input update
uint32_t piano_key_state() { uint32_t piano_key_state() {
if (!enabled() || current_mode() != Mode::Piano) { if (!enabled() || current_mode() != Mode::Piano) {
return 0; return 0;
} }
std::lock_guard<std::mutex> lock(state_mutex); std::lock_guard<std::mutex> lock(state_mutex);
if (current_mode() != Mode::Piano || !touch_geometry.valid()) { if (current_mode() != Mode::Piano || !touch_geometry.valid()) {
return 0; return 0;
} }
uint32_t state = 0; uint32_t state = 0;
for (const auto &contact : active_contacts) { for (const auto &contact : active_contacts) {
// invalid position or mode-button contact; ignore these contacts // invalid position or mode-button contact; ignore these contacts
if (!contact.second.client_position_valid || contact.second.mode_button) { if (!contact.second.client_position_valid || contact.second.mode_button) {
continue; continue;
} }
const auto &position = contact.second.position; const auto &position = contact.second.position;
// outside the client area or on the mode button; ignore these contacts // outside the client area or on the mode button; ignore these contacts
if (position.x < 0 || position.x >= touch_geometry.client_width || if (position.x < 0 || position.x >= touch_geometry.client_width ||
position.y < 0 || position.y >= touch_geometry.client_height || position.y < 0 || position.y >= touch_geometry.client_height ||
PtInRect(&touch_geometry.mode_button, position)) { PtInRect(&touch_geometry.mode_button, position)) {
continue; continue;
} }
// divide the inset width evenly; touches in either side gap clamp to // divide the inset width evenly; touches in either side gap clamp to
// the nearest outer key so the physical screen edges remain playable // the nearest outer key so the physical screen edges remain playable
const auto piano_width = const auto piano_width =
touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP; touch_geometry.client_width - PIANO_LEFT_GAP - PIANO_RIGHT_GAP;
uint32_t key = 0; uint32_t key = 0;
if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) { if (position.x >= touch_geometry.client_width - PIANO_RIGHT_GAP) {
key = PIANO_KEY_COUNT - 1; key = PIANO_KEY_COUNT - 1;
} else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) { } else if (position.x >= PIANO_LEFT_GAP && piano_width > 0) {
key = static_cast<uint32_t>( key = static_cast<uint32_t>(
(position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width); (position.x - PIANO_LEFT_GAP) * PIANO_KEY_COUNT / piano_width);
} }
state |= UINT32_C(1) << key; state |= UINT32_C(1) << key;
} }
return state; return state;
} }
// update native contacts and report whether this event should be hidden from the game // update native contacts and report whether this event should be hidden from the game
bool filter_native_touch(const nativetouch::NativeTouchEvent &event) { bool filter_native_touch(const nativetouch::NativeTouchEvent &event) {
// synthetic events are outside this hardware-only feature // synthetic events are outside this hardware-only feature
if (!enabled() || event.synthetic) { if (!enabled() || event.synthetic) {
// false leaves the event visible to the game // false leaves the event visible to the game
return false; return false;
} }
// snapshot routing before an up event can commit a pending mode switch // snapshot routing before an up event can commit a pending mode switch
const bool piano_mode_before_update = current_mode() == Mode::Piano; const bool piano_mode_before_update = current_mode() == Mode::Piano;
// update the contact lifetime and commit any pending switch when safe // update the contact lifetime and commit any pending switch when safe
const bool mode_button_contact = update_touch_state(event); const bool mode_button_contact = update_touch_state(event);
// hide every event in a contact that began on the mode switch button // hide every event in a contact that began on the mode switch button
if (mode_button_contact) { if (mode_button_contact) {
return true; return true;
} }
// piano mode consumes hardware events; nav mode forwards them to the game // piano mode consumes hardware events; nav mode forwards them to the game
return piano_mode_before_update; return piano_mode_before_update;
} }
} }
+27 -27
View File
@@ -1,27 +1,27 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <windows.h> #include <windows.h>
#include "touch/native/nativetouchhook.h" #include "touch/native/nativetouchhook.h"
namespace games::nost::touch_mode { namespace games::nost::touch_mode {
// nav mode forwards contacts to the game; piano mode converts them into piano keys // nav mode forwards contacts to the game; piano mode converts them into piano keys
enum class Mode { enum class Mode {
Nav, Nav,
Piano, Piano,
}; };
void enable(); void enable();
void disable(); void disable();
bool enabled(); bool enabled();
Mode current_mode(); Mode current_mode();
void publish_button_bounds(HWND window, const RECT &client_bounds); void publish_button_bounds(HWND window, const RECT &client_bounds);
uint32_t piano_key_state(); uint32_t piano_key_state();
bool filter_native_touch(const nativetouch::NativeTouchEvent &event); bool filter_native_touch(const nativetouch::NativeTouchEvent &event);
} }
+143 -143
View File
@@ -1,143 +1,143 @@
#include "touch_debug.h" #include "touch_debug.h"
#include <array> #include <array>
#include <atomic> #include <atomic>
#include <cstring> #include <cstring>
#include <mutex> #include <mutex>
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#include "games/rb/rb.h" #include "games/rb/rb.h"
#include "games/rb/touch_defs.h" #include "games/rb/touch_defs.h"
namespace games::rb { namespace games::rb {
struct TouchDebugState { struct TouchDebugState {
std::array<unsigned char, TOUCH_PACKET_SIZE> packet {}; std::array<unsigned char, TOUCH_PACKET_SIZE> packet {};
bool is_landscape = false; bool is_landscape = false;
}; };
std::atomic_bool TOUCH_DEBUG_OVERLAY = false; std::atomic_bool TOUCH_DEBUG_OVERLAY = false;
static std::atomic_bool TOUCH_ACTIVE = false; static std::atomic_bool TOUCH_ACTIVE = false;
static std::mutex TOUCH_DEBUG_STATE_M; static std::mutex TOUCH_DEBUG_STATE_M;
static TouchDebugState TOUCH_DEBUG_STATE; static TouchDebugState TOUCH_DEBUG_STATE;
static float touch_scale_factor() { static float touch_scale_factor() {
return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT; return TOUCH_SCALING / (float) TOUCH_SCALE_DEFAULT;
} }
static void clear_touch_debug_state() { static void clear_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M); std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
TOUCH_DEBUG_STATE = {}; TOUCH_DEBUG_STATE = {};
} }
static TouchDebugState get_touch_debug_state() { static TouchDebugState get_touch_debug_state() {
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M); std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
return TOUCH_DEBUG_STATE; return TOUCH_DEBUG_STATE;
} }
static bool packet_bit_active( static bool packet_bit_active(
const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) { const std::array<unsigned char, TOUCH_PACKET_SIZE> &packet, int bit) {
return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0; return (packet[TOUCH_PACKET_DATA_OFFSET + bit / 8] & (1u << (bit % 8))) != 0;
} }
static int sensor_center(int sensor, int sensor_count, int extent) { static int sensor_center(int sensor, int sensor_count, int extent) {
return ((sensor * 2 + 1) * extent) / (sensor_count * 2); return ((sensor * 2 + 1) * extent) / (sensor_count * 2);
} }
static float sensor_span_position(int sensor, int sensor_count, int extent) { static float sensor_span_position(int sensor, int sensor_count, int extent) {
return sensor * (extent - 1) / (float) (sensor_count - 1); return sensor * (extent - 1) / (float) (sensor_count - 1);
} }
bool touch_debug_overlay_enabled() { bool touch_debug_overlay_enabled() {
return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire); return TOUCH_DEBUG_OVERLAY && TOUCH_ACTIVE.load(std::memory_order_acquire);
} }
void touch_draw_debug_overlay() { void touch_draw_debug_overlay() {
if (!touch_debug_overlay_enabled()) { if (!touch_debug_overlay_enabled()) {
return; return;
} }
const auto &io = ImGui::GetIO(); const auto &io = ImGui::GetIO();
int width = static_cast<int>(io.DisplaySize.x); int width = static_cast<int>(io.DisplaySize.x);
int height = static_cast<int>(io.DisplaySize.y); int height = static_cast<int>(io.DisplaySize.y);
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
return; return;
} }
const float scale_factor = touch_scale_factor(); const float scale_factor = touch_scale_factor();
const float left = width * (1.f - scale_factor) / 2.f; const float left = width * (1.f - scale_factor) / 2.f;
const float top = height * (1.f - scale_factor) / 2.f; const float top = height * (1.f - scale_factor) / 2.f;
const float right = width - left; const float right = width - left;
const float bottom = height - top; const float bottom = height - top;
TouchDebugState state = get_touch_debug_state(); TouchDebugState state = get_touch_debug_state();
ImDrawList *draw_list = ImGui::GetBackgroundDrawList(); ImDrawList *draw_list = ImGui::GetBackgroundDrawList();
auto draw_line = [&](float x1, float y1, float x2, float y2) { auto draw_line = [&](float x1, float y1, float x2, float y2) {
draw_list->AddLine( draw_list->AddLine(
ImVec2(x1, y1), ImVec2(x2, y2), ImVec2(x1, y1), ImVec2(x2, y2),
IM_COL32(0, 255, 64, 255), 2.f); IM_COL32(0, 255, 64, 255), 2.f);
}; };
// show the valid input area when touch scaling restricts it // show the valid input area when touch scaling restricts it
if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) { if (TOUCH_SCALING != TOUCH_SCALE_DEFAULT) {
draw_list->AddRect( draw_list->AddRect(
ImVec2(left, top), ImVec2(right, bottom), ImVec2(left, top), ImVec2(right, bottom),
IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f); IM_COL32(255, 255, 255, 255), 0.f, 0, 2.f);
} }
// spread the usable X sensors 2..45 from edge to edge // spread the usable X sensors 2..45 from edge to edge
for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) { for (int sensor = X_SENSOR_FIRST_ACTIVE; sensor <= X_SENSOR_LAST_ACTIVE; sensor++) {
if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) { if (!packet_bit_active(state.packet, X_SENSOR_FIRST_BIT + sensor)) {
continue; continue;
} }
float position = sensor_span_position( float position = sensor_span_position(
sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT, sensor - X_SENSOR_FIRST_ACTIVE, X_SENSOR_ACTIVE_COUNT,
state.is_landscape ? height : width); state.is_landscape ? height : width);
if (state.is_landscape) { if (state.is_landscape) {
float y = top + position * scale_factor; float y = top + position * scale_factor;
draw_line(left, y, right, y); draw_line(left, y, right, y);
} else { } else {
float x = left + position * scale_factor; float x = left + position * scale_factor;
draw_line(x, top, x, bottom); draw_line(x, top, x, bottom);
} }
} }
for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) { for (int sensor = 0; sensor < Y_SENSOR_COUNT; sensor++) {
if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) { if (!packet_bit_active(state.packet, Y_SENSOR_FIRST_BIT - sensor)) {
continue; continue;
} }
int position = sensor_center( int position = sensor_center(
sensor, Y_SENSOR_COUNT, sensor, Y_SENSOR_COUNT,
state.is_landscape ? width : height); state.is_landscape ? width : height);
if (state.is_landscape) { if (state.is_landscape) {
float x = right - position * scale_factor; float x = right - position * scale_factor;
draw_line(x, top, x, bottom); draw_line(x, top, x, bottom);
} else { } else {
float y = top + position * scale_factor; float y = top + position * scale_factor;
draw_line(left, y, right, y); draw_line(left, y, right, y);
} }
} }
} }
void touch_debug_attach() { void touch_debug_attach() {
clear_touch_debug_state(); clear_touch_debug_state();
TOUCH_ACTIVE.store(true, std::memory_order_release); TOUCH_ACTIVE.store(true, std::memory_order_release);
} }
void touch_debug_detach() { void touch_debug_detach() {
TOUCH_ACTIVE.store(false, std::memory_order_release); TOUCH_ACTIVE.store(false, std::memory_order_release);
clear_touch_debug_state(); clear_touch_debug_state();
} }
void touch_debug_publish(const unsigned char *data, bool is_landscape) { void touch_debug_publish(const unsigned char *data, bool is_landscape) {
if (!TOUCH_DEBUG_OVERLAY) { if (!TOUCH_DEBUG_OVERLAY) {
return; return;
} }
std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M); std::lock_guard<std::mutex> lock(TOUCH_DEBUG_STATE_M);
memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE); memcpy(TOUCH_DEBUG_STATE.packet.data(), data, TOUCH_PACKET_SIZE);
TOUCH_DEBUG_STATE.is_landscape = is_landscape; TOUCH_DEBUG_STATE.is_landscape = is_landscape;
} }
} }
+14 -14
View File
@@ -1,14 +1,14 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
namespace games::rb { namespace games::rb {
extern std::atomic_bool TOUCH_DEBUG_OVERLAY; extern std::atomic_bool TOUCH_DEBUG_OVERLAY;
bool touch_debug_overlay_enabled(); bool touch_debug_overlay_enabled();
void touch_draw_debug_overlay(); void touch_draw_debug_overlay();
void touch_debug_attach(); void touch_debug_attach();
void touch_debug_detach(); void touch_debug_detach();
void touch_debug_publish(const unsigned char *data, bool is_landscape); void touch_debug_publish(const unsigned char *data, bool is_landscape);
} }
+17 -17
View File
@@ -1,17 +1,17 @@
#pragma once #pragma once
namespace games::rb { namespace games::rb {
inline constexpr int TOUCH_SCALE_DEFAULT = 1000; inline constexpr int TOUCH_SCALE_DEFAULT = 1000;
inline constexpr int TOUCH_PACKET_SIZE = 20; inline constexpr int TOUCH_PACKET_SIZE = 20;
inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3; inline constexpr int TOUCH_PACKET_DATA_OFFSET = 3;
inline constexpr int X_SENSOR_COUNT = 48; inline constexpr int X_SENSOR_COUNT = 48;
inline constexpr int X_SENSOR_FIRST_ACTIVE = 2; inline constexpr int X_SENSOR_FIRST_ACTIVE = 2;
inline constexpr int X_SENSOR_LAST_ACTIVE = 45; inline constexpr int X_SENSOR_LAST_ACTIVE = 45;
inline constexpr int X_SENSOR_ACTIVE_COUNT = inline constexpr int X_SENSOR_ACTIVE_COUNT =
X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1; X_SENSOR_LAST_ACTIVE - X_SENSOR_FIRST_ACTIVE + 1;
inline constexpr int X_SENSOR_FIRST_BIT = 88; inline constexpr int X_SENSOR_FIRST_BIT = 88;
inline constexpr int Y_SENSOR_COUNT = 76; inline constexpr int Y_SENSOR_COUNT = 76;
inline constexpr int Y_SENSOR_FIRST_BIT = 75; inline constexpr int Y_SENSOR_FIRST_BIT = 75;
} }
+13 -26
View File
@@ -3,6 +3,7 @@
#if SPICE64 #if SPICE64
#include <cstdint> #include <cstdint>
#include "api/client.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -349,43 +350,29 @@ namespace games::sdvx {
* 9 - v unit - 258 bytes - 86 colors * 9 - v unit - 258 bytes - 86 colors
* *
* data is stored in RGB order, 3 bytes per color * data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/ */
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
};
// check index bounds // check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) { if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
auto &map = mapping[index]; auto &map = TAPELED_MAPPING[index];
const auto data_size = map.data.size();
// pick a color to use // pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size); const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
// program the lights into API // program the lights into API
auto &lights = get_lights(); auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
} }
if (This != custom_node) { if (This != custom_node) {
+13
View File
@@ -61,6 +61,19 @@ namespace games::sdvx {
static HKEY real_asio_reg_handle = nullptr; static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr; static HKEY real_asio_device_reg_handle = nullptr;
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
};
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr && if (lpSubKey != nullptr &&
phkResult != nullptr && phkResult != nullptr &&
+4
View File
@@ -6,6 +6,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/tapeled.h"
namespace games::sdvx { namespace games::sdvx {
@@ -25,6 +26,9 @@ namespace games::sdvx {
// states // states
extern bool SHOW_VM_MONITOR_WARNING; extern bool SHOW_VM_MONITOR_WARNING;
constexpr int SDVX_TAPELED_TOTAL = 10;
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
static inline bool is_valkyrie_model() { static inline bool is_valkyrie_model() {
return ( return (
avs::game::is_model("KFC") && avs::game::is_model("KFC") &&
+72 -72
View File
@@ -1,72 +1,72 @@
#include "sdvx_live2d.h" #include "sdvx_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the whole feature is // only the Live2D-capable SDVX versions are 64-bit, so the whole feature is
// compiled out of 32-bit builds. // compiled out of 32-bit builds.
#ifdef SPICE64 #ifdef SPICE64
#include <string> #include <string>
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/logger.h" #include "launcher/logger.h"
#include "util/logging.h" #include "util/logging.h"
namespace games::sdvx { namespace games::sdvx {
// Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option). // Live2D in-game scene detection (for the -sdvxnolive2d "ingame" option).
// //
// the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in // the game logs scene transitions as "I:Attach: in <SCENE>" / "I:Detach: in
// <SCENE>". several scenes correspond to in-song gameplay (with the heavy // <SCENE>". several scenes correspond to in-song gameplay (with the heavy
// Live2D rendering); we watch those log lines and keep the shared flag // Live2D rendering); we watch those log lines and keep the shared flag
// the d3d9 backend reads up to date. the hook never alters the log output // the d3d9 backend reads up to date. the hook never alters the log output
// (always returns false). // (always returns false).
static bool live2d_scene_log_hook( static bool live2d_scene_log_hook(
void *user, const std::string &data, logger::Style style, std::string &out) { void *user, const std::string &data, logger::Style style, std::string &out) {
// any of these scenes counts as in-song gameplay (different play modes) // any of these scenes counts as in-song gameplay (different play modes)
static const char *const gameplay_scenes[] = { static const char *const gameplay_scenes[] = {
"in ALTERNATIVE_GAME_SCENE", "in ALTERNATIVE_GAME_SCENE",
"in MEGAMIX_GAME_SCENE", "in MEGAMIX_GAME_SCENE",
"in MEGAMIX_BATTLE", "in MEGAMIX_BATTLE",
"in BATTLE_GAME_SCENE", "in BATTLE_GAME_SCENE",
"in AUTOMATION_GAME_SCENE", "in AUTOMATION_GAME_SCENE",
"in ARENA_GAME_SCENE", "in ARENA_GAME_SCENE",
}; };
bool in_gameplay_scene = false; bool in_gameplay_scene = false;
for (const auto *scene : gameplay_scenes) { for (const auto *scene : gameplay_scenes) {
if (data.find(scene) != std::string::npos) { if (data.find(scene) != std::string::npos) {
in_gameplay_scene = true; in_gameplay_scene = true;
break; break;
} }
} }
if (!in_gameplay_scene) { if (!in_gameplay_scene) {
return false; return false;
} }
// note: log messages here must NOT contain any matched scene token, else // note: log messages here must NOT contain any matched scene token, else
// this hook would re-enter itself when the message is pushed. // this hook would re-enter itself when the message is pushed.
if (data.find("I:Attach: in ") != std::string::npos) { if (data.find("I:Attach: in ") != std::string::npos) {
if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) { if (!GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(true, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: entering gameplay"); log_info("sdvx", "Live2D skip: entering gameplay");
} }
} else if (data.find("I:Detach: in ") != std::string::npos) { } else if (data.find("I:Detach: in ") != std::string::npos) {
if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) { if (GRAPHICS_SDVX_LIVE2D_IN_GAMEPLAY.exchange(false, std::memory_order_relaxed)) {
log_info("sdvx", "Live2D skip: leaving gameplay"); log_info("sdvx", "Live2D skip: leaving gameplay");
} }
} }
return false; return false;
} }
void live2d_scene_detection_init() { void live2d_scene_detection_init() {
static bool installed = false; static bool installed = false;
if (installed) { if (installed) {
return; return;
} }
installed = true; installed = true;
// the logger's hook list is a persistent static, so registering here is // the logger's hook list is a persistent static, so registering here is
// safe even though this runs before logger::start(). we intentionally do // safe even though this runs before logger::start(). we intentionally do
// NOT log a confirmation now: at this point the log file isn't open yet // NOT log a confirmation now: at this point the log file isn't open yet
// and the message would be dropped. the entering/leaving-gameplay lines // and the message would be dropped. the entering/leaving-gameplay lines
// above provide runtime confirmation once the logger is running. // above provide runtime confirmation once the logger is running.
logger::hook_add(live2d_scene_log_hook, nullptr); logger::hook_add(live2d_scene_log_hook, nullptr);
} }
} }
#endif // SPICE64 #endif // SPICE64
+14 -14
View File
@@ -1,14 +1,14 @@
#pragma once #pragma once
namespace games::sdvx { namespace games::sdvx {
#ifdef SPICE64 #ifdef SPICE64
// installs the Live2D in-game scene-detection log hook used by the // installs the Live2D in-game scene-detection log hook used by the
// -sdvxnolive2d "ingame" option. does not require the SDVX game module to // -sdvxnolive2d "ingame" option. does not require the SDVX game module to
// be attached, so it can be enabled purely from the launcher option. // be attached, so it can be enabled purely from the launcher option.
// only the Live2D-capable SDVX versions are 64-bit, so this is compiled out // only the Live2D-capable SDVX versions are 64-bit, so this is compiled out
// of 32-bit builds. // of 32-bit builds.
void live2d_scene_detection_init(); void live2d_scene_detection_init();
#endif #endif
} }
+84 -84
View File
@@ -1,84 +1,84 @@
#include "asio_driver_scan.h" #include "asio_driver_scan.h"
#include <algorithm> #include <algorithm>
#include <windows.h> #include <windows.h>
#include "util/utils.h" #include "util/utils.h"
namespace hooks::audio { namespace hooks::audio {
static constexpr char ASIO_REG_PATH[] = "software\\asio"; static constexpr char ASIO_REG_PATH[] = "software\\asio";
static constexpr char ASIO_REG_DESC[] = "description"; static constexpr char ASIO_REG_DESC[] = "description";
// enumerate a single registry view, appending to entries while merging // enumerate a single registry view, appending to entries while merging
// duplicates discovered in another view. Drivers are matched by name (not // duplicates discovered in another view. Drivers are matched by name (not
// CLSID): the game's ASIO loader selects drivers by name, and some vendors // CLSID): the game's ASIO loader selects drivers by name, and some vendors
// register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR // register the same CLSID under different 32-bit/64-bit names (e.g. "XONAR
// SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices. // SOUND CARD" vs "XONAR SOUND CARD(64)"), which are distinct user choices.
static void scan_view( static void scan_view(
REGSAM wow64_flag, REGSAM wow64_flag,
bool is_64bit, bool is_64bit,
std::vector<AsioDriverScanEntry> &entries) { std::vector<AsioDriverScanEntry> &entries) {
HKEY hkEnum = nullptr; HKEY hkEnum = nullptr;
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0, if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, ASIO_REG_PATH, 0,
KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) { KEY_READ | wow64_flag, &hkEnum) != ERROR_SUCCESS) {
return; return;
} }
char key_name[256]; char key_name[256];
for (DWORD index = 0; for (DWORD index = 0;
RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS; RegEnumKeyA(hkEnum, index, key_name, sizeof(key_name)) == ERROR_SUCCESS;
index++) { index++) {
// read description (display name), fall back to the key name. // read description (display name), fall back to the key name.
// RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA // RegOpenKeyExA + RegQueryValueExA is used instead of RegGetValueA
// because the latter is unavailable on Windows XP. // because the latter is unavailable on Windows XP.
char desc[256] = { 0 }; char desc[256] = { 0 };
DWORD size = sizeof(desc); DWORD size = sizeof(desc);
std::string name = key_name; std::string name = key_name;
HKEY hkDriver = nullptr; HKEY hkDriver = nullptr;
if (RegOpenKeyExA(hkEnum, key_name, 0, if (RegOpenKeyExA(hkEnum, key_name, 0,
KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) { KEY_QUERY_VALUE | wow64_flag, &hkDriver) == ERROR_SUCCESS) {
DWORD type = 0; DWORD type = 0;
if (RegQueryValueExA(hkDriver, if (RegQueryValueExA(hkDriver,
ASIO_REG_DESC, ASIO_REG_DESC,
nullptr, nullptr,
&type, &type,
reinterpret_cast<LPBYTE>(desc), reinterpret_cast<LPBYTE>(desc),
&size) == ERROR_SUCCESS &size) == ERROR_SUCCESS
&& type == REG_SZ && desc[0]) { && type == REG_SZ && desc[0]) {
// ensure null termination // ensure null termination
desc[sizeof(desc) - 1] = '\0'; desc[sizeof(desc) - 1] = '\0';
name = desc; name = desc;
} }
RegCloseKey(hkDriver); RegCloseKey(hkDriver);
} }
// merge with an existing entry from the other view (match by name) // merge with an existing entry from the other view (match by name)
const std::string name_lower = strtolower(name); const std::string name_lower = strtolower(name);
auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) { auto it = std::find_if(entries.begin(), entries.end(), [&](const auto &e) {
return strtolower(e.name) == name_lower; return strtolower(e.name) == name_lower;
}); });
if (it == entries.end()) { if (it == entries.end()) {
entries.push_back({ name }); entries.push_back({ name });
it = entries.end() - 1; it = entries.end() - 1;
} }
it->found_32bit |= !is_64bit; it->found_32bit |= !is_64bit;
it->found_64bit |= is_64bit; it->found_64bit |= is_64bit;
} }
RegCloseKey(hkEnum); RegCloseKey(hkEnum);
} }
std::vector<AsioDriverScanEntry> scan_asio_drivers() { std::vector<AsioDriverScanEntry> scan_asio_drivers() {
std::vector<AsioDriverScanEntry> entries; std::vector<AsioDriverScanEntry> entries;
// 64-bit view first so it wins ordering when present in both // 64-bit view first so it wins ordering when present in both
scan_view(KEY_WOW64_64KEY, true, entries); scan_view(KEY_WOW64_64KEY, true, entries);
scan_view(KEY_WOW64_32KEY, false, entries); scan_view(KEY_WOW64_32KEY, false, entries);
return entries; return entries;
} }
} }
+15 -15
View File
@@ -1,15 +1,15 @@
#pragma once #pragma once
#include <string> #include <string>
#include <vector> #include <vector>
namespace hooks::audio { namespace hooks::audio {
struct AsioDriverScanEntry { struct AsioDriverScanEntry {
std::string name; std::string name;
bool found_32bit = false; bool found_32bit = false;
bool found_64bit = false; bool found_64bit = false;
}; };
std::vector<AsioDriverScanEntry> scan_asio_drivers(); std::vector<AsioDriverScanEntry> scan_asio_drivers();
} }
File diff suppressed because it is too large Load Diff
+240 -240
View File
@@ -1,240 +1,240 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <memory> #include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include "external/asio/asio.h" #include "external/asio/asio.h"
#include "external/asio/iasiodrv.h" #include "external/asio/iasiodrv.h"
namespace hooks::audio::asio { namespace hooks::audio::asio {
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver. // returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate // ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
// it against the system's registered ASIO drivers to avoid false positives // it against the system's registered ASIO drivers to avoid false positives
bool is_asio_creation(REFCLSID rclsid, REFIID riid); bool is_asio_creation(REFCLSID rclsid, REFIID riid);
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and // wrap a real ASIO driver instance, taking ownership of the supplied reference, and
// return a proxy that forwards every call to it. also records it as the cached // return a proxy that forwards every call to it. also records it as the cached
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing) // instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
IUnknown *wrap(REFCLSID clsid, void *real); IUnknown *wrap(REFCLSID clsid, void *real);
// if a cached wrapper already exists for this CLSID, return it (with an added // if a cached wrapper already exists for this CLSID, return it (with an added
// reference); otherwise nullptr to signal the caller to create the real driver and // reference); otherwise nullptr to signal the caller to create the real driver and
// wrap it. lets the host reuse one driver instance instead of re-instantiating it // wrap it. lets the host reuse one driver instance instead of re-instantiating it
IUnknown *wrap_existing(REFCLSID clsid); IUnknown *wrap_existing(REFCLSID clsid);
// drop the process-lifetime references taken by wrap() so cached drivers can be released // drop the process-lifetime references taken by wrap() so cached drivers can be released
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host // at shutdown. only relinquishes our pin, so a real driver is torn down once the host
// has released its own references too. call from a controlled shutdown point, never from // has released its own references too. call from a controlled shutdown point, never from
// a static destructor (the driver DLL may already be unloaded) // a static destructor (the driver DLL may already be unloaded)
void release_all_wrappers(); void release_all_wrappers();
} }
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic // transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
struct WrappedAsio final : IAsio { struct WrappedAsio final : IAsio {
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name) WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
: pReal(real), clsid(clsid), driver_name(std::move(name)) { : pReal(real), clsid(clsid), driver_name(std::move(name)) {
} }
WrappedAsio(const WrappedAsio &) = delete; WrappedAsio(const WrappedAsio &) = delete;
WrappedAsio &operator=(const WrappedAsio &) = delete; WrappedAsio &operator=(const WrappedAsio &) = delete;
virtual ~WrappedAsio(); virtual ~WrappedAsio();
// selects which source channel pair of a multichannel ASIO output reaches the device's // selects which source channel pair of a multichannel ASIO output reaches the device's
// 2.0 front pair. when not None, the proxy presents the game's expected multichannel // 2.0 front pair. when not None, the proxy presents the game's expected multichannel
// layout to the host so it proceeds to create_buffers, then opens only a two-channel // layout to the host so it proceeds to create_buffers, then opens only a two-channel
// stream on the real device and routes the selected pair onto it (see create_buffers). // stream on the real device and routes the selected pair onto it (see create_buffers).
// Front is the plain "force two channel" case (forward the device's own front pair); // Front is the plain "force two channel" case (forward the device's own front pair);
// the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed). // the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed).
// set once at boot, before any wrapper exists, so it needs no synchronization // set once at boot, before any wrapper exists, so it needs no synchronization
enum class StereoDownmix { enum class StereoDownmix {
None, // feature disabled - full multichannel passthrough None, // feature disabled - full multichannel passthrough
Front, // channels 0/1 - the device front pair is forwarded as-is (no copy) Front, // channels 0/1 - the device front pair is forwarded as-is (no copy)
Center, // channel 2 duplicated to both 0 and 1 Center, // channel 2 duplicated to both 0 and 1
Rear, // channels 4/5 -> 0/1 Rear, // channels 4/5 -> 0/1
Side, // channels 6/7 -> 0/1 Side, // channels 6/7 -> 0/1
}; };
static StereoDownmix STEREO_DOWNMIX; static StereoDownmix STEREO_DOWNMIX;
// true when a stereo extraction is configured, i.e. the real device should open a 2.0 // true when a stereo extraction is configured, i.e. the real device should open a 2.0
// stream and only the selected pair should reach it. the former standalone // stream and only the selected pair should reach it. the former standalone
// FORCE_TWO_CHANNELS flag is now just the Front case of this // FORCE_TWO_CHANNELS flag is now just the Front case of this
static bool force_two_channels() { static bool force_two_channels() {
return STEREO_DOWNMIX != StereoDownmix::None; return STEREO_DOWNMIX != StereoDownmix::None;
} }
// some games hardcode a multichannel ASIO output and bail before create_buffers if // some games hardcode a multichannel ASIO output and bail before create_buffers if
// get_channels reports fewer, so we report at least this many output channels when a // get_channels reports fewer, so we report at least this many output channels when a
// stereo extraction is active // stereo extraction is active
static constexpr long FORCED_OUTPUT_CHANNELS = 8; static constexpr long FORCED_OUTPUT_CHANNELS = 8;
// maps an option string ("front", "center", "rear", "side") to a StereoDownmix value, // maps an option string ("front", "center", "rear", "side") to a StereoDownmix value,
// returning None for anything unrecognized // returning None for anything unrecognized
static StereoDownmix name_to_stereo_downmix(const char *name); static StereoDownmix name_to_stereo_downmix(const char *name);
#pragma region IUnknown #pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override; ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion #pragma endregion
#pragma region IAsio #pragma region IAsio
AsioBool __thiscall init(void *sys_handle) override; AsioBool __thiscall init(void *sys_handle) override;
void __thiscall get_driver_name(char *name) override; void __thiscall get_driver_name(char *name) override;
long __thiscall get_driver_version() override; long __thiscall get_driver_version() override;
void __thiscall get_error_message(char *string) override; void __thiscall get_error_message(char *string) override;
AsioError __thiscall start() override; AsioError __thiscall start() override;
AsioError __thiscall stop() override; AsioError __thiscall stop() override;
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override; AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override; AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
AsioError __thiscall get_buffer_size( AsioError __thiscall get_buffer_size(
long *min_size, long *min_size,
long *max_size, long *max_size,
long *preferred_size, long *preferred_size,
long *granularity) override; long *granularity) override;
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override; AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override; AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override; AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override; AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
AsioError __thiscall set_clock_source(long reference) override; AsioError __thiscall set_clock_source(long reference) override;
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override; AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override; AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
AsioError __thiscall create_buffers( AsioError __thiscall create_buffers(
AsioBufferInfo *buffer_infos, AsioBufferInfo *buffer_infos,
long num_channels, long num_channels,
long buffer_size, long buffer_size,
AsioCallbacks *callbacks) override; AsioCallbacks *callbacks) override;
AsioError __thiscall dispose_buffers() override; AsioError __thiscall dispose_buffers() override;
AsioError __thiscall control_panel() override; AsioError __thiscall control_panel() override;
AsioError __thiscall future(long selector, void *opt) override; AsioError __thiscall future(long selector, void *opt) override;
AsioError __thiscall output_ready() override; AsioError __thiscall output_ready() override;
#pragma endregion #pragma endregion
// quiesces any leftover stream/buffer state before the cached wrapper is handed back // quiesces any leftover stream/buffer state before the cached wrapper is handed back
// for reuse, without destroying the real driver (see wrap_existing) // for reuse, without destroying the real driver (see wrap_existing)
void quiesce_for_reuse(); void quiesce_for_reuse();
private: private:
// create_buffers implementation used when a stereo extraction is active: forwards only // create_buffers implementation used when a stereo extraction is active: forwards only
// the channels the real device has and hands the game throwaway buffers for the rest // the channels the real device has and hands the game throwaway buffers for the rest
AsioError create_buffers_front_pair( AsioError create_buffers_front_pair(
AsioBufferInfo *buffer_infos, AsioBufferInfo *buffer_infos,
long num_channels, long num_channels,
long buffer_size, long buffer_size,
AsioCallbacks *callbacks); AsioCallbacks *callbacks);
// if any post-processing effect (volume boost or stereo downmix) is active, saves the // if any post-processing effect (volume boost or stereo downmix) is active, saves the
// game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to // game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to
// hand the real driver instead, so we can rework its output buffers after the game // hand the real driver instead, so we can rework its output buffers after the game
// fills them. otherwise returns the game's callbacks unchanged. called at create_buffers // fills them. otherwise returns the game's callbacks unchanged. called at create_buffers
// time, before the stream starts // time, before the stream starts
AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks); AsioCallbacks *install_proxy_callbacks(AsioCallbacks *game_callbacks);
// records a device output channel whose buffers we scale by the volume boost. queries // records a device output channel whose buffers we scale by the volume boost. queries
// the real driver for the channel's sample format. called at create_buffers time // the real driver for the channel's sample format. called at create_buffers time
void record_volume_output_channel(const AsioBufferInfo &info); void record_volume_output_channel(const AsioBufferInfo &info);
// the real device's output sample format, queried from its first output channel. all // the real device's output sample format, queried from its first output channel. all
// output channels of a device share one format, so this characterizes them all. returns // output channels of a device share one format, so this characterizes them all. returns
// ASIOSTLastEntry if the device has no output channels or the query fails // ASIOSTLastEntry if the device has no output channels or the query fails
AsioSampleType device_output_sample_type(); AsioSampleType device_output_sample_type();
// locates the destination pair (device channels 0/1) and the configured source channels // locates the destination pair (device channels 0/1) and the configured source channels
// in the game's buffer set so the realtime path can copy the selected pair onto 0/1. // in the game's buffer set so the realtime path can copy the selected pair onto 0/1.
// a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time // a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time
void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size); void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size);
// publishes the captured post-process state to the realtime thread once the buffers // publishes the captured post-process state to the realtime thread once the buffers
// exist, making our trampolines start reworking output. called at the end of either // exist, making our trampolines start reworking output. called at the end of either
// create_buffers path // create_buffers path
void publish_post_process(long buffer_size); void publish_post_process(long buffer_size);
// detaches this instance from the realtime trampolines so they stop touching its // detaches this instance from the realtime trampolines so they stop touching its
// buffers. called from dispose_buffers and the destructor // buffers. called from dispose_buffers and the destructor
void detach_post_process(); void detach_post_process();
// multiplies every recorded output channel's buffer for the given double-buffer index // multiplies every recorded output channel's buffer for the given double-buffer index
// by the volume boost. runs on the driver's realtime thread from our buffer switch // by the volume boost. runs on the driver's realtime thread from our buffer switch
void apply_output_volume(long double_buffer_index); void apply_output_volume(long double_buffer_index);
// copies the configured source channel pair onto device channels 0/1 for the given // copies the configured source channel pair onto device channels 0/1 for the given
// double-buffer index. runs on the driver's realtime thread from our buffer switch // double-buffer index. runs on the driver's realtime thread from our buffer switch
void apply_downmix(long double_buffer_index); void apply_downmix(long double_buffer_index);
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real // realtime-thread trampolines for the buffer-switch callbacks, handed to the real
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the // driver in place of the game's; ASIO callbacks carry no user data, so they reach the
// active wrapper through active_instance, call the game's original, then rework output. // active wrapper through active_instance, call the game's original, then rework output.
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the // the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
// game's own pointers, so they need no trampoline // game's own pointers, so they need no trampoline
static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process); static void __cdecl proxy_buffer_switch(long double_buffer_index, AsioBool direct_process);
static AsioTime * __cdecl proxy_buffer_switch_time_info( static AsioTime * __cdecl proxy_buffer_switch_time_info(
AsioTime *params, long double_buffer_index, AsioBool direct_process); AsioTime *params, long double_buffer_index, AsioBool direct_process);
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with // the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
// one running stream); read by the static trampolines to reach the right wrapper // one running stream); read by the static trampolines to reach the right wrapper
static std::atomic<WrappedAsio *> active_instance; static std::atomic<WrappedAsio *> active_instance;
IAsio *const pReal; IAsio *const pReal;
const CLSID clsid; const CLSID clsid;
// registry name of the driver (not get_driver_name), used in our logs as a single // registry name of the driver (not get_driver_name), used in our logs as a single
// unambiguous name; constant for our lifetime // unambiguous name; constant for our lifetime
std::string driver_name; std::string driver_name;
// the real driver is initialized exactly once; repeat init() calls are a no-op success // the real driver is initialized exactly once; repeat init() calls are a no-op success
bool initialized = false; bool initialized = false;
// whether the real driver currently has a buffer set / running stream. used to quiesce // whether the real driver currently has a buffer set / running stream. used to quiesce
// leftover state when the cached wrapper is reused (see quiesce_for_reuse) // leftover state when the cached wrapper is reused (see quiesce_for_reuse)
bool buffers_created = false; bool buffers_created = false;
bool started = false; bool started = false;
// our own reference count; we hold one reference on pReal and release it when this // our own reference count; we hold one reference on pReal and release it when this
// drops to zero // drops to zero
std::atomic<ULONG> ref_count {1}; std::atomic<ULONG> ref_count {1};
// throwaway double buffers handed to the channels we discard when a stereo extraction // throwaway double buffers handed to the channels we discard when a stereo extraction
// is active (see create_buffers). owned for the lifetime of the buffer set and freed // is active (see create_buffers). owned for the lifetime of the buffer set and freed
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us // in dispose_buffers; only read by the game from its own bufferSwitch, never by us
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers; std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
// one device output channel scaled by the volume boost in our buffer switch // one device output channel scaled by the volume boost in our buffer switch
struct VolumeOutputChannel { struct VolumeOutputChannel {
void *buffers[2]; void *buffers[2];
AsioSampleType type; AsioSampleType type;
}; };
// the game's original callbacks (captured when we install our proxy set) and the proxy // the game's original callbacks (captured when we install our proxy set) and the proxy
// set we hand the real driver; the realtime trampolines reach the game's buffer_switch // set we hand the real driver; the realtime trampolines reach the game's buffer_switch
// through game_callbacks regardless of which effect is active // through game_callbacks regardless of which effect is active
AsioCallbacks game_callbacks {}; AsioCallbacks game_callbacks {};
AsioCallbacks proxy_callbacks {}; AsioCallbacks proxy_callbacks {};
// volume boost state, captured at create_buffers time and published to the realtime // volume boost state, captured at create_buffers time and published to the realtime
// thread via active_instance once fully built; untouched while the stream runs. // thread via active_instance once fully built; untouched while the stream runs.
// volume_active gates whether the realtime path scales any buffers // volume_active gates whether the realtime path scales any buffers
bool volume_active = false; bool volume_active = false;
float volume_gain = 1.0f; float volume_gain = 1.0f;
long volume_buffer_size = 0; long volume_buffer_size = 0;
std::vector<VolumeOutputChannel> volume_channels; std::vector<VolumeOutputChannel> volume_channels;
// one device channel (0 or 1) fed by a source channel during stereo downmix; both // one device channel (0 or 1) fed by a source channel during stereo downmix; both
// buffer pointers are indexed by the ASIO double-buffer index, the same as the channels // buffer pointers are indexed by the ASIO double-buffer index, the same as the channels
struct DownmixCopy { struct DownmixCopy {
void *dst[2]; void *dst[2];
void *src[2]; void *src[2];
}; };
// stereo downmix state, captured at create_buffers time and published alongside the // stereo downmix state, captured at create_buffers time and published alongside the
// volume state; untouched while the stream runs. downmix_active gates whether the // volume state; untouched while the stream runs. downmix_active gates whether the
// realtime path copies the selected source pair onto device channels 0/1. copies[0] // realtime path copies the selected source pair onto device channels 0/1. copies[0]
// feeds device channel 0, copies[1] feeds device channel 1 // feeds device channel 0, copies[1] feeds device channel 1
bool downmix_active = false; bool downmix_active = false;
DownmixCopy downmix_copies[2] {}; DownmixCopy downmix_copies[2] {};
size_t downmix_bytes = 0; size_t downmix_bytes = 0;
}; };
@@ -1,43 +1,43 @@
#pragma once #pragma once
#include <stdint.h> #include <stdint.h>
#include <endpointvolume.h> #include <endpointvolume.h>
struct WrappedIAudioEndpointVolume : IAudioEndpointVolume { struct WrappedIAudioEndpointVolume : IAudioEndpointVolume {
explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {} explicit WrappedIAudioEndpointVolume(IAudioEndpointVolume *orig) : pReal(orig) {}
WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete; WrappedIAudioEndpointVolume(const WrappedIAudioEndpointVolume &) = delete;
WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete; WrappedIAudioEndpointVolume &operator=(const WrappedIAudioEndpointVolume &) = delete;
virtual ~WrappedIAudioEndpointVolume() = default; virtual ~WrappedIAudioEndpointVolume() = default;
#pragma region IUnknown #pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override; ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion #pragma endregion
#pragma region IAudioEndpointVolume #pragma region IAudioEndpointVolume
HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; HRESULT STDMETHODCALLTYPE RegisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override; HRESULT STDMETHODCALLTYPE UnregisterControlChangeNotify(IAudioEndpointVolumeCallback *pNotify) override;
HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override; HRESULT STDMETHODCALLTYPE GetChannelCount(uint32_t *pnChannelCount) override;
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE SetMasterVolumeLevel(float fLevelDB, LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE SetMasterVolumeLevelScalar(float fLevel, LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override; HRESULT STDMETHODCALLTYPE GetMasterVolumeLevel(float *fLevelDB) override;
HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override; HRESULT STDMETHODCALLTYPE GetMasterVolumeLevelScalar(float *fLevel) override;
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE SetChannelVolumeLevel(uint32_t nChannel, float fLevelDB, LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE SetChannelVolumeLevelScalar(uint32_t nChannel, float fLevel, LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override; HRESULT STDMETHODCALLTYPE GetChannelVolumeLevel(uint32_t nChannel, float *fLevelDB) override;
HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override; HRESULT STDMETHODCALLTYPE GetChannelVolumeLevelScalar(uint32_t nChannel, float *fLevel) override;
HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE SetMute(WINBOOL bMute, LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override; HRESULT STDMETHODCALLTYPE GetMute(WINBOOL *bMute) override;
HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override; HRESULT STDMETHODCALLTYPE GetVolumeStepInfo(uint32_t *pnStep, uint32_t *pnStepCount) override;
HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE VolumeStepUp(LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override; HRESULT STDMETHODCALLTYPE VolumeStepDown(LPCGUID pguidEventContext) override;
HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override; HRESULT STDMETHODCALLTYPE QueryHardwareSupport(DWORD *pdwHardwareSupportMask) override;
HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override; HRESULT STDMETHODCALLTYPE GetVolumeRange(float *pflVolumeMindB, float *pflVolumeMaxdB, float *pflVolumeIncrementdB) override;
#pragma endregion #pragma endregion
private: private:
IAudioEndpointVolume *const pReal; IAudioEndpointVolume *const pReal;
}; };
@@ -1,185 +1,185 @@
#include "null_device.h" #include "null_device.h"
#include <atomic> #include <atomic>
#include <cstring> #include <cstring>
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "hooks/audio/audio_private.h" #include "hooks/audio/audio_private.h"
#include "hooks/audio/backends/wasapi/dummy_audio_client.h" #include "hooks/audio/backends/wasapi/dummy_audio_client.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
#include "null_discard_backend.h" #include "null_discard_backend.h"
// friendly name reported by the synthetic device. must contain "Realtek" so the // friendly name reported by the synthetic device. must contain "Realtek" so the
// gitadora arena device search matches it. // gitadora arena device search matches it.
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio"; static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
// arbitrary identifier reported by the synthetic device. // arbitrary identifier reported by the synthetic device.
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}"; static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h // PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = { static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } }, { 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
14 14
}; };
bool null_render_device_enabled() { bool null_render_device_enabled() {
return hooks::audio::INJECT_FAKE_REALTEK_AUDIO; return hooks::audio::INJECT_FAKE_REALTEK_AUDIO;
} }
// duplicate a wide string into CoTaskMem so the caller can free it with // duplicate a wide string into CoTaskMem so the caller can free it with
// CoTaskMemFree / PropVariantClear as the COM API contract requires. // CoTaskMemFree / PropVariantClear as the COM API contract requires.
static LPWSTR co_task_wcsdup(const wchar_t *src) { static LPWSTR co_task_wcsdup(const wchar_t *src) {
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t); const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes)); auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
if (dst != nullptr) { if (dst != nullptr) {
memcpy(dst, src, bytes); memcpy(dst, src, bytes);
} }
return dst; return dst;
} }
namespace { namespace {
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName. // minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
struct NullPropertyStore : IPropertyStore { struct NullPropertyStore : IPropertyStore {
std::atomic<ULONG> ref_cnt = 1; std::atomic<ULONG> ref_cnt = 1;
virtual ~NullPropertyStore() = default; virtual ~NullPropertyStore() = default;
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override { HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
if (ppvObj == nullptr) { if (ppvObj == nullptr) {
return E_POINTER; return E_POINTER;
} }
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) { if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
this->AddRef(); this->AddRef();
*ppvObj = this; *ppvObj = this;
return S_OK; return S_OK;
} }
*ppvObj = nullptr; *ppvObj = nullptr;
return E_NOINTERFACE; return E_NOINTERFACE;
} }
ULONG STDMETHODCALLTYPE AddRef() override { ULONG STDMETHODCALLTYPE AddRef() override {
return ++this->ref_cnt; return ++this->ref_cnt;
} }
ULONG STDMETHODCALLTYPE Release() override { ULONG STDMETHODCALLTYPE Release() override {
const ULONG refs = --this->ref_cnt; const ULONG refs = --this->ref_cnt;
if (refs == 0) { if (refs == 0) {
delete this; delete this;
} }
return refs; return refs;
} }
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override { HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
if (cProps == nullptr) { if (cProps == nullptr) {
return E_POINTER; return E_POINTER;
} }
*cProps = 1; *cProps = 1;
return S_OK; return S_OK;
} }
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override { HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
if (pkey == nullptr) { if (pkey == nullptr) {
return E_POINTER; return E_POINTER;
} }
if (iProp != 0) { if (iProp != 0) {
return E_INVALIDARG; return E_INVALIDARG;
} }
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL; *pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
return S_OK; return S_OK;
} }
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override { HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
if (pv == nullptr) { if (pv == nullptr) {
return E_POINTER; return E_POINTER;
} }
PropVariantInit(pv); PropVariantInit(pv);
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) { && key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME); pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
if (pv->pwszVal == nullptr) { if (pv->pwszVal == nullptr) {
return E_OUTOFMEMORY; return E_OUTOFMEMORY;
} }
pv->vt = VT_LPWSTR; pv->vt = VT_LPWSTR;
} }
// unknown keys are returned as VT_EMPTY / S_OK // unknown keys are returned as VT_EMPTY / S_OK
return S_OK; return S_OK;
} }
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override { HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
return STG_E_ACCESSDENIED; return STG_E_ACCESSDENIED;
} }
HRESULT STDMETHODCALLTYPE Commit() override { HRESULT STDMETHODCALLTYPE Commit() override {
return S_OK; return S_OK;
} }
}; };
} }
#pragma region IUnknown #pragma region IUnknown
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) { HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) { if (ppvObj == nullptr) {
return E_POINTER; return E_POINTER;
} }
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) { if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
this->AddRef(); this->AddRef();
*ppvObj = this; *ppvObj = this;
return S_OK; return S_OK;
} }
*ppvObj = nullptr; *ppvObj = nullptr;
return E_NOINTERFACE; return E_NOINTERFACE;
} }
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() { ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
return ++this->ref_cnt; return ++this->ref_cnt;
} }
ULONG STDMETHODCALLTYPE NullMMDevice::Release() { ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
const ULONG refs = --this->ref_cnt; const ULONG refs = --this->ref_cnt;
if (refs == 0) { if (refs == 0) {
delete this; delete this;
} }
return refs; return refs;
} }
#pragma endregion #pragma endregion
#pragma region IMMDevice #pragma region IMMDevice
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate( HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
REFIID iid, REFIID iid,
DWORD, DWORD,
PROPVARIANT *, PROPVARIANT *,
void **ppInterface) void **ppInterface)
{ {
if (ppInterface == nullptr) { if (ppInterface == nullptr) {
return E_POINTER; return E_POINTER;
} }
*ppInterface = nullptr; *ppInterface = nullptr;
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid)); log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
if (iid == IID_IAudioClient) { if (iid == IID_IAudioClient) {
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend())); auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
*ppInterface = client; *ppInterface = client;
return S_OK; return S_OK;
} }
return E_NOINTERFACE; return E_NOINTERFACE;
} }
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) { HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
if (ppProperties == nullptr) { if (ppProperties == nullptr) {
return E_POINTER; return E_POINTER;
} }
*ppProperties = new NullPropertyStore(); *ppProperties = new NullPropertyStore();
return S_OK; return S_OK;
} }
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) { HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
if (ppstrId == nullptr) { if (ppstrId == nullptr) {
return E_POINTER; return E_POINTER;
} }
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID); *ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY; return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
} }
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) { HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
if (pdwState == nullptr) { if (pdwState == nullptr) {
return E_POINTER; return E_POINTER;
} }
*pdwState = DEVICE_STATE_ACTIVE; *pdwState = DEVICE_STATE_ACTIVE;
return S_OK; return S_OK;
} }
#pragma endregion #pragma endregion
@@ -1,39 +1,39 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <mmdeviceapi.h> #include <mmdeviceapi.h>
// returns true when a synthetic render endpoint should be injected into device // returns true when a synthetic render endpoint should be injected into device
// enumeration. games like gitadora arena search the render endpoint list for a // enumeration. games like gitadora arena search the render endpoint list for a
// device whose friendly name contains "Realtek" and crash with a null pointer // device whose friendly name contains "Realtek" and crash with a null pointer
// dereference when no match exists. presenting a fake match that routes to the // dereference when no match exists. presenting a fake match that routes to the
// null audio backend lets the search succeed while discarding the audio. // null audio backend lets the search succeed while discarding the audio.
bool null_render_device_enabled(); bool null_render_device_enabled();
// fake IMMDevice that reports a "Realtek" friendly name and activates straight // fake IMMDevice that reports a "Realtek" friendly name and activates straight
// into the null audio backend, never touching real hardware. // into the null audio backend, never touching real hardware.
struct NullMMDevice : IMMDevice { struct NullMMDevice : IMMDevice {
NullMMDevice() = default; NullMMDevice() = default;
NullMMDevice(const NullMMDevice &) = delete; NullMMDevice(const NullMMDevice &) = delete;
NullMMDevice &operator=(const NullMMDevice &) = delete; NullMMDevice &operator=(const NullMMDevice &) = delete;
virtual ~NullMMDevice() = default; virtual ~NullMMDevice() = default;
#pragma region IUnknown #pragma region IUnknown
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override; HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
ULONG STDMETHODCALLTYPE AddRef() override; ULONG STDMETHODCALLTYPE AddRef() override;
ULONG STDMETHODCALLTYPE Release() override; ULONG STDMETHODCALLTYPE Release() override;
#pragma endregion #pragma endregion
#pragma region IMMDevice #pragma region IMMDevice
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override; HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override; HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override; HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override; HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
#pragma endregion #pragma endregion
private: private:
std::atomic<ULONG> ref_cnt = 1; std::atomic<ULONG> ref_cnt = 1;
}; };
@@ -1,139 +1,139 @@
#include "null_discard_backend.h" #include "null_discard_backend.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <thread> #include <thread>
#include "hooks/audio/util.h" #include "hooks/audio/util.h"
#include "util/logging.h" #include "util/logging.h"
NullDiscardBackend::~NullDiscardBackend() { NullDiscardBackend::~NullDiscardBackend() {
this->running = false; this->running = false;
if (this->pacing_thread.joinable()) { if (this->pacing_thread.joinable()) {
this->pacing_thread.join(); this->pacing_thread.join();
} }
} }
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept { const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
return this->format_; return this->format_;
} }
HRESULT NullDiscardBackend::on_initialize( HRESULT NullDiscardBackend::on_initialize(
AUDCLNT_SHAREMODE *, AUDCLNT_SHAREMODE *,
DWORD *, DWORD *,
REFERENCE_TIME *hnsBufferDuration, REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *, REFERENCE_TIME *,
const WAVEFORMATEX *pFormat, const WAVEFORMATEX *pFormat,
LPCGUID) LPCGUID)
{ {
copy_wave_format(&this->format_, pFormat); copy_wave_format(&this->format_, pFormat);
// honor the game's requested buffer duration, falling back to 10 ms // honor the game's requested buffer duration, falling back to 10 ms
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0) this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
? *hnsBufferDuration ? *hnsBufferDuration
: DEFAULT_REFTIME; : DEFAULT_REFTIME;
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>( this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
static_cast<double>(this->format_.Format.nSamplesPerSec) static_cast<double>(this->format_.Format.nSamplesPerSec)
* this->period_reftime / 10000000.0 + 0.5)); * this->period_reftime / 10000000.0 + 0.5));
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit", log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
this->format_.Format.nChannels, this->format_.Format.nChannels,
this->format_.Format.nSamplesPerSec, this->format_.Format.nSamplesPerSec,
this->format_.Format.wBitsPerSample); this->format_.Format.wBitsPerSample);
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) { HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
*buffer_frames = this->buffer_frames; *buffer_frames = this->buffer_frames;
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) { HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
*latency = this->period_reftime; *latency = this->period_reftime;
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) { HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
// discarded immediately, so the buffer always reads as fully drained // discarded immediately, so the buffer always reads as fully drained
padding_frames = 0; padding_frames = 0;
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_is_format_supported( HRESULT NullDiscardBackend::on_is_format_supported(
AUDCLNT_SHAREMODE *, AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *, const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch) WAVEFORMATEX **ppClosestMatch)
{ {
if (ppClosestMatch) { if (ppClosestMatch) {
*ppClosestMatch = nullptr; *ppClosestMatch = nullptr;
} }
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) { HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
return E_NOTIMPL; return E_NOTIMPL;
} }
HRESULT NullDiscardBackend::on_get_device_period( HRESULT NullDiscardBackend::on_get_device_period(
REFERENCE_TIME *default_device_period, REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period) REFERENCE_TIME *minimum_device_period)
{ {
if (default_device_period) { if (default_device_period) {
*default_device_period = this->period_reftime; *default_device_period = this->period_reftime;
} }
if (minimum_device_period) { if (minimum_device_period) {
*minimum_device_period = this->period_reftime; *minimum_device_period = this->period_reftime;
} }
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_start() { HRESULT NullDiscardBackend::on_start() {
if (!this->running.exchange(true)) { if (!this->running.exchange(true)) {
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this); this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
} }
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_stop() { HRESULT NullDiscardBackend::on_stop() {
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) { HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
// keep the game's event so pace_loop() can wake it; there is no real device behind it // keep the game's event so pace_loop() can wake it; there is no real device behind it
this->relay_handle = *event_handle; this->relay_handle = *event_handle;
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) { HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
const size_t buffer_size = const size_t buffer_size =
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested; static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
if (this->scratch.size() < buffer_size) { if (this->scratch.size() < buffer_size) {
this->scratch.resize(buffer_size); this->scratch.resize(buffer_size);
} }
*ppData = this->scratch.data(); *ppData = this->scratch.data();
return S_OK; return S_OK;
} }
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) { HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
// discard the audio entirely // discard the audio entirely
return S_OK; return S_OK;
} }
void NullDiscardBackend::pace_loop() { void NullDiscardBackend::pace_loop() {
using namespace std::chrono; using namespace std::chrono;
// audio is discarded, so timing precision and drift do not matter; just wake the // audio is discarded, so timing precision and drift do not matter; just wake the
// game once per buffer period to keep its render thread from blocking on the event. // game once per buffer period to keep its render thread from blocking on the event.
const auto period = duration_cast<steady_clock::duration>( const auto period = duration_cast<steady_clock::duration>(
duration<double>(this->period_reftime / 10000000.0)); duration<double>(this->period_reftime / 10000000.0));
while (this->running.load()) { while (this->running.load()) {
if (this->relay_handle) { if (this->relay_handle) {
SetEvent(this->relay_handle); SetEvent(this->relay_handle);
} }
std::this_thread::sleep_for(period); std::this_thread::sleep_for(period);
} }
} }
@@ -1,54 +1,54 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <optional> #include <optional>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/implementations/backend.h" #include "hooks/audio/implementations/backend.h"
// discards all audio while pacing the game's event handle once per buffer period, so the game // discards all audio while pacing the game's event handle once per buffer period, so the game
// keeps running normally with nothing output to any real device. routed through the shared // keeps running normally with nothing output to any real device. routed through the shared
// DummyIAudioClient, the same plumbing the asio backend uses. // DummyIAudioClient, the same plumbing the asio backend uses.
struct NullDiscardBackend final : AudioBackend { struct NullDiscardBackend final : AudioBackend {
~NullDiscardBackend() final; ~NullDiscardBackend() final;
const WAVEFORMATEXTENSIBLE &format() const noexcept override; const WAVEFORMATEXTENSIBLE &format() const noexcept override;
HRESULT on_initialize( HRESULT on_initialize(
AUDCLNT_SHAREMODE *, AUDCLNT_SHAREMODE *,
DWORD *, DWORD *,
REFERENCE_TIME *hnsBufferDuration, REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *, REFERENCE_TIME *,
const WAVEFORMATEX *pFormat, const WAVEFORMATEX *pFormat,
LPCGUID) override; LPCGUID) override;
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override; HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override; HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override; HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
HRESULT on_is_format_supported( HRESULT on_is_format_supported(
AUDCLNT_SHAREMODE *, AUDCLNT_SHAREMODE *,
const WAVEFORMATEX *, const WAVEFORMATEX *,
WAVEFORMATEX **ppClosestMatch) override; WAVEFORMATEX **ppClosestMatch) override;
HRESULT on_get_mix_format(WAVEFORMATEX **) override; HRESULT on_get_mix_format(WAVEFORMATEX **) override;
HRESULT on_get_device_period( HRESULT on_get_device_period(
REFERENCE_TIME *default_device_period, REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period) override; REFERENCE_TIME *minimum_device_period) override;
HRESULT on_start() override; HRESULT on_start() override;
HRESULT on_stop() override; HRESULT on_stop() override;
HRESULT on_set_event_handle(HANDLE *event_handle) override; HRESULT on_set_event_handle(HANDLE *event_handle) override;
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override; HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
HRESULT on_release_buffer(uint32_t, DWORD) override; HRESULT on_release_buffer(uint32_t, DWORD) override;
private: private:
void pace_loop(); void pace_loop();
WAVEFORMATEXTENSIBLE format_ {}; WAVEFORMATEXTENSIBLE format_ {};
uint32_t buffer_frames = 0; uint32_t buffer_frames = 0;
REFERENCE_TIME period_reftime = 0; REFERENCE_TIME period_reftime = 0;
HANDLE relay_handle = nullptr; HANDLE relay_handle = nullptr;
std::vector<BYTE> scratch; std::vector<BYTE> scratch;
std::thread pacing_thread; std::thread pacing_thread;
std::atomic<bool> running = false; std::atomic<bool> running = false;
}; };
@@ -1,264 +1,264 @@
#include "downmix.h" #include "downmix.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <audioclient.h> #include <audioclient.h>
#include <ks.h> #include <ks.h>
#include <ksmedia.h> #include <ksmedia.h>
#include "util/logging.h" #include "util/logging.h"
#include "util.h" #include "util.h"
namespace hooks::audio { namespace hooks::audio {
namespace { namespace {
constexpr float ATT_3DB = 0.70710678f; constexpr float ATT_3DB = 0.70710678f;
// speakers routed to the left/right output; anything else (center) feeds both sides // speakers routed to the left/right output; anything else (center) feeds both sides
constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT constexpr DWORD LEFT_SPEAKERS = SPEAKER_FRONT_LEFT | SPEAKER_BACK_LEFT | SPEAKER_SIDE_LEFT
| SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT; | SPEAKER_FRONT_LEFT_OF_CENTER | SPEAKER_TOP_FRONT_LEFT | SPEAKER_TOP_BACK_LEFT;
constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT constexpr DWORD RIGHT_SPEAKERS = SPEAKER_FRONT_RIGHT | SPEAKER_BACK_RIGHT | SPEAKER_SIDE_RIGHT
| SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT; | SPEAKER_FRONT_RIGHT_OF_CENTER | SPEAKER_TOP_FRONT_RIGHT | SPEAKER_TOP_BACK_RIGHT;
// the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats // the speaker mask is only present on WAVE_FORMAT_EXTENSIBLE formats
DWORD read_channel_mask(const WAVEFORMATEX *fmt) { DWORD read_channel_mask(const WAVEFORMATEX *fmt) {
if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE if (fmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE
&& fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { && fmt->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask; return reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(fmt)->dwChannelMask;
} }
return 0; return 0;
} }
// call visit(channel_index, speaker_bit) for each present speaker, in channel order // call visit(channel_index, speaker_bit) for each present speaker, in channel order
template <typename F> template <typename F>
void for_each_speaker(DWORD mask, int channels, F &&visit) { void for_each_speaker(DWORD mask, int channels, F &&visit) {
int channel = 0; int channel = 0;
for (int bit = 0; bit < 18 && channel < channels; bit++) { for (int bit = 0; bit < 18 && channel < channels; bit++) {
const DWORD speaker = 1u << bit; const DWORD speaker = 1u << bit;
if (mask & speaker) { if (mask & speaker) {
visit(channel++, speaker); visit(channel++, speaker);
} }
} }
} }
} }
void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, void Downmix::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm) { DownmixAlgorithm algorithm) {
this->enabled = true; this->enabled = true;
this->algorithm = algorithm; this->algorithm = algorithm;
this->bytes_per_sample = game_format->wBitsPerSample / 8; this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = game_format->nChannels * this->bytes_per_sample; this->game_frame_size = game_format->nChannels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format); this->is_float = is_ieee_float(game_format);
// supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence // supported: 16/24/32-bit integer PCM and 32-bit float; anything else mixes to silence
const bool supported = this->is_float const bool supported = this->is_float
? this->bytes_per_sample == 4 ? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) { if (!supported) {
log_fatal( log_fatal(
"audio::downmix", "audio::downmix",
"unsupported sample format ({}-bit {}), downmix will output silence", "unsupported sample format ({}-bit {}), downmix will output silence",
game_format->wBitsPerSample, this->is_float ? "float" : "int"); game_format->wBitsPerSample, this->is_float ? "float" : "int");
} }
this->left_mix.clear(); this->left_mix.clear();
this->right_mix.clear(); this->right_mix.clear();
this->build_layout_mix(game_format); this->build_layout_mix(game_format);
make_stereo_format(game_format, stereo_out); make_stereo_format(game_format, stereo_out);
} }
void Downmix::make_stereo_format(const WAVEFORMATEX *game_format, void Downmix::make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out) { WAVEFORMATEXTENSIBLE *stereo_out) {
const int bytes_per_sample = game_format->wBitsPerSample / 8; const int bytes_per_sample = game_format->wBitsPerSample / 8;
memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE)); memcpy(stereo_out, game_format, sizeof(WAVEFORMATEXTENSIBLE));
stereo_out->Format.nChannels = 2; stereo_out->Format.nChannels = 2;
stereo_out->Format.nBlockAlign = 2 * bytes_per_sample; stereo_out->Format.nBlockAlign = 2 * bytes_per_sample;
stereo_out->Format.nAvgBytesPerSec = stereo_out->Format.nAvgBytesPerSec =
game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign; game_format->nSamplesPerSec * stereo_out->Format.nBlockAlign;
stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; stereo_out->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
} }
HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, HRESULT Downmix::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) { const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the smaller stereo buffer can end up unaligned for the device when the game sized the // the smaller stereo buffer can end up unaligned for the device when the game sized the
// duration for its larger multi-channel format; the helper recovers from that. // duration for its larger multi-channel format; the helper recovers from that.
return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags, return initialize_with_alignment_retry(real, "audio::downmix", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid); buffer_duration, periodicity, device_format, session_guid);
} }
void Downmix::add_channel(int channel, DWORD speaker, float gain) { void Downmix::add_channel(int channel, DWORD speaker, float gain) {
if (speaker & LEFT_SPEAKERS) { if (speaker & LEFT_SPEAKERS) {
this->left_mix.push_back({ channel, gain }); this->left_mix.push_back({ channel, gain });
} else if (speaker & RIGHT_SPEAKERS) { } else if (speaker & RIGHT_SPEAKERS) {
this->right_mix.push_back({ channel, gain }); this->right_mix.push_back({ channel, gain });
} else { // center: feed both sides } else { // center: feed both sides
this->left_mix.push_back({ channel, gain }); this->left_mix.push_back({ channel, gain });
this->right_mix.push_back({ channel, gain }); this->right_mix.push_back({ channel, gain });
} }
} }
// AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped // AC-4 stereo downmix (ETSI TS 103 190-1): front pair at unity, everything else -3 dB, LFE dropped
void Downmix::build_ac4_mix(DWORD mask, int channels) { void Downmix::build_ac4_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker == SPEAKER_LOW_FREQUENCY) { if (speaker == SPEAKER_LOW_FREQUENCY) {
return; return;
} }
const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); const bool front_pair = speaker & (SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB); this->add_channel(ch, speaker, front_pair ? 1.0f : ATT_3DB);
}); });
} }
// keep only the channels in `keep` (front/rear/side), each at unity gain // keep only the channels in `keep` (front/rear/side), each at unity gain
void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) { void Downmix::build_extract_mix(DWORD mask, int channels, DWORD keep) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker & keep) { if (speaker & keep) {
this->add_channel(ch, speaker, 1.0f); this->add_channel(ch, speaker, 1.0f);
} }
}); });
} }
// keep every channel (LFE dropped), then average each side so its gains sum to unity // keep every channel (LFE dropped), then average each side so its gains sum to unity
void Downmix::build_normalize_mix(DWORD mask, int channels) { void Downmix::build_normalize_mix(DWORD mask, int channels) {
for_each_speaker(mask, channels, [&](int ch, DWORD speaker) { for_each_speaker(mask, channels, [&](int ch, DWORD speaker) {
if (speaker != SPEAKER_LOW_FREQUENCY) { if (speaker != SPEAKER_LOW_FREQUENCY) {
this->add_channel(ch, speaker, 1.0f); this->add_channel(ch, speaker, 1.0f);
} }
}); });
for (auto *mix : { &this->left_mix, &this->right_mix }) { for (auto *mix : { &this->left_mix, &this->right_mix }) {
if (!mix->empty()) { if (!mix->empty()) {
const float gain = 1.0f / mix->size(); const float gain = 1.0f / mix->size();
for (auto &c : *mix) { for (auto &c : *mix) {
c.gain = gain; c.gain = gain;
} }
} }
} }
} }
// fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right) // fallback when no speaker mask is present: fold interleaved L/R pairs (even->left, odd->right)
void Downmix::build_pairs_mix(int channels, float gain) { void Downmix::build_pairs_mix(int channels, float gain) {
for (int ch = 0; ch < channels; ch++) { for (int ch = 0; ch < channels; ch++) {
(((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain }); (((ch & 1) == 0) ? this->left_mix : this->right_mix).push_back({ ch, gain });
} }
} }
void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) { void Downmix::build_layout_mix(const WAVEFORMATEX *game_format) {
const int channels = game_format->nChannels; const int channels = game_format->nChannels;
const DWORD mask = read_channel_mask(game_format); const DWORD mask = read_channel_mask(game_format);
// without a mask the layout is unknown: extract/normalize have nothing to act on, so all // without a mask the layout is unknown: extract/normalize have nothing to act on, so all
// algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB) // algorithms fall back to folding L/R pairs (AC-4 still attenuates by -3 dB)
if (mask == 0) { if (mask == 0) {
this->build_pairs_mix(channels, this->build_pairs_mix(channels,
this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f); this->algorithm == DownmixAlgorithm::AC4 ? ATT_3DB : 1.0f);
return; return;
} }
switch (this->algorithm) { switch (this->algorithm) {
case DownmixAlgorithm::FrontOnly: case DownmixAlgorithm::FrontOnly:
this->build_extract_mix(mask, channels, this->build_extract_mix(mask, channels,
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT); SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT);
break; break;
case DownmixAlgorithm::RearOnly: case DownmixAlgorithm::RearOnly:
this->build_extract_mix(mask, channels, this->build_extract_mix(mask, channels,
SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER); SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT | SPEAKER_BACK_CENTER);
break; break;
case DownmixAlgorithm::SideOnly: case DownmixAlgorithm::SideOnly:
this->build_extract_mix(mask, channels, this->build_extract_mix(mask, channels,
SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT); SPEAKER_SIDE_LEFT | SPEAKER_SIDE_RIGHT);
break; break;
case DownmixAlgorithm::Normalize: case DownmixAlgorithm::Normalize:
this->build_normalize_mix(mask, channels); this->build_normalize_mix(mask, channels);
break; break;
case DownmixAlgorithm::AC4: case DownmixAlgorithm::AC4:
this->build_ac4_mix(mask, channels); this->build_ac4_mix(mask, channels);
break; break;
} }
} }
void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const { void Downmix::process(BYTE *dst, const BYTE *src, UINT32 frames) const {
const int bps = this->bytes_per_sample; const int bps = this->bytes_per_sample;
const int src_stride = this->game_frame_size; const int src_stride = this->game_frame_size;
const int dst_stride = 2 * bps; const int dst_stride = 2 * bps;
if (dst == nullptr || src == nullptr || bps <= 0) { if (dst == nullptr || src == nullptr || bps <= 0) {
return; return;
} }
// sum each speaker's source channels into the matching stereo output // sum each speaker's source channels into the matching stereo output
for (UINT32 i = 0; i < frames; i++) { for (UINT32 i = 0; i < frames; i++) {
const BYTE *in = src + (size_t) i * src_stride; const BYTE *in = src + (size_t) i * src_stride;
BYTE *out = dst + (size_t) i * dst_stride; BYTE *out = dst + (size_t) i * dst_stride;
float left = 0.0f; float left = 0.0f;
float right = 0.0f; float right = 0.0f;
for (const auto &c : this->left_mix) { for (const auto &c : this->left_mix) {
left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; left += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
} }
for (const auto &c : this->right_mix) { for (const auto &c : this->right_mix) {
right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain; right += read_sample(in + c.channel * bps, bps, this->is_float) * c.gain;
} }
write_sample(out, bps, this->is_float, left); write_sample(out, bps, this->is_float, left);
write_sample(out + bps, bps, this->is_float, right); write_sample(out + bps, bps, this->is_float, right);
} }
} }
HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) { HRESULT Downmix::get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size; const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) { if (this->scratch.size() < needed) {
this->scratch.resize(needed); this->scratch.resize(needed);
} }
HRESULT ret = real->GetBuffer(frames, &this->device_buffer); HRESULT ret = real->GetBuffer(frames, &this->device_buffer);
if (FAILED(ret)) { if (FAILED(ret)) {
this->device_buffer = nullptr; this->device_buffer = nullptr;
return ret; return ret;
} }
*ppData = this->scratch.data(); *ppData = this->scratch.data();
return S_OK; return S_OK;
} }
HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) { HRESULT Downmix::get_scratch(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size; const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) { if (this->scratch.size() < needed) {
this->scratch.resize(needed); this->scratch.resize(needed);
} }
*ppData = this->scratch.data(); *ppData = this->scratch.data();
return S_OK; return S_OK;
} }
void Downmix::downmix_into(BYTE *dst, UINT32 frames) const { void Downmix::downmix_into(BYTE *dst, UINT32 frames) const {
this->process(dst, this->scratch.data(), frames); this->process(dst, this->scratch.data(), frames);
} }
void Downmix::write_device_buffer(UINT32 frames, DWORD flags) { void Downmix::write_device_buffer(UINT32 frames, DWORD flags) {
const int bps = this->bytes_per_sample; const int bps = this->bytes_per_sample;
const int dst_stride = 2 * bps; const int dst_stride = 2 * bps;
if (this->device_buffer == nullptr || frames == 0 || bps <= 0) { if (this->device_buffer == nullptr || frames == 0 || bps <= 0) {
return; return;
} }
// mute the first few buffers to avoid a pop on stream start // mute the first few buffers to avoid a pop on stream start
if (this->buffers_to_mute > 0) { if (this->buffers_to_mute > 0) {
memset(this->device_buffer, 0, (size_t) frames * dst_stride); memset(this->device_buffer, 0, (size_t) frames * dst_stride);
this->buffers_to_mute--; this->buffers_to_mute--;
} else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) { } else if ((flags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->process(this->device_buffer, this->scratch.data(), frames); this->process(this->device_buffer, this->scratch.data(), frames);
} }
} }
} }
+150 -150
View File
@@ -1,150 +1,150 @@
#pragma once #pragma once
#include <optional> #include <optional>
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
struct IAudioClient; struct IAudioClient;
struct IAudioRenderClient; struct IAudioRenderClient;
namespace hooks::audio { namespace hooks::audio {
// Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the // Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the
// game keeps writing its native multi-channel audio into a scratch buffer; on release that // game keeps writing its native multi-channel audio into a scratch buffer; on release that
// buffer is mixed down into the two front channels. // buffer is mixed down into the two front channels.
// //
// The mix is derived from the source format's speaker mask according to the selected // The mix is derived from the source format's speaker mask according to the selected
// DownmixAlgorithm: // DownmixAlgorithm:
// FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side // FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side
// AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right // AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 §6.2.17): front left/right
// pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped // pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped
// Normalize - every channel folded in (center to both sides) with each output side averaged // Normalize - every channel folded in (center to both sides) with each output side averaged
// so its channels are equally loud, LFE dropped // so its channels are equally loud, LFE dropped
struct Downmix { struct Downmix {
// a source channel routed into one output speaker at the given gain // a source channel routed into one output speaker at the given gain
struct Contribution { struct Contribution {
int channel; int channel;
float gain; float gain;
}; };
// map an option value (front/rear/side/ac4/normalize) to its algorithm. // map an option value (front/rear/side/ac4/normalize) to its algorithm.
static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) { static std::optional<DownmixAlgorithm> name_to_algorithm(const char *value) {
if (_stricmp(value, "front") == 0) { if (_stricmp(value, "front") == 0) {
return DownmixAlgorithm::FrontOnly; return DownmixAlgorithm::FrontOnly;
} else if (_stricmp(value, "rear") == 0) { } else if (_stricmp(value, "rear") == 0) {
return DownmixAlgorithm::RearOnly; return DownmixAlgorithm::RearOnly;
} else if (_stricmp(value, "side") == 0) { } else if (_stricmp(value, "side") == 0) {
return DownmixAlgorithm::SideOnly; return DownmixAlgorithm::SideOnly;
} else if (_stricmp(value, "ac4") == 0) { } else if (_stricmp(value, "ac4") == 0) {
return DownmixAlgorithm::AC4; return DownmixAlgorithm::AC4;
} else if (_stricmp(value, "normalize") == 0) { } else if (_stricmp(value, "normalize") == 0) {
return DownmixAlgorithm::Normalize; return DownmixAlgorithm::Normalize;
} }
return std::nullopt; return std::nullopt;
} }
// human-readable name of an algorithm, for logging. // human-readable name of an algorithm, for logging.
static const char *algorithm_name(DownmixAlgorithm algorithm) { static const char *algorithm_name(DownmixAlgorithm algorithm) {
switch (algorithm) { switch (algorithm) {
case DownmixAlgorithm::FrontOnly: return "front"; case DownmixAlgorithm::FrontOnly: return "front";
case DownmixAlgorithm::RearOnly: return "rear"; case DownmixAlgorithm::RearOnly: return "rear";
case DownmixAlgorithm::SideOnly: return "side"; case DownmixAlgorithm::SideOnly: return "side";
case DownmixAlgorithm::AC4: return "ac4"; case DownmixAlgorithm::AC4: return "ac4";
case DownmixAlgorithm::Normalize: return "normalize"; case DownmixAlgorithm::Normalize: return "normalize";
default: return "unknown"; default: return "unknown";
} }
} }
// whether the downmix is active for the current stream // whether the downmix is active for the current stream
bool enabled = false; bool enabled = false;
// algorithm used to fold the multi-channel audio into stereo // algorithm used to fold the multi-channel audio into stereo
DownmixAlgorithm algorithm = DownmixAlgorithm::AC4; DownmixAlgorithm algorithm = DownmixAlgorithm::AC4;
// size in bytes of one frame of the game's multi-channel format // size in bytes of one frame of the game's multi-channel format
int game_frame_size = 0; int game_frame_size = 0;
// size in bytes of a single sample (per channel) // size in bytes of a single sample (per channel)
int bytes_per_sample = 0; int bytes_per_sample = 0;
// whether samples are IEEE floating point rather than integer PCM // whether samples are IEEE floating point rather than integer PCM
bool is_float = false; bool is_float = false;
// enable the downmix for the given game format and fill stereo_out with the equivalent // enable the downmix for the given game format and fill stereo_out with the equivalent
// stereo format to open the real device with. // stereo format to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out,
DownmixAlgorithm algorithm); DownmixAlgorithm algorithm);
// build the stereo format equivalent to game_format (same sample rate and bit depth). // build the stereo format equivalent to game_format (same sample rate and bit depth).
static void make_stereo_format(const WAVEFORMATEX *game_format, static void make_stereo_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *stereo_out); WAVEFORMATEXTENSIBLE *stereo_out);
// initialize the real device with the stereo format. downmixing reduces the channel count, // initialize the real device with the stereo format. downmixing reduces the channel count,
// shrinking the buffer's byte size, so the duration the game sized for its multi-channel // shrinking the buffer's byte size, so the duration the game sized for its multi-channel
// format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED // format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED
// this performs the standard WASAPI realignment and retries. // this performs the standard WASAPI realignment and retries.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid); const WAVEFORMATEX *device_format, LPCGUID session_guid);
// mix `frames` frames of multi-channel `src` down into stereo `dst`. // mix `frames` frames of multi-channel `src` down into stereo `dst`.
void process(BYTE *dst, const BYTE *src, UINT32 frames) const; void process(BYTE *dst, const BYTE *src, UINT32 frames) const;
// grab the real stereo device buffer and hand the game the scratch buffer to write into. // grab the real stereo device buffer and hand the game the scratch buffer to write into.
HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData); HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData);
// size the scratch and hand it to the game without acquiring a device buffer. used when a // size the scratch and hand it to the game without acquiring a device buffer. used when a
// later stage (the resampler) owns the device interaction. // later stage (the resampler) owns the device interaction.
HRESULT get_scratch(UINT32 frames, BYTE **ppData); HRESULT get_scratch(UINT32 frames, BYTE **ppData);
// downmix the scratch the game wrote into the caller's stereo buffer, without touching the // downmix the scratch the game wrote into the caller's stereo buffer, without touching the
// device. used to feed the resampler when the two stages are chained. // device. used to feed the resampler when the two stages are chained.
void downmix_into(BYTE *dst, UINT32 frames) const; void downmix_into(BYTE *dst, UINT32 frames) const;
// mix the scratch buffer into the stereo device buffer held since get_buffer. the caller // mix the scratch buffer into the stereo device buffer held since get_buffer. the caller
// owns releasing the device buffer afterwards (see current_buffer / buffer_released). // owns releasing the device buffer afterwards (see current_buffer / buffer_released).
void write_device_buffer(UINT32 frames, DWORD flags); void write_device_buffer(UINT32 frames, DWORD flags);
// the real device buffer currently held, or null. // the real device buffer currently held, or null.
BYTE *current_buffer() const { return this->device_buffer; } BYTE *current_buffer() const { return this->device_buffer; }
// forget the held device buffer once the caller has released it. // forget the held device buffer once the caller has released it.
void buffer_released() { this->device_buffer = nullptr; } void buffer_released() { this->device_buffer = nullptr; }
private: private:
// build the mix from the source speaker layout for the selected algorithm // build the mix from the source speaker layout for the selected algorithm
void build_layout_mix(const WAVEFORMATEX *game_format); void build_layout_mix(const WAVEFORMATEX *game_format);
// per-algorithm builders, each filling left_mix / right_mix from the speaker mask // per-algorithm builders, each filling left_mix / right_mix from the speaker mask
void build_ac4_mix(DWORD mask, int channels); void build_ac4_mix(DWORD mask, int channels);
void build_extract_mix(DWORD mask, int channels, DWORD keep); void build_extract_mix(DWORD mask, int channels, DWORD keep);
void build_normalize_mix(DWORD mask, int channels); void build_normalize_mix(DWORD mask, int channels);
// fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain` // fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain`
void build_pairs_mix(int channels, float gain); void build_pairs_mix(int channels, float gain);
// append one source channel to the output side(s) matching its speaker, at `gain` // append one source channel to the output side(s) matching its speaker, at `gain`
void add_channel(int channel, DWORD speaker, float gain); void add_channel(int channel, DWORD speaker, float gain);
// source channels summed into each output speaker // source channels summed into each output speaker
std::vector<Contribution> left_mix; std::vector<Contribution> left_mix;
std::vector<Contribution> right_mix; std::vector<Contribution> right_mix;
// buffer the game writes its multi-channel audio into between get/release // buffer the game writes its multi-channel audio into between get/release
std::vector<BYTE> scratch; std::vector<BYTE> scratch;
// the real stereo device buffer currently held, or null // the real stereo device buffer currently held, or null
BYTE *device_buffer = nullptr; BYTE *device_buffer = nullptr;
// leading buffers to silence to avoid a pop on stream start // leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16; int buffers_to_mute = 16;
}; };
} }
@@ -1,437 +1,437 @@
#include "resample.h" #include "resample.h"
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdint> #include <cstdint>
#include <cstring> #include <cstring>
#include <mutex> #include <mutex>
#include <audioclient.h> #include <audioclient.h>
#include "util/logging.h" #include "util/logging.h"
#include "util.h" #include "util.h"
namespace hooks::audio { namespace hooks::audio {
namespace { namespace {
constexpr double PI = 3.14159265358979323846; constexpr double PI = 3.14159265358979323846;
// normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in // normalized sinc: sin(pi*x) / (pi*x), with the removable singularity at 0 filled in
inline double sinc(double x) { inline double sinc(double x) {
if (x == 0.0) { if (x == 0.0) {
return 1.0; return 1.0;
} }
const double px = PI * x; const double px = PI * x;
return std::sin(px) / px; return std::sin(px) / px;
} }
// Blackman window across the kernel radius; zero at +/- radius // Blackman window across the kernel radius; zero at +/- radius
inline double blackman(double x, double radius) { inline double blackman(double x, double radius) {
const double n = (x + radius) / (2.0 * radius); const double n = (x + radius) / (2.0 * radius);
if (n <= 0.0 || n >= 1.0) { if (n <= 0.0 || n >= 1.0) {
return 0.0; return 0.0;
} }
return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n); return 0.42 - 0.5 * std::cos(2.0 * PI * n) + 0.08 * std::cos(4.0 * PI * n);
} }
} }
std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) { std::optional<uint32_t> Resampler::resolve(const WAVEFORMATEX *game_format) {
if (game_format == nullptr || !RESAMPLE_RATE.has_value()) { if (game_format == nullptr || !RESAMPLE_RATE.has_value()) {
return std::nullopt; return std::nullopt;
} }
if (game_format->nSamplesPerSec == 0 if (game_format->nSamplesPerSec == 0
|| game_format->nSamplesPerSec == RESAMPLE_RATE.value()) { || game_format->nSamplesPerSec == RESAMPLE_RATE.value()) {
return std::nullopt; return std::nullopt;
} }
return RESAMPLE_RATE; return RESAMPLE_RATE;
} }
void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, void Resampler::setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate) { uint32_t target_rate) {
this->enabled = true; this->enabled = true;
this->channels = game_format->nChannels; this->channels = game_format->nChannels;
this->bytes_per_sample = game_format->wBitsPerSample / 8; this->bytes_per_sample = game_format->wBitsPerSample / 8;
this->game_frame_size = this->channels * this->bytes_per_sample; this->game_frame_size = this->channels * this->bytes_per_sample;
this->is_float = is_ieee_float(game_format); this->is_float = is_ieee_float(game_format);
const bool supported = this->is_float const bool supported = this->is_float
? this->bytes_per_sample == 4 ? this->bytes_per_sample == 4
: (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4); : (this->bytes_per_sample >= 2 && this->bytes_per_sample <= 4);
if (!supported) { if (!supported) {
log_fatal( log_fatal(
"audio::resample", "audio::resample",
"unsupported sample format ({}-bit {}) for -resample", "unsupported sample format ({}-bit {}) for -resample",
game_format->wBitsPerSample, this->is_float ? "float" : "int"); game_format->wBitsPerSample, this->is_float ? "float" : "int");
} }
this->src_rate = game_format->nSamplesPerSec; this->src_rate = game_format->nSamplesPerSec;
this->dst_rate = target_rate; this->dst_rate = target_rate;
// anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating // anti-alias cutoff: full bandwidth when upsampling, scaled down when decimating
this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate); this->cutoff = std::min(1.0, (double) this->dst_rate / (double) this->src_rate);
this->half_taps = 16; this->half_taps = 16;
// precompute the windowed-sinc kernel now that cutoff is known // precompute the windowed-sinc kernel now that cutoff is known
this->build_kernel(); this->build_kernel();
// prime the queue with half a window of silence so the first outputs have left history // prime the queue with half a window of silence so the first outputs have left history
this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f); this->in_queue.assign((size_t) this->half_taps * this->channels, 0.0f);
this->in_pos = this->half_taps; this->in_pos = this->half_taps;
this->make_device_format(game_format, device_out, target_rate); this->make_device_format(game_format, device_out, target_rate);
} }
void Resampler::make_device_format(const WAVEFORMATEX *game_format, void Resampler::make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) { WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate) {
const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize; const size_t src_size = sizeof(WAVEFORMATEX) + game_format->cbSize;
memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE)); memset(device_out, 0, sizeof(WAVEFORMATEXTENSIBLE));
memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE))); memcpy(device_out, game_format, std::min(src_size, sizeof(WAVEFORMATEXTENSIBLE)));
device_out->Format.nSamplesPerSec = target_rate; device_out->Format.nSamplesPerSec = target_rate;
device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign; device_out->Format.nAvgBytesPerSec = target_rate * device_out->Format.nBlockAlign;
} }
HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, HRESULT Resampler::initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode,
DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid) { const WAVEFORMATEX *device_format, LPCGUID session_guid) {
// the resampler bypasses the OS mixer and talks to the device directly, so it only makes // the resampler bypasses the OS mixer and talks to the device directly, so it only makes
// sense (and only works) for exclusive streams. shared streams are already resampled by // sense (and only works) for exclusive streams. shared streams are already resampled by
// the Windows audio engine, so refuse loudly rather than silently doing nothing. // the Windows audio engine, so refuse loudly rather than silently doing nothing.
if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) { if (share_mode != AUDCLNT_SHAREMODE_EXCLUSIVE) {
log_fatal("audio::resample", log_fatal("audio::resample",
"-resample requires WASAPI exclusive mode, but this stream is shared " "-resample requires WASAPI exclusive mode, but this stream is shared "
"(Windows already resamples shared streams)"); "(Windows already resamples shared streams)");
} }
// record the pacing model. event-driven streams fill the whole device buffer each period // record the pacing model. event-driven streams fill the whole device buffer each period
// (produce_exact); timer-driven streams poll padding and write variable partial chunks, so // (produce_exact); timer-driven streams poll padding and write variable partial chunks, so
// they drain the pending output to the device's free space each call (flush_timer). // they drain the pending output to the device's free space each call (flush_timer).
this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0; this->event_driven = (stream_flags & AUDCLNT_STREAMFLAGS_EVENTCALLBACK) != 0;
return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags, return initialize_with_alignment_retry(real, "audio::resample", share_mode, stream_flags,
buffer_duration, periodicity, device_format, session_guid); buffer_duration, periodicity, device_format, session_guid);
} }
UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const { UINT32 Resampler::frames_device_to_game(UINT32 device_frames) const {
if (this->dst_rate == 0) { if (this->dst_rate == 0) {
return device_frames; return device_frames;
} }
// round down so the game never believes it has more room than the device can hold // round down so the game never believes it has more room than the device can hold
return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate); return (UINT32) (((double) device_frames * this->src_rate) / this->dst_rate);
} }
UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const { UINT32 Resampler::padding_device_to_game(UINT32 device_padding) const {
if (this->dst_rate == 0) { if (this->dst_rate == 0) {
return device_padding; return device_padding;
} }
// round up so the reported free space stays conservative // round up so the reported free space stays conservative
return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate); return (UINT32) std::ceil(((double) device_padding * this->src_rate) / this->dst_rate);
} }
HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) { HRESULT Resampler::get_buffer(UINT32 frames, BYTE **ppData) {
const size_t needed = (size_t) frames * this->game_frame_size; const size_t needed = (size_t) frames * this->game_frame_size;
if (this->scratch.size() < needed) { if (this->scratch.size() < needed) {
this->scratch.resize(needed); this->scratch.resize(needed);
} }
*ppData = this->scratch.data(); *ppData = this->scratch.data();
return S_OK; return S_OK;
} }
void Resampler::enqueue_input(UINT32 frames, bool silent) { void Resampler::enqueue_input(UINT32 frames, bool silent) {
const int bps = this->bytes_per_sample; const int bps = this->bytes_per_sample;
const int ch = this->channels; const int ch = this->channels;
const size_t base = this->in_queue.size(); const size_t base = this->in_queue.size();
this->in_queue.resize(base + (size_t) frames * ch); this->in_queue.resize(base + (size_t) frames * ch);
if (silent || bps <= 0 || ch <= 0) { if (silent || bps <= 0 || ch <= 0) {
std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f); std::fill(this->in_queue.begin() + base, this->in_queue.end(), 0.0f);
return; return;
} }
const BYTE *src = this->scratch.data(); const BYTE *src = this->scratch.data();
for (UINT32 f = 0; f < frames; f++) { for (UINT32 f = 0; f < frames; f++) {
for (int c = 0; c < ch; c++) { for (int c = 0; c < ch; c++) {
const size_t s = (size_t) f * ch + c; const size_t s = (size_t) f * ch + c;
this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float); this->in_queue[base + s] = read_sample(src + s * bps, bps, this->is_float);
} }
} }
} }
void Resampler::build_kernel() { void Resampler::build_kernel() {
const int taps = 2 * this->half_taps; const int taps = 2 * this->half_taps;
const int phases = this->kernel_phases; const int phases = this->kernel_phases;
const double cut = this->cutoff; const double cut = this->cutoff;
const double radius = (double) this->half_taps; const double radius = (double) this->half_taps;
// one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely // one extra row at frac == 1.0 so emit_frame can interpolate against row p + 1 safely
this->kernel_table.resize((size_t) (phases + 1) * taps); this->kernel_table.resize((size_t) (phases + 1) * taps);
for (int p = 0; p <= phases; p++) { for (int p = 0; p <= phases; p++) {
const double frac = (double) p / (double) phases; const double frac = (double) p / (double) phases;
for (int k = 0; k < taps; k++) { for (int k = 0; k < taps; k++) {
// tap k maps to input offset t = k - (half_taps - 1), matching emit_frame // tap k maps to input offset t = k - (half_taps - 1), matching emit_frame
const double x = frac - (double) (k - (this->half_taps - 1)); const double x = frac - (double) (k - (this->half_taps - 1));
this->kernel_table[(size_t) p * taps + k] = this->kernel_table[(size_t) p * taps + k] =
(float) (cut * sinc(cut * x) * blackman(x, radius)); (float) (cut * sinc(cut * x) * blackman(x, radius));
} }
} }
} }
void Resampler::emit_frame() { void Resampler::emit_frame() {
const int ch = this->channels; const int ch = this->channels;
const int radius = this->half_taps; const int radius = this->half_taps;
const int taps = 2 * radius; const int taps = 2 * radius;
const long avail = (long) (this->in_queue.size() / ch); const long avail = (long) (this->in_queue.size() / ch);
const long center = (long) std::floor(this->in_pos); const long center = (long) std::floor(this->in_pos);
// pick the two kernel rows bracketing this fractional position and the blend between them // pick the two kernel rows bracketing this fractional position and the blend between them
const double frac = this->in_pos - (double) center; const double frac = this->in_pos - (double) center;
const double fp = frac * (double) this->kernel_phases; const double fp = frac * (double) this->kernel_phases;
const int p0 = (int) fp; const int p0 = (int) fp;
const float blend = (float) (fp - (double) p0); const float blend = (float) (fp - (double) p0);
const float *row0 = &this->kernel_table[(size_t) p0 * taps]; const float *row0 = &this->kernel_table[(size_t) p0 * taps];
const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps]; const float *row1 = &this->kernel_table[(size_t) (p0 + 1) * taps];
// base input index for tap 0 (t = -(radius - 1)) // base input index for tap 0 (t = -(radius - 1))
const long base = center - (radius - 1); const long base = center - (radius - 1);
for (int c = 0; c < ch; c++) { for (int c = 0; c < ch; c++) {
double acc = 0.0; double acc = 0.0;
for (int k = 0; k < taps; k++) { for (int k = 0; k < taps; k++) {
const long idx = base + k; const long idx = base + k;
if (idx < 0 || idx >= avail) { if (idx < 0 || idx >= avail) {
continue; continue;
} }
const float w = row0[k] + blend * (row1[k] - row0[k]); const float w = row0[k] + blend * (row1[k] - row0[k]);
acc += (double) this->in_queue[(size_t) idx * ch + c] * w; acc += (double) this->in_queue[(size_t) idx * ch + c] * w;
} }
this->out_float.push_back((float) acc); this->out_float.push_back((float) acc);
} }
} }
void Resampler::drop_consumed() { void Resampler::drop_consumed() {
const int ch = this->channels; const int ch = this->channels;
const long drop = (long) std::floor(this->in_pos) - this->half_taps; const long drop = (long) std::floor(this->in_pos) - this->half_taps;
if (drop > 0) { if (drop > 0) {
const size_t drop_samples = (size_t) drop * ch; const size_t drop_samples = (size_t) drop * ch;
if (drop_samples <= this->in_queue.size()) { if (drop_samples <= this->in_queue.size()) {
this->in_queue.erase(this->in_queue.begin(), this->in_queue.erase(this->in_queue.begin(),
this->in_queue.begin() + drop_samples); this->in_queue.begin() + drop_samples);
this->in_pos -= drop; this->in_pos -= drop;
} }
} }
} }
UINT32 Resampler::produce_exact(UINT32 out_frames) { UINT32 Resampler::produce_exact(UINT32 out_frames) {
const int ch = this->channels; const int ch = this->channels;
this->out_float.clear(); this->out_float.clear();
if (ch <= 0 || out_frames == 0) { if (ch <= 0 || out_frames == 0) {
return 0; return 0;
} }
this->out_float.reserve((size_t) out_frames * ch); this->out_float.reserve((size_t) out_frames * ch);
// resample ratio. drive it from the buffer size actually advertised to the game rather // resample ratio. drive it from the buffer size actually advertised to the game rather
// than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game // than the nominal src/dst ratio: GetBufferSize reports floor(dev_buf * src/dst) game
// frames, so the game only ever delivers that many input frames per device period. // frames, so the game only ever delivers that many input frames per device period.
// consuming at the nominal ratio would eat slightly more input than arrives on any device // consuming at the nominal ratio would eat slightly more input than arrives on any device
// where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly // where dev_buf * src/dst is non-integer (e.g. 144 -> 132.3, floored to 132), slowly
// draining the queue until it underruns to permanent silence. using the advertised integer // draining the queue until it underruns to permanent silence. using the advertised integer
// ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3% // ratio keeps input and output exactly balanced; the resulting pitch error is below 0.3%
// and inaudible, and it collapses to the exact ratio when the division is integer (160 -> // and inaudible, and it collapses to the exact ratio when the division is integer (160 ->
// 147 stays 147/160 = 44100/48000). // 147 stays 147/160 = 44100/48000).
const double step = (double) this->frames_device_to_game(this->device_buffer_frames) const double step = (double) this->frames_device_to_game(this->device_buffer_frames)
/ (double) this->device_buffer_frames; / (double) this->device_buffer_frames;
// input frames the block will touch: from in_pos through the right edge of the sinc kernel // input frames the block will touch: from in_pos through the right edge of the sinc kernel
// at the final output sample. if the queue is short of this, the kernel tail reads past the // at the final output sample. if the queue is short of this, the kernel tail reads past the
// end and distorts every buffer, so buffer one extra block of input before the first output // end and distorts every buffer, so buffer one extra block of input before the first output
// (emitting silence without consuming) to build a cushion the kernel can always reach into. // (emitting silence without consuming) to build a cushion the kernel can always reach into.
const long avail = (long) (this->in_queue.size() / ch); const long avail = (long) (this->in_queue.size() / ch);
const long need = (long) std::ceil(this->in_pos + step * (double) out_frames) const long need = (long) std::ceil(this->in_pos + step * (double) out_frames)
+ this->half_taps; + this->half_taps;
if (this->priming) { if (this->priming) {
if (avail < need + (long) out_frames) { if (avail < need + (long) out_frames) {
this->out_float.assign((size_t) out_frames * ch, 0.0f); this->out_float.assign((size_t) out_frames * ch, 0.0f);
return out_frames; return out_frames;
} }
this->priming = false; this->priming = false;
} }
for (UINT32 o = 0; o < out_frames; o++) { for (UINT32 o = 0; o < out_frames; o++) {
this->emit_frame(); this->emit_frame();
this->in_pos += step; this->in_pos += step;
} }
this->drop_consumed(); this->drop_consumed();
return out_frames; return out_frames;
} }
UINT32 Resampler::produce_variable() { UINT32 Resampler::produce_variable() {
const int ch = this->channels; const int ch = this->channels;
if (ch <= 0) { if (ch <= 0) {
return 0; return 0;
} }
// input frames consumed per output frame. timer-driven streams write variable partial // input frames consumed per output frame. timer-driven streams write variable partial
// chunks, so produce however many output frames the currently queued input can fully // chunks, so produce however many output frames the currently queued input can fully
// support and leave the rest for the next call; this keeps input and output balanced at // support and leave the rest for the next call; this keeps input and output balanced at
// the exact src/dst ratio over time without depending on the device buffer size. // the exact src/dst ratio over time without depending on the device buffer size.
const double step = (double) this->src_rate / (double) this->dst_rate; const double step = (double) this->src_rate / (double) this->dst_rate;
const long avail = (long) (this->in_queue.size() / ch); const long avail = (long) (this->in_queue.size() / ch);
// emit only while the sinc kernel's right edge stays within the queued input. the kernel // emit only while the sinc kernel's right edge stays within the queued input. the kernel
// reaches from in_pos out to half_taps frames ahead, so stop once that would read past the // reaches from in_pos out to half_taps frames ahead, so stop once that would read past the
// end; the remaining input becomes the next block's lookahead. // end; the remaining input becomes the next block's lookahead.
UINT32 produced = 0; UINT32 produced = 0;
while ((long) std::ceil(this->in_pos) + this->half_taps < avail) { while ((long) std::ceil(this->in_pos) + this->half_taps < avail) {
this->emit_frame(); this->emit_frame();
this->in_pos += step; this->in_pos += step;
produced++; produced++;
} }
this->drop_consumed(); this->drop_consumed();
return produced; return produced;
} }
void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const { void Resampler::write_output(BYTE *dst, UINT32 frames, float gain) const {
const int bps = this->bytes_per_sample; const int bps = this->bytes_per_sample;
const int ch = this->channels; const int ch = this->channels;
const size_t count = (size_t) frames * ch; const size_t count = (size_t) frames * ch;
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain); write_sample(dst + i * bps, bps, this->is_float, this->out_float[i] * gain);
} }
} }
HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, HRESULT Resampler::flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames,
DWORD flags, float boost) { DWORD flags, float boost) {
if (!this->enabled) { if (!this->enabled) {
return S_OK; return S_OK;
} }
// cache the device buffer size once // cache the device buffer size once
if (this->device_buffer_frames == 0) { if (this->device_buffer_frames == 0) {
client->GetBufferSize(&this->device_buffer_frames); client->GetBufferSize(&this->device_buffer_frames);
} }
if (this->device_buffer_frames == 0) { if (this->device_buffer_frames == 0) {
return S_OK; return S_OK;
} }
const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0; const bool silent = (flags & AUDCLNT_BUFFERFLAGS_SILENT) != 0;
this->enqueue_input(frames, silent); this->enqueue_input(frames, silent);
// confirm once that conversion actually started producing output // confirm once that conversion actually started producing output
static std::once_flag active_printed; static std::once_flag active_printed;
std::call_once(active_printed, [this]() { std::call_once(active_printed, [this]() {
log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})", log_info("audio::resample", "resample active: {} Hz -> {} Hz ({} ch, {})",
this->src_rate, this->dst_rate, this->channels, this->src_rate, this->dst_rate, this->channels,
this->event_driven ? "event-driven" : "timer-driven"); this->event_driven ? "event-driven" : "timer-driven");
}); });
// the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer // the boost is applied here (inside write_output) rather than in the standard ReleaseBuffer
// path, so log it once for parity with that path's "volume boost active" line. // path, so log it once for parity with that path's "volume boost active" line.
if (boost != 1.0f) { if (boost != 1.0f) {
static std::once_flag boost_printed; static std::once_flag boost_printed;
std::call_once(boost_printed, [boost]() { std::call_once(boost_printed, [boost]() {
log_info("audio::resample", "volume boost active (resample): gain={}", boost); log_info("audio::resample", "volume boost active (resample): gain={}", boost);
}); });
} }
return this->event_driven return this->event_driven
? this->flush_event(real, boost) ? this->flush_event(real, boost)
: this->flush_timer(real, client, boost); : this->flush_timer(real, client, boost);
} }
HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) { HRESULT Resampler::flush_event(IAudioRenderClient *real, float boost) {
// event-driven exclusive streams must hand the device a full buffer every period and may // event-driven exclusive streams must hand the device a full buffer every period and may
// not push partial counts. resample the whole input block into exactly the device buffer // not push partial counts. resample the whole input block into exactly the device buffer
// size. // size.
const UINT32 produced = this->produce_exact(this->device_buffer_frames); const UINT32 produced = this->produce_exact(this->device_buffer_frames);
if (produced == 0) { if (produced == 0) {
return S_OK; return S_OK;
} }
BYTE *dev = nullptr; BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(produced, &dev); HRESULT ret = real->GetBuffer(produced, &dev);
if (FAILED(ret) || dev == nullptr) { if (FAILED(ret) || dev == nullptr) {
return ret; return ret;
} }
// mute the first few buffers to avoid a pop on stream start // mute the first few buffers to avoid a pop on stream start
float gain = boost; float gain = boost;
if (this->buffers_to_mute > 0) { if (this->buffers_to_mute > 0) {
gain = 0.0f; gain = 0.0f;
this->buffers_to_mute--; this->buffers_to_mute--;
} }
this->write_output(dev, produced, gain); this->write_output(dev, produced, gain);
return real->ReleaseBuffer(produced, 0); return real->ReleaseBuffer(produced, 0);
} }
HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) { HRESULT Resampler::flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost) {
// convert everything currently queued into the pending output FIFO (out_float). timer- // convert everything currently queued into the pending output FIFO (out_float). timer-
// driven games write variable partial chunks, so produce only what the queued input can // driven games write variable partial chunks, so produce only what the queued input can
// fully support and keep the remainder for the next call. // fully support and keep the remainder for the next call.
this->produce_variable(); this->produce_variable();
const int ch = this->channels; const int ch = this->channels;
if (ch <= 0) { if (ch <= 0) {
return S_OK; return S_OK;
} }
const UINT32 pending = (UINT32) (this->out_float.size() / ch); const UINT32 pending = (UINT32) (this->out_float.size() / ch);
if (pending == 0) { if (pending == 0) {
return S_OK; return S_OK;
} }
// push as many frames as the device currently has free, keeping the rest queued for the // push as many frames as the device currently has free, keeping the rest queued for the
// next call. timer-driven games poll padding and write whenever there is room, so matching // next call. timer-driven games poll padding and write whenever there is room, so matching
// the device's free space here avoids overflowing the ring while staying device-paced. // the device's free space here avoids overflowing the ring while staying device-paced.
UINT32 padding = 0; UINT32 padding = 0;
if (FAILED(client->GetCurrentPadding(&padding))) { if (FAILED(client->GetCurrentPadding(&padding))) {
return S_OK; return S_OK;
} }
const UINT32 device_free = this->device_buffer_frames > padding const UINT32 device_free = this->device_buffer_frames > padding
? this->device_buffer_frames - padding ? this->device_buffer_frames - padding
: 0; : 0;
if (device_free == 0) { if (device_free == 0) {
return S_OK; return S_OK;
} }
const UINT32 to_write = std::min(pending, device_free); const UINT32 to_write = std::min(pending, device_free);
BYTE *dev = nullptr; BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(to_write, &dev); HRESULT ret = real->GetBuffer(to_write, &dev);
if (FAILED(ret) || dev == nullptr) { if (FAILED(ret) || dev == nullptr) {
return ret; return ret;
} }
// mute the first few buffers to avoid a pop on stream start // mute the first few buffers to avoid a pop on stream start
float gain = boost; float gain = boost;
if (this->buffers_to_mute > 0) { if (this->buffers_to_mute > 0) {
gain = 0.0f; gain = 0.0f;
this->buffers_to_mute--; this->buffers_to_mute--;
} }
this->write_output(dev, to_write, gain); this->write_output(dev, to_write, gain);
ret = real->ReleaseBuffer(to_write, 0); ret = real->ReleaseBuffer(to_write, 0);
// drop the frames just written from the front of the pending FIFO // drop the frames just written from the front of the pending FIFO
this->out_float.erase(this->out_float.begin(), this->out_float.erase(this->out_float.begin(),
this->out_float.begin() + (size_t) to_write * ch); this->out_float.begin() + (size_t) to_write * ch);
return ret; return ret;
} }
} }
+149 -149
View File
@@ -1,149 +1,149 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <optional> #include <optional>
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
struct IAudioClient; struct IAudioClient;
struct IAudioRenderClient; struct IAudioRenderClient;
namespace hooks::audio { namespace hooks::audio {
// Streaming sample-rate converter for the WASAPI render path. The real device is opened at the // Streaming sample-rate converter for the WASAPI render path. The real device is opened at the
// target rate while the game keeps writing its native-rate audio into a scratch buffer; on // target rate while the game keeps writing its native-rate audio into a scratch buffer; on
// release that buffer is converted with a windowed-sinc kernel and pushed to the device. // release that buffer is converted with a windowed-sinc kernel and pushed to the device.
// Channel count and sample format are preserved; only the sample rate changes. // Channel count and sample format are preserved; only the sample rate changes.
// //
// Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful: // Frame counts differ between the two rates, so unlike the per-frame downmix this is stateful:
// a fractional read position and a window of input history carry across ReleaseBuffer calls, // a fractional read position and a window of input history carry across ReleaseBuffer calls,
// and the device buffer is only filled up to the space the device currently has free. // and the device buffer is only filled up to the space the device currently has free.
struct Resampler { struct Resampler {
// whether the resampler is active for the current stream // whether the resampler is active for the current stream
bool enabled = false; bool enabled = false;
// whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven // whether the stream is event-driven (AUDCLNT_STREAMFLAGS_EVENTCALLBACK). timer-driven
// streams instead poll padding and write variable partial chunks, so they drain the // streams instead poll padding and write variable partial chunks, so they drain the
// pending output to the device's free space rather than pushing a full buffer per period. // pending output to the device's free space rather than pushing a full buffer per period.
bool event_driven = true; bool event_driven = true;
// decide whether the stream should be resampled and to which rate. returns the target rate // decide whether the stream should be resampled and to which rate. returns the target rate
// when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt. // when RESAMPLE_RATE is set and differs from the game's rate, otherwise nullopt.
static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format); static std::optional<uint32_t> resolve(const WAVEFORMATEX *game_format);
// enable resampling for game_format and fill device_out with the equivalent format at the // enable resampling for game_format and fill device_out with the equivalent format at the
// target rate to open the real device with. // target rate to open the real device with.
void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out, void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *device_out,
uint32_t target_rate); uint32_t target_rate);
// build the device format equivalent to game_format at target_rate (same channels/depth). // build the device format equivalent to game_format at target_rate (same channels/depth).
static void make_device_format(const WAVEFORMATEX *game_format, static void make_device_format(const WAVEFORMATEX *game_format,
WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate); WAVEFORMATEXTENSIBLE *device_out, uint32_t target_rate);
// initialize the real device at the target rate, performing the standard WASAPI buffer // initialize the real device at the target rate, performing the standard WASAPI buffer
// realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED. // realignment retry on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED.
HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags,
REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity,
const WAVEFORMATEX *device_format, LPCGUID session_guid); const WAVEFORMATEX *device_format, LPCGUID session_guid);
// translate a device-rate frame count to the equivalent game-rate count, so the buffer-size // translate a device-rate frame count to the equivalent game-rate count, so the buffer-size
// and padding values reported to the game stay paced at the game's native rate. // and padding values reported to the game stay paced at the game's native rate.
UINT32 frames_device_to_game(UINT32 device_frames) const; UINT32 frames_device_to_game(UINT32 device_frames) const;
UINT32 padding_device_to_game(UINT32 device_padding) const; UINT32 padding_device_to_game(UINT32 device_padding) const;
// hand the game a scratch buffer sized for `frames` of its native format to write into. // hand the game a scratch buffer sized for `frames` of its native format to write into.
HRESULT get_buffer(UINT32 frames, BYTE **ppData); HRESULT get_buffer(UINT32 frames, BYTE **ppData);
// pointer to the input scratch (sized by get_buffer). when chained after the downmix, the // pointer to the input scratch (sized by get_buffer). when chained after the downmix, the
// downmix writes its stereo output here for the resampler to consume on the next flush. // downmix writes its stereo output here for the resampler to consume on the next flush.
BYTE *input_data() { return this->scratch.data(); } BYTE *input_data() { return this->scratch.data(); }
// convert the `frames` the game wrote and push output to the real render client. `boost` // convert the `frames` the game wrote and push output to the real render client. `boost`
// is applied to the converted output. event-driven streams fill exactly one device buffer // is applied to the converted output. event-driven streams fill exactly one device buffer
// per period; timer-driven streams push as many converted frames as the device has free. // per period; timer-driven streams push as many converted frames as the device has free.
HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags, HRESULT flush(IAudioRenderClient *real, IAudioClient *client, UINT32 frames, DWORD flags,
float boost); float boost);
private: private:
// append `frames` of the scratch buffer (native format), or silence, to the input queue // append `frames` of the scratch buffer (native format), or silence, to the input queue
void enqueue_input(UINT32 frames, bool silent); void enqueue_input(UINT32 frames, bool silent);
// event-driven path: produce exactly one full device buffer and push it. // event-driven path: produce exactly one full device buffer and push it.
HRESULT flush_event(IAudioRenderClient *real, float boost); HRESULT flush_event(IAudioRenderClient *real, float boost);
// timer-driven path: convert all queued input into the pending output FIFO, then push as // timer-driven path: convert all queued input into the pending output FIFO, then push as
// many frames as the device currently has free, keeping the remainder for the next call. // many frames as the device currently has free, keeping the remainder for the next call.
HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost); HRESULT flush_timer(IAudioRenderClient *real, IAudioClient *client, float boost);
// produce exactly out_frames output frames using the fixed src/dst ratio. event-driven // produce exactly out_frames output frames using the fixed src/dst ratio. event-driven
// exclusive streams must fill the whole device buffer every period; a small input cushion // exclusive streams must fill the whole device buffer every period; a small input cushion
// is buffered first (see priming) so the sinc kernel always has lookahead. // is buffered first (see priming) so the sinc kernel always has lookahead.
UINT32 produce_exact(UINT32 out_frames); UINT32 produce_exact(UINT32 out_frames);
// convert all input the kernel can fully support into the pending output FIFO (out_float), // convert all input the kernel can fully support into the pending output FIFO (out_float),
// appending without clearing. returns the number of frames produced. used by the // appending without clearing. returns the number of frames produced. used by the
// timer-driven path where output is drained to the device in device-paced chunks. // timer-driven path where output is drained to the device in device-paced chunks.
UINT32 produce_variable(); UINT32 produce_variable();
// convolve the windowed-sinc kernel at the current in_pos and append the resulting frame // convolve the windowed-sinc kernel at the current in_pos and append the resulting frame
// (one sample per channel) to out_float // (one sample per channel) to out_float
void emit_frame(); void emit_frame();
// precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so // precompute the windowed-sinc kernel sampled at kernel_phases sub-sample positions, so
// emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too // emit_frame is a table lookup instead of recomputing sin/cos per tap (which is far too
// expensive to run per sample on the audio callback thread and causes underrun crackle). // expensive to run per sample on the audio callback thread and causes underrun crackle).
void build_kernel(); void build_kernel();
// drop input frames that in_pos has advanced past, keeping a window of history for the // drop input frames that in_pos has advanced past, keeping a window of history for the
// next block's left context // next block's left context
void drop_consumed(); void drop_consumed();
// convert the first `frames` of out_float to the device format, scaled by `gain` // convert the first `frames` of out_float to the device format, scaled by `gain`
void write_output(BYTE *dst, UINT32 frames, float gain) const; void write_output(BYTE *dst, UINT32 frames, float gain) const;
// sample format of the stream // sample format of the stream
int channels = 0; int channels = 0;
int bytes_per_sample = 0; int bytes_per_sample = 0;
bool is_float = false; bool is_float = false;
int game_frame_size = 0; int game_frame_size = 0;
uint32_t src_rate = 0; uint32_t src_rate = 0;
uint32_t dst_rate = 0; uint32_t dst_rate = 0;
// sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius // sinc low-pass cutoff (1.0 when upsampling, dst/src when downsampling) and window radius
double cutoff = 1.0; double cutoff = 1.0;
int half_taps = 16; int half_taps = 16;
// precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the // precomputed kernel: (kernel_phases + 1) rows of 2*half_taps weights, indexed by the
// fractional sample position (linearly interpolated between adjacent rows in emit_frame) // fractional sample position (linearly interpolated between adjacent rows in emit_frame)
std::vector<float> kernel_table; std::vector<float> kernel_table;
int kernel_phases = 1024; int kernel_phases = 1024;
// interleaved float input queue and the fractional read position within it (in frames) // interleaved float input queue and the fractional read position within it (in frames)
std::vector<float> in_queue; std::vector<float> in_queue;
double in_pos = 0.0; double in_pos = 0.0;
// emit silence until a full block of input lookahead has accumulated, so the sinc kernel // emit silence until a full block of input lookahead has accumulated, so the sinc kernel
// never reads past the end of the queue (which would distort the tail of every buffer) // never reads past the end of the queue (which would distort the tail of every buffer)
bool priming = true; bool priming = true;
// interleaved float scratch for produced output // interleaved float scratch for produced output
std::vector<float> out_float; std::vector<float> out_float;
// buffer the game writes its native-rate audio into between get_buffer / flush // buffer the game writes its native-rate audio into between get_buffer / flush
std::vector<BYTE> scratch; std::vector<BYTE> scratch;
// cached device buffer size (frames); a full buffer is produced every period // cached device buffer size (frames); a full buffer is produced every period
UINT32 device_buffer_frames = 0; UINT32 device_buffer_frames = 0;
// leading buffers to silence to avoid a pop on stream start // leading buffers to silence to avoid a pop on stream start
int buffers_to_mute = 16; int buffers_to_mute = 16;
}; };
} }
+187 -187
View File
@@ -1,187 +1,187 @@
#include "shared.h" #include "shared.h"
#include <algorithm> #include <algorithm>
#include <audioclient.h> #include <audioclient.h>
#include "hooks/audio/audio.h" #include "hooks/audio/audio.h"
#include "util/logging.h" #include "util/logging.h"
#include "util.h" #include "util.h"
#include "defs.h" #include "defs.h"
namespace hooks::audio { namespace hooks::audio {
// whether the engine's PCM converter can handle this format. PCM / float only; non-PCM // whether the engine's PCM converter can handle this format. PCM / float only; non-PCM
// bitstream (AC-3 / DTS passthrough) must be left alone. // bitstream (AC-3 / DTS passthrough) must be left alone.
static bool is_pcm_or_float(const WAVEFORMATEX *format) { static bool is_pcm_or_float(const WAVEFORMATEX *format) {
if (format == nullptr) { if (format == nullptr) {
return false; return false;
} }
switch (format->wFormatTag) { switch (format->wFormatTag) {
case WAVE_FORMAT_PCM: case WAVE_FORMAT_PCM:
case WAVE_FORMAT_IEEE_FLOAT: case WAVE_FORMAT_IEEE_FLOAT:
return true; return true;
case WAVE_FORMAT_EXTENSIBLE: { case WAVE_FORMAT_EXTENSIBLE: {
// SubFormat is only valid when the extra-bytes block is large enough // SubFormat is only valid when the extra-bytes block is large enough
if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { if (format->cbSize < sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
return false; return false;
} }
const auto *ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format); const auto *ext = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format);
return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM return ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_PCM
|| ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; || ext->SubFormat == GUID_KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
} }
default: default:
return false; return false;
} }
} }
bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) { bool SharedRedirect::wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format) {
// only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can // only redirect PCM / float exclusive streams: the engine converter (AUTOCONVERTPCM) can
// handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode, // handle those, but non-PCM bitstream (AC-3 / DTS passthrough) would fail in shared mode,
// so leave it in exclusive untouched. // so leave it in exclusive untouched.
return hooks::audio::WASAPI_COMPATIBILITY_MODE return hooks::audio::WASAPI_COMPATIBILITY_MODE
&& share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE && share_mode == AUDCLNT_SHAREMODE_EXCLUSIVE
&& is_pcm_or_float(format); && is_pcm_or_float(format);
} }
void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, void SharedRedirect::apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags,
REFERENCE_TIME *periodicity) { REFERENCE_TIME *periodicity) {
// shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's // shared mode requires periodicity == 0; AUTOCONVERTPCM lets the engine accept the game's
// native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT). // native format (else shared Initialize returns AUDCLNT_E_UNSUPPORTED_FORMAT).
log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode"); log_info("audio::wasapi", "redirecting exclusive WASAPI to shared mode");
*share_mode = AUDCLNT_SHAREMODE_SHARED; *share_mode = AUDCLNT_SHAREMODE_SHARED;
*periodicity = 0; *periodicity = 0;
*stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; *stream_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
this->redirected_from_exclusive = true; this->redirected_from_exclusive = true;
} }
UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 SharedRedirect::clamp_buffer_size(IAudioClient *real, uint32_t sample_rate,
UINT32 device_frames) { UINT32 device_frames) {
if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) { if (!this->redirected_from_exclusive || real == nullptr || sample_rate == 0 || device_frames == 0) {
this->reported_frames = device_frames; this->reported_frames = device_frames;
return device_frames; return device_frames;
} }
// GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so // GetDevicePeriod returns REFERENCE_TIME units (100 ns), 10^7 per second, so
// period_frames = period * sample_rate / 10^7. // period_frames = period * sample_rate / 10^7.
REFERENCE_TIME period = 0; REFERENCE_TIME period = 0;
if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) { if (SUCCEEDED(real->GetDevicePeriod(&period, nullptr)) && period > 0) {
const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000); const UINT32 period_frames = (UINT32) ((period * sample_rate) / 10000000);
if (period_frames > 0 && period_frames < device_frames) { if (period_frames > 0 && period_frames < device_frames) {
this->reported_frames = period_frames; this->reported_frames = period_frames;
return period_frames; return period_frames;
} }
} }
this->reported_frames = device_frames; this->reported_frames = device_frames;
return device_frames; return device_frames;
} }
void SharedRedirect::enable_bridge(int frame_bytes) { void SharedRedirect::enable_bridge(int frame_bytes) {
if (!this->redirected_from_exclusive || frame_bytes <= 0) { if (!this->redirected_from_exclusive || frame_bytes <= 0) {
return; return;
} }
this->frame_bytes = frame_bytes; this->frame_bytes = frame_bytes;
this->device_buffer_frames = 0; this->device_buffer_frames = 0;
this->fifo.clear(); this->fifo.clear();
log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)", log_info("audio::wasapi", "shared-mode buffer bridge enabled (frame size {} bytes)",
frame_bytes); frame_bytes);
} }
BYTE *SharedRedirect::begin_write(UINT32 frames) { BYTE *SharedRedirect::begin_write(UINT32 frames) {
// reserve space at the FIFO tail and let the game write straight into it - no scratch copy. // reserve space at the FIFO tail and let the game write straight into it - no scratch copy.
this->pending_write_offset = this->fifo.size(); this->pending_write_offset = this->fifo.size();
this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes); this->fifo.resize(this->pending_write_offset + (size_t) frames * this->frame_bytes);
return this->fifo.data() + this->pending_write_offset; return this->fifo.data() + this->pending_write_offset;
} }
void SharedRedirect::commit_write(UINT32 frames, bool silent) { void SharedRedirect::commit_write(UINT32 frames, bool silent) {
// trim the tail reservation to the frames actually written; zero it in place if silent. // trim the tail reservation to the frames actually written; zero it in place if silent.
const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes; const size_t end = this->pending_write_offset + (size_t) frames * this->frame_bytes;
if (silent) { if (silent) {
std::fill(this->fifo.begin() + this->pending_write_offset, std::fill(this->fifo.begin() + this->pending_write_offset,
this->fifo.begin() + end, (BYTE) 0); this->fifo.begin() + end, (BYTE) 0);
} }
this->fifo.resize(end); this->fifo.resize(end);
} }
UINT32 SharedRedirect::pending_frames() const { UINT32 SharedRedirect::pending_frames() const {
if (this->frame_bytes <= 0) { if (this->frame_bytes <= 0) {
return 0; return 0;
} }
return (UINT32) (this->fifo.size() / this->frame_bytes); return (UINT32) (this->fifo.size() / this->frame_bytes);
} }
UINT32 SharedRedirect::virtual_padding() const { UINT32 SharedRedirect::virtual_padding() const {
const UINT32 pending = this->pending_frames(); const UINT32 pending = this->pending_frames();
return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending; return this->reported_frames > 0 ? std::min(pending, this->reported_frames) : pending;
} }
HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client, HRESULT SharedRedirect::drain(IAudioRenderClient *real, IAudioClient *client,
const WAVEFORMATEXTENSIBLE &device_format, float boost) { const WAVEFORMATEXTENSIBLE &device_format, float boost) {
if (!this->bridge_enabled()) { if (!this->bridge_enabled()) {
return S_OK; return S_OK;
} }
// cache the real device buffer size once; it is fixed for the life of the stream. // cache the real device buffer size once; it is fixed for the life of the stream.
if (this->device_buffer_frames == 0) { if (this->device_buffer_frames == 0) {
if (FAILED(client->GetBufferSize(&this->device_buffer_frames)) if (FAILED(client->GetBufferSize(&this->device_buffer_frames))
|| this->device_buffer_frames == 0) { || this->device_buffer_frames == 0) {
return S_OK; return S_OK;
} }
} }
const UINT32 pending = this->pending_frames(); const UINT32 pending = this->pending_frames();
if (pending == 0) { if (pending == 0) {
return S_OK; return S_OK;
} }
// push only as many frames as the device currently has free, keeping the rest queued. this // push only as many frames as the device currently has free, keeping the rest queued. this
// self-paces to the engine's real consumption so a full-buffer write never overflows. // self-paces to the engine's real consumption so a full-buffer write never overflows.
UINT32 padding = 0; UINT32 padding = 0;
if (FAILED(client->GetCurrentPadding(&padding))) { if (FAILED(client->GetCurrentPadding(&padding))) {
return S_OK; return S_OK;
} }
const UINT32 device_free = this->device_buffer_frames > padding const UINT32 device_free = this->device_buffer_frames > padding
? this->device_buffer_frames - padding ? this->device_buffer_frames - padding
: 0; : 0;
if (device_free == 0) { if (device_free == 0) {
return S_OK; return S_OK;
} }
const UINT32 to_write = std::min(pending, device_free); const UINT32 to_write = std::min(pending, device_free);
BYTE *dev = nullptr; BYTE *dev = nullptr;
HRESULT ret = real->GetBuffer(to_write, &dev); HRESULT ret = real->GetBuffer(to_write, &dev);
if (FAILED(ret) || dev == nullptr) { if (FAILED(ret) || dev == nullptr) {
return ret; return ret;
} }
const size_t bytes = (size_t) to_write * this->frame_bytes; const size_t bytes = (size_t) to_write * this->frame_bytes;
std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev); std::copy(this->fifo.begin(), this->fifo.begin() + bytes, dev);
// mute the first few buffers to avoid a startup pop, then apply the volume boost. // mute the first few buffers to avoid a startup pop, then apply the volume boost.
if (this->buffers_to_mute > 0) { if (this->buffers_to_mute > 0) {
std::fill(dev, dev + bytes, (BYTE) 0); std::fill(dev, dev + bytes, (BYTE) 0);
this->buffers_to_mute--; this->buffers_to_mute--;
} else if (boost != 1.0f) { } else if (boost != 1.0f) {
apply_gain(dev, to_write, device_format, boost); apply_gain(dev, to_write, device_format, boost);
} }
ret = real->ReleaseBuffer(to_write, 0); ret = real->ReleaseBuffer(to_write, 0);
// drop the frames just handed to the device from the front of the FIFO. // drop the frames just handed to the device from the front of the FIFO.
this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes); this->fifo.erase(this->fifo.begin(), this->fifo.begin() + bytes);
return ret; return ret;
} }
} }
@@ -1,83 +1,83 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include <mmreg.h> #include <mmreg.h>
#include <audioclient.h> #include <audioclient.h>
struct IAudioRenderClient; struct IAudioRenderClient;
namespace hooks::audio { namespace hooks::audio {
// The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps // The -wasapishared option redirects an exclusive WASAPI stream to shared mode, so other apps
// can play sound and devices that can't open the exclusive format still work, at the cost of // can play sound and devices that can't open the exclusive format still work, at the cost of
// some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone. // some latency. Only PCM / float is converted; bitstream (AC-3 / DTS) is left alone.
struct SharedRedirect { struct SharedRedirect {
// true once apply() has redirected an exclusive request. gates the buffer clamp; stays false // true once apply() has redirected an exclusive request. gates the buffer clamp; stays false
// for a natively-shared stream (it paces itself, so must not be clamped). // for a natively-shared stream (it paces itself, so must not be clamped).
bool redirected_from_exclusive = false; bool redirected_from_exclusive = false;
// whether an exclusive-mode request should be redirected, given the -wasapishared option. // whether an exclusive-mode request should be redirected, given the -wasapishared option.
// only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode. // only PCM / float is eligible; bitstream (AC-3 / DTS) is left in exclusive mode.
static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format); static bool wants(AUDCLNT_SHAREMODE share_mode, const WAVEFORMATEX *format);
// redirect an exclusive request to shared mode. caller must have checked wants() first. // redirect an exclusive request to shared mode. caller must have checked wants() first.
void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity); void apply(AUDCLNT_SHAREMODE *share_mode, DWORD *stream_flags, REFERENCE_TIME *periodicity);
// clamp a reported buffer size to one device period. the FIFO bridge below is what prevents // clamp a reported buffer size to one device period. the FIFO bridge below is what prevents
// the overflow; this just keeps the game's per-event writes small so the bridge adds minimal // the overflow; this just keeps the game's per-event writes small so the bridge adds minimal
// latency. caches the chosen value for virtual_padding. a no-op unless redirected. // latency. caches the chosen value for virtual_padding. a no-op unless redirected.
UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames); UINT32 clamp_buffer_size(IAudioClient *real, uint32_t sample_rate, UINT32 device_frames);
// FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own // FIFO bridge: the redirected game writes a whole reported buffer per event paced by its own
// callback, not the shared engine clock, so a full-buffer write can intermittently exceed the // callback, not the shared engine clock, so a full-buffer write can intermittently exceed the
// double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes // double-buffered shared free space (AUDCLNT_E_BUFFER_TOO_LARGE). The game instead writes
// directly into a FIFO that is drained to the device only as fast as it frees space - the // directly into a FIFO that is drained to the device only as fast as it frees space - the
// same free-space-clamped approach the timer-driven resampler uses. // same free-space-clamped approach the timer-driven resampler uses.
// arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size // arm the bridge once the redirected stream is initialized. frame_bytes is one frame's size
// in the game's (== device, via AUTOCONVERTPCM) format. // in the game's (== device, via AUTOCONVERTPCM) format.
void enable_bridge(int frame_bytes); void enable_bridge(int frame_bytes);
// whether the FIFO bridge is active (a redirect was applied and armed). // whether the FIFO bridge is active (a redirect was applied and armed).
bool bridge_enabled() const { return this->frame_bytes > 0; } bool bridge_enabled() const { return this->frame_bytes > 0; }
// reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place. // reserve `frames` at the FIFO tail and hand the game a pointer into it to write in place.
// must be paired with commit_write, which trims the reservation to the frames written. // must be paired with commit_write, which trims the reservation to the frames written.
BYTE *begin_write(UINT32 frames); BYTE *begin_write(UINT32 frames);
// trim the reservation from begin_write to the `frames` actually written (zeroing if silent). // trim the reservation from begin_write to the `frames` actually written (zeroing if silent).
void commit_write(UINT32 frames, bool silent); void commit_write(UINT32 frames, bool silent);
// padding to report to a game that polls GetCurrentPadding while the bridge is active: the // padding to report to a game that polls GetCurrentPadding while the bridge is active: the
// FIFO fill level, capped to the reported buffer size so the game's free-space calculation // FIFO fill level, capped to the reported buffer size so the game's free-space calculation
// (reported_buffer - padding) reflects room in the virtual buffer rather than the device's. // (reported_buffer - padding) reflects room in the virtual buffer rather than the device's.
UINT32 virtual_padding() const; UINT32 virtual_padding() const;
// push as many queued frames as the real device has free, applying `boost`, keeping the rest // push as many queued frames as the real device has free, applying `boost`, keeping the rest
// for the next call. `real` is the wrapped render client's underlying interface; `client` is // for the next call. `real` is the wrapped render client's underlying interface; `client` is
// the underlying audio client used to query the device's free space. // the underlying audio client used to query the device's free space.
HRESULT drain(IAudioRenderClient *real, IAudioClient *client, HRESULT drain(IAudioRenderClient *real, IAudioClient *client,
const WAVEFORMATEXTENSIBLE &device_format, float boost); const WAVEFORMATEXTENSIBLE &device_format, float boost);
private: private:
// frames currently queued in the FIFO and not yet handed to the device. // frames currently queued in the FIFO and not yet handed to the device.
UINT32 pending_frames() const; UINT32 pending_frames() const;
// FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the // FIFO bridge state (see enable_bridge). fifo holds audio queued for the device in the
// game's interleaved frame format; the game writes new frames directly into its tail between // game's interleaved frame format; the game writes new frames directly into its tail between
// begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see // begin_write and commit_write. frame_bytes > 0 doubles as the "bridge armed" flag (see
// bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write. // bridge_enabled). pending_write_offset marks the tail reservation handed to begin_write.
int frame_bytes = 0; int frame_bytes = 0;
UINT32 device_buffer_frames = 0; UINT32 device_buffer_frames = 0;
UINT32 reported_frames = 0; UINT32 reported_frames = 0;
int buffers_to_mute = 4; int buffers_to_mute = 4;
size_t pending_write_offset = 0; size_t pending_write_offset = 0;
std::vector<BYTE> fifo; std::vector<BYTE> fifo;
}; };
} }
+392 -392
View File
@@ -1,393 +1,393 @@
#include "xact.h" #include "xact.h"
#include <atomic> #include <atomic>
#include <string> #include <string>
#include <windows.h> #include <windows.h>
#include <initguid.h> #include <initguid.h>
#include <mmreg.h> #include <mmreg.h>
#include <objbase.h> #include <objbase.h>
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
namespace hooks::audio::xact { namespace hooks::audio::xact {
// XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2 // XAudio 2.7 is a COM API. Newer Windows SDKs expose a different IXAudio2
// layout, so keep this proxy pinned to the legacy ABI used by libxact. // layout, so keep this proxy pinned to the legacy ABI used by libxact.
struct XAudio2DeviceDetails { struct XAudio2DeviceDetails {
WCHAR device_id[256]; WCHAR device_id[256];
WCHAR display_name[256]; WCHAR display_name[256];
DWORD role; DWORD role;
WAVEFORMATEXTENSIBLE output_format; WAVEFORMATEXTENSIBLE output_format;
}; };
struct XAudio2EffectChain { struct XAudio2EffectChain {
UINT32 effect_count; UINT32 effect_count;
const void *effect_descriptors; const void *effect_descriptors;
}; };
struct IXAudio2_27 { struct IXAudio2_27 {
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0; virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef() = 0; virtual ULONG STDMETHODCALLTYPE AddRef() = 0;
virtual ULONG STDMETHODCALLTYPE Release() = 0; virtual ULONG STDMETHODCALLTYPE Release() = 0;
virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0; virtual HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) = 0;
virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails( virtual HRESULT STDMETHODCALLTYPE GetDeviceDetails(
UINT32 device_index, UINT32 device_index,
XAudio2DeviceDetails *device_details) = 0; XAudio2DeviceDetails *device_details) = 0;
virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0; virtual HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) = 0;
virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0; virtual HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) = 0;
virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0; virtual void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice( virtual HRESULT STDMETHODCALLTYPE CreateSourceVoice(
void **source_voice, void **source_voice,
const WAVEFORMATEX *source_format, const WAVEFORMATEX *source_format,
UINT32 flags, UINT32 flags,
float max_frequency_ratio, float max_frequency_ratio,
void *callback, void *callback,
const void *send_list, const void *send_list,
const XAudio2EffectChain *effect_chain) = 0; const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice( virtual HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
void **submix_voice, void **submix_voice,
UINT32 input_channels, UINT32 input_channels,
UINT32 input_sample_rate, UINT32 input_sample_rate,
UINT32 flags, UINT32 flags,
UINT32 processing_stage, UINT32 processing_stage,
const void *send_list, const void *send_list,
const XAudio2EffectChain *effect_chain) = 0; const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice( virtual HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
void **mastering_voice, void **mastering_voice,
UINT32 input_channels, UINT32 input_channels,
UINT32 input_sample_rate, UINT32 input_sample_rate,
UINT32 flags, UINT32 flags,
UINT32 device_index, UINT32 device_index,
const XAudio2EffectChain *effect_chain) = 0; const XAudio2EffectChain *effect_chain) = 0;
virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0; virtual HRESULT STDMETHODCALLTYPE StartEngine() = 0;
virtual void STDMETHODCALLTYPE StopEngine() = 0; virtual void STDMETHODCALLTYPE StopEngine() = 0;
virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0; virtual HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) = 0;
virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0; virtual void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) = 0;
virtual void STDMETHODCALLTYPE SetDebugConfiguration( virtual void STDMETHODCALLTYPE SetDebugConfiguration(
const void *debug_configuration, const void *debug_configuration,
void *reserved) = 0; void *reserved) = 0;
}; };
// XAudio2 2.7 COM class and interface. // XAudio2 2.7 COM class and interface.
DEFINE_GUID(CLSID_XAudio2_7_LEGACY, DEFINE_GUID(CLSID_XAudio2_7_LEGACY,
0x5a508685, 0xa254, 0x4fba, 0x5a508685, 0xa254, 0x4fba,
0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf); 0x9b, 0x82, 0x9a, 0x24, 0xb0, 0x03, 0x06, 0xaf);
DEFINE_GUID(IID_IXAudio2_7_LEGACY, DEFINE_GUID(IID_IXAudio2_7_LEGACY,
0x8bcf1f58, 0x9fe7, 0x4583, 0x8bcf1f58, 0x9fe7, 0x4583,
0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb); 0x8a, 0xc6, 0xe2, 0xad, 0xc4, 0x65, 0xc8, 0xbb);
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr; static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32); using CreateFX_t = HRESULT (WINAPI *)(REFCLSID, IUnknown **, const void *, UINT32);
static CreateFX_t CreateFX_orig = nullptr; static CreateFX_t CreateFX_orig = nullptr;
static std::string describe_wave_format(const WAVEFORMATEX *format) { static std::string describe_wave_format(const WAVEFORMATEX *format) {
if (format == nullptr) { if (format == nullptr) {
return "null"; return "null";
} }
DWORD channel_mask = 0; DWORD channel_mask = 0;
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE && if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE &&
format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) { format->cbSize >= sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX)) {
channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask; channel_mask = reinterpret_cast<const WAVEFORMATEXTENSIBLE *>(format)->dwChannelMask;
} }
return fmt::format( return fmt::format(
"tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}", "tag=0x{:04x}, channels={}, rate={} Hz, bits={}, valid_block={} B, avg={} B/s, mask=0x{:08x}",
format->wFormatTag, format->wFormatTag,
format->nChannels, format->nChannels,
format->nSamplesPerSec, format->nSamplesPerSec,
format->wBitsPerSample, format->wBitsPerSample,
format->nBlockAlign, format->nBlockAlign,
format->nAvgBytesPerSec, format->nAvgBytesPerSec,
channel_mask); channel_mask);
} }
template <size_t Size> template <size_t Size>
static std::string narrow_fixed(const WCHAR (&value)[Size]) { static std::string narrow_fixed(const WCHAR (&value)[Size]) {
size_t length = 0; size_t length = 0;
while (length < Size && value[length] != L'\0') { while (length < Size && value[length] != L'\0') {
length++; length++;
} }
return ws2s(std::wstring(value, length)); return ws2s(std::wstring(value, length));
} }
class WrappedXAudio2 final : public IXAudio2_27 { class WrappedXAudio2 final : public IXAudio2_27 {
public: public:
explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) { explicit WrappedXAudio2(IXAudio2_27 *real) : real(real) {
log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real)); log_info("audio::xaudio2", "wrapping IXAudio2 2.7 engine {}", static_cast<void *>(real));
} }
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override { HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **object) override {
if (object == nullptr) { if (object == nullptr) {
return E_POINTER; return E_POINTER;
} }
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) { if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, IID_IXAudio2_7_LEGACY)) {
*object = this; *object = this;
AddRef(); AddRef();
log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid)); log_info("audio::xaudio2", "IXAudio2::QueryInterface({}) -> proxy", guid2s(riid));
return S_OK; return S_OK;
} }
const auto result = real->QueryInterface(riid, object); const auto result = real->QueryInterface(riid, object);
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::QueryInterface({}) -> {}, object={}", "IXAudio2::QueryInterface({}) -> {}, object={}",
guid2s(riid), guid2s(riid),
FMT_HRESULT(result), FMT_HRESULT(result),
object != nullptr ? *object : nullptr); object != nullptr ? *object : nullptr);
return result; return result;
} }
ULONG STDMETHODCALLTYPE AddRef() override { ULONG STDMETHODCALLTYPE AddRef() override {
return ++ref_count; return ++ref_count;
} }
ULONG STDMETHODCALLTYPE Release() override { ULONG STDMETHODCALLTYPE Release() override {
const auto remaining = --ref_count; const auto remaining = --ref_count;
if (remaining == 0) { if (remaining == 0) {
log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy"); log_info("audio::xaudio2", "destroying IXAudio2 2.7 proxy");
real->Release(); real->Release();
delete this; delete this;
} }
return remaining; return remaining;
} }
HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override { HRESULT STDMETHODCALLTYPE GetDeviceCount(UINT32 *device_count) override {
const auto result = real->GetDeviceCount(device_count); const auto result = real->GetDeviceCount(device_count);
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::GetDeviceCount -> {}, count={}", "IXAudio2::GetDeviceCount -> {}, count={}",
FMT_HRESULT(result), FMT_HRESULT(result),
SUCCEEDED(result) && device_count != nullptr ? *device_count : 0); SUCCEEDED(result) && device_count != nullptr ? *device_count : 0);
return result; return result;
} }
HRESULT STDMETHODCALLTYPE GetDeviceDetails( HRESULT STDMETHODCALLTYPE GetDeviceDetails(
UINT32 device_index, UINT32 device_index,
XAudio2DeviceDetails *device_details) override { XAudio2DeviceDetails *device_details) override {
const auto result = real->GetDeviceDetails(device_index, device_details); const auto result = real->GetDeviceDetails(device_index, device_details);
if (SUCCEEDED(result) && device_details != nullptr) { if (SUCCEEDED(result) && device_details != nullptr) {
const auto device_name = narrow_fixed(device_details->display_name); const auto device_name = narrow_fixed(device_details->display_name);
if (!device_details_logged.exchange(true, std::memory_order_relaxed)) { if (!device_details_logged.exchange(true, std::memory_order_relaxed)) {
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}", "IXAudio2::GetDeviceDetails({}) -> {}, id='{}', name='{}', role=0x{:08x}, {}",
device_index, device_index,
FMT_HRESULT(result), FMT_HRESULT(result),
narrow_fixed(device_details->device_id), narrow_fixed(device_details->device_id),
device_name, device_name,
device_details->role, device_details->role,
describe_wave_format(&device_details->output_format.Format)); describe_wave_format(&device_details->output_format.Format));
} }
const auto channels = device_details->output_format.Format.nChannels; const auto channels = device_details->output_format.Format.nChannels;
if (channels != 2 && channels != 6 && if (channels != 2 && channels != 6 &&
!channel_warning_logged.exchange(true, std::memory_order_relaxed)) { !channel_warning_logged.exchange(true, std::memory_order_relaxed)) {
log_warning( log_warning(
"audio::xaudio2", "audio::xaudio2",
"output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output", "output device '{}' has {} channels; Nostalgia requires stereo or 5.1 output",
device_name, device_name,
channels); channels);
deferredlogs::defer_error_messages({ deferredlogs::defer_error_messages({
"unsupported audio output channel count detected!", "unsupported audio output channel count detected!",
fmt::format(" device: {}", device_name), fmt::format(" device: {}", device_name),
fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels), fmt::format(" detected {} channels; Nostalgia requires 2 (stereo) or 6 (5.1)", channels),
" * configure the default Windows playback device for stereo or 5.1 output", " * configure the default Windows playback device for stereo or 5.1 output",
" * disable 7.1 surround sound or spatial audio for this device", " * disable 7.1 surround sound or spatial audio for this device",
}); });
} }
} else { } else {
log_warning( log_warning(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::GetDeviceDetails({}) -> {}", "IXAudio2::GetDeviceDetails({}) -> {}",
device_index, device_index,
FMT_HRESULT(result)); FMT_HRESULT(result));
} }
return result; return result;
} }
HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override { HRESULT STDMETHODCALLTYPE Initialize(UINT32 flags, UINT32 processor) override {
const auto result = real->Initialize(flags, processor); const auto result = real->Initialize(flags, processor);
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}", "IXAudio2::Initialize(flags=0x{:08x}, processor=0x{:08x}) -> {}",
flags, flags,
processor, processor,
FMT_HRESULT(result)); FMT_HRESULT(result));
return result; return result;
} }
HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override { HRESULT STDMETHODCALLTYPE RegisterForCallbacks(void *callback) override {
const auto result = real->RegisterForCallbacks(callback); const auto result = real->RegisterForCallbacks(callback);
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::RegisterForCallbacks({}) -> {}", "IXAudio2::RegisterForCallbacks({}) -> {}",
callback, callback,
FMT_HRESULT(result)); FMT_HRESULT(result));
return result; return result;
} }
void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override { void STDMETHODCALLTYPE UnregisterForCallbacks(void *callback) override {
log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback); log_info("audio::xaudio2", "IXAudio2::UnregisterForCallbacks({})", callback);
real->UnregisterForCallbacks(callback); real->UnregisterForCallbacks(callback);
} }
HRESULT STDMETHODCALLTYPE CreateSourceVoice( HRESULT STDMETHODCALLTYPE CreateSourceVoice(
void **source_voice, void **source_voice,
const WAVEFORMATEX *source_format, const WAVEFORMATEX *source_format,
UINT32 flags, UINT32 flags,
float max_frequency_ratio, float max_frequency_ratio,
void *callback, void *callback,
const void *send_list, const void *send_list,
const XAudio2EffectChain *effect_chain) override { const XAudio2EffectChain *effect_chain) override {
return real->CreateSourceVoice( return real->CreateSourceVoice(
source_voice, source_voice,
source_format, source_format,
flags, flags,
max_frequency_ratio, max_frequency_ratio,
callback, callback,
send_list, send_list,
effect_chain); effect_chain);
} }
HRESULT STDMETHODCALLTYPE CreateSubmixVoice( HRESULT STDMETHODCALLTYPE CreateSubmixVoice(
void **submix_voice, void **submix_voice,
UINT32 input_channels, UINT32 input_channels,
UINT32 input_sample_rate, UINT32 input_sample_rate,
UINT32 flags, UINT32 flags,
UINT32 processing_stage, UINT32 processing_stage,
const void *send_list, const void *send_list,
const XAudio2EffectChain *effect_chain) override { const XAudio2EffectChain *effect_chain) override {
return real->CreateSubmixVoice( return real->CreateSubmixVoice(
submix_voice, submix_voice,
input_channels, input_channels,
input_sample_rate, input_sample_rate,
flags, flags,
processing_stage, processing_stage,
send_list, send_list,
effect_chain); effect_chain);
} }
HRESULT STDMETHODCALLTYPE CreateMasteringVoice( HRESULT STDMETHODCALLTYPE CreateMasteringVoice(
void **mastering_voice, void **mastering_voice,
UINT32 input_channels, UINT32 input_channels,
UINT32 input_sample_rate, UINT32 input_sample_rate,
UINT32 flags, UINT32 flags,
UINT32 device_index, UINT32 device_index,
const XAudio2EffectChain *effect_chain) override { const XAudio2EffectChain *effect_chain) override {
const auto result = real->CreateMasteringVoice( const auto result = real->CreateMasteringVoice(
mastering_voice, mastering_voice,
input_channels, input_channels,
input_sample_rate, input_sample_rate,
flags, flags,
device_index, device_index,
effect_chain); effect_chain);
log_info( log_info(
"audio::xaudio2", "audio::xaudio2",
"IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}", "IXAudio2::CreateMasteringVoice(channels={}, rate={} Hz, flags=0x{:08x}, device={}, effects={}) -> {}, voice={}",
input_channels, input_channels,
input_sample_rate, input_sample_rate,
flags, flags,
device_index, device_index,
effect_chain != nullptr ? effect_chain->effect_count : 0, effect_chain != nullptr ? effect_chain->effect_count : 0,
FMT_HRESULT(result), FMT_HRESULT(result),
mastering_voice != nullptr ? *mastering_voice : nullptr); mastering_voice != nullptr ? *mastering_voice : nullptr);
return result; return result;
} }
HRESULT STDMETHODCALLTYPE StartEngine() override { HRESULT STDMETHODCALLTYPE StartEngine() override {
const auto result = real->StartEngine(); const auto result = real->StartEngine();
log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result)); log_info("audio::xaudio2", "IXAudio2::StartEngine -> {}", FMT_HRESULT(result));
return result; return result;
} }
void STDMETHODCALLTYPE StopEngine() override { void STDMETHODCALLTYPE StopEngine() override {
log_info("audio::xaudio2", "IXAudio2::StopEngine"); log_info("audio::xaudio2", "IXAudio2::StopEngine");
real->StopEngine(); real->StopEngine();
} }
HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override { HRESULT STDMETHODCALLTYPE CommitChanges(UINT32 operation_set) override {
return real->CommitChanges(operation_set); return real->CommitChanges(operation_set);
} }
void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override { void STDMETHODCALLTYPE GetPerformanceData(void *performance_data) override {
real->GetPerformanceData(performance_data); real->GetPerformanceData(performance_data);
} }
void STDMETHODCALLTYPE SetDebugConfiguration( void STDMETHODCALLTYPE SetDebugConfiguration(
const void *debug_configuration, const void *debug_configuration,
void *reserved) override { void *reserved) override {
log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration); log_info("audio::xaudio2", "IXAudio2::SetDebugConfiguration({})", debug_configuration);
real->SetDebugConfiguration(debug_configuration, reserved); real->SetDebugConfiguration(debug_configuration, reserved);
} }
private: private:
std::atomic<ULONG> ref_count = 1; std::atomic<ULONG> ref_count = 1;
std::atomic_bool device_details_logged = false; std::atomic_bool device_details_logged = false;
std::atomic_bool channel_warning_logged = false; std::atomic_bool channel_warning_logged = false;
IXAudio2_27 *real; IXAudio2_27 *real;
}; };
static HRESULT STDAPICALLTYPE CoCreateInstance_hook( static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
REFCLSID clsid, REFCLSID clsid,
LPUNKNOWN outer, LPUNKNOWN outer,
DWORD class_context, DWORD class_context,
REFIID iid, REFIID iid,
LPVOID *object) { LPVOID *object) {
const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object); const auto result = CoCreateInstance_orig(clsid, outer, class_context, iid, object);
log_info( log_info(
"audio::xact", "audio::xact",
"CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}", "CoCreateInstance(clsid={}, iid={}, context=0x{:08x}) -> {}, object={}",
guid2s(clsid), guid2s(clsid),
guid2s(iid), guid2s(iid),
class_context, class_context,
FMT_HRESULT(result), FMT_HRESULT(result),
object != nullptr ? *object : nullptr); object != nullptr ? *object : nullptr);
if (SUCCEEDED(result) && object != nullptr && *object != nullptr && if (SUCCEEDED(result) && object != nullptr && *object != nullptr &&
IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) && IsEqualCLSID(clsid, CLSID_XAudio2_7_LEGACY) &&
IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) { IsEqualIID(iid, IID_IXAudio2_7_LEGACY)) {
*object = static_cast<IXAudio2_27 *>( *object = static_cast<IXAudio2_27 *>(
new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object))); new WrappedXAudio2(static_cast<IXAudio2_27 *>(*object)));
} }
return result; return result;
} }
static HRESULT WINAPI CreateFX_hook( static HRESULT WINAPI CreateFX_hook(
REFCLSID clsid, REFCLSID clsid,
IUnknown **effect, IUnknown **effect,
const void *init_data, const void *init_data,
UINT32 init_data_size) { UINT32 init_data_size) {
const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size); const auto result = CreateFX_orig(clsid, effect, init_data, init_data_size);
log_info( log_info(
"audio::xapofx", "audio::xapofx",
"CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}", "CreateFX(clsid={}, init_data={}, size={}) -> {}, effect={}",
guid2s(clsid), guid2s(clsid),
init_data, init_data,
init_data_size, init_data_size,
FMT_HRESULT(result), FMT_HRESULT(result),
effect != nullptr ? static_cast<void *>(*effect) : nullptr); effect != nullptr ? static_cast<void *>(*effect) : nullptr);
return result; return result;
} }
void init() { void init() {
const auto libxact = GetModuleHandleW(L"libxact.dll"); const auto libxact = GetModuleHandleW(L"libxact.dll");
if (libxact == nullptr) { if (libxact == nullptr) {
return; return;
} }
CoCreateInstance_orig = detour::iat_try( CoCreateInstance_orig = detour::iat_try(
"CoCreateInstance", CoCreateInstance_hook, libxact); "CoCreateInstance", CoCreateInstance_hook, libxact);
CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact); CreateFX_orig = detour::iat_try("CreateFX", CreateFX_hook, libxact);
log_info( log_info(
"audio::xact", "audio::xact",
"libxact hooks installed: CoCreateInstance={}, CreateFX={}", "libxact hooks installed: CoCreateInstance={}, CreateFX={}",
CoCreateInstance_orig != nullptr, CoCreateInstance_orig != nullptr,
CreateFX_orig != nullptr); CreateFX_orig != nullptr);
} }
} }
+4 -4
View File
@@ -1,5 +1,5 @@
#pragma once #pragma once
namespace hooks::audio::xact { namespace hooks::audio::xact {
void init(); void init();
} }
@@ -1,304 +1,304 @@
// dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports // dx11 / dxgi hook entrypoint. trampolines d3d11.dll / dxgi.dll exports
// the moment those DLLs appear (LDR notification + poll-thread fallback), // the moment those DLLs appear (LDR notification + poll-thread fallback),
// then drives proactive vtable capture so we don't lose the race against // then drives proactive vtable capture so we don't lose the race against
// the execexe loader. per-vtable hook implementations live in the sibling // the execexe loader. per-vtable hook implementations live in the sibling
// files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture / // files (d3d11_swapchain / d3d11_factory / d3d11_vtable_capture /
// d3d11_screenshot). // d3d11_screenshot).
// //
// note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and // note: never LoadLibrary d3d11/dxgi -- execexe pre-loads them itself and
// fails (error 0xa) if they're already in the loader's module list. // fails (error 0xa) if they're already in the loader's module list.
// //
// 64-bit only. // 64-bit only.
#include "d3d11_backend.h" #include "d3d11_backend.h"
#ifndef SPICE_D3D11 #ifndef SPICE_D3D11
void graphics_d3d11_init() {} void graphics_d3d11_init() {}
void graphics_d3d11_shutdown() {} void graphics_d3d11_shutdown() {}
#else #else
#include <atomic> #include <atomic>
#include <thread> #include <thread>
#include <chrono> #include <chrono>
#include <cwchar> #include <cwchar>
#include <mutex> #include <mutex>
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h> #include <dxgi.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include "d3d11_internal.h" #include "d3d11_internal.h"
#include "util/nt_loader.h" #include "util/nt_loader.h"
namespace { namespace {
using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)( using D3D11CreateDeviceAndSwapChain_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT, const D3D_FEATURE_LEVEL *, UINT, UINT,
const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **, const DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **); using CreateDXGIFactory_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr; D3D11CreateDeviceAndSwapChain_t D3D11CreateDeviceAndSwapChain_orig = nullptr;
CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr; CreateDXGIFactory_t CreateDXGIFactory_orig = nullptr;
CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr; CreateDXGIFactory1_t CreateDXGIFactory1_orig = nullptr;
CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr; CreateDXGIFactory2_t CreateDXGIFactory2_orig = nullptr;
std::atomic<bool> g_d3d11_exports_hooked { false }; std::atomic<bool> g_d3d11_exports_hooked { false };
std::atomic<bool> g_dxgi_exports_hooked { false }; std::atomic<bool> g_dxgi_exports_hooked { false };
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// top-level export hooks // top-level export hooks
HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook( HRESULT WINAPI D3D11CreateDeviceAndSwapChain_hook(
IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags, IDXGIAdapter *pAdapter, D3D_DRIVER_TYPE DriverType, HMODULE Software, UINT Flags,
const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion, const D3D_FEATURE_LEVEL *pFeatureLevels, UINT FeatureLevels, UINT SDKVersion,
const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain, const DXGI_SWAP_CHAIN_DESC *pSwapChainDesc, IDXGISwapChain **ppSwapChain,
ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel, ID3D11Device **ppDevice, D3D_FEATURE_LEVEL *pFeatureLevel,
ID3D11DeviceContext **ppImmediateContext) ID3D11DeviceContext **ppImmediateContext)
{ {
HRESULT res = D3D11CreateDeviceAndSwapChain_orig( HRESULT res = D3D11CreateDeviceAndSwapChain_orig(
pAdapter, DriverType, Software, Flags, pAdapter, DriverType, Software, Flags,
pFeatureLevels, FeatureLevels, SDKVersion, pFeatureLevels, FeatureLevels, SDKVersion,
pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext); pSwapChainDesc, ppSwapChain, ppDevice, pFeatureLevel, ppImmediateContext);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pSwapChainDesc) { if (pSwapChainDesc) {
d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow); d3d11_hooks::note_main_hwnd(pSwapChainDesc->OutputWindow);
} }
d3d11_hooks::install_swapchain_hooks(*ppSwapChain); d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
} }
return res; return res;
} }
#define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \ #define DEFINE_FACTORY_HOOK(NAME, SIG_PARAMS, ORIG_ARGS) \
HRESULT WINAPI NAME##_hook SIG_PARAMS { \ HRESULT WINAPI NAME##_hook SIG_PARAMS { \
HRESULT res = NAME##_orig ORIG_ARGS; \ HRESULT res = NAME##_orig ORIG_ARGS; \
if (SUCCEEDED(res) && ppFactory && *ppFactory) { \ if (SUCCEEDED(res) && ppFactory && *ppFactory) { \
d3d11_hooks::install_factory_hooks( \ d3d11_hooks::install_factory_hooks( \
reinterpret_cast<IUnknown *>(*ppFactory)); \ reinterpret_cast<IUnknown *>(*ppFactory)); \
} \ } \
return res; \ return res; \
} }
DEFINE_FACTORY_HOOK(CreateDXGIFactory, DEFINE_FACTORY_HOOK(CreateDXGIFactory,
(REFIID riid, void **ppFactory), (REFIID riid, void **ppFactory),
(riid, ppFactory)) (riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory1, DEFINE_FACTORY_HOOK(CreateDXGIFactory1,
(REFIID riid, void **ppFactory), (REFIID riid, void **ppFactory),
(riid, ppFactory)) (riid, ppFactory))
DEFINE_FACTORY_HOOK(CreateDXGIFactory2, DEFINE_FACTORY_HOOK(CreateDXGIFactory2,
(UINT Flags, REFIID riid, void **ppFactory), (UINT Flags, REFIID riid, void **ppFactory),
(Flags, riid, ppFactory)) (Flags, riid, ppFactory))
#undef DEFINE_FACTORY_HOOK #undef DEFINE_FACTORY_HOOK
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// export trampoline plumbing // export trampoline plumbing
// serializes trampoline_export() so the LDR notification callback and the // serializes trampoline_export() so the LDR notification callback and the
// poll thread don't race each other into MinHook against the same target. // poll thread don't race each other into MinHook against the same target.
std::mutex g_export_mutex; std::mutex g_export_mutex;
bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) { bool trampoline_export(const char *dll, const char *name, void *hook, void **orig) {
std::lock_guard<std::mutex> lock(g_export_mutex); std::lock_guard<std::mutex> lock(g_export_mutex);
if (*orig) { if (*orig) {
return true; return true;
} }
HMODULE mod = GetModuleHandleA(dll); HMODULE mod = GetModuleHandleA(dll);
if (!mod) { if (!mod) {
return false; return false;
} }
void *addr = reinterpret_cast<void *>(GetProcAddress(mod, name)); void *addr = reinterpret_cast<void *>(GetProcAddress(mod, name));
if (!addr) { if (!addr) {
return false; return false;
} }
*orig = addr; // trampoline_try reads *orig before overwriting it. *orig = addr; // trampoline_try reads *orig before overwriting it.
if (!detour::trampoline_try(addr, hook, orig)) { if (!detour::trampoline_try(addr, hook, orig)) {
*orig = nullptr; *orig = nullptr;
return false; return false;
} }
log_info("graphics::d3d11", "trampolined {}!{}", dll, name); log_info("graphics::d3d11", "trampolined {}!{}", dll, name);
return true; return true;
} }
void try_install_d3d11_exports() { void try_install_d3d11_exports() {
if (g_d3d11_exports_hooked) { if (g_d3d11_exports_hooked) {
return; return;
} }
if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain", if (trampoline_export("d3d11.dll", "D3D11CreateDeviceAndSwapChain",
(void *) D3D11CreateDeviceAndSwapChain_hook, (void *) D3D11CreateDeviceAndSwapChain_hook,
(void **) &D3D11CreateDeviceAndSwapChain_orig)) { (void **) &D3D11CreateDeviceAndSwapChain_orig)) {
g_d3d11_exports_hooked = true; g_d3d11_exports_hooked = true;
} }
} }
void try_install_dxgi_exports() { void try_install_dxgi_exports() {
if (g_dxgi_exports_hooked) { if (g_dxgi_exports_hooked) {
return; return;
} }
struct entry { const char *name; void *hook; void **orig; }; struct entry { const char *name; void *hook; void **orig; };
const entry entries[] = { const entry entries[] = {
{ "CreateDXGIFactory", (void *) CreateDXGIFactory_hook, { "CreateDXGIFactory", (void *) CreateDXGIFactory_hook,
(void **) &CreateDXGIFactory_orig }, (void **) &CreateDXGIFactory_orig },
{ "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook, { "CreateDXGIFactory1", (void *) CreateDXGIFactory1_hook,
(void **) &CreateDXGIFactory1_orig }, (void **) &CreateDXGIFactory1_orig },
{ "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook, { "CreateDXGIFactory2", (void *) CreateDXGIFactory2_hook,
(void **) &CreateDXGIFactory2_orig }, (void **) &CreateDXGIFactory2_orig },
}; };
bool any = false; bool any = false;
for (auto &e : entries) { for (auto &e : entries) {
any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig); any |= trampoline_export("dxgi.dll", e.name, e.hook, e.orig);
} }
if (any) { if (any) {
g_dxgi_exports_hooked = true; g_dxgi_exports_hooked = true;
} }
} }
void try_capture_if_ready() { void try_capture_if_ready() {
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables(); d3d11_hooks::try_capture_vtables();
} }
} }
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// LDR notification + polling fallback // LDR notification + polling fallback
bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) { bool dll_name_ends_with(PCUNICODE_STRING name, const wchar_t *suffix) {
if (!name || !name->Buffer) { if (!name || !name->Buffer) {
return false; return false;
} }
const size_t n = name->Length / sizeof(WCHAR); const size_t n = name->Length / sizeof(WCHAR);
const size_t s = wcslen(suffix); const size_t s = wcslen(suffix);
return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0; return n >= s && _wcsnicmp(name->Buffer + n - s, suffix, s) == 0;
} }
VOID CALLBACK ldr_dll_notification( VOID CALLBACK ldr_dll_notification(
ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/) ULONG reason, PCLDR_DLL_NOTIFICATION_DATA data, PVOID /*context*/)
{ {
if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) { if (reason != LDR_DLL_NOTIFICATION_REASON_LOADED || !data) {
return; return;
} }
if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) { if (dll_name_ends_with(data->Loaded.BaseDllName, L"d3d11.dll")) {
try_install_d3d11_exports(); try_install_d3d11_exports();
} else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) { } else if (dll_name_ends_with(data->Loaded.BaseDllName, L"dxgi.dll")) {
try_install_dxgi_exports(); try_install_dxgi_exports();
} }
} }
// execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the // execexe maps d3d11/dxgi via a path that bypasses LdrLoadDll, so the
// notification above never fires for those DLLs and we have to poll. // notification above never fires for those DLLs and we have to poll.
std::atomic<bool> g_stop { false }; std::atomic<bool> g_stop { false };
std::thread g_poll_thread; std::thread g_poll_thread;
std::mutex g_init_mutex; std::mutex g_init_mutex;
PVOID g_ldr_cookie = nullptr; PVOID g_ldr_cookie = nullptr;
void poll_thread() { void poll_thread() {
using namespace std::chrono_literals; using namespace std::chrono_literals;
for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) { for (int32_t i = 0; i < 120 && !g_stop.load(); ++i) {
try_install_d3d11_exports(); try_install_d3d11_exports();
try_install_dxgi_exports(); try_install_dxgi_exports();
if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) { if (g_d3d11_exports_hooked && g_dxgi_exports_hooked) {
d3d11_hooks::try_capture_vtables(); d3d11_hooks::try_capture_vtables();
return; return;
} }
// sliced so shutdown doesn't have to wait a full second. // sliced so shutdown doesn't have to wait a full second.
for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) { for (int32_t s = 0; s < 10 && !g_stop.load(); ++s) {
std::this_thread::sleep_for(100ms); std::this_thread::sleep_for(100ms);
} }
} }
} }
// the overlay's imgui dx11 backend needs D3DCompile (d3dcompiler_XX.dll) to // the overlay's imgui dx11 backend needs D3DCompile (d3dcompiler_XX.dll) to
// build its shaders. _43 ships with the DX June 2010 redist on stock Win7; // build its shaders. _43 ships with the DX June 2010 redist on stock Win7;
// _46/_47 come with newer Windows. // _46/_47 come with newer Windows.
bool d3dcompiler_available() { bool d3dcompiler_available() {
static const wchar_t *names[] = { static const wchar_t *names[] = {
L"d3dcompiler_47.dll", L"d3dcompiler_47.dll",
L"d3dcompiler_46.dll", L"d3dcompiler_46.dll",
L"d3dcompiler_43.dll", L"d3dcompiler_43.dll",
}; };
for (auto name : names) { for (auto name : names) {
HMODULE mod = GetModuleHandleW(name); HMODULE mod = GetModuleHandleW(name);
if (!mod) { if (!mod) {
mod = LoadLibraryW(name); mod = LoadLibraryW(name);
} }
if (mod && GetProcAddress(mod, "D3DCompile")) { if (mod && GetProcAddress(mod, "D3DCompile")) {
return true; return true;
} }
} }
return false; return false;
} }
} // namespace } // namespace
void graphics_d3d11_init() { void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps // dx11 titles always run under execexe. skipping on pure-dx9 games keeps
// their startup path completely untouched (no exports patched, no poll // their startup path completely untouched (no exports patched, no poll
// thread, no LDR callback). // thread, no LDR callback).
if (!GetModuleHandleW(L"execexe.dll")) { if (!GetModuleHandleW(L"execexe.dll")) {
return; return;
} }
// no d3dcompiler -> overlay can't build shaders; skip dx11 overlay // no d3dcompiler -> overlay can't build shaders; skip dx11 overlay
if (!d3dcompiler_available()) { if (!d3dcompiler_available()) {
log_warning( log_warning(
"graphics::d3d11", "graphics::d3d11",
"d3dcompiler not found; dx11 overlay disabled"); "d3dcompiler not found; dx11 overlay disabled");
return; return;
} }
std::lock_guard<std::mutex> lock(g_init_mutex); std::lock_guard<std::mutex> lock(g_init_mutex);
if (g_poll_thread.joinable()) { if (g_poll_thread.joinable()) {
return; // already initialized return; // already initialized
} }
log_info("graphics::d3d11", "initializing"); log_info("graphics::d3d11", "initializing");
// trampoline now if either DLL is already in the PEB. // trampoline now if either DLL is already in the PEB.
try_install_d3d11_exports(); try_install_d3d11_exports();
try_install_dxgi_exports(); try_install_dxgi_exports();
try_capture_if_ready(); try_capture_if_ready();
// catches standard LdrLoadDll loads. // catches standard LdrLoadDll loads.
auto reg = reinterpret_cast<decltype(&LdrRegisterDllNotification)>( auto reg = reinterpret_cast<decltype(&LdrRegisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification")); GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrRegisterDllNotification"));
if (reg) { if (reg) {
NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie); NTSTATUS st = reg(0, ldr_dll_notification, nullptr, &g_ldr_cookie);
if (NT_SUCCESS(st)) { if (NT_SUCCESS(st)) {
log_info("graphics::d3d11", "registered LDR DLL notification"); log_info("graphics::d3d11", "registered LDR DLL notification");
} else { } else {
g_ldr_cookie = nullptr; g_ldr_cookie = nullptr;
log_warning("graphics::d3d11", log_warning("graphics::d3d11",
"LdrRegisterDllNotification failed: {:#x}", (unsigned long)st); "LdrRegisterDllNotification failed: {:#x}", (unsigned long)st);
} }
} }
// catches the execexe loader path that bypasses LdrLoadDll. // catches the execexe loader path that bypasses LdrLoadDll.
g_poll_thread = std::thread(poll_thread); g_poll_thread = std::thread(poll_thread);
} }
void graphics_d3d11_shutdown() { void graphics_d3d11_shutdown() {
std::lock_guard<std::mutex> lock(g_init_mutex); std::lock_guard<std::mutex> lock(g_init_mutex);
// unregister first so the callback can't fire mid-teardown. // unregister first so the callback can't fire mid-teardown.
if (g_ldr_cookie) { if (g_ldr_cookie) {
auto unreg = reinterpret_cast<decltype(&LdrUnregisterDllNotification)>( auto unreg = reinterpret_cast<decltype(&LdrUnregisterDllNotification)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification")); GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "LdrUnregisterDllNotification"));
if (unreg) { if (unreg) {
unreg(g_ldr_cookie); unreg(g_ldr_cookie);
} }
g_ldr_cookie = nullptr; g_ldr_cookie = nullptr;
} }
g_stop.store(true); g_stop.store(true);
if (g_poll_thread.joinable()) { if (g_poll_thread.joinable()) {
g_poll_thread.join(); g_poll_thread.join();
} }
} }
#endif // SPICE_D3D11 #endif // SPICE_D3D11
@@ -1,24 +1,24 @@
#pragma once #pragma once
#include "overlay/overlay.h" #include "overlay/overlay.h"
void graphics_d3d11_init(); void graphics_d3d11_init();
void graphics_d3d11_shutdown(); void graphics_d3d11_shutdown();
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
struct ID3D11Device; struct ID3D11Device;
struct ID3D11DeviceContext; struct ID3D11DeviceContext;
struct ID3D11RenderTargetView; struct ID3D11RenderTargetView;
struct IDXGISwapChain; struct IDXGISwapChain;
namespace overlay::d3d11 { namespace overlay::d3d11 {
void render(ID3D11Device *device, void render(ID3D11Device *device,
ID3D11DeviceContext *context, ID3D11DeviceContext *context,
IDXGISwapChain *swapchain, IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv); ID3D11RenderTargetView **rtv);
} }
#endif #endif
@@ -1,102 +1,102 @@
// dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd // dx11 factory vtable hooks. patches CreateSwapChain / CreateSwapChainForHwnd
// so we can install_swapchain_hooks against every newly-created swapchain. // so we can install_swapchain_hooks against every newly-created swapchain.
#include "d3d11_backend.h" #include "d3d11_backend.h"
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
#include <mutex> #include <mutex>
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h> #include <dxgi.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include "d3d11_internal.h" #include "d3d11_internal.h"
namespace { namespace {
using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)( using CreateSwapChain_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **); IDXGIFactory *, IUnknown *, DXGI_SWAP_CHAIN_DESC *, IDXGISwapChain **);
using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)( using CreateSwapChainForHwnd_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGIFactory2 *, IUnknown *, HWND, IDXGIFactory2 *, IUnknown *, HWND,
const DXGI_SWAP_CHAIN_DESC1 *, const DXGI_SWAP_CHAIN_DESC1 *,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *, const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *,
IDXGIOutput *, IDXGISwapChain1 **); IDXGIOutput *, IDXGISwapChain1 **);
CreateSwapChain_t CreateSwapChain_orig = nullptr; CreateSwapChain_t CreateSwapChain_orig = nullptr;
CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr; CreateSwapChainForHwnd_t CreateSwapChainForHwnd_orig = nullptr;
bool g_factory_hooked = false; bool g_factory_hooked = false;
bool g_factory2_hooked = false; bool g_factory2_hooked = false;
std::mutex g_hook_mutex; std::mutex g_hook_mutex;
HRESULT STDMETHODCALLTYPE CreateSwapChain_hook( HRESULT STDMETHODCALLTYPE CreateSwapChain_hook(
IDXGIFactory *factory, IUnknown *pDevice, IDXGIFactory *factory, IUnknown *pDevice,
DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain) DXGI_SWAP_CHAIN_DESC *pDesc, IDXGISwapChain **ppSwapChain)
{ {
HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain); HRESULT res = CreateSwapChain_orig(factory, pDevice, pDesc, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
if (pDesc) { if (pDesc) {
d3d11_hooks::note_main_hwnd(pDesc->OutputWindow); d3d11_hooks::note_main_hwnd(pDesc->OutputWindow);
} }
d3d11_hooks::install_swapchain_hooks(*ppSwapChain); d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
} }
return res; return res;
} }
HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook( HRESULT STDMETHODCALLTYPE CreateSwapChainForHwnd_hook(
IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd, IDXGIFactory2 *factory, IUnknown *pDevice, HWND hWnd,
const DXGI_SWAP_CHAIN_DESC1 *pDesc, const DXGI_SWAP_CHAIN_DESC1 *pDesc,
const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc, const DXGI_SWAP_CHAIN_FULLSCREEN_DESC *pFullscreenDesc,
IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain) IDXGIOutput *pRestrictToOutput, IDXGISwapChain1 **ppSwapChain)
{ {
HRESULT res = CreateSwapChainForHwnd_orig( HRESULT res = CreateSwapChainForHwnd_orig(
factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain); factory, pDevice, hWnd, pDesc, pFullscreenDesc, pRestrictToOutput, ppSwapChain);
if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) { if (SUCCEEDED(res) && ppSwapChain && *ppSwapChain) {
d3d11_hooks::note_main_hwnd(hWnd); d3d11_hooks::note_main_hwnd(hWnd);
d3d11_hooks::install_swapchain_hooks(*ppSwapChain); d3d11_hooks::install_swapchain_hooks(*ppSwapChain);
} }
return res; return res;
} }
// QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths. // QI-and-hook helper: dedupes the IDXGIFactory / IDXGIFactory2 install paths.
template<typename Iface> template<typename Iface>
void install_on(IUnknown *factory, bool &flag, void install_on(IUnknown *factory, bool &flag,
size_t vtbl_index, void *hook, void **orig, const char *name) size_t vtbl_index, void *hook, void **orig, const char *name)
{ {
if (flag) { if (flag) {
return; return;
} }
Iface *f = nullptr; Iface *f = nullptr;
if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) { if (FAILED(factory->QueryInterface(IID_PPV_ARGS(&f))) || !f) {
return; return;
} }
if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) { if (d3d11_hooks::hook_vtbl(f, vtbl_index, hook, orig, name)) {
flag = true; flag = true;
} }
f->Release(); f->Release();
} }
} // namespace } // namespace
namespace d3d11_hooks { namespace d3d11_hooks {
void install_factory_hooks(IUnknown *factory) { void install_factory_hooks(IUnknown *factory) {
if (!factory) { if (!factory) {
return; return;
} }
std::lock_guard<std::mutex> lock(g_hook_mutex); std::lock_guard<std::mutex> lock(g_hook_mutex);
install_on<IDXGIFactory>(factory, g_factory_hooked, 10, install_on<IDXGIFactory>(factory, g_factory_hooked, 10,
(void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig, (void *) CreateSwapChain_hook, (void **) &CreateSwapChain_orig,
"IDXGIFactory::CreateSwapChain"); "IDXGIFactory::CreateSwapChain");
install_on<IDXGIFactory2>(factory, g_factory2_hooked, 15, install_on<IDXGIFactory2>(factory, g_factory2_hooked, 15,
(void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig, (void *) CreateSwapChainForHwnd_hook, (void **) &CreateSwapChainForHwnd_orig,
"IDXGIFactory2::CreateSwapChainForHwnd"); "IDXGIFactory2::CreateSwapChainForHwnd");
} }
} }
#endif // SPICE_D3D11 #endif // SPICE_D3D11
@@ -1,59 +1,59 @@
#pragma once #pragma once
// internal glue for the dx11 backend. all symbols gated on SPICE_D3D11. // internal glue for the dx11 backend. all symbols gated on SPICE_D3D11.
#include "overlay/overlay.h" #include "overlay/overlay.h"
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
#include <memory> #include <memory>
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
struct HWND__; typedef HWND__ *HWND; struct HWND__; typedef HWND__ *HWND;
struct IUnknown; struct IUnknown;
struct IDXGISwapChain; struct IDXGISwapChain;
namespace d3d11_hooks { namespace d3d11_hooks {
void install_swapchain_hooks(IDXGISwapChain *swapchain); void install_swapchain_hooks(IDXGISwapChain *swapchain);
void install_factory_hooks(IUnknown *factory); void install_factory_hooks(IUnknown *factory);
void try_capture_vtables(); void try_capture_vtables();
// first non-null swapchain HWND wins; later ones (sub-screens, IME // first non-null swapchain HWND wins; later ones (sub-screens, IME
// helpers) are ignored. the dummy capture window is exempted via // helpers) are ignored. the dummy capture window is exempted via
// ignore_hwnd. // ignore_hwnd.
void note_main_hwnd(HWND hwnd); void note_main_hwnd(HWND hwnd);
HWND main_hwnd(); HWND main_hwnd();
void ignore_hwnd(HWND hwnd); void ignore_hwnd(HWND hwnd);
// capture backbuffer to PNG if a screenshot was requested. // capture backbuffer to PNG if a screenshot was requested.
void try_screenshot(IDXGISwapChain *swapchain); void try_screenshot(IDXGISwapChain *swapchain);
// trampoline a virtual method by vtable index. on failure *orig is null. // trampoline a virtual method by vtable index. on failure *orig is null.
inline bool hook_vtbl(void *iface, size_t index, inline bool hook_vtbl(void *iface, size_t index,
void *hook, void **orig, const char *name) void *hook, void **orig, const char *name)
{ {
void **vtbl = *reinterpret_cast<void ***>(iface); void **vtbl = *reinterpret_cast<void ***>(iface);
void *target = vtbl[index]; void *target = vtbl[index];
// trampoline_try reads *orig before overwriting it. // trampoline_try reads *orig before overwriting it.
*orig = target; *orig = target;
if (!detour::trampoline_try(target, hook, orig)) { if (!detour::trampoline_try(target, hook, orig)) {
*orig = nullptr; *orig = nullptr;
log_warning("graphics::d3d11", "failed to hook {}", name); log_warning("graphics::d3d11", "failed to hook {}", name);
return false; return false;
} }
log_info("graphics::d3d11", "hooked {}", name); log_info("graphics::d3d11", "hooked {}", name);
return true; return true;
} }
// minimal COM RAII used by capture / screenshot paths. // minimal COM RAII used by capture / screenshot paths.
struct com_release { struct com_release {
void operator()(IUnknown *p) const { if (p) p->Release(); } void operator()(IUnknown *p) const { if (p) p->Release(); }
}; };
template<typename T> using com_ptr = std::unique_ptr<T, com_release>; template<typename T> using com_ptr = std::unique_ptr<T, com_release>;
} }
#endif #endif
@@ -1,165 +1,165 @@
// dx11 screenshot capture. mirrors the d3d9 backend: copy the current // dx11 screenshot capture. mirrors the d3d9 backend: copy the current
// backbuffer into a staging texture, force alpha=255, write PNG via // backbuffer into a staging texture, force alpha=255, write PNG via
// stb_image_write, push to clipboard and notify. // stb_image_write, push to clipboard and notify.
#include "d3d11_backend.h" #include "d3d11_backend.h"
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h> #include <dxgi.h>
#include "d3d11_internal.h" #include "d3d11_internal.h"
#include "external/stb_image_write.h" #include "external/stb_image_write.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "misc/clipboard.h" #include "misc/clipboard.h"
#include "overlay/notifications.h" #include "overlay/notifications.h"
#include "util/fileutils.h" #include "util/fileutils.h"
using d3d11_hooks::com_ptr; using d3d11_hooks::com_ptr;
namespace { namespace {
// copy the swapchain backbuffer into a CPU-readable staging texture and // copy the swapchain backbuffer into a CPU-readable staging texture and
// flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled, // flatten it into an RGBA8 buffer (BGRA backbuffers are swizzled,
// alpha is forced to 255). // alpha is forced to 255).
bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain, bool copy_backbuffer_to_rgba(IDXGISwapChain *swapchain,
ID3D11Device *device, ID3D11Device *device,
ID3D11DeviceContext *context, ID3D11DeviceContext *context,
std::vector<uint8_t> &out, std::vector<uint8_t> &out,
uint32_t &out_w, uint32_t &out_h) uint32_t &out_w, uint32_t &out_h)
{ {
ID3D11Texture2D *raw_bb = nullptr; ID3D11Texture2D *raw_bb = nullptr;
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) { if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&raw_bb))) || !raw_bb) {
return false; return false;
} }
com_ptr<ID3D11Texture2D> backbuffer(raw_bb); com_ptr<ID3D11Texture2D> backbuffer(raw_bb);
D3D11_TEXTURE2D_DESC desc {}; D3D11_TEXTURE2D_DESC desc {};
backbuffer->GetDesc(&desc); backbuffer->GetDesc(&desc);
// MSAA backbuffers can't be CopyResource'd into a non-MS staging target. // MSAA backbuffers can't be CopyResource'd into a non-MS staging target.
com_ptr<ID3D11Texture2D> resolved; com_ptr<ID3D11Texture2D> resolved;
ID3D11Texture2D *source = backbuffer.get(); ID3D11Texture2D *source = backbuffer.get();
if (desc.SampleDesc.Count > 1) { if (desc.SampleDesc.Count > 1) {
D3D11_TEXTURE2D_DESC rd = desc; D3D11_TEXTURE2D_DESC rd = desc;
rd.SampleDesc.Count = 1; rd.SampleDesc.Count = 1;
rd.SampleDesc.Quality = 0; rd.SampleDesc.Quality = 0;
rd.Usage = D3D11_USAGE_DEFAULT; rd.Usage = D3D11_USAGE_DEFAULT;
rd.BindFlags = D3D11_BIND_RENDER_TARGET; rd.BindFlags = D3D11_BIND_RENDER_TARGET;
rd.CPUAccessFlags = 0; rd.CPUAccessFlags = 0;
rd.MiscFlags = 0; rd.MiscFlags = 0;
ID3D11Texture2D *r = nullptr; ID3D11Texture2D *r = nullptr;
if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) { if (FAILED(device->CreateTexture2D(&rd, nullptr, &r)) || !r) {
return false; return false;
} }
resolved.reset(r); resolved.reset(r);
context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format); context->ResolveSubresource(resolved.get(), 0, backbuffer.get(), 0, desc.Format);
source = resolved.get(); source = resolved.get();
} }
D3D11_TEXTURE2D_DESC sd {}; D3D11_TEXTURE2D_DESC sd {};
sd.Width = desc.Width; sd.Width = desc.Width;
sd.Height = desc.Height; sd.Height = desc.Height;
sd.MipLevels = 1; sd.MipLevels = 1;
sd.ArraySize = 1; sd.ArraySize = 1;
sd.Format = desc.Format; sd.Format = desc.Format;
sd.SampleDesc.Count = 1; sd.SampleDesc.Count = 1;
sd.Usage = D3D11_USAGE_STAGING; sd.Usage = D3D11_USAGE_STAGING;
sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ; sd.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
ID3D11Texture2D *raw_staging = nullptr; ID3D11Texture2D *raw_staging = nullptr;
if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) { if (FAILED(device->CreateTexture2D(&sd, nullptr, &raw_staging)) || !raw_staging) {
return false; return false;
} }
com_ptr<ID3D11Texture2D> staging(raw_staging); com_ptr<ID3D11Texture2D> staging(raw_staging);
context->CopyResource(staging.get(), source); context->CopyResource(staging.get(), source);
D3D11_MAPPED_SUBRESOURCE mapped {}; D3D11_MAPPED_SUBRESOURCE mapped {};
if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) { if (FAILED(context->Map(staging.get(), 0, D3D11_MAP_READ, 0, &mapped))) {
return false; return false;
} }
// backbuffers from GetDesc are always fully-typed (never _TYPELESS). // backbuffers from GetDesc are always fully-typed (never _TYPELESS).
const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM const bool is_bgra = desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM
|| desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB; || desc.Format == DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
out.resize(static_cast<size_t>(desc.Width) * desc.Height * 4); out.resize(static_cast<size_t>(desc.Width) * desc.Height * 4);
const uint8_t *src_base = reinterpret_cast<const uint8_t *>(mapped.pData); const uint8_t *src_base = reinterpret_cast<const uint8_t *>(mapped.pData);
for (uint32_t y = 0; y < desc.Height; ++y) { for (uint32_t y = 0; y < desc.Height; ++y) {
const uint8_t *row = src_base + static_cast<size_t>(y) * mapped.RowPitch; const uint8_t *row = src_base + static_cast<size_t>(y) * mapped.RowPitch;
uint8_t *dst = out.data() + static_cast<size_t>(y) * desc.Width * 4; uint8_t *dst = out.data() + static_cast<size_t>(y) * desc.Width * 4;
for (uint32_t x = 0; x < desc.Width; ++x) { for (uint32_t x = 0; x < desc.Width; ++x) {
dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)]; dst[x * 4 + 0] = row[x * 4 + (is_bgra ? 2 : 0)];
dst[x * 4 + 1] = row[x * 4 + 1]; dst[x * 4 + 1] = row[x * 4 + 1];
dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)]; dst[x * 4 + 2] = row[x * 4 + (is_bgra ? 0 : 2)];
dst[x * 4 + 3] = 255; dst[x * 4 + 3] = 255;
} }
} }
context->Unmap(staging.get(), 0); context->Unmap(staging.get(), 0);
out_w = desc.Width; out_w = desc.Width;
out_h = desc.Height; out_h = desc.Height;
return true; return true;
} }
} // namespace } // namespace
namespace d3d11_hooks { namespace d3d11_hooks {
void try_screenshot(IDXGISwapChain *swapchain) { void try_screenshot(IDXGISwapChain *swapchain) {
if (!swapchain || !graphics_screenshot_consume()) { if (!swapchain || !graphics_screenshot_consume()) {
return; return;
} }
auto file_path = graphics_screenshot_genpath(); auto file_path = graphics_screenshot_genpath();
if (file_path.empty()) { if (file_path.empty()) {
return; return;
} }
ID3D11Device *raw_device = nullptr; ID3D11Device *raw_device = nullptr;
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) { if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&raw_device))) || !raw_device) {
return; return;
} }
com_ptr<ID3D11Device> device(raw_device); com_ptr<ID3D11Device> device(raw_device);
ID3D11DeviceContext *raw_ctx = nullptr; ID3D11DeviceContext *raw_ctx = nullptr;
device->GetImmediateContext(&raw_ctx); device->GetImmediateContext(&raw_ctx);
if (!raw_ctx) { if (!raw_ctx) {
return; return;
} }
com_ptr<ID3D11DeviceContext> context(raw_ctx); com_ptr<ID3D11DeviceContext> context(raw_ctx);
std::vector<uint8_t> pixels; std::vector<uint8_t> pixels;
uint32_t w = 0, h = 0; uint32_t w = 0, h = 0;
if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) { if (!copy_backbuffer_to_rgba(swapchain, device.get(), context.get(), pixels, w, h)) {
log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer"); log_warning("graphics::d3d11", "screenshot: failed to capture backbuffer");
overlay::notifications::add( overlay::notifications::add(
overlay::notifications::Severity::Error, overlay::notifications::Severity::Error,
"Screenshot failed to capture"); "Screenshot failed to capture");
return; return;
} }
log_info("graphics::d3d11", "saving screenshot to {}", file_path); log_info("graphics::d3d11", "saving screenshot to {}", file_path);
if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4, if (stbi_write_png(file_path.c_str(), (int) w, (int) h, 4,
pixels.data(), (int) w * 4)) pixels.data(), (int) w * 4))
{ {
clipboard::copy_image(file_path); clipboard::copy_image(file_path);
overlay::notifications::add( overlay::notifications::add(
overlay::notifications::Severity::Success, overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else { } else {
log_warning("graphics::d3d11", "screenshot: stbi_write_png failed"); log_warning("graphics::d3d11", "screenshot: stbi_write_png failed");
overlay::notifications::add( overlay::notifications::add(
overlay::notifications::Severity::Error, overlay::notifications::Severity::Error,
"Screenshot failed to save"); "Screenshot failed to save");
} }
} }
} }
#endif // SPICE_D3D11 #endif // SPICE_D3D11
@@ -1,294 +1,343 @@
// dx11 swapchain vtable hooks + per-frame overlay pump. // dx11 swapchain vtable hooks + per-frame overlay pump.
// //
// dxgi shares vtables across swapchain instances, so we only need to patch // dxgi shares vtables across swapchain instances, so we only need to patch
// Present / Present1 / ResizeBuffers once on the first instance we see. // Present / Present1 / ResizeBuffers once on the first instance we see.
// each frame we lazily attach the overlay to whichever swapchain is // each frame we lazily attach the overlay to whichever swapchain is
// presenting, then drive its imgui update / new_frame / render cycle. // presenting, then drive its imgui update / new_frame / render cycle.
#include "d3d11_backend.h" #include "d3d11_backend.h"
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
#include <atomic> #include <atomic>
#include <mutex> #include <mutex>
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h> #include <dxgi.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include "d3d11_internal.h" #include "d3d11_internal.h"
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#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 "util/utils.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h" // --------------------------------------------------------------------------
#include "util/utils.h" // overlay render bridge
// -------------------------------------------------------------------------- namespace overlay::d3d11 {
// overlay render bridge
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
namespace overlay::d3d11 { // sRGB-encoded, so an extra linear->sRGB conversion would wash the
// overlay out white.
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
// sRGB-encoded, so an extra linear->sRGB conversion would wash the switch (fmt) {
// overlay out white. case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) { case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
switch (fmt) { default: return fmt;
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM; }
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM; }
default: return fmt;
} static void ensure_rtv(ID3D11Device *device,
} IDXGISwapChain *swapchain,
ID3D11RenderTargetView **rtv)
static void ensure_rtv(ID3D11Device *device, {
IDXGISwapChain *swapchain, if (*rtv || !device || !swapchain) {
ID3D11RenderTargetView **rtv) return;
{ }
if (*rtv || !device || !swapchain) { ID3D11Texture2D *backbuffer = nullptr;
return; if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
} return;
ID3D11Texture2D *backbuffer = nullptr; }
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) { D3D11_TEXTURE2D_DESC td {};
return; backbuffer->GetDesc(&td);
} const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
D3D11_TEXTURE2D_DESC td {}; if (view_fmt != td.Format) {
backbuffer->GetDesc(&td); D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format); rtvd.Format = view_fmt;
if (view_fmt != td.Format) { rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
D3D11_RENDER_TARGET_VIEW_DESC rtvd {}; device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
rtvd.Format = view_fmt; } else {
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D; device->CreateRenderTargetView(backbuffer, nullptr, rtv);
device->CreateRenderTargetView(backbuffer, &rtvd, rtv); }
} else { backbuffer->Release();
device->CreateRenderTargetView(backbuffer, nullptr, rtv); }
}
backbuffer->Release(); // bind the backbuffer (lazily creating the RTV) and draw the imgui
} // frame on top. reset_invalidate releases *rtv on ResizeBuffers.
void render(ID3D11Device *device,
// bind the backbuffer (lazily creating the RTV) and draw the imgui ID3D11DeviceContext *context,
// frame on top. reset_invalidate releases *rtv on ResizeBuffers. IDXGISwapChain *swapchain,
void render(ID3D11Device *device, ID3D11RenderTargetView **rtv)
ID3D11DeviceContext *context, {
IDXGISwapChain *swapchain, ensure_rtv(device, swapchain, rtv);
ID3D11RenderTargetView **rtv) if (!*rtv || !context) {
{ return;
ensure_rtv(device, swapchain, rtv); }
if (!*rtv || !context) { // present happens immediately after, so no need to save the previous
return; // RT binding (flip-model resets it anyway).
} context->OMSetRenderTargets(1, rtv, nullptr);
// present happens immediately after, so no need to save the previous ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
// RT binding (flip-model resets it anyway). }
context->OMSetRenderTargets(1, rtv, nullptr);
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData()); }
}
// --------------------------------------------------------------------------
} // file-local state + per-frame helpers
// -------------------------------------------------------------------------- namespace {
// file-local state + per-frame helpers
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
namespace { IDXGISwapChain *, UINT, UINT);
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
using Present_t = HRESULT(STDMETHODCALLTYPE *)( IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
IDXGISwapChain *, UINT, UINT); using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)( IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
using Present1_t = HRESULT(STDMETHODCALLTYPE *)( Present_t Present_orig = nullptr;
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *); ResizeBuffers_t ResizeBuffers_orig = nullptr;
Present1_t Present1_orig = nullptr;
Present_t Present_orig = nullptr;
ResizeBuffers_t ResizeBuffers_orig = nullptr; bool g_swapchain_hooked = false;
Present1_t Present1_orig = nullptr; bool g_swapchain1_hooked = false;
bool g_swapchain_hooked = false; // sub-screens / IME helpers are usually child or zero-sized windows.
bool g_swapchain1_hooked = false; // visibility isn't checked - the game may present before showing the window.
bool looks_like_game_window(HWND hwnd) {
void try_create_overlay(IDXGISwapChain *swapchain) { RECT client {};
if (!swapchain || overlay::OVERLAY) { return GetAncestor(hwnd, GA_ROOT) == hwnd
return; && GetClientRect(hwnd, &client)
} && client.right > client.left
&& client.bottom > client.top;
DXGI_SWAP_CHAIN_DESC desc {}; }
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return; // only the main game window; ignore sub-screens / IME helpers.
} bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
// only attach to the main game window; ignore sub-screens / IME helpers. if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
HWND main = d3d11_hooks::main_hwnd(); return false;
if (main && desc.OutputWindow != main) { }
return;
} HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// theme the native title bar; first present is the only reliable point for // no creation hook recorded a window, so fall back to the presenting one;
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll) // the choice is permanent, so require a plausible game window
set_window_dark_titlebar(desc.OutputWindow); if (!looks_like_game_window(desc.OutputWindow)) {
return false;
ID3D11Device *device = nullptr; }
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
return; log_misc(
} "graphics::d3d11",
ID3D11DeviceContext *context = nullptr; "try to notemain hwnd from swapchain present: 0x{:x}",
device->GetImmediateContext(&context); (uintptr_t)desc.OutputWindow);
if (context) { d3d11_hooks::note_main_hwnd(desc.OutputWindow);
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
RECT cr {}; // it may have been ignored, or another thread may have won the slot
::GetClientRect(desc.OutputWindow, &cr); main = d3d11_hooks::main_hwnd();
log_info("graphics::d3d11", }
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}", return desc.OutputWindow == main;
(uintptr_t) desc.OutputWindow, }
desc.BufferDesc.Width, desc.BufferDesc.Height,
cr.right - cr.left, cr.bottom - cr.top); // checks are ordered cheapest first, since this runs on every present
context->Release(); void try_create_overlay(IDXGISwapChain *swapchain) {
} if (!swapchain) {
device->Release(); return;
} }
// rising-edge screenshot hotkey poll (mirrors d3d9 backend behaviour). // overlay is disabled by user
void poll_screenshot_hotkey() { if (!overlay::ENABLED) {
static bool s_down = false; return;
auto buttons = games::get_buttons_overlay(eamuse_get_game()); }
const bool pressed = buttons
&& (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) // overlay is already enabled and attached
&& GameAPI::Buttons::getState(RI_MGR, if (overlay::OVERLAY) {
buttons->at(games::OverlayButtons::Screenshot)); return;
if (pressed && !s_down) { }
graphics_screenshot_trigger();
} // ignore sub windows
s_down = pressed; if (!is_main_game_swapchain(swapchain)) {
} return;
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) { DXGI_SWAP_CHAIN_DESC desc {};
return; if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
} return;
}
poll_screenshot_hotkey();
// theme the native title bar; first present is the only reliable point for
// size imgui to the backbuffer (not window client). dxgi may upscale // windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
// a small backbuffer into a larger client rect; without this override set_window_dark_titlebar(desc.OutputWindow);
// imgui would draw past the RTV and the mouse mapping would be off.
DXGI_SWAP_CHAIN_DESC desc {}; ID3D11Device *device = nullptr;
if (SUCCEEDED(swapchain->GetDesc(&desc))) { if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
ImGui_ImplSpice_SetDisplaySizeOverride( return;
(float) desc.BufferDesc.Width, }
(float) desc.BufferDesc.Height); ID3D11DeviceContext *context = nullptr;
} device->GetImmediateContext(&context);
overlay::OVERLAY->update(); if (context) {
overlay::OVERLAY->new_frame(); overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
overlay::OVERLAY->render(); RECT cr {};
::GetClientRect(desc.OutputWindow, &cr);
// after overlay render so toasts/menus end up in the saved image. log_info("graphics::d3d11",
d3d11_hooks::try_screenshot(swapchain); "attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
} (uintptr_t) desc.OutputWindow,
desc.BufferDesc.Width, desc.BufferDesc.Height,
// ---------------------------------------------------------------------- cr.right - cr.left, cr.bottom - cr.top);
// swapchain method hooks context->Release();
}
HRESULT STDMETHODCALLTYPE Present_hook( device->Release();
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags) }
{
try_create_overlay(swapchain); // screenshots have to keep working with the overlay disabled, so they are not gated on it
pump_overlay(swapchain); void pump_frame(IDXGISwapChain *swapchain) {
return Present_orig(swapchain, SyncInterval, Flags); const bool has_overlay =
} overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
HRESULT STDMETHODCALLTYPE Present1_hook( return;
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags, }
const DXGI_PRESENT_PARAMETERS *pParams)
{ graphics_poll_screenshot_hotkey();
try_create_overlay(swapchain);
pump_overlay(swapchain); // before the overlay render so the screenshot excludes it
return Present1_orig(swapchain, SyncInterval, Flags, pParams); if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
} d3d11_hooks::try_screenshot(swapchain);
}
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height, if (has_overlay) {
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
{ // size imgui to the backbuffer (not window client). dxgi may upscale
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain); // a small backbuffer into a larger client rect; without this override
if (ours) { // imgui would draw past the RTV and the mouse mapping would be off.
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}", DXGI_SWAP_CHAIN_DESC desc {};
Width, Height, (int32_t) NewFormat); if (SUCCEEDED(swapchain->GetDesc(&desc))) {
overlay::OVERLAY->reset_invalidate(); ImGui_ImplSpice_SetDisplaySizeOverride(
} (float) desc.BufferDesc.Width,
HRESULT res = ResizeBuffers_orig( (float) desc.BufferDesc.Height);
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags); }
if (ours && SUCCEEDED(res)) {
overlay::OVERLAY->reset_recreate(); overlay::OVERLAY->update();
} overlay::OVERLAY->new_frame();
return res; overlay::OVERLAY->render();
} }
} // namespace // after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
// -------------------------------------------------------------------------- d3d11_hooks::try_screenshot(swapchain);
// d3d11_hooks public surface: main-window tracking + vtable install. }
}
namespace d3d11_hooks {
// ----------------------------------------------------------------------
namespace { // swapchain method hooks
std::atomic<HWND> g_main_hwnd { nullptr };
std::atomic<HWND> g_ignored_hwnd { nullptr }; HRESULT STDMETHODCALLTYPE Present_hook(
} IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
{
void note_main_hwnd(HWND hwnd) { // a test present doesn't display anything; don't pick a window or take a screenshot off it
if (!hwnd || hwnd == g_ignored_hwnd.load()) { if (!(Flags & DXGI_PRESENT_TEST)) {
return; try_create_overlay(swapchain);
} pump_frame(swapchain);
HWND expected = nullptr; }
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) { return Present_orig(swapchain, SyncInterval, Flags);
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}", }
(uintptr_t) hwnd);
} HRESULT STDMETHODCALLTYPE Present1_hook(
} IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
const DXGI_PRESENT_PARAMETERS *pParams)
HWND main_hwnd() { {
return g_main_hwnd.load(); if (!(Flags & DXGI_PRESENT_TEST)) {
} try_create_overlay(swapchain);
pump_frame(swapchain);
void ignore_hwnd(HWND hwnd) { }
g_ignored_hwnd.store(hwnd); return Present1_orig(swapchain, SyncInterval, Flags, pParams);
} }
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented) HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
// IDXGISwapChain1::Present1. idempotent; flag is set only after success IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
// so failed attempts can be retried on the next swapchain. DXGI_FORMAT NewFormat, UINT SwapChainFlags)
void install_swapchain_hooks(IDXGISwapChain *swapchain) { {
if (!swapchain) { const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
return; if (ours) {
} log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
static std::mutex s_hook_mutex; Width, Height, (int32_t) NewFormat);
std::lock_guard<std::mutex> lock(s_hook_mutex); overlay::OVERLAY->reset_invalidate();
}
if (!g_swapchain_hooked) { HRESULT res = ResizeBuffers_orig(
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook, swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
(void **) &Present_orig, "IDXGISwapChain::Present"); if (ours && SUCCEEDED(res)) {
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook, overlay::OVERLAY->reset_recreate();
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers"); }
if (a && b) { return res;
g_swapchain_hooked = true; }
}
} } // namespace
if (!g_swapchain1_hooked) { // --------------------------------------------------------------------------
IDXGISwapChain1 *sc1 = nullptr; // d3d11_hooks public surface: main-window tracking + vtable install.
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
if (hook_vtbl(sc1, 22, (void *) Present1_hook, namespace d3d11_hooks {
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
g_swapchain1_hooked = true; namespace {
} std::atomic<HWND> g_main_hwnd { nullptr };
sc1->Release(); std::atomic<HWND> g_ignored_hwnd { nullptr };
} }
}
} void note_main_hwnd(HWND hwnd) {
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
} return;
}
#endif // SPICE_D3D11 HWND expected = nullptr;
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
(uintptr_t) hwnd);
}
}
HWND main_hwnd() {
return g_main_hwnd.load();
}
void ignore_hwnd(HWND hwnd) {
g_ignored_hwnd.store(hwnd);
}
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
// so failed attempts can be retried on the next swapchain.
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
if (!swapchain) {
return;
}
static std::mutex s_hook_mutex;
std::lock_guard<std::mutex> lock(s_hook_mutex);
if (!g_swapchain_hooked) {
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
(void **) &Present_orig, "IDXGISwapChain::Present");
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
if (a && b) {
g_swapchain_hooked = true;
}
}
if (!g_swapchain1_hooked) {
IDXGISwapChain1 *sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
g_swapchain1_hooked = true;
}
sc1->Release();
}
}
}
}
#endif // SPICE_D3D11
@@ -1,175 +1,175 @@
// proactive vtable capture for the dx11 backend. // proactive vtable capture for the dx11 backend.
// //
// titles under the execexe loader routinely race past our export-level // titles under the execexe loader routinely race past our export-level
// trampolines, so the game's first real swapchain never goes through us. // trampolines, so the game's first real swapchain never goes through us.
// we sidestep that by creating a throwaway device + swapchain ourselves // we sidestep that by creating a throwaway device + swapchain ourselves
// the moment d3d11.dll + dxgi.dll appear, which patches the shared // the moment d3d11.dll + dxgi.dll appear, which patches the shared
// IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game. // IDXGISwapChain[1] / IDXGIFactory[2] vtables ahead of the game.
#include "d3d11_backend.h" #include "d3d11_backend.h"
#ifdef SPICE_D3D11 #ifdef SPICE_D3D11
#include <atomic> #include <atomic>
#include <memory> #include <memory>
#include <windows.h> #include <windows.h>
#include <d3d11.h> #include <d3d11.h>
#include <dxgi.h> #include <dxgi.h>
#include <dxgi1_2.h> #include <dxgi1_2.h>
#include "d3d11_internal.h" #include "d3d11_internal.h"
using d3d11_hooks::com_ptr; using d3d11_hooks::com_ptr;
namespace { namespace {
using D3D11CreateDevice_t = HRESULT(WINAPI *)( using D3D11CreateDevice_t = HRESULT(WINAPI *)(
IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT, IDXGIAdapter *, D3D_DRIVER_TYPE, HMODULE, UINT,
const D3D_FEATURE_LEVEL *, UINT, UINT, const D3D_FEATURE_LEVEL *, UINT, UINT,
ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **); ID3D11Device **, D3D_FEATURE_LEVEL *, ID3D11DeviceContext **);
using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **); using CreateDXGIFactory1_t = HRESULT(WINAPI *)(REFIID, void **);
using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **); using CreateDXGIFactory2_t = HRESULT(WINAPI *)(UINT, REFIID, void **);
std::atomic<bool> g_vtables_captured { false }; std::atomic<bool> g_vtables_captured { false };
template<typename Fn> template<typename Fn>
Fn resolve(HMODULE mod, const char *name) { Fn resolve(HMODULE mod, const char *name) {
return reinterpret_cast<Fn>(GetProcAddress(mod, name)); return reinterpret_cast<Fn>(GetProcAddress(mod, name));
} }
com_ptr<IDXGIFactory2> create_factory2(CreateDXGIFactory2_t f2, com_ptr<IDXGIFactory2> create_factory2(CreateDXGIFactory2_t f2,
CreateDXGIFactory1_t f1) CreateDXGIFactory1_t f1)
{ {
IDXGIFactory2 *raw = nullptr; IDXGIFactory2 *raw = nullptr;
if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) { if (f2 && SUCCEEDED(f2(0, IID_PPV_ARGS(&raw))) && raw) {
return com_ptr<IDXGIFactory2>(raw); return com_ptr<IDXGIFactory2>(raw);
} }
IDXGIFactory1 *factory1 = nullptr; IDXGIFactory1 *factory1 = nullptr;
if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) { if (f1 && SUCCEEDED(f1(IID_PPV_ARGS(&factory1))) && factory1) {
factory1->QueryInterface(IID_PPV_ARGS(&raw)); factory1->QueryInterface(IID_PPV_ARGS(&raw));
factory1->Release(); factory1->Release();
} }
return com_ptr<IDXGIFactory2>(raw); return com_ptr<IDXGIFactory2>(raw);
} }
bool create_dummy_device(D3D11CreateDevice_t create, bool create_dummy_device(D3D11CreateDevice_t create,
com_ptr<ID3D11Device> &device, com_ptr<ID3D11Device> &device,
com_ptr<ID3D11DeviceContext> &context) com_ptr<ID3D11DeviceContext> &context)
{ {
static constexpr D3D_FEATURE_LEVEL levels[] = { static constexpr D3D_FEATURE_LEVEL levels[] = {
D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL_10_0,
}; };
// hardware first, then WARP so headless / unusual configs still work. // hardware first, then WARP so headless / unusual configs still work.
for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) { for (auto type : { D3D_DRIVER_TYPE_HARDWARE, D3D_DRIVER_TYPE_WARP }) {
ID3D11Device *d = nullptr; ID3D11Device *d = nullptr;
ID3D11DeviceContext *c = nullptr; ID3D11DeviceContext *c = nullptr;
D3D_FEATURE_LEVEL got; D3D_FEATURE_LEVEL got;
if (SUCCEEDED(create(nullptr, type, nullptr, 0, if (SUCCEEDED(create(nullptr, type, nullptr, 0,
levels, ARRAYSIZE(levels), D3D11_SDK_VERSION, levels, ARRAYSIZE(levels), D3D11_SDK_VERSION,
&d, &got, &c)) && d) { &d, &got, &c)) && d) {
device.reset(d); device.reset(d);
context.reset(c); context.reset(c);
return true; return true;
} }
} }
return false; return false;
} }
} // namespace } // namespace
namespace d3d11_hooks { namespace d3d11_hooks {
// create a throwaway device + swapchain to patch the shared vtables before // create a throwaway device + swapchain to patch the shared vtables before
// the game's loader races past our export trampolines. safe to call // the game's loader races past our export trampolines. safe to call
// repeatedly; runs at most once. // repeatedly; runs at most once.
void try_capture_vtables() { void try_capture_vtables() {
if (g_vtables_captured.load()) { if (g_vtables_captured.load()) {
return; return;
} }
HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll"); HMODULE d3d11 = GetModuleHandleW(L"d3d11.dll");
HMODULE dxgi = GetModuleHandleW(L"dxgi.dll"); HMODULE dxgi = GetModuleHandleW(L"dxgi.dll");
if (!d3d11 || !dxgi) { if (!d3d11 || !dxgi) {
return; return;
} }
auto create_device = resolve<D3D11CreateDevice_t>(d3d11, "D3D11CreateDevice"); auto create_device = resolve<D3D11CreateDevice_t>(d3d11, "D3D11CreateDevice");
auto f2 = resolve<CreateDXGIFactory2_t>(dxgi, "CreateDXGIFactory2"); auto f2 = resolve<CreateDXGIFactory2_t>(dxgi, "CreateDXGIFactory2");
auto f1 = resolve<CreateDXGIFactory1_t>(dxgi, "CreateDXGIFactory1"); auto f1 = resolve<CreateDXGIFactory1_t>(dxgi, "CreateDXGIFactory1");
if (!create_device || (!f1 && !f2)) { if (!create_device || (!f1 && !f2)) {
return; return;
} }
// serialize concurrent calls (poll thread + LDR notification). only // serialize concurrent calls (poll thread + LDR notification). only
// flip g_vtables_captured after success so failed attempts remain // flip g_vtables_captured after success so failed attempts remain
// retriable on the next tick. // retriable on the next tick.
static std::atomic<bool> in_progress { false }; static std::atomic<bool> in_progress { false };
if (in_progress.exchange(true)) { if (in_progress.exchange(true)) {
return; return;
} }
struct scope_clear { struct scope_clear {
std::atomic<bool> &flag; std::atomic<bool> &flag;
~scope_clear() { flag.store(false); } ~scope_clear() { flag.store(false); }
} clear { in_progress }; } clear { in_progress };
// hidden message-only window; STATIC is always registered by user32. // hidden message-only window; STATIC is always registered by user32.
HWND dummy_hwnd = CreateWindowExW( HWND dummy_hwnd = CreateWindowExW(
0, L"STATIC", L"", 0, 0, 0, 1, 1, 0, L"STATIC", L"", 0, 0, 0, 1, 1,
HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr); HWND_MESSAGE, nullptr, GetModuleHandleW(nullptr), nullptr);
if (!dummy_hwnd) { if (!dummy_hwnd) {
log_warning("graphics::d3d11", log_warning("graphics::d3d11",
"vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError()); "vtable capture: CreateWindowExW failed (gle={})", (unsigned long)GetLastError());
return; return;
} }
auto destroy_hwnd = std::unique_ptr<HWND__, decltype(&DestroyWindow)>( auto destroy_hwnd = std::unique_ptr<HWND__, decltype(&DestroyWindow)>(
dummy_hwnd, &DestroyWindow); dummy_hwnd, &DestroyWindow);
// if the game's CreateDXGIFactory_hook already raced us, our // if the game's CreateDXGIFactory_hook already raced us, our
// CreateSwapChainForHwnd call below would trip the hook and try to // CreateSwapChainForHwnd call below would trip the hook and try to
// record dummy_hwnd as the main window. block that. // record dummy_hwnd as the main window. block that.
ignore_hwnd(dummy_hwnd); ignore_hwnd(dummy_hwnd);
auto factory2 = create_factory2(f2, f1); auto factory2 = create_factory2(f2, f1);
if (!factory2) { if (!factory2) {
log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed"); log_warning("graphics::d3d11", "vtable capture: CreateDXGIFactory* failed");
return; return;
} }
com_ptr<ID3D11Device> device; com_ptr<ID3D11Device> device;
com_ptr<ID3D11DeviceContext> context; com_ptr<ID3D11DeviceContext> context;
if (!create_dummy_device(create_device, device, context)) { if (!create_dummy_device(create_device, device, context)) {
log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed"); log_warning("graphics::d3d11", "vtable capture: D3D11CreateDevice failed");
return; return;
} }
DXGI_SWAP_CHAIN_DESC1 desc {}; DXGI_SWAP_CHAIN_DESC1 desc {};
desc.Width = 1; desc.Width = 1;
desc.Height = 1; desc.Height = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
desc.SampleDesc.Count = 1; desc.SampleDesc.Count = 1;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.BufferCount = 2; desc.BufferCount = 2;
desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; desc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
IDXGISwapChain1 *raw_sc = nullptr; IDXGISwapChain1 *raw_sc = nullptr;
HRESULT hr = factory2->CreateSwapChainForHwnd( HRESULT hr = factory2->CreateSwapChainForHwnd(
device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc); device.get(), dummy_hwnd, &desc, nullptr, nullptr, &raw_sc);
if (FAILED(hr) || !raw_sc) { if (FAILED(hr) || !raw_sc) {
log_warning("graphics::d3d11", log_warning("graphics::d3d11",
"vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr); "vtable capture: CreateSwapChainForHwnd failed (hr={:#x})", (unsigned long)hr);
return; return;
} }
com_ptr<IDXGISwapChain1> swapchain(raw_sc); com_ptr<IDXGISwapChain1> swapchain(raw_sc);
install_swapchain_hooks(swapchain.get()); install_swapchain_hooks(swapchain.get());
install_factory_hooks(factory2.get()); install_factory_hooks(factory2.get());
g_vtables_captured.store(true); g_vtables_captured.store(true);
log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)"); log_info("graphics::d3d11", "vtable capture complete (via dummy swapchain)");
} }
} }
#endif // SPICE_D3D11 #endif // SPICE_D3D11
@@ -1,15 +1,12 @@
#include "d3d9_backend.h" #include "d3d9_backend.h"
#include <atomic>
#include <cassert> #include <cassert>
#include <memory> #include <memory>
#include <thread> #include <thread>
#include <vector> #include <vector>
#include <external/robin_hood.h>
#include <d3d9.h> #include <d3d9.h>
#ifdef __GNUC__
#include <d3dx9tex.h>
#endif
#include "avs/game.h" #include "avs/game.h"
#include "cfg/screen_resize.h" #include "cfg/screen_resize.h"
@@ -18,28 +15,23 @@
#include "games/popn/popn.h" #include "games/popn/popn.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "games/mfc/mfc.h" #include "games/mfc/mfc.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
#include "launcher/launcher.h" #include "launcher/launcher.h"
#include "launcher/options.h" #include "launcher/options.h"
#include "launcher/signal.h" #include "launcher/signal.h"
#include "launcher/shutdown.h" #include "launcher/shutdown.h"
#include "misc/clipboard.h"
#include "misc/eamuse.h"
#include "misc/wintouchemu.h" #include "misc/wintouchemu.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/notifications.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/deferlog.h" #include "util/deferlog.h"
#include "util/fileutils.h"
#include "util/flags_helper.h" #include "util/flags_helper.h"
#include "util/libutils.h" #include "util/libutils.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
#include "util/memutils.h" #include "util/memutils.h"
#include "util/threadpool.h"
#include "d3d9_device.h" #include "d3d9_device.h"
#include "d3d9_screenshot.h"
#ifdef min #ifdef min
#undef min #undef min
@@ -63,19 +55,6 @@
return __ret; \ return __ret; \
} while (0) } while (0)
#ifdef __GNUC__
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
#else
#define D3DXIFF_PNG ((DWORD) 3)
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
LPCSTR pDestFile,
DWORD DestFormat,
LPDIRECT3DSURFACE9 pSrcSurface,
CONST PALETTEENTRY *pSrcPalette,
CONST RECT *pSrcRect);
#endif
/* /*
* 9 on 12 * 9 on 12
*/ */
@@ -99,8 +78,6 @@ typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr; static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16]; static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
// settings // settings
std::optional<UINT> D3D9_ADAPTER = std::nullopt; std::optional<UINT> D3D9_ADAPTER = std::nullopt;
DWORD D3D9_BEHAVIOR_DISABLE = 0; DWORD D3D9_BEHAVIOR_DISABLE = 0;
@@ -113,6 +90,12 @@ static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false; static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr; static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
// main and subscreen presents may occur on different threads.
static std::atomic_bool SUBSCREEN_PRESENTED_SINCE_LAST_MAIN = false;
// do not mistake the fallback Present below for a game-originated Present.
static thread_local bool SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
static void graphics_d3d9_ldj_init_sub_screen( static void graphics_d3d9_ldj_init_sub_screen(
IDirect3DDevice9Ex *device, IDirect3DDevice9Ex *device,
D3DPRESENT_PARAMETERS *present_params, D3DPRESENT_PARAMETERS *present_params,
@@ -1445,6 +1428,12 @@ IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
return surface; return surface;
} }
void graphics_d3d9_notify_subscreen_present() {
if (SUBSCREEN_FORCE_REDRAW && !SUBSCREEN_FORCE_REDRAW_IN_PROGRESS) {
SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.store(true, std::memory_order_relaxed);
}
}
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) { static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx // iidx/sdvx
int swapchain = 1; int swapchain = 1;
@@ -1472,188 +1461,24 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// //
// early versions of popn HC needs this as well, but not on by default as it can cause // early versions of popn HC needs this as well, but not on by default as it can cause
// graphical glitches on some GPUs // graphical glitches on some GPUs
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) { //
// treat forced redraw as a fallback so it does not duplicate a successful game present.
const bool force_redraw = SUBSCREEN_FORCE_REDRAW &&
!SUBSCREEN_PRESENTED_SINCE_LAST_MAIN.exchange(false, std::memory_order_relaxed);
if (GRAPHICS_WINDOWED || force_redraw) {
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = true;
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0); SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
SUBSCREEN_FORCE_REDRAW_IN_PROGRESS = false;
} }
} }
} }
static void save_capture(
int screen,
D3DFORMAT format,
UINT width,
UINT height,
IDirect3DSurface9 *surface) {
HRESULT hr;
// lock surface to be able to access the data
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// copy pixel data
size_t pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
auto pixels = new uint8_t[width * height * 3];
for (size_t row = 0; row < height; row++) {
size_t offset_pixels = 0;
size_t offset_row = row * width * 3;
switch (format) {
case D3DFMT_R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 3) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8R8G8B8:
case D3DFMT_A8R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[2];
pixel[1] = cell[1];
pixel[2] = cell[0];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8B8G8R8:
case D3DFMT_A8B8G8R8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
default: {
for (size_t offset = 0; offset < width; offset++) {
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
offset_pixels += 3;
}
}
}
}
// unlock surface
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// enqueue
graphics_capture_enqueue(screen, pixels, width, height);
}
static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) {
HRESULT hr;
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// set alpha channel to 255
{
auto pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
for (size_t i = 0; i < height; i++) {
for (int j = 3; j < pitch; j += 4) {
data[i * pitch + j] = 255;
}
}
}
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// lazy load function
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
// check if function was not found, likely because d3dx9 is not loaded
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
for (size_t i = 43; i >= 24; i--) {
auto lib_name = fmt::format("d3dx9_{}.dll", i);
auto d3dx9 = libutils::try_library(lib_name);
// Check if library was not found
if (d3dx9 == nullptr) {
continue;
}
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
d3dx9, "D3DXSaveSurfaceToFileA");
// Check if function was not found
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
FreeLibrary(d3dx9);
d3dx9 = nullptr;
continue;
}
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
break;
}
}
}
if (D3DXSaveSurfaceToFileA_ptr != nullptr) {
// save to file
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "Failed to save screenshot");
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
return;
}
// save to clipboard
clipboard::copy_image(file_path);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(file_path)));
} else {
log_warning("graphics::d3d9", "Direct3D save helper function not available");
}
}
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
@@ -1664,6 +1489,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.
@@ -1683,6 +1515,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;
@@ -1696,136 +1537,6 @@ void graphics_d3d9_on_present(
if (is_mfc) { if (is_mfc) {
wintouchemu::update(); wintouchemu::update();
} }
// check screenshot key
static bool trigger_last = 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::Screenshot)))
{
if (!trigger_last) {
graphics_screenshot_trigger();
}
trigger_last = true;
} else {
trigger_last = false;
}
// process pending screenshot
bool screenshot = false;
bool capture = false;
int capture_screen = 0;
if ((screenshot = graphics_screenshot_consume())
|| ((capture = graphics_capture_consume(&capture_screen)))) {
HRESULT hr = S_OK;
// TODO: verify capture_screen is a valid swapchain
// get back buffer
IDirect3DSurface9 *buffer = nullptr;
if (SUB_SWAP_CHAIN != nullptr && capture_screen & 1) {
hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
} else {
hr = device->GetBackBuffer(capture_screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer);
}
if (FAILED(hr) || buffer == nullptr) {
log_warning("graphics::d3d9",
"failed to get back buffer, hr={}",
FMT_HRESULT(hr));
if (capture) {
graphics_capture_skip(capture_screen);
}
return;
}
D3DSURFACE_DESC desc {};
hr = buffer->GetDesc(&desc);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr));
buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return;
}
// TODO: cache render targets
IDirect3DSurface9 *temp_surface = nullptr;
hr = device->CreateRenderTarget(
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
if (FAILED(hr) || temp_surface == nullptr) {
log_warning("graphics::d3d9",
"failed to acquire temporary surface, hr={}",
FMT_HRESULT(hr));
buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return;
}
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to copy back buffer contents, hr={}",
FMT_HRESULT(hr));
temp_surface->Release();
buffer->Release();
if (capture) {
graphics_capture_skip(capture_screen);
}
return;
}
// release original back buffer reference
buffer->Release();
// function for storing the surface
auto surface_process = [=]() {
// capture
if (capture) {
save_capture(capture_screen, desc.Format, desc.Width, desc.Height, temp_surface);
}
// screenshot
if (screenshot) {
// check where we can save it
auto file_path = graphics_screenshot_genpath();
if (!file_path.empty()) {
// write to file
save_screenshot(file_path, desc.Height, temp_surface);
}
}
// release surface
temp_surface->Release();
};
// list of games that crash when running the screenshot processor on another thread
static const robin_hood::unordered_set<std::string> THREAD_BAN {
"JMA",
#ifndef SPICE64
"KFC",
#endif
"KMA",
"KLP",
"LMA",
};
// run the save operation on another thread for supported games
if (THREAD_BAN.contains(avs::game::MODEL)) {
surface_process();
} else {
static auto pool = ThreadPool(2);
pool.add(surface_process);
}
}
} }
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,9 @@ 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();
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen(); IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen();
@@ -23,6 +23,11 @@
#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]] { \
@@ -134,6 +139,10 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release(); this->main_swapchain->Release();
this->main_swapchain = nullptr; this->main_swapchain = nullptr;
} }
if (this->implicit_sub_swapchain) {
this->implicit_sub_swapchain->Release();
this->implicit_sub_swapchain = nullptr;
}
for (auto &sc : this->sub_swapchain) { for (auto &sc : this->sub_swapchain) {
if (sc) { if (sc) {
sc->Release(); sc->Release();
@@ -332,20 +341,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;
@@ -357,6 +371,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(
@@ -487,6 +506,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
fake_sub_swapchain[0]->AddRef(); fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]); *ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain);
return D3D_OK;
} else if (SUBSCREEN_FORCE_REDRAW) {
// store implicit sub swap chain
if (!implicit_sub_swapchain) {
IDirect3DSwapChain9 *real_swapchain = nullptr;
HRESULT ret = pReal->GetSwapChain(iSwapChain, &real_swapchain);
if (FAILED(ret)) {
return ret;
}
implicit_sub_swapchain = new WrappedIDirect3DSwapChain9(this, real_swapchain);
implicit_sub_swapchain->should_run_hooks = false;
}
implicit_sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(implicit_sub_swapchain);
graphics_screens_register(iSwapChain); graphics_screens_register(iSwapChain);
return D3D_OK; return D3D_OK;
} }
@@ -517,6 +554,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)
{ {
@@ -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>
@@ -234,6 +235,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;
@@ -262,6 +267,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1; std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr; WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *implicit_sub_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr }; WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr }; FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
@@ -4,10 +4,12 @@
#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;
@@ -1,144 +1,144 @@
#include "d3d9_live2d.h" #include "d3d9_live2d.h"
// only the Live2D-capable SDVX versions are 64-bit, so the entire implementation // only the Live2D-capable SDVX versions are 64-bit, so the entire implementation
// is compiled out of 32-bit builds (the header supplies inline no-op stubs there). // is compiled out of 32-bit builds (the header supplies inline no-op stubs there).
#ifdef SPICE64 #ifdef SPICE64
#include <cstdint> #include <cstdint>
#include <unordered_set> #include <unordered_set>
#include "hooks/graphics/graphics.h" #include "hooks/graphics/graphics.h"
// how the Live2D draw filtering works // how the Live2D draw filtering works
// ------------------------------------ // ------------------------------------
// SDVX draws its Live2D characters with a small, fixed set of pixel and // SDVX draws its Live2D characters with a small, fixed set of pixel and
// vertex shaders. to skip those draws (and save GPU) we have to recognise them at // vertex shaders. to skip those draws (and save GPU) we have to recognise them at
// the exact moment the game issues a draw call. the d3d9 device hooks feed three // the exact moment the game issues a draw call. the d3d9 device hooks feed three
// kinds of events into this module: // kinds of events into this module:
// //
// 1. shader creation (on_create_pixel_shader / on_create_vertex_shader) // 1. shader creation (on_create_pixel_shader / on_create_vertex_shader)
// the game compiles its shaders once at load. we can't trust the shader // the game compiles its shaders once at load. we can't trust the shader
// *object pointer* to identify a shader (it's just a heap address that // *object pointer* to identify a shader (it's just a heap address that
// varies per run and can be recycled), so instead we hash the shader's // varies per run and can be recycled), so instead we hash the shader's
// D3D9 *bytecode* - that fingerprint is stable across runs because the // D3D9 *bytecode* - that fingerprint is stable across runs because the
// game ships the same shaders. if the hash matches a known Live2D shader // game ships the same shaders. if the hash matches a known Live2D shader
// we remember that object pointer in g_live2d_shaders. // we remember that object pointer in g_live2d_shaders.
// //
// 2. shader binding (on_set_pixel_shader / on_set_vertex_shader) // 2. shader binding (on_set_pixel_shader / on_set_vertex_shader)
// whenever the game binds a shader we look it up in that set once and cache // whenever the game binds a shader we look it up in that set once and cache
// the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen // the yes/no answer in g_cur_ps_is_live2d / g_cur_vs_is_live2d. binds happen
// far less often than draws, so this is where the lookup cost lives. // far less often than draws, so this is where the lookup cost lives.
// //
// 3. draw call (should_skip_draw, called from every Draw* hook) // 3. draw call (should_skip_draw, called from every Draw* hook)
// the per-draw question "is this a Live2D draw?" is then just reading those // the per-draw question "is this a Live2D draw?" is then just reading those
// two cached bools - no hashing, no map lookups. if the skip is currently // two cached bools - no hashing, no map lookups. if the skip is currently
// active (see graphics_sdvx_live2d_should_skip) and either bound shader is // active (see graphics_sdvx_live2d_should_skip) and either bound shader is
// Live2D, the Draw* hook drops the call instead of forwarding it. // Live2D, the Draw* hook drops the call instead of forwarding it.
// //
// everything is gated on the feature being enabled (mode != Off); when it's Off // everything is gated on the feature being enabled (mode != Off); when it's Off
// every entry point is a single predicted-not-taken branch. d3d9 rendering for a // every entry point is a single predicted-not-taken branch. d3d9 rendering for a
// device is single-threaded, so none of this state needs locking. // device is single-threaded, so none of this state needs locking.
namespace { namespace {
// shader state is tracked whenever the feature might act (mode != Off) so the // shader state is tracked whenever the feature might act (mode != Off) so the
// known-shader set is populated before a song starts. when Off, every entry // known-shader set is populated before a song starts. when Off, every entry
// point is a single cheap branch. // point is a single cheap branch.
bool tracking_enabled() { bool tracking_enabled() {
return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off; return GRAPHICS_SDVX_LIVE2D_MODE != SdvxLive2dMode::Off;
} }
// the set of shader objects (pixel or vertex) whose bytecode matched a known // the set of shader objects (pixel or vertex) whose bytecode matched a known
// Live2D fingerprint. only matching shaders are stored, so this stays tiny. // Live2D fingerprint. only matching shaders are stored, so this stays tiny.
std::unordered_set<void *> g_live2d_shaders; std::unordered_set<void *> g_live2d_shaders;
// whether the currently-bound shaders are known Live2D shaders. cached at set // whether the currently-bound shaders are known Live2D shaders. cached at set
// time so the per-draw check is just two bool reads. // time so the per-draw check is just two bool reads.
bool g_cur_ps_is_live2d = false; bool g_cur_ps_is_live2d = false;
bool g_cur_vs_is_live2d = false; bool g_cur_vs_is_live2d = false;
// FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF) // FNV-1a 64 over a D3D9 shader token stream (ends with D3DSIO_END = 0x0000FFFF)
uint64_t bytecode_hash(const DWORD *func) { uint64_t bytecode_hash(const DWORD *func) {
if (func == nullptr) { if (func == nullptr) {
return 0; return 0;
} }
const DWORD *p = func; const DWORD *p = func;
const DWORD *cap = func + 65536; // safety bound const DWORD *cap = func + 65536; // safety bound
while (p < cap && *p != 0x0000FFFF) { while (p < cap && *p != 0x0000FFFF) {
p++; p++;
} }
const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD); const size_t n_bytes = ((size_t)(p - func) + 1) * sizeof(DWORD);
uint64_t h = 1469598103934665603ULL; uint64_t h = 1469598103934665603ULL;
const auto *bytes = reinterpret_cast<const uint8_t *>(func); const auto *bytes = reinterpret_cast<const uint8_t *>(func);
for (size_t i = 0; i < n_bytes; i++) { for (size_t i = 0; i < n_bytes; i++) {
h ^= bytes[i]; h ^= bytes[i];
h *= 1099511628211ULL; h *= 1099511628211ULL;
} }
return h; return h;
} }
// known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable // known SDVX Live2D shader bytecode hashes (4 pixel + 3 vertex). stable
// across runs because the game ships fixed shaders. the two sets are disjoint so // across runs because the game ships fixed shaders. the two sets are disjoint so
// a single shader can be classified by its own hash alone. // a single shader can be classified by its own hash alone.
bool hash_is_live2d(uint64_t hash) { bool hash_is_live2d(uint64_t hash) {
switch (hash) { switch (hash) {
case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song) case 0x75c89951817421a4ULL: // pixel: dominant model draw (~4.9M prims/120f in-song)
case 0x2d7ce428c6b4775dULL: // pixel: masked model draw case 0x2d7ce428c6b4775dULL: // pixel: masked model draw
case 0x3ce00cc6111c10e7ULL: // pixel: mask generation case 0x3ce00cc6111c10e7ULL: // pixel: mask generation
case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant) case 0x8bb3a2f37150ac34ULL: // pixel: mask generation (variant)
case 0xe9cf898c331e2a51ULL: // vertex case 0xe9cf898c331e2a51ULL: // vertex
case 0x94dc84e7b7c0f437ULL: // vertex case 0x94dc84e7b7c0f437ULL: // vertex
case 0xc872937c5cc04309ULL: // vertex case 0xc872937c5cc04309ULL: // vertex
return true; return true;
} }
return false; return false;
} }
// classify a shader at creation time and record it if it is Live2D. erasing on a // classify a shader at creation time and record it if it is Live2D. erasing on a
// miss keeps the set correct if the runtime reuses a freed shader pointer. // miss keeps the set correct if the runtime reuses a freed shader pointer.
void classify_shader(void *shader, const DWORD *func) { void classify_shader(void *shader, const DWORD *func) {
if (hash_is_live2d(bytecode_hash(func))) { if (hash_is_live2d(bytecode_hash(func))) {
g_live2d_shaders.insert(shader); g_live2d_shaders.insert(shader);
} else { } else {
g_live2d_shaders.erase(shader); g_live2d_shaders.erase(shader);
} }
} }
} // namespace } // namespace
namespace d3d9_live2d { namespace d3d9_live2d {
// stage 1: fingerprint each shader as the game creates it // stage 1: fingerprint each shader as the game creates it
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) { void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] { if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func); classify_shader(shader, func);
} }
} }
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) { void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func) {
if (tracking_enabled() && shader != nullptr) [[unlikely]] { if (tracking_enabled() && shader != nullptr) [[unlikely]] {
classify_shader(shader, func); classify_shader(shader, func);
} }
} }
// stage 2: remember whether the just-bound shader is a Live2D one // stage 2: remember whether the just-bound shader is a Live2D one
void on_set_vertex_shader(IDirect3DVertexShader9 *shader) { void on_set_vertex_shader(IDirect3DVertexShader9 *shader) {
if (tracking_enabled()) [[unlikely]] { if (tracking_enabled()) [[unlikely]] {
g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0; g_cur_vs_is_live2d = g_live2d_shaders.count(shader) != 0;
} }
} }
void on_set_pixel_shader(IDirect3DPixelShader9 *shader) { void on_set_pixel_shader(IDirect3DPixelShader9 *shader) {
if (tracking_enabled()) [[unlikely]] { if (tracking_enabled()) [[unlikely]] {
g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0; g_cur_ps_is_live2d = g_live2d_shaders.count(shader) != 0;
} }
} }
// stage 3: drop the draw if the skip is active and a Live2D shader is bound // stage 3: drop the draw if the skip is active and a Live2D shader is bound
bool should_skip_draw() { bool should_skip_draw() {
return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d); return graphics_sdvx_live2d_should_skip() && (g_cur_ps_is_live2d || g_cur_vs_is_live2d);
} }
} // namespace d3d9_live2d } // namespace d3d9_live2d
#endif // SPICE64 #endif // SPICE64
@@ -1,44 +1,44 @@
#pragma once #pragma once
#include <windows.h> #include <windows.h>
#include <d3d9.h> #include <d3d9.h>
// SDVX Live2D draw-skip support for the D3D9 backend. // SDVX Live2D draw-skip support for the D3D9 backend.
// //
// SDVX renders its Live2D navigator / in-song character through a fixed set of // SDVX renders its Live2D navigator / in-song character through a fixed set of
// shaders. when the skip is active (see graphics_sdvx_live2d_should_skip) // shaders. when the skip is active (see graphics_sdvx_live2d_should_skip)
// the matching draw calls are dropped to save GPU. shaders are identified by a // the matching draw calls are dropped to save GPU. shaders are identified by a
// stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode // stable hash of their D3D9 bytecode (object pointers vary per run, the bytecode
// does not). the hashes were captured with the draw-call fingerprinting tool. // does not). the hashes were captured with the draw-call fingerprinting tool.
// //
// every entry point is a no-op unless the feature is enabled (mode != Off), and // every entry point is a no-op unless the feature is enabled (mode != Off), and
// d3d9 rendering for a device is single-threaded, so none of this needs locking. // d3d9 rendering for a device is single-threaded, so none of this needs locking.
namespace d3d9_live2d { namespace d3d9_live2d {
#ifdef SPICE64 #ifdef SPICE64
// record a shader's bytecode fingerprint at creation time // record a shader's bytecode fingerprint at creation time
void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func); void on_create_vertex_shader(IDirect3DVertexShader9 *shader, const DWORD *func);
void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func); void on_create_pixel_shader(IDirect3DPixelShader9 *shader, const DWORD *func);
// remember the currently-bound shaders // remember the currently-bound shaders
void on_set_vertex_shader(IDirect3DVertexShader9 *shader); void on_set_vertex_shader(IDirect3DVertexShader9 *shader);
void on_set_pixel_shader(IDirect3DPixelShader9 *shader); void on_set_pixel_shader(IDirect3DPixelShader9 *shader);
// true if the current draw call should be dropped (skip active AND the bound // true if the current draw call should be dropped (skip active AND the bound
// shaders identify it as SDVX Live2D) // shaders identify it as SDVX Live2D)
bool should_skip_draw(); bool should_skip_draw();
#else // !SPICE64 #else // !SPICE64
// only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point // only the Live2D-capable SDVX versions are 64-bit; on 32-bit every entry point
// compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their // compiles away to nothing, so the d3d9 device hooks need no #ifdefs at their
// call sites. // call sites.
inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {} inline void on_create_vertex_shader(IDirect3DVertexShader9 *, const DWORD *) {}
inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {} inline void on_create_pixel_shader(IDirect3DPixelShader9 *, const DWORD *) {}
inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {} inline void on_set_vertex_shader(IDirect3DVertexShader9 *) {}
inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {} inline void on_set_pixel_shader(IDirect3DPixelShader9 *) {}
inline bool should_skip_draw() { return false; } inline bool should_skip_draw() { return false; }
#endif // SPICE64 #endif // SPICE64
} }
@@ -0,0 +1,470 @@
#include "d3d9_screenshot.h"
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <external/robin_hood.h>
#ifdef __GNUC__
#include <d3dx9tex.h>
#endif
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "misc/clipboard.h"
#include "overlay/notifications.h"
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/threadpool.h"
#include "d3d9_device.h"
#ifdef __GNUC__
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
#else
#define D3DXIFF_PNG ((DWORD) 3)
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
LPCSTR pDestFile,
DWORD DestFormat,
LPDIRECT3DSURFACE9 pSrcSurface,
CONST PALETTEENTRY *pSrcPalette,
CONST RECT *pSrcRect);
#endif
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
// genpath picks free filenames by probing the disk, so only one save may run at a time
static std::mutex SCREENSHOT_SAVE_M;
namespace {
enum class ImageRequestKind {
Screenshot,
Capture,
};
struct ImageRequest {
ImageRequestKind kind;
int screen;
};
struct SurfaceReleaser {
void operator()(IDirect3DSurface9 *surface) const {
surface->Release();
}
};
using SurfacePtr = std::unique_ptr<IDirect3DSurface9, SurfaceReleaser>;
struct BackbufferCopy {
int screen {};
D3DSURFACE_DESC desc {};
SurfacePtr surface;
};
} // namespace
static void save_capture(
int screen,
D3DFORMAT format,
UINT width,
UINT height,
IDirect3DSurface9 *surface) {
HRESULT hr;
// lock surface to be able to access the data
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// normalize supported D3D formats to packed RGB for API capture
size_t pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
auto pixels = std::unique_ptr<uint8_t[]>(new uint8_t[width * height * 3]);
for (size_t row = 0; row < height; row++) {
size_t offset_row = row * width * 3;
switch (format) {
case D3DFMT_R8G8B8: {
for (size_t column = 0; column < width; column++) {
auto cell = data + row * pitch + column * 3;
auto pixel = &pixels[offset_row + column * 3];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
}
break;
}
case D3DFMT_X8R8G8B8:
case D3DFMT_A8R8G8B8: {
for (size_t column = 0; column < width; column++) {
auto cell = data + row * pitch + column * 4;
auto pixel = &pixels[offset_row + column * 3];
pixel[0] = cell[2];
pixel[1] = cell[1];
pixel[2] = cell[0];
}
break;
}
case D3DFMT_X8B8G8R8:
case D3DFMT_A8B8G8R8: {
for (size_t column = 0; column < width; column++) {
auto cell = data + row * pitch + column * 4;
auto pixel = &pixels[offset_row + column * 3];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
}
break;
}
default: {
for (size_t column = 0; column < width; column++) {
auto pixel = &pixels[offset_row + column * 3];
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
}
}
}
}
// unlock surface
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// enqueue
graphics_capture_enqueue(screen, pixels.release(), width, height);
}
static bool save_screenshot(
const std::string &file_path,
D3DFORMAT format,
UINT width,
UINT height,
IDirect3DSurface9 *surface) {
// 32-bit XRGB and ARGB surfaces use byte 3 as alpha; force opaque PNG output
if (format == D3DFMT_X8R8G8B8 || format == D3DFMT_A8R8G8B8 ||
format == D3DFMT_X8B8G8R8 || format == D3DFMT_A8B8G8R8) {
D3DLOCKED_RECT finished_copy {};
HRESULT hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
return false;
}
const size_t pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
for (size_t row = 0; row < height; row++) {
for (size_t column = 0; column < width; column++) {
data[row * pitch + column * 4 + 3] = 255;
}
}
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
return false;
}
}
// lazy load function
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
// check if function was not found, likely because d3dx9 is not loaded
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
// prefer the newest installed helper while supporting older D3DX9 runtimes
for (size_t i = 43; i >= 24; i--) {
auto lib_name = fmt::format("d3dx9_{}.dll", i);
auto d3dx9 = libutils::try_library(lib_name);
// Check if library was not found
if (d3dx9 == nullptr) {
continue;
}
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
d3dx9, "D3DXSaveSurfaceToFileA");
// Check if function was not found
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
FreeLibrary(d3dx9);
d3dx9 = nullptr;
continue;
}
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
break;
}
}
}
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
log_warning("graphics::d3d9", "Direct3D save helper function not available");
return false;
}
// save to file
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr(
file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
if (FAILED(save_result)) {
log_warning("graphics::d3d9", "Failed to save screenshot");
return false;
}
return true;
}
// screen 0 keeps the plain name so existing tooling and the clipboard copy are unaffected
static std::string screenshot_path_for_screen(const std::string &primary_path, int screen) {
if (screen == 0) {
return primary_path;
}
const std::filesystem::path path(primary_path);
return (path.parent_path() /
fmt::format("{}_{}{}", path.stem().string(), screen, path.extension().string()))
.string();
}
static std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
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 std::nullopt;
}
D3DSURFACE_DESC desc {};
hr = buffer->GetDesc(&desc);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr));
buffer->Release();
return std::nullopt;
}
// TODO: cache render targets
IDirect3DSurface9 *temp_surface = nullptr;
hr = device->CreateRenderTarget(
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
if (FAILED(hr) || temp_surface == nullptr) {
log_warning("graphics::d3d9",
"failed to acquire temporary surface, hr={}",
FMT_HRESULT(hr));
buffer->Release();
return std::nullopt;
}
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to copy back buffer contents, hr={}",
FMT_HRESULT(hr));
temp_surface->Release();
buffer->Release();
return std::nullopt;
}
// release original back buffer reference
buffer->Release();
return BackbufferCopy {
.screen = screen,
.desc = desc,
.surface = SurfacePtr(temp_surface),
};
}
static void dispatch_surface_save(
const ImageRequest &request,
std::vector<BackbufferCopy> copies,
size_t screen_count) {
auto surface_process = [request, screen_count, copies = std::move(copies)]() {
switch (request.kind) {
case ImageRequestKind::Capture: {
const auto &copy = copies.front();
save_capture(
request.screen,
copy.desc.Format,
copy.desc.Width,
copy.desc.Height,
copy.surface.get());
break;
}
case ImageRequestKind::Screenshot: {
std::lock_guard<std::mutex> lock(SCREENSHOT_SAVE_M);
std::vector<int> screens;
screens.reserve(copies.size());
for (const auto &copy : copies) {
screens.push_back(copy.screen);
}
const auto base_path = graphics_screenshot_genpath(screens);
if (base_path.empty()) {
break;
}
// screens missing from copies already failed to be acquired
size_t failed = screen_count - copies.size();
std::string primary_path;
std::string notify_path;
for (const auto &copy : copies) {
const auto path = screenshot_path_for_screen(base_path, copy.screen);
if (!save_screenshot(
path,
copy.desc.Format,
copy.desc.Width,
copy.desc.Height,
copy.surface.get())) {
failed++;
continue;
}
if (notify_path.empty()) {
notify_path = path;
}
if (copy.screen == 0) {
primary_path = path;
}
}
// only the primary screen goes to the clipboard, but any saved file is a success
if (!primary_path.empty()) {
clipboard::copy_image(primary_path);
}
if (!notify_path.empty()) {
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("Screenshot saved: {}", fileutils::basename(notify_path)));
} else {
overlay::notifications::add(
overlay::notifications::Severity::Error,
"Screenshot failed to save");
}
if (failed > 0) {
log_warning("graphics::d3d9", "{} screenshot screen(s) missing", failed);
}
break;
}
}
};
// list of games that crash when running the screenshot processor on another thread
static const robin_hood::unordered_set<std::string> THREAD_BAN {
"JMA",
#ifndef SPICE64
// KFC only crashes under threaded processing in 32-bit builds
"KFC",
#endif
"KMA",
"KLP",
"LMA",
};
// run the save operation on another thread for supported games
if (THREAD_BAN.contains(avs::game::MODEL)) {
surface_process();
} else {
static auto pool = ThreadPool(2);
pool.add(std::move(surface_process));
}
}
static void process_image_request(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device,
const ImageRequest &request) {
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
std::vector<int> screens { request.screen };
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
screens.clear();
wrapped_device->get_screenshot_screens(screens);
}
std::vector<BackbufferCopy> copies;
copies.reserve(screens.size());
for (const int screen : screens) {
std::optional<BackbufferCopy> copy;
IDirect3DSwapChain9 *swap_chain = nullptr;
HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
if (FAILED(hr) || swap_chain == nullptr) {
log_warning("graphics::d3d9",
"failed to get swap chain for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
} else {
copy = acquire_backbuffer_copy(device, swap_chain, screen);
swap_chain->Release();
}
if (copy.has_value()) {
copies.emplace_back(std::move(*copy));
} else if (!screenshot) {
graphics_capture_skip(request.screen);
return;
}
}
if (copies.empty()) {
return;
}
dispatch_surface_save(request, std::move(copies), screens.size());
}
void graphics_d3d9_process_screenshot(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device) {
if (graphics_screenshot_consume()) {
process_image_request(device, wrapped_device, ImageRequest {
.kind = ImageRequestKind::Screenshot,
.screen = 0,
});
}
}
void graphics_d3d9_process_capture(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device) {
int screen = 0;
if (graphics_capture_consume(&screen)) {
process_image_request(device, wrapped_device, ImageRequest {
.kind = ImageRequestKind::Capture,
.screen = screen,
});
}
}
@@ -0,0 +1,13 @@
#pragma once
#include <d3d9.h>
struct WrappedIDirect3DDevice9;
void graphics_d3d9_process_screenshot(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device);
void graphics_d3d9_process_capture(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device);
@@ -96,6 +96,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
pDirtyRegion, pDirtyRegion,
dwFlags); dwFlags);
// a successful game subscreen present suppresses the next forced fallback present.
if (SUCCEEDED(result) &&
(this == pDev->implicit_sub_swapchain || this == pDev->sub_swapchain[0])) {
graphics_d3d9_notify_subscreen_present();
}
// Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL // Some drivers report S_PRESENT_MODE_CHANGED after Windows moves the portrait SMALL
// head into the requested exclusive mode, leaving both group heads black. Toggle SMALL // head into the requested exclusive mode, leaving both group heads black. Toggle SMALL
// through another supported mode and restore it once, then retry the interrupted MAIN // through another supported mode and restore it once, then retry the interrupted MAIN
+70 -5
View File
@@ -17,11 +17,13 @@
#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/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 +33,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"
@@ -116,6 +119,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 +217,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,8 +629,10 @@ 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 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 +735,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 +760,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);
@@ -1100,6 +1138,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 +1363,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 +1423,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;
} }
@@ -1522,7 +1577,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
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 +1602,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;
} }
+6 -1
View File
@@ -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,6 +137,7 @@ 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();
void graphics_capture_trigger(int screen); void graphics_capture_trigger(int screen);
@@ -144,7 +148,8 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
bool rgb = true, int quality = 80, bool downsample = true, int divide = 0, bool rgb = true, int quality = 80, bool downsample = true, 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(); // 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);
+480 -480
View File
@@ -1,480 +1,480 @@
#include "nvapi_impl.h" #include "nvapi_impl.h"
#ifdef SPICE64 #ifdef SPICE64
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <windows.h> #include <windows.h>
#include "external/nvapi/nvapi.h" #include "external/nvapi/nvapi.h"
#include "hooks/libraryhook.h" #include "hooks/libraryhook.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/sysutils.h" #include "util/sysutils.h"
namespace nvapi_impl { namespace nvapi_impl {
namespace { namespace {
constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828; constexpr unsigned int NVAPI_INITIALIZE_ID = 0x0150E828;
constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F; constexpr unsigned int NVAPI_INITIALIZE_EX_ID = 0xAD298D3F;
constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E; constexpr unsigned int NVAPI_UNLOAD_ID = 0xD22BDD7E;
constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F; constexpr unsigned int NVAPI_ENUM_PHYSICAL_GPUS_ID = 0xE5AC921F;
constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2; constexpr unsigned int NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID = 0x0078DBA2;
constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31; constexpr unsigned int NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID = 0x1E9D8A31;
constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8; constexpr unsigned int NVAPI_DISP_GET_DISPLAY_CONFIG_ID = 0x11ABCCF8;
constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE; constexpr unsigned int NVAPI_DISP_SET_DISPLAY_CONFIG_ID = 0x5D8CF8DE;
constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll"; constexpr char NVAPI_DLL_NAME_A[] = "nvapi64.dll";
struct SyntheticDisplay { struct SyntheticDisplay {
NvU32 display_id; NvU32 display_id;
NvU32 width; NvU32 width;
NvU32 height; NvU32 height;
NvU32 color_depth; NvU32 color_depth;
NvS32 x; NvS32 x;
NvS32 y; NvS32 y;
NvU32 refresh_rate_1k; NvU32 refresh_rate_1k;
NV_ROTATE rotation; NV_ROTATE rotation;
bool primary; bool primary;
}; };
static bool provider_initialized = false; static bool provider_initialized = false;
static bool nvapi_initialized = false; static bool nvapi_initialized = false;
static int gpu_handle_storage = 0; static int gpu_handle_storage = 0;
// snapshot of the Win32 display state exposed through synthetic NVAPI // snapshot of the Win32 display state exposed through synthetic NVAPI
static std::vector<SyntheticDisplay> displays; static std::vector<SyntheticDisplay> displays;
static NvPhysicalGpuHandle get_gpu_handle() { static NvPhysicalGpuHandle get_gpu_handle() {
return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage); return reinterpret_cast<NvPhysicalGpuHandle>(&gpu_handle_storage);
} }
static NV_ROTATE get_rotation(DWORD orientation) { static NV_ROTATE get_rotation(DWORD orientation) {
switch (orientation) { switch (orientation) {
case DMDO_90: case DMDO_90:
return NV_ROTATE_90; return NV_ROTATE_90;
case DMDO_180: case DMDO_180:
return NV_ROTATE_180; return NV_ROTATE_180;
case DMDO_270: case DMDO_270:
return NV_ROTATE_270; return NV_ROTATE_270;
default: default:
return NV_ROTATE_0; return NV_ROTATE_0;
} }
} }
static std::vector<SyntheticDisplay> enumerate_displays( static std::vector<SyntheticDisplay> enumerate_displays(
uint32_t main_refresh_hz, uint32_t main_refresh_hz,
uint32_t sub_refresh_hz) { uint32_t sub_refresh_hz) {
std::vector<SyntheticDisplay> result; std::vector<SyntheticDisplay> result;
// reuse the active monitor list, then read live modes after -mainmonitor changes // reuse the active monitor list, then read live modes after -mainmonitor changes
for (const auto &monitor : sysutils::enumerate_monitors()) { for (const auto &monitor : sysutils::enumerate_monitors()) {
DEVMODEA mode {}; DEVMODEA mode {};
mode.dmSize = sizeof(mode); mode.dmSize = sizeof(mode);
if (!EnumDisplaySettingsExA( if (!EnumDisplaySettingsExA(
monitor.display_name.c_str(), monitor.display_name.c_str(),
ENUM_CURRENT_SETTINGS, ENUM_CURRENT_SETTINGS,
&mode, &mode,
0)) { 0)) {
continue; continue;
} }
const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0; const bool primary = mode.dmPosition.x == 0 && mode.dmPosition.y == 0;
result.push_back({ result.push_back({
.display_id = 0, .display_id = 0,
.width = mode.dmPelsWidth, .width = mode.dmPelsWidth,
.height = mode.dmPelsHeight, .height = mode.dmPelsHeight,
.color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32, .color_depth = mode.dmBitsPerPel > 0 ? mode.dmBitsPerPel : 32,
.x = mode.dmPosition.x, .x = mode.dmPosition.x,
.y = mode.dmPosition.y, .y = mode.dmPosition.y,
.refresh_rate_1k = 0, .refresh_rate_1k = 0,
.rotation = get_rotation(mode.dmDisplayOrientation), .rotation = get_rotation(mode.dmDisplayOrientation),
.primary = primary, .primary = primary,
}); });
} }
std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) { std::stable_sort(result.begin(), result.end(), [](const auto &left, const auto &right) {
return left.primary && !right.primary; return left.primary && !right.primary;
}); });
if (result.size() > 2) { if (result.size() > 2) {
result.resize(2); result.resize(2);
} }
if (result.empty()) { if (result.empty()) {
result.push_back({ result.push_back({
.display_id = 0, .display_id = 0,
.width = 1920, .width = 1920,
.height = 1080, .height = 1080,
.color_depth = 32, .color_depth = 32,
.x = 0, .x = 0,
.y = 0, .y = 0,
.refresh_rate_1k = 0, .refresh_rate_1k = 0,
.rotation = NV_ROTATE_0, .rotation = NV_ROTATE_0,
.primary = true, .primary = true,
}); });
} }
for (size_t index = 0; index < result.size(); index++) { for (size_t index = 0; index < result.size(); index++) {
auto &display = result[index]; auto &display = result[index];
display.primary = index == 0; display.primary = index == 0;
display.display_id = 0x80000000u | static_cast<NvU32>(index + 1); display.display_id = 0x80000000u | static_cast<NvU32>(index + 1);
const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz; const uint32_t refresh_hz = index == 0 ? main_refresh_hz : sub_refresh_hz;
display.refresh_rate_1k = refresh_hz * 1000; display.refresh_rate_1k = refresh_hz * 1000;
} }
return result; return result;
} }
// initializes NVAPI for the calling process. // initializes NVAPI for the calling process.
// marks the synthetic provider initialized without contacting a driver. // marks the synthetic provider initialized without contacting a driver.
static NvAPI_Status __cdecl NvAPI_Initialize_impl() { static NvAPI_Status __cdecl NvAPI_Initialize_impl() {
log_misc("nvapi_impl", "NvAPI_Initialize"); log_misc("nvapi_impl", "NvAPI_Initialize");
nvapi_initialized = true; nvapi_initialized = true;
return NVAPI_OK; return NVAPI_OK;
} }
// initializes NVAPI with additional client flags. // initializes NVAPI with additional client flags.
// accepts the flags and marks the synthetic provider initialized. // accepts the flags and marks the synthetic provider initialized.
static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) { static NvAPI_Status __cdecl NvAPI_InitializeEx_impl(NvU32 flags) {
log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags); log_misc("nvapi_impl", "NvAPI_InitializeEx(flags={:#x})", flags);
nvapi_initialized = true; nvapi_initialized = true;
return NVAPI_OK; return NVAPI_OK;
} }
// releases NVAPI state held for the calling process. // releases NVAPI state held for the calling process.
// clears the synthetic initialization state while leaving the provider installed. // clears the synthetic initialization state while leaving the provider installed.
static NvAPI_Status __cdecl NvAPI_Unload_impl() { static NvAPI_Status __cdecl NvAPI_Unload_impl() {
log_misc("nvapi_impl", "NvAPI_Unload"); log_misc("nvapi_impl", "NvAPI_Unload");
nvapi_initialized = false; nvapi_initialized = false;
return NVAPI_OK; return NVAPI_OK;
} }
// enumerates physical GPU handles managed by the NVIDIA driver. // enumerates physical GPU handles managed by the NVIDIA driver.
// returns one stable synthetic GPU containing all exposed displays. // returns one stable synthetic GPU containing all exposed displays.
static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl( static NvAPI_Status __cdecl NvAPI_EnumPhysicalGPUs_impl(
NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS], NvPhysicalGpuHandle gpu_handles[NVAPI_MAX_PHYSICAL_GPUS],
NvU32 *gpu_count) { NvU32 *gpu_count) {
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_EnumPhysicalGPUs(handles={}, count={})", "NvAPI_EnumPhysicalGPUs(handles={}, count={})",
fmt::ptr(gpu_handles), fmt::ptr(gpu_handles),
fmt::ptr(gpu_count)); fmt::ptr(gpu_count));
if (!nvapi_initialized) { if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED; return NVAPI_API_NOT_INITIALIZED;
} }
if (gpu_handles == nullptr || gpu_count == nullptr) { if (gpu_handles == nullptr || gpu_count == nullptr) {
return NVAPI_INVALID_ARGUMENT; return NVAPI_INVALID_ARGUMENT;
} }
gpu_handles[0] = get_gpu_handle(); gpu_handles[0] = get_gpu_handle();
*gpu_count = 1; *gpu_count = 1;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_EnumPhysicalGPUs - gpu={}, count={}", "NvAPI_EnumPhysicalGPUs - gpu={}, count={}",
fmt::ptr(gpu_handles[0]), fmt::ptr(gpu_handles[0]),
*gpu_count); *gpu_count);
return NVAPI_OK; return NVAPI_OK;
} }
// returns connected display descriptors for a physical GPU. // returns connected display descriptors for a physical GPU.
// exposes the monitor snapshot as DP primary and HDMI secondary displays. // exposes the monitor snapshot as DP primary and HDMI secondary displays.
static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl( static NvAPI_Status __cdecl NvAPI_GPU_GetConnectedDisplayIds_impl(
NvPhysicalGpuHandle gpu_handle, NvPhysicalGpuHandle gpu_handle,
NV_GPU_DISPLAYIDS *display_ids, NV_GPU_DISPLAYIDS *display_ids,
NvU32 *display_id_count, NvU32 *display_id_count,
NvU32 flags) { NvU32 flags) {
const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0; const NvU32 input_count = display_id_count != nullptr ? *display_id_count : 0;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})", "NvAPI_GPU_GetConnectedDisplayIds(gpu={}, ids={}, count={}, flags={:#x})",
fmt::ptr(gpu_handle), fmt::ptr(gpu_handle),
fmt::ptr(display_ids), fmt::ptr(display_ids),
input_count, input_count,
flags); flags);
if (!nvapi_initialized) { if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED; return NVAPI_API_NOT_INITIALIZED;
} }
if (gpu_handle != get_gpu_handle()) { if (gpu_handle != get_gpu_handle()) {
return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE; return NVAPI_EXPECTED_PHYSICAL_GPU_HANDLE;
} }
if (display_id_count == nullptr) { if (display_id_count == nullptr) {
return NVAPI_INVALID_ARGUMENT; return NVAPI_INVALID_ARGUMENT;
} }
const NvU32 required_count = static_cast<NvU32>(displays.size()); const NvU32 required_count = static_cast<NvU32>(displays.size());
if (display_ids == nullptr) { if (display_ids == nullptr) {
*display_id_count = required_count; *display_id_count = required_count;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds - required_count={}", "NvAPI_GPU_GetConnectedDisplayIds - required_count={}",
required_count); required_count);
return NVAPI_OK; return NVAPI_OK;
} }
const NvU32 capacity = *display_id_count; const NvU32 capacity = *display_id_count;
*display_id_count = required_count; *display_id_count = required_count;
if (capacity < required_count) { if (capacity < required_count) {
return NVAPI_INSUFFICIENT_BUFFER; return NVAPI_INSUFFICIENT_BUFFER;
} }
for (NvU32 index = 0; index < required_count; index++) { for (NvU32 index = 0; index < required_count; index++) {
const auto &source = displays[index]; const auto &source = displays[index];
auto &destination = display_ids[index]; auto &destination = display_ids[index];
destination = {}; destination = {};
destination.version = NV_GPU_DISPLAYIDS_VER; destination.version = NV_GPU_DISPLAYIDS_VER;
destination.connectorType = source.primary ? destination.connectorType = source.primary ?
NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI; NV_MONITOR_CONN_TYPE_DP : NV_MONITOR_CONN_TYPE_HDMI;
destination.displayId = source.display_id; destination.displayId = source.display_id;
destination.isActive = 1; destination.isActive = 1;
destination.isOSVisible = 1; destination.isOSVisible = 1;
destination.isConnected = 1; destination.isConnected = 1;
destination.isPhysicallyConnected = 1; destination.isPhysicallyConnected = 1;
} }
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_GPU_GetConnectedDisplayIds - returned_count={}", "NvAPI_GPU_GetConnectedDisplayIds - returned_count={}",
required_count); required_count);
return NVAPI_OK; return NVAPI_OK;
} }
// returns the NVAPI display ID associated with the Windows GDI primary. // returns the NVAPI display ID associated with the Windows GDI primary.
// returns the first synthetic display, ordered from the live desktop origin. // returns the first synthetic display, ordered from the live desktop origin.
static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) { static NvAPI_Status __cdecl NvAPI_DISP_GetGDIPrimaryDisplayId_impl(NvU32 *display_id) {
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})", "NvAPI_DISP_GetGDIPrimaryDisplayId(display_id={})",
fmt::ptr(display_id)); fmt::ptr(display_id));
if (!nvapi_initialized) { if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED; return NVAPI_API_NOT_INITIALIZED;
} }
if (display_id == nullptr || displays.empty()) { if (display_id == nullptr || displays.empty()) {
return NVAPI_INVALID_ARGUMENT; return NVAPI_INVALID_ARGUMENT;
} }
*display_id = displays.front().display_id; *display_id = displays.front().display_id;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}", "NvAPI_DISP_GetGDIPrimaryDisplayId - display_id={:#x}",
*display_id); *display_id);
return NVAPI_OK; return NVAPI_OK;
} }
static void fill_source_mode( static void fill_source_mode(
NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination, NV_DISPLAYCONFIG_SOURCE_MODE_INFO *destination,
const SyntheticDisplay &source) { const SyntheticDisplay &source) {
if (destination == nullptr) { if (destination == nullptr) {
return; return;
} }
*destination = {}; *destination = {};
destination->resolution.width = source.width; destination->resolution.width = source.width;
destination->resolution.height = source.height; destination->resolution.height = source.height;
destination->resolution.colorDepth = source.color_depth; destination->resolution.colorDepth = source.color_depth;
destination->colorFormat = NV_FORMAT_A8R8G8B8; destination->colorFormat = NV_FORMAT_A8R8G8B8;
destination->position.x = source.x; destination->position.x = source.x;
destination->position.y = source.y; destination->position.y = source.y;
destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE; destination->spanningOrientation = NV_DISPLAYCONFIG_SPAN_NONE;
destination->bGDIPrimary = source.primary ? 1 : 0; destination->bGDIPrimary = source.primary ? 1 : 0;
} }
static NvAPI_Status fill_target( static NvAPI_Status fill_target(
NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination, NV_DISPLAYCONFIG_PATH_TARGET_INFO *destination,
const SyntheticDisplay &source, const SyntheticDisplay &source,
NvU32 target_id) { NvU32 target_id) {
if (destination == nullptr) { if (destination == nullptr) {
return NVAPI_OK; return NVAPI_OK;
} }
auto *details = destination->details; auto *details = destination->details;
destination->displayId = source.display_id; destination->displayId = source.display_id;
destination->targetId = target_id; destination->targetId = target_id;
if (details == nullptr) { if (details == nullptr) {
return NVAPI_OK; return NVAPI_OK;
} }
if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) { if (details->version != NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER) {
return NVAPI_INCOMPATIBLE_STRUCT_VERSION; return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
} }
*details = {}; *details = {};
details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER; details->version = NV_DISPLAYCONFIG_PATH_ADVANCED_TARGET_INFO_VER;
details->rotation = source.rotation; details->rotation = source.rotation;
details->scaling = NV_SCALING_DEFAULT; details->scaling = NV_SCALING_DEFAULT;
details->refreshRate1K = source.refresh_rate_1k; details->refreshRate1K = source.refresh_rate_1k;
details->timingOverride = NV_TIMING_OVERRIDE_CURRENT; details->timingOverride = NV_TIMING_OVERRIDE_CURRENT;
return NVAPI_OK; return NVAPI_OK;
} }
// retrieves the current global display topology through NVAPI's three-pass contract. // retrieves the current global display topology through NVAPI's three-pass contract.
// fills caller-owned buffers from the synthetic monitor snapshot and configured rates. // fills caller-owned buffers from the synthetic monitor snapshot and configured rates.
static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl( static NvAPI_Status __cdecl NvAPI_DISP_GetDisplayConfig_impl(
NvU32 *path_info_count, NvU32 *path_info_count,
NV_DISPLAYCONFIG_PATH_INFO *path_info) { NV_DISPLAYCONFIG_PATH_INFO *path_info) {
const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0; const NvU32 input_count = path_info_count != nullptr ? *path_info_count : 0;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_GetDisplayConfig(count={}, paths={})", "NvAPI_DISP_GetDisplayConfig(count={}, paths={})",
input_count, input_count,
fmt::ptr(path_info)); fmt::ptr(path_info));
if (!nvapi_initialized) { if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED; return NVAPI_API_NOT_INITIALIZED;
} }
if (path_info_count == nullptr) { if (path_info_count == nullptr) {
return NVAPI_INVALID_ARGUMENT; return NVAPI_INVALID_ARGUMENT;
} }
const NvU32 required_count = static_cast<NvU32>(displays.size()); const NvU32 required_count = static_cast<NvU32>(displays.size());
if (path_info == nullptr) { if (path_info == nullptr) {
*path_info_count = required_count; *path_info_count = required_count;
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_GetDisplayConfig - required_count={}", "NvAPI_DISP_GetDisplayConfig - required_count={}",
required_count); required_count);
return NVAPI_OK; return NVAPI_OK;
} }
const NvU32 capacity = *path_info_count; const NvU32 capacity = *path_info_count;
*path_info_count = required_count; *path_info_count = required_count;
if (capacity < required_count) { if (capacity < required_count) {
return NVAPI_INSUFFICIENT_BUFFER; return NVAPI_INSUFFICIENT_BUFFER;
} }
for (NvU32 index = 0; index < required_count; index++) { for (NvU32 index = 0; index < required_count; index++) {
auto &path = path_info[index]; auto &path = path_info[index];
if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) { if (path.version != NV_DISPLAYCONFIG_PATH_INFO_VER2) {
return NVAPI_INCOMPATIBLE_STRUCT_VERSION; return NVAPI_INCOMPATIBLE_STRUCT_VERSION;
} }
if (path.targetInfo != nullptr && path.targetInfoCount < 1) { if (path.targetInfo != nullptr && path.targetInfoCount < 1) {
return NVAPI_INSUFFICIENT_BUFFER; return NVAPI_INSUFFICIENT_BUFFER;
} }
const auto &display = displays[index]; const auto &display = displays[index];
path.sourceId = index; path.sourceId = index;
path.targetInfoCount = 1; path.targetInfoCount = 1;
path.IsNonNVIDIAAdapter = 0; path.IsNonNVIDIAAdapter = 0;
path.pOSAdapterID = nullptr; path.pOSAdapterID = nullptr;
fill_source_mode(path.sourceModeInfo, display); fill_source_mode(path.sourceModeInfo, display);
const NvAPI_Status status = fill_target(path.targetInfo, display, index); const NvAPI_Status status = fill_target(path.targetInfo, display, index);
if (status != NVAPI_OK) { if (status != NVAPI_OK) {
return status; return status;
} }
} }
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_GetDisplayConfig - returned_count={}", "NvAPI_DISP_GetDisplayConfig - returned_count={}",
required_count); required_count);
return NVAPI_OK; return NVAPI_OK;
} }
// applies a supplied global display topology through the NVIDIA driver. // applies a supplied global display topology through the NVIDIA driver.
// accepts the cabinet topology without making any changes to Windows. // accepts the cabinet topology without making any changes to Windows.
static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl( static NvAPI_Status __cdecl NvAPI_DISP_SetDisplayConfig_impl(
NvU32 path_info_count, NvU32 path_info_count,
NV_DISPLAYCONFIG_PATH_INFO *path_info, NV_DISPLAYCONFIG_PATH_INFO *path_info,
NvU32 flags) { NvU32 flags) {
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})", "NvAPI_DISP_SetDisplayConfig(count={}, paths={}, flags={:#x})",
path_info_count, path_info_count,
fmt::ptr(path_info), fmt::ptr(path_info),
flags); flags);
if (!nvapi_initialized) { if (!nvapi_initialized) {
return NVAPI_API_NOT_INITIALIZED; return NVAPI_API_NOT_INITIALIZED;
} }
log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success"); log_misc("nvapi_impl", "NvAPI_DISP_SetDisplayConfig - return synthetic success");
return NVAPI_OK; return NVAPI_OK;
} }
template<typename T> template<typename T>
static uintptr_t *query_result(T function) { static uintptr_t *query_result(T function) {
return reinterpret_cast<uintptr_t *>(function); return reinterpret_cast<uintptr_t *>(function);
} }
// resolves an NVAPI function ID to its implementation address. // resolves an NVAPI function ID to its implementation address.
// exposes only the synthetic entry points used by KFC and rejects all others. // exposes only the synthetic entry points used by KFC and rejects all others.
static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) { static uintptr_t *__cdecl NvAPI_QueryInterface_impl(unsigned int function_id) {
uintptr_t *result = nullptr; uintptr_t *result = nullptr;
switch (function_id) { switch (function_id) {
case NVAPI_INITIALIZE_ID: case NVAPI_INITIALIZE_ID:
result = query_result(NvAPI_Initialize_impl); result = query_result(NvAPI_Initialize_impl);
break; break;
case NVAPI_INITIALIZE_EX_ID: case NVAPI_INITIALIZE_EX_ID:
result = query_result(NvAPI_InitializeEx_impl); result = query_result(NvAPI_InitializeEx_impl);
break; break;
case NVAPI_UNLOAD_ID: case NVAPI_UNLOAD_ID:
result = query_result(NvAPI_Unload_impl); result = query_result(NvAPI_Unload_impl);
break; break;
case NVAPI_ENUM_PHYSICAL_GPUS_ID: case NVAPI_ENUM_PHYSICAL_GPUS_ID:
result = query_result(NvAPI_EnumPhysicalGPUs_impl); result = query_result(NvAPI_EnumPhysicalGPUs_impl);
break; break;
case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID: case NVAPI_GPU_GET_CONNECTED_DISPLAY_IDS_ID:
result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl); result = query_result(NvAPI_GPU_GetConnectedDisplayIds_impl);
break; break;
case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID: case NVAPI_DISP_GET_GDI_PRIMARY_DISPLAY_ID:
result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl); result = query_result(NvAPI_DISP_GetGDIPrimaryDisplayId_impl);
break; break;
case NVAPI_DISP_GET_DISPLAY_CONFIG_ID: case NVAPI_DISP_GET_DISPLAY_CONFIG_ID:
result = query_result(NvAPI_DISP_GetDisplayConfig_impl); result = query_result(NvAPI_DISP_GetDisplayConfig_impl);
break; break;
case NVAPI_DISP_SET_DISPLAY_CONFIG_ID: case NVAPI_DISP_SET_DISPLAY_CONFIG_ID:
result = query_result(NvAPI_DISP_SetDisplayConfig_impl); result = query_result(NvAPI_DISP_SetDisplayConfig_impl);
break; break;
default: default:
break; break;
} }
log_misc( log_misc(
"nvapi_impl", "nvapi_impl",
"NvAPI_QueryInterface(0x{:x}) - {}", "NvAPI_QueryInterface(0x{:x}) - {}",
function_id, function_id,
result != nullptr ? "implemented" : "unsupported"); result != nullptr ? "implemented" : "unsupported");
return result; return result;
} }
} }
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) { bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz) {
if (provider_initialized) { if (provider_initialized) {
return true; return true;
} }
if (dll == nullptr) { if (dll == nullptr) {
log_warning("nvapi_impl", "invalid synthetic module handle"); log_warning("nvapi_impl", "invalid synthetic module handle");
return false; return false;
} }
displays = enumerate_displays(main_refresh_hz, sub_refresh_hz); displays = enumerate_displays(main_refresh_hz, sub_refresh_hz);
libraryhook_hook_library(NVAPI_DLL_NAME_A, dll); libraryhook_hook_library(NVAPI_DLL_NAME_A, dll);
libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl); libraryhook_hook_proc("nvapi_QueryInterface", NvAPI_QueryInterface_impl);
libraryhook_enable(); libraryhook_enable();
provider_initialized = true; provider_initialized = true;
log_info( log_info(
"nvapi_impl", "nvapi_impl",
"synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz", "synthetic {} enabled with {} display(s), main={} Hz, sub={} Hz",
NVAPI_DLL_NAME_A, NVAPI_DLL_NAME_A,
displays.size(), displays.size(),
main_refresh_hz, main_refresh_hz,
sub_refresh_hz); sub_refresh_hz);
return true; return true;
} }
} }
#endif #endif
+14 -14
View File
@@ -1,14 +1,14 @@
#pragma once #pragma once
#ifdef SPICE64 #ifdef SPICE64
#include <cstdint> #include <cstdint>
#include <windows.h> #include <windows.h>
namespace nvapi_impl { namespace nvapi_impl {
bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz); bool initialize(HINSTANCE dll, uint32_t main_refresh_hz, uint32_t sub_refresh_hz);
} }
#endif #endif
+60 -3
View File
@@ -13,7 +13,9 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/iidx/iidx.h" #include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h" #include "games/gitadora/gitadora.h"
#include "games/popn/popn.h"
#include "games/sdvx/sdvx.h" #include "games/sdvx/sdvx.h"
#include "util/deferlog.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -24,12 +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(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr; static decltype(IsDBCSLeadByteEx) *IsDBCSLeadByteEx_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
@@ -182,9 +185,32 @@ static BOOL WINAPI IsDBCSLeadByte_hook (
BYTE TestChar BYTE TestChar
) )
{ {
if (IsDBCSLeadByteEx_orig) {
return IsDBCSLeadByteEx_orig(CODEPAGE_SHIFT_JIS, TestChar);
}
return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar); return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
} }
static BOOL WINAPI IsDBCSLeadByteEx_hook(
UINT CodePage,
BYTE TestChar)
{
switch (CodePage) {
case CP_ACP:
case CP_THREAD_ACP:
CodePage = CODEPAGE_SHIFT_JIS;
break;
default:
break;
}
return IsDBCSLeadByteEx_orig(CodePage, TestChar);
}
#endif
static static
int int
WINAPI WINAPI
@@ -220,6 +246,8 @@ WideCharToMultiByte_hook(
lpUsedDefaultChar); lpUsedDefaultChar);
} }
#ifdef SPICE64
int int
WINAPI WINAPI
GetLocaleInfoA_hook( GetLocaleInfoA_hook(
@@ -250,6 +278,18 @@ GetLocaleInfoA_hook(
void hooks::lang::early_init() { void hooks::lang::early_init() {
log_info("hooks::lang", "early initialization"); log_info("hooks::lang", "early initialization");
const auto native_code_page = GetACP();
if (native_code_page == CP_UTF8) {
log_warning(
"hooks::lang",
"Windows is using UTF-8 as the system code page; "
"some games may render text incorrectly or behave unexpectedly");
deferredlogs::defer_error_messages({
"Windows is using UTF-8 as the system code page",
" some games may render text incorrectly or behave unexpectedly"});
}
// hooking these two functions fixes the jubeat mojibake // hooking these two functions fixes the jubeat mojibake
detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig); detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig); detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);
@@ -307,7 +347,25 @@ void hooks::lang::early_init() {
&IsDBCSLeadByte_orig); &IsDBCSLeadByte_orig);
} }
if (games::gitadora::is_arena_model() || avs::game::is_model("T44")) { if (games::popn::is_pikapika_model() && native_code_page == CP_UTF8) {
detour::trampoline_try(
"kernel32.dll",
"IsDBCSLeadByteEx",
IsDBCSLeadByteEx_hook,
&IsDBCSLeadByteEx_orig);
}
#endif
#ifdef SPICE64
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model("T44");
#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"); log_info("hooks::lang", "hooking WideCharToMultiByte");
detour::trampoline_try( detour::trampoline_try(
"kernel32.dll", "kernel32.dll",
@@ -315,7 +373,6 @@ void hooks::lang::early_init() {
WideCharToMultiByte_hook, WideCharToMultiByte_hook,
&WideCharToMultiByte_orig); &WideCharToMultiByte_orig);
} }
#endif
} }
+37 -29
View File
@@ -5,7 +5,6 @@
#include <cmath> #include <cmath>
#include <cstdlib> #include <cstdlib>
#include <assert.h>
#include <shlwapi.h> #include <shlwapi.h>
#include <windows.h> #include <windows.h>
#include <cfg/configurator.h> #include <cfg/configurator.h>
@@ -88,13 +87,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"
@@ -524,22 +523,25 @@ int main_implementation(int argc, char *argv[]) {
games::iidx::FLIP_CAMS = true; games::iidx::FLIP_CAMS = true;
} }
// IIDX CONNECT_CAMERA logic here for cases where user is running without -iidx module // Resolve the IIDX camera policy here so it also applies without the -iidx module.
// for now, we assume that user may be running on a cab const auto &cab_camera_access = options[launcher::Options::IIDXCabCamAccess];
// (games::iidx::DISABLE_CAMS starts out as false unless user overrides) if (cab_camera_access.is_active()) {
// we will check again in IIDX module with a different default const auto value = cab_camera_access.value_text();
assert(!games::iidx::DISABLE_CAMS.has_value()); if (value == "off") {
if (options[launcher::Options::IIDXDisableCameras].value_bool()) { games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
games::iidx::DISABLE_CAMS = true; } else if (value == "on") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::on;
} else if (value == "legacy") {
games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::legacy;
}
} }
if (options[launcher::Options::IIDXCabCamAccess].is_active() &&
options[launcher::Options::IIDXCabCamAccess].value_text() == "off") { if (options[launcher::Options::IIDXDisableCameras].value_bool()) {
games::iidx::DISABLE_CAMS = true; games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
} }
if (options[launcher::Options::IIDXCamHook].value_bool()) { if (options[launcher::Options::IIDXCamHook].value_bool()) {
games::iidx::TDJ_CAMERA = true; games::iidx::TDJ_CAMERA = true;
// Disable legacy behaviour to avoid conflict games::iidx::CAB_CAMERA_ACCESS = games::iidx::cab_camera_access_mode::off;
games::iidx::DISABLE_CAMS = true;
} }
// CONNECT_CAMERA env var will be set once logging is enabled // CONNECT_CAMERA env var will be set once logging is enabled
@@ -1101,6 +1103,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;
} }
@@ -1717,14 +1725,13 @@ int main_implementation(int argc, char *argv[]) {
GRAPHICS_FS_ORIENTATION_SWAP = true; GRAPHICS_FS_ORIENTATION_SWAP = true;
} }
// apply an explicit off outside of the -iidx module so it also works on cabinets.
// for cab usage - set environment variables (outside of -iidx module) if (games::iidx::CAB_CAMERA_ACCESS == games::iidx::cab_camera_access_mode::off &&
if (games::iidx::DISABLE_CAMS.has_value() &&
games::iidx::DISABLE_CAMS.value() &&
!cfg::CONFIGURATOR_STANDALONE) { !cfg::CONFIGURATOR_STANDALONE) {
log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0"); log_misc("launcher::iidx", "CONNECT_CAMERA env var set to 0");
SetEnvironmentVariable("CONNECT_CAMERA", "0"); SetEnvironmentVariable("CONNECT_CAMERA", "0");
} }
if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() && if (games::iidx::SOUND_OUTPUT_DEVICE.has_value() &&
games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" && games::iidx::SOUND_OUTPUT_DEVICE.value() != "auto" &&
!cfg::CONFIGURATOR_STANDALONE) { !cfg::CONFIGURATOR_STANDALONE) {
@@ -1776,9 +1783,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;
@@ -2451,6 +2457,7 @@ int main_implementation(int argc, char *argv[]) {
// initialize raw input // initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>(); RI_MGR = std::make_unique<rawinput::RawInputManager>();
hotkeys::enable_raw_input();
for (const auto &device : sextet_devices) { for (const auto &device : sextet_devices) {
RI_MGR->sextet_register(device); RI_MGR->sextet_register(device);
} }
@@ -2473,6 +2480,9 @@ 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_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;
@@ -2702,9 +2712,6 @@ int main_implementation(int argc, char *argv[]) {
API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]); API_CONTROLLER->listen_serial(api_serial_port[i], api_serial_baud[i]);
} }
// start coin input thread
eamuse_coin_start_thread();
// pin macro // pin macro
if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) { if (!cfg::CONFIGURATOR_STANDALONE && PIN_MACRO_ENABLED) {
eamuse_pin_macro_start_thread(); eamuse_pin_macro_start_thread();
@@ -2754,6 +2761,9 @@ int main_implementation(int argc, char *argv[]) {
log_info("launcher", "calling game entry"); log_info("launcher", "calling game entry");
avs::game::entry_main(); avs::game::entry_main();
// stop screenshot and coin polling; mapped SuperExit and ALT+F4 remain active
hotkeys::disable_input();
// clear presence // clear presence
richpresence::shutdown(); richpresence::shutdown();
@@ -2791,9 +2801,6 @@ int main_implementation(int argc, char *argv[]) {
// free api controller // free api controller
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
@@ -2804,6 +2811,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
@@ -2836,8 +2844,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();
+26 -6
View File
@@ -640,17 +640,21 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
}, },
{ {
// IIDXCabCamAccess // IIDXCabCamAccess
.title = "IIDX Use Official AC Cams", .title = "IIDX Official AC Camera Access",
.name = "iidxcabcams", .name = "iidxcabcams",
.desc = "For IIDX25+, allow direct access to cameras from real arcade cabinets. " .desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n"
"Only turn this on if you have OFFICIAL arcade cameras connected to the correct USB ports. Default: auto.", "auto (default): use [off] when the IIDX module is enabled; otherwise, use [on].\n\n"
"on: game discovers and directly accesses cameras; requires official cameras on correct USB ports.\n\n"
"legacy: for IIDX 25/26 only; allow camera access with emulated discovery.\n\n"
"off: prevent game from accessing USB cameras.",
.type = OptionType::Enum, .type = OptionType::Enum,
.game_name = "Beatmania IIDX", .game_name = "Beatmania IIDX",
.category = "Cab Peripherals", .category = "Cab Peripherals",
.elements = { .elements = {
{"auto", ""}, {"auto", ""},
{"on", ""},
{"legacy", ""},
{"off", ""}, {"off", ""},
{"on", ""}
}, },
}, },
{ {
@@ -977,7 +981,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.display_name = "sdvxsubredraw", .display_name = "sdvxsubredraw",
.aliases= "sdvxsubredraw", .aliases= "sdvxsubredraw",
.desc = "Check if submonitor in fullscreen mode doesn't update every frame; " .desc = "Check if submonitor in fullscreen mode doesn't update every frame; "
"this option forces subscreen to redraw every frame.", "this option presents the subscreen when the game does not.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Sound Voltex", .game_name = "Sound Voltex",
.category = "Advanced Game Options", .category = "Advanced Game Options",
@@ -1171,7 +1175,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Pop'n Music PikaPika Subscreen Force Redraw", .title = "Pop'n Music PikaPika Subscreen Force Redraw",
.name = "popnsubredraw", .name = "popnsubredraw",
.desc = "Check if submonitor in fullscreen mode appears stuck; " .desc = "Check if submonitor in fullscreen mode appears stuck; "
"this option forces subscreen to redraw every frame.", "this option presents the subscreen when the game does not.",
.type = OptionType::Bool, .type = OptionType::Bool,
.game_name = "Pop'n Music", .game_name = "Pop'n Music",
.category = "Advanced Game Options", .category = "Advanced Game Options",
@@ -3393,6 +3397,22 @@ 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",
},
}; };
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) { const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+3 -1
View File
@@ -320,7 +320,9 @@ namespace launcher {
OBSWebSocketHost, OBSWebSocketHost,
OBSWebSocketPort, OBSWebSocketPort,
OBSWebSocketPassword, OBSWebSocketPassword,
OBSWebSocketDebug OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens
}; };
enum class OptionsCategory { enum class OptionsCategory {
+18 -95
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;
// 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;
}
}
break;
}
default:
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...");
launcher::shutdown();
}
}
if (overlay_exit) {
if (has_focus()) {
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown();
}
}
// slow down
Sleep(100);
}
return nullptr;
});
}
void disable() {
if (!THREAD) {
return; return;
} }
if (cfg::CONFIGURATOR_STANDALONE) {
// stop old thread return;
THREAD_RUNNING = false; }
THREAD->join(); if (!has_focus()) {
return;
// delete thread }
delete THREAD; if (alt_f4) {
THREAD = nullptr; log_info("superexit", "detected ALT+F4, exiting...");
launcher::shutdown();
// log return;
log_info("superexit", "disabled"); }
log_info("superexit", "detected Force Exit Game overlay shortcut, exiting...");
launcher::shutdown();
} }
} }
+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();
} }
+24 -55
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};
@@ -292,78 +290,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,
return true; stock - amount,
std::memory_order_relaxed)) {
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 log_info("eamuse", "coin inserted while blocked");
COIN_INPUT_THREAD_ACTIVE = true; } else {
log_info("eamuse", "coin insert");
// create thread COIN_STOCK.fetch_add(1, std::memory_order_relaxed);
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");
else {
log_info("eamuse", "coin insert");
COIN_STOCK++;
}
}
COIN_INPUT_KEY_STATE = true;
} else {
COIN_INPUT_KEY_STATE = false;
}
// 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() {
+1 -3
View File
@@ -58,9 +58,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();
}
+265 -265
View File
@@ -1,265 +1,265 @@
#include "notifications.h" #include "notifications.h"
#include <atomic> #include <atomic>
#include <deque> #include <deque>
#include <mutex> #include <mutex>
#include <unordered_map> #include <unordered_map>
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#include "external/imgui/imgui_internal.h" #include "external/imgui/imgui_internal.h"
#include "external/fmt/include/fmt/format.h" #include "external/fmt/include/fmt/format.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "util/time.h" #include "util/time.h"
namespace overlay::notifications { namespace overlay::notifications {
bool ENABLED = true; bool ENABLED = true;
Position POSITION = Position::BottomRight; Position POSITION = Position::BottomRight;
struct Notification { struct Notification {
uint64_t id; uint64_t id;
std::string text; std::string text;
Severity severity; Severity severity;
double created_ms; double created_ms;
float duration_s; float duration_s;
}; };
static std::mutex g_mutex; static std::mutex g_mutex;
static std::deque<Notification> g_items; static std::deque<Notification> g_items;
static std::atomic<uint64_t> g_next_id { 1 }; static std::atomic<uint64_t> g_next_id { 1 };
static std::atomic<size_t> g_count { 0 }; static std::atomic<size_t> g_count { 0 };
// duration in seconds each notification stays visible // duration in seconds each notification stays visible
static constexpr float DURATION_S = 3.0f; static constexpr float DURATION_S = 3.0f;
// maximum number of notifications kept in the queue (oldest dropped beyond this) // maximum number of notifications kept in the queue (oldest dropped beyond this)
static constexpr size_t MAX_NOTIFICATIONS = 6; static constexpr size_t MAX_NOTIFICATIONS = 6;
// time (ms) over which a toast fades out at the end of its lifetime // time (ms) over which a toast fades out at the end of its lifetime
static constexpr float FADE_OUT_MS = 400.0f; static constexpr float FADE_OUT_MS = 400.0f;
// fixed width of each toast window, in unscaled pixels // fixed width of each toast window, in unscaled pixels
static constexpr float TOAST_WIDTH = 320.0f; static constexpr float TOAST_WIDTH = 320.0f;
// gap between the toast stack and the screen edges (right + bottom) // gap between the toast stack and the screen edges (right + bottom)
static constexpr float TOAST_MARGIN = 20.0f; static constexpr float TOAST_MARGIN = 20.0f;
// vertical gap between stacked toasts // vertical gap between stacked toasts
static constexpr float TOAST_SPACING = 8.0f; static constexpr float TOAST_SPACING = 8.0f;
// inner padding inside a toast window (horizontal / vertical) // inner padding inside a toast window (horizontal / vertical)
static constexpr float TOAST_PAD_X = 10.0f; static constexpr float TOAST_PAD_X = 10.0f;
static constexpr float TOAST_PAD_Y = 8.0f; static constexpr float TOAST_PAD_Y = 8.0f;
// width of the colored severity accent bar drawn on the left edge // width of the colored severity accent bar drawn on the left edge
static constexpr float TOAST_ACCENT_W = 6.0f; static constexpr float TOAST_ACCENT_W = 6.0f;
// base opacity of the toast background (0..1), multiplied by the fade alpha // base opacity of the toast background (0..1), multiplied by the fade alpha
static constexpr float TOAST_BG_ALPHA = 0.85f; static constexpr float TOAST_BG_ALPHA = 0.85f;
static constexpr ImGuiWindowFlags TOAST_FLAGS = static constexpr ImGuiWindowFlags TOAST_FLAGS =
ImGuiWindowFlags_NoDecoration ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoInputs
| ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoFocusOnAppearing
| ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoBringToFrontOnFocus
| ImGuiWindowFlags_AlwaysAutoResize; | ImGuiWindowFlags_AlwaysAutoResize;
static ImU32 severity_accent(Severity sev) { static ImU32 severity_accent(Severity sev) {
switch (sev) { switch (sev) {
case Severity::Success: return IM_COL32(80, 200, 120, 255); case Severity::Success: return IM_COL32(80, 200, 120, 255);
case Severity::Warning: return IM_COL32(230, 180, 60, 255); case Severity::Warning: return IM_COL32(230, 180, 60, 255);
case Severity::Error: return IM_COL32(220, 60, 60, 255); case Severity::Error: return IM_COL32(220, 60, 60, 255);
case Severity::Info: case Severity::Info:
default: return IM_COL32(90, 160, 230, 255); default: return IM_COL32(90, 160, 230, 255);
} }
} }
static bool is_expired(const Notification &n, double now_ms) { static bool is_expired(const Notification &n, double now_ms) {
return (now_ms - n.created_ms) >= (n.duration_s * 1000.0); return (now_ms - n.created_ms) >= (n.duration_s * 1000.0);
} }
// returns 0.0 .. 1.0 fade alpha based on time remaining // returns 0.0 .. 1.0 fade alpha based on time remaining
static float compute_alpha(const Notification &n, double now_ms) { static float compute_alpha(const Notification &n, double now_ms) {
const double remaining_ms = (n.duration_s * 1000.0) - (now_ms - n.created_ms); const double remaining_ms = (n.duration_s * 1000.0) - (now_ms - n.created_ms);
if (remaining_ms >= FADE_OUT_MS) { if (remaining_ms >= FADE_OUT_MS) {
return 1.0f; return 1.0f;
} }
if (remaining_ms <= 0.0) { if (remaining_ms <= 0.0) {
return 0.0f; return 0.0f;
} }
return static_cast<float>(remaining_ms / FADE_OUT_MS); return static_cast<float>(remaining_ms / FADE_OUT_MS);
} }
// drop expired items and copy the rest under a single lock acquisition // drop expired items and copy the rest under a single lock acquisition
static std::vector<Notification> snapshot_and_prune(double now_ms) { static std::vector<Notification> snapshot_and_prune(double now_ms) {
std::vector<Notification> snapshot; std::vector<Notification> snapshot;
std::lock_guard<std::mutex> lock(g_mutex); std::lock_guard<std::mutex> lock(g_mutex);
for (auto it = g_items.begin(); it != g_items.end();) { for (auto it = g_items.begin(); it != g_items.end();) {
if (is_expired(*it, now_ms)) { if (is_expired(*it, now_ms)) {
it = g_items.erase(it); it = g_items.erase(it);
} else { } else {
++it; ++it;
} }
} }
g_count.store(g_items.size(), std::memory_order_release); g_count.store(g_items.size(), std::memory_order_release);
snapshot.assign(g_items.begin(), g_items.end()); snapshot.assign(g_items.begin(), g_items.end());
return snapshot; return snapshot;
} }
// is the configured anchor on the right edge of the screen? // is the configured anchor on the right edge of the screen?
static bool position_is_right(Position p) { static bool position_is_right(Position p) {
return p == Position::BottomRight || p == Position::TopRight; return p == Position::BottomRight || p == Position::TopRight;
} }
// is the configured anchor on the bottom edge of the screen? // is the configured anchor on the bottom edge of the screen?
static bool position_is_bottom(Position p) { static bool position_is_bottom(Position p) {
return p == Position::BottomRight || p == Position::BottomLeft; return p == Position::BottomRight || p == Position::BottomLeft;
} }
// draw a single toast anchored to the configured corner; `cursor_y` is the // draw a single toast anchored to the configured corner; `cursor_y` is the
// y-coordinate of the toast edge nearest the anchor (top edge for Top* anchors, // y-coordinate of the toast edge nearest the anchor (top edge for Top* anchors,
// bottom edge for Bottom* anchors). returns its height in pixels. // bottom edge for Bottom* anchors). returns its height in pixels.
static float draw_toast(const Notification &n, float cursor_y, float alpha) { static float draw_toast(const Notification &n, float cursor_y, float alpha) {
const float toast_width = apply_scaling(TOAST_WIDTH); const float toast_width = apply_scaling(TOAST_WIDTH);
const float margin = apply_scaling(TOAST_MARGIN); const float margin = apply_scaling(TOAST_MARGIN);
const ImVec2 &display = ImGui::GetIO().DisplaySize; const ImVec2 &display = ImGui::GetIO().DisplaySize;
const Position pos = POSITION; const Position pos = POSITION;
const auto window_id = fmt::format("##spice_notif_{}", n.id); const auto window_id = fmt::format("##spice_notif_{}", n.id);
// anchor x/pivot.x select the screen edge; pivot.y matches cursor_y semantics // anchor x/pivot.x select the screen edge; pivot.y matches cursor_y semantics
const float anchor_x = position_is_right(pos) ? (display.x - margin) : margin; const float anchor_x = position_is_right(pos) ? (display.x - margin) : margin;
const float pivot_x = position_is_right(pos) ? 1.0f : 0.0f; const float pivot_x = position_is_right(pos) ? 1.0f : 0.0f;
const float pivot_y = position_is_bottom(pos) ? 1.0f : 0.0f; const float pivot_y = position_is_bottom(pos) ? 1.0f : 0.0f;
ImGui::SetNextWindowPos(ImVec2(anchor_x, cursor_y), ImGui::SetNextWindowPos(ImVec2(anchor_x, cursor_y),
ImGuiCond_Always, ImVec2(pivot_x, pivot_y)); ImGuiCond_Always, ImVec2(pivot_x, pivot_y));
ImGui::SetNextWindowSize(ImVec2(toast_width, 0.f), ImGuiCond_Always); ImGui::SetNextWindowSize(ImVec2(toast_width, 0.f), ImGuiCond_Always);
ImGui::SetNextWindowBgAlpha(TOAST_BG_ALPHA * alpha); ImGui::SetNextWindowBgAlpha(TOAST_BG_ALPHA * alpha);
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha); ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding,
ImVec2(apply_scaling(TOAST_PAD_X), apply_scaling(TOAST_PAD_Y))); ImVec2(apply_scaling(TOAST_PAD_X), apply_scaling(TOAST_PAD_Y)));
float height = 0.f; float height = 0.f;
if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) { if (ImGui::Begin(window_id.c_str(), nullptr, TOAST_FLAGS)) {
// keep toasts above other overlay windows (e.g. the persistent FPS // keep toasts above other overlay windows (e.g. the persistent FPS
// window, which may be toggled on after a toast already exists), but // window, which may be toggled on after a toast already exists), but
// tuck them behind a blocking modal so they get dimmed/occluded by the // tuck them behind a blocking modal so they get dimmed/occluded by the
// modal backdrop instead of floating on top of it. // modal backdrop instead of floating on top of it.
ImGuiWindow *toast_window = ImGui::GetCurrentWindow(); ImGuiWindow *toast_window = ImGui::GetCurrentWindow();
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) { if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
ImGui::BringWindowToDisplayBehind(toast_window, modal); ImGui::BringWindowToDisplayBehind(toast_window, modal);
} else { } else {
ImGui::BringWindowToDisplayFront(toast_window); ImGui::BringWindowToDisplayFront(toast_window);
} }
const ImVec2 win_pos = ImGui::GetWindowPos(); const ImVec2 win_pos = ImGui::GetWindowPos();
const ImVec2 win_size = ImGui::GetWindowSize(); const ImVec2 win_size = ImGui::GetWindowSize();
// accent bar on the left edge of the window // accent bar on the left edge of the window
const ImU32 accent = severity_accent(n.severity); const ImU32 accent = severity_accent(n.severity);
const ImU32 accent_faded = const ImU32 accent_faded =
(accent & 0x00FFFFFFu) | (static_cast<ImU32>(alpha * 255.0f) << 24); (accent & 0x00FFFFFFu) | (static_cast<ImU32>(alpha * 255.0f) << 24);
ImGui::GetWindowDrawList()->AddRectFilled( ImGui::GetWindowDrawList()->AddRectFilled(
win_pos, win_pos,
ImVec2(win_pos.x + apply_scaling(TOAST_ACCENT_W), win_pos.y + win_size.y), ImVec2(win_pos.x + apply_scaling(TOAST_ACCENT_W), win_pos.y + win_size.y),
accent_faded); accent_faded);
// 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)); ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X));
ImGui::TextUnformatted(n.text.c_str()); ImGui::TextUnformatted(n.text.c_str());
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
height = ImGui::GetWindowSize().y; height = ImGui::GetWindowSize().y;
} }
ImGui::End(); ImGui::End();
ImGui::PopStyleVar(2); ImGui::PopStyleVar(2);
return height; return height;
} }
uint64_t add(Severity severity, std::string text) { uint64_t add(Severity severity, std::string text) {
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) { if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
return 0; return 0;
} }
Notification n { Notification n {
.id = g_next_id.fetch_add(1, std::memory_order_relaxed), .id = g_next_id.fetch_add(1, std::memory_order_relaxed),
.text = std::move(text), .text = std::move(text),
.severity = severity, .severity = severity,
.created_ms = get_performance_milliseconds(), .created_ms = get_performance_milliseconds(),
.duration_s = DURATION_S, .duration_s = DURATION_S,
}; };
{ {
std::lock_guard<std::mutex> lock(g_mutex); std::lock_guard<std::mutex> lock(g_mutex);
g_items.push_back(std::move(n)); g_items.push_back(std::move(n));
while (g_items.size() > MAX_NOTIFICATIONS) { while (g_items.size() > MAX_NOTIFICATIONS) {
g_items.pop_front(); g_items.pop_front();
} }
g_count.store(g_items.size(), std::memory_order_release); g_count.store(g_items.size(), std::memory_order_release);
} }
return n.id; return n.id;
} }
uint64_t add_throttled(Severity severity, const std::string &key, uint64_t add_throttled(Severity severity, const std::string &key,
double cooldown_seconds, std::string text) { double cooldown_seconds, std::string text) {
if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) { if (!ENABLED || !overlay::ENABLED || overlay::OVERLAY == nullptr) {
return 0; return 0;
} }
// per-key last-emit timestamps live behind their own lock so we don't // per-key last-emit timestamps live behind their own lock so we don't
// hold g_mutex across the map lookup. // hold g_mutex across the map lookup.
static std::mutex throttle_mutex; static std::mutex throttle_mutex;
static std::unordered_map<std::string, double> last_emit_ms; static std::unordered_map<std::string, double> last_emit_ms;
const double now_ms = get_performance_milliseconds(); const double now_ms = get_performance_milliseconds();
{ {
std::lock_guard<std::mutex> lock(throttle_mutex); std::lock_guard<std::mutex> lock(throttle_mutex);
auto it = last_emit_ms.find(key); auto it = last_emit_ms.find(key);
if (it != last_emit_ms.end() if (it != last_emit_ms.end()
&& (now_ms - it->second) < (cooldown_seconds * 1000.0)) { && (now_ms - it->second) < (cooldown_seconds * 1000.0)) {
return 0; return 0;
} }
last_emit_ms[key] = now_ms; last_emit_ms[key] = now_ms;
} }
return add(severity, std::move(text)); return add(severity, std::move(text));
} }
bool has_pending() { bool has_pending() {
return g_count.load(std::memory_order_acquire) > 0; return g_count.load(std::memory_order_acquire) > 0;
} }
void draw() { void draw() {
const double now_ms = get_performance_milliseconds(); const double now_ms = get_performance_milliseconds();
const auto snapshot = snapshot_and_prune(now_ms); const auto snapshot = snapshot_and_prune(now_ms);
if (snapshot.empty()) { if (snapshot.empty()) {
return; return;
} }
// stack from the anchored edge with newest toast at the anchor. // stack from the anchored edge with newest toast at the anchor.
// Bottom* anchors stack upward; Top* anchors stack downward. // Bottom* anchors stack upward; Top* anchors stack downward.
const float spacing = apply_scaling(TOAST_SPACING); const float spacing = apply_scaling(TOAST_SPACING);
const float margin = apply_scaling(TOAST_MARGIN); const float margin = apply_scaling(TOAST_MARGIN);
const bool bottom = position_is_bottom(POSITION); const bool bottom = position_is_bottom(POSITION);
float cursor_y = bottom float cursor_y = bottom
? (ImGui::GetIO().DisplaySize.y - margin) ? (ImGui::GetIO().DisplaySize.y - margin)
: margin; : margin;
for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) { for (auto it = snapshot.rbegin(); it != snapshot.rend(); ++it) {
const float alpha = compute_alpha(*it, now_ms); const float alpha = compute_alpha(*it, now_ms);
const float height = draw_toast(*it, cursor_y, alpha); const float height = draw_toast(*it, cursor_y, alpha);
cursor_y += bottom ? -(height + spacing) : (height + spacing); cursor_y += bottom ? -(height + spacing) : (height + spacing);
} }
} }
void apply_game_default_position(const std::string &game_name) { void apply_game_default_position(const std::string &game_name) {
if (game_name == "Reflec Beat") { if (game_name == "Reflec Beat") {
POSITION = Position::TopRight; POSITION = Position::TopRight;
} }
// others keep the default (BottomRight) // others keep the default (BottomRight)
} }
} }
+55 -55
View File
@@ -1,55 +1,55 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <string> #include <string>
namespace overlay::notifications { namespace overlay::notifications {
// master switch for the notification system; when false, add() is a no-op. // master switch for the notification system; when false, add() is a no-op.
// controlled by selecting "none" for the -notifypos launcher option. // controlled by selecting "none" for the -notifypos launcher option.
extern bool ENABLED; extern bool ENABLED;
enum class Severity { enum class Severity {
Info, Info,
Success, Success,
Warning, Warning,
Error, Error,
}; };
// screen anchor for the toast stack. toasts stack away from the anchored edge. // screen anchor for the toast stack. toasts stack away from the anchored edge.
enum class Position { enum class Position {
BottomRight, BottomRight,
BottomLeft, BottomLeft,
TopRight, TopRight,
TopLeft, TopLeft,
}; };
// current toast anchor. defaults to BottomRight; may be reassigned by // current toast anchor. defaults to BottomRight; may be reassigned by
// apply_game_default_position() or by the user via -notifypos. // apply_game_default_position() or by the user via -notifypos.
extern Position POSITION; extern Position POSITION;
// apply the default toast position appropriate for a game (by display name, // apply the default toast position appropriate for a game (by display name,
// as returned by eamuse_get_game()). called once after game autodetect, before // as returned by eamuse_get_game()). called once after game autodetect, before
// any user -notifypos override is applied. // any user -notifypos override is applied.
void apply_game_default_position(const std::string &game_name); void apply_game_default_position(const std::string &game_name);
// add a notification (thread-safe). returns the assigned id, or 0 if the // add a notification (thread-safe). returns the assigned id, or 0 if the
// notification was dropped (overlay disabled or notifications disabled). // notification was dropped (overlay disabled or notifications disabled).
uint64_t add(Severity severity, std::string text); uint64_t add(Severity severity, std::string text);
// rate-limited variant of add(). suppresses the toast if another call with // rate-limited variant of add(). suppresses the toast if another call with
// the same `key` succeeded within the last `cooldown_seconds`. useful for // the same `key` succeeded within the last `cooldown_seconds`. useful for
// events that can fire every frame (e.g. a button held down). returns the // events that can fire every frame (e.g. a button held down). returns the
// assigned id, or 0 if the toast was suppressed or dropped. thread-safe. // assigned id, or 0 if the toast was suppressed or dropped. thread-safe.
uint64_t add_throttled(Severity severity, const std::string &key, uint64_t add_throttled(Severity severity, const std::string &key,
double cooldown_seconds, std::string text); double cooldown_seconds, std::string text);
// true if there is at least one notification that still needs to be drawn. // true if there is at least one notification that still needs to be drawn.
// safe to call from the render thread without locking the underlying store. // safe to call from the render thread without locking the underlying store.
bool has_pending(); bool has_pending();
// draw all active notifications and prune expired ones. // draw all active notifications and prune expired ones.
// must be called from the ImGui render thread inside a NewFrame/EndFrame pair. // must be called from the ImGui render thread inside a NewFrame/EndFrame pair.
void draw(); void draw();
} }
+8
View File
@@ -158,6 +158,11 @@ void overlay::destroy(HWND hWnd) {
} }
} }
bool overlay::global_hotkeys_triggered() {
const std::lock_guard<std::mutex> lock(OVERLAY_MUTEX);
return !overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered();
}
overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device) overlay::SpiceOverlay::SpiceOverlay(HWND hWnd, IDirect3D9 *d3d, IDirect3DDevice9 *device)
: renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) { : renderer(OverlayRenderer::D3D9), hWnd(hWnd), d3d(d3d), device(device) {
log_info("overlay", "initializing (D3D9)"); log_info("overlay", "initializing (D3D9)");
@@ -924,6 +929,9 @@ bool overlay::SpiceOverlay::has_focus() {
} }
bool overlay::SpiceOverlay::hotkeys_triggered() { bool overlay::SpiceOverlay::hotkeys_triggered() {
const std::lock_guard<std::mutex> lock(this->hotkeys_mutex);
// this query also consumes the shared HotkeyToggle edge; the mutex gives all callers one latch
// prevent hotkeys in spicecfg // prevent hotkeys in spicecfg
if (cfg::CONFIGURATOR_STANDALONE) { if (cfg::CONFIGURATOR_STANDALONE) {
return false; return false;
+2
View File
@@ -212,6 +212,7 @@ namespace overlay {
bool fps_down = false; bool fps_down = false;
bool hotkey_toggle = false; bool hotkey_toggle = false;
bool hotkey_toggle_last = false; bool hotkey_toggle_last = false;
std::mutex hotkeys_mutex;
// true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(), // true between new_frame()'s ImGui::NewFrame() and render()'s ImGui::Render(),
// so render() never runs without a matching NewFrame. // so render() never runs without a matching NewFrame.
@@ -235,4 +236,5 @@ namespace overlay {
#endif #endif
void create_software(HWND hWnd); void create_software(HWND hWnd);
void destroy(HWND hWnd = nullptr); void destroy(HWND hWnd = nullptr);
bool global_hotkeys_triggered();
} }
@@ -25,6 +25,15 @@ namespace overlay::windows {
const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f); const ImVec4 YELLOW(1.f, 1.f, 0.f, 1.f);
const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f); const ImVec4 WHITE(1.f, 1.f, 1.f, 1.f);
// a subscreen is an already-composited backbuffer, so its alpha bits are not
// meaningful for display. Use AddImage's diffuse alpha (opaque by default)
// instead of the texture alpha while preserving ImGui's normal blend state.
static void ignore_texture_alpha(const ImDrawList *, const ImDrawCmd *command) {
auto device = static_cast<IDirect3DDevice9 *>(command->UserCallbackData);
device->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE);
device->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_SELECTARG2);
}
GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) { GenericSubScreen::GenericSubScreen(SpiceOverlay *overlay) : Window(overlay), device(overlay->get_device()) {
this->remove_window_padding = true; this->remove_window_padding = true;
// ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image // ImGuiWindowFlags_NoBackground is needed as the background is drawn on top of the subscreen image
@@ -239,10 +248,13 @@ namespace overlay::windows {
auto draw_list = this->draws_window auto draw_list = this->draws_window
? ImGui::GetWindowDrawList() ? ImGui::GetWindowDrawList()
: ImGui::GetBackgroundDrawList(); : ImGui::GetBackgroundDrawList();
draw_list->AddCallback(ignore_texture_alpha, this->device);
draw_list->AddImage( draw_list->AddImage(
reinterpret_cast<ImTextureID>(this->texture), reinterpret_cast<ImTextureID>(this->texture),
overlay_content_top_left, overlay_content_top_left,
bottom_right); bottom_right);
// following ImGui draws should resume normal texture-alpha modulation
draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_ResetRenderState);
if (this->draws_window) { if (this->draws_window) {
// draw an invisible button so that it swallows mouse input // draw an invisible button so that it swallows mouse input
@@ -1,149 +1,149 @@
#include "nostalgia_touch_piano.h" #include "nostalgia_touch_piano.h"
#include <cmath> #include <cmath>
#include "external/imgui/imgui_internal.h" #include "external/imgui/imgui_internal.h"
#include "games/nost/touch_mode.h" #include "games/nost/touch_mode.h"
namespace overlay::windows { namespace overlay::windows {
static constexpr float BUTTON_WIDTH = 144.f; static constexpr float BUTTON_WIDTH = 144.f;
static constexpr float BUTTON_HEIGHT = 40.f; static constexpr float BUTTON_HEIGHT = 40.f;
static constexpr float WINDOW_PADDING = 4.f; static constexpr float WINDOW_PADDING = 4.f;
static constexpr float EDGE_MARGIN = 4.f; static constexpr float EDGE_MARGIN = 4.f;
static constexpr float PIANO_HEIGHT_RATIO = 0.08f; static constexpr float PIANO_HEIGHT_RATIO = 0.08f;
static constexpr float PIANO_LEFT_GAP = 11.f; static constexpr float PIANO_LEFT_GAP = 11.f;
static constexpr float PIANO_RIGHT_GAP = 10.f; static constexpr float PIANO_RIGHT_GAP = 10.f;
static constexpr uint32_t PIANO_KEY_COUNT = 28; static constexpr uint32_t PIANO_KEY_COUNT = 28;
static constexpr ImU32 PIANO_KEY_COLOR = IM_COL32(255, 255, 255, 50); static constexpr ImU32 PIANO_KEY_COLOR = IM_COL32(255, 255, 255, 50);
static constexpr ImU32 PIANO_KEY_ACTIVE_COLOR = IM_COL32(255, 48, 48, 160); static constexpr ImU32 PIANO_KEY_ACTIVE_COLOR = IM_COL32(255, 48, 48, 160);
static constexpr ImU32 PIANO_KEY_BORDER_COLOR = IM_COL32(0, 0, 0, 100); static constexpr ImU32 PIANO_KEY_BORDER_COLOR = IM_COL32(0, 0, 0, 100);
struct ButtonPalette { struct ButtonPalette {
ImVec4 normal; ImVec4 normal;
ImVec4 hovered; ImVec4 hovered;
ImVec4 active; ImVec4 active;
}; };
static const ButtonPalette NAV_MODE_PALETTE { static const ButtonPalette NAV_MODE_PALETTE {
ImVec4(0.10f, 0.45f, 0.28f, 0.72f), ImVec4(0.10f, 0.45f, 0.28f, 0.72f),
ImVec4(0.14f, 0.58f, 0.36f, 0.82f), ImVec4(0.14f, 0.58f, 0.36f, 0.82f),
ImVec4(0.08f, 0.34f, 0.21f, 0.90f), ImVec4(0.08f, 0.34f, 0.21f, 0.90f),
}; };
static const ButtonPalette PIANO_MODE_PALETTE { static const ButtonPalette PIANO_MODE_PALETTE {
ImVec4(0.15f, 0.32f, 0.62f, 0.72f), ImVec4(0.15f, 0.32f, 0.62f, 0.72f),
ImVec4(0.20f, 0.42f, 0.78f, 0.82f), ImVec4(0.20f, 0.42f, 0.78f, 0.82f),
ImVec4(0.10f, 0.24f, 0.50f, 0.90f), ImVec4(0.10f, 0.24f, 0.50f, 0.90f),
}; };
static void draw_piano_keys( static void draw_piano_keys(
const ImVec2 &display_size, const ImVec2 &display_size,
LONG client_width, LONG client_width,
uint32_t key_state) { uint32_t key_state) {
if (display_size.x <= 0.f || display_size.y <= 0.f || client_width <= 0) { if (display_size.x <= 0.f || display_size.y <= 0.f || client_width <= 0) {
return; return;
} }
// this is only a visual guide; native touch routing owns the actual input // this is only a visual guide; native touch routing owns the actual input
const float left_gap = PIANO_LEFT_GAP * display_size.x / client_width; const float left_gap = PIANO_LEFT_GAP * display_size.x / client_width;
const float right_gap = PIANO_RIGHT_GAP * display_size.x / client_width; const float right_gap = PIANO_RIGHT_GAP * display_size.x / client_width;
const float piano_width = display_size.x - left_gap - right_gap; const float piano_width = display_size.x - left_gap - right_gap;
if (piano_width <= 0.f) { if (piano_width <= 0.f) {
return; return;
} }
const float key_width = piano_width / PIANO_KEY_COUNT; const float key_width = piano_width / PIANO_KEY_COUNT;
const float key_top = display_size.y * (1.f - PIANO_HEIGHT_RATIO); const float key_top = display_size.y * (1.f - PIANO_HEIGHT_RATIO);
auto *draw_list = ImGui::GetBackgroundDrawList(); auto *draw_list = ImGui::GetBackgroundDrawList();
for (uint32_t key = 0; key < PIANO_KEY_COUNT; key++) { for (uint32_t key = 0; key < PIANO_KEY_COUNT; key++) {
const ImVec2 key_min(left_gap + key * key_width, key_top); const ImVec2 key_min(left_gap + key * key_width, key_top);
const ImVec2 key_max(left_gap + (key + 1) * key_width, display_size.y); const ImVec2 key_max(left_gap + (key + 1) * key_width, display_size.y);
const bool active = (key_state & (UINT32_C(1) << key)) != 0; const bool active = (key_state & (UINT32_C(1) << key)) != 0;
draw_list->AddRectFilled( draw_list->AddRectFilled(
key_min, key_min,
key_max, key_max,
active ? PIANO_KEY_ACTIVE_COLOR : PIANO_KEY_COLOR); active ? PIANO_KEY_ACTIVE_COLOR : PIANO_KEY_COLOR);
draw_list->AddRect(key_min, key_max, PIANO_KEY_BORDER_COLOR); draw_list->AddRect(key_min, key_max, PIANO_KEY_BORDER_COLOR);
} }
} }
NostalgiaTouchPiano::NostalgiaTouchPiano(SpiceOverlay *overlay) : Window(overlay) { NostalgiaTouchPiano::NostalgiaTouchPiano(SpiceOverlay *overlay) : Window(overlay) {
this->title = "Nostalgia Touch Piano"; this->title = "Nostalgia Touch Piano";
this->flags = ImGuiWindowFlags_NoTitleBar this->flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoDocking
| ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoBackground
| ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoNav
| ImGuiWindowFlags_NoBringToFrontOnFocus; | ImGuiWindowFlags_NoBringToFrontOnFocus;
this->window_padding = overlay::apply_scaling_to_vector(WINDOW_PADDING, WINDOW_PADDING); this->window_padding = overlay::apply_scaling_to_vector(WINDOW_PADDING, WINDOW_PADDING);
this->set_active(true); this->set_active(true);
} }
void NostalgiaTouchPiano::calculate_initial_window() { void NostalgiaTouchPiano::calculate_initial_window() {
this->init_size = overlay::apply_scaling_to_vector( this->init_size = overlay::apply_scaling_to_vector(
BUTTON_WIDTH + WINDOW_PADDING * 2, BUTTON_WIDTH + WINDOW_PADDING * 2,
BUTTON_HEIGHT + WINDOW_PADDING * 2); BUTTON_HEIGHT + WINDOW_PADDING * 2);
this->init_pos = overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN); this->init_pos = overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN);
} }
void NostalgiaTouchPiano::build_content() { void NostalgiaTouchPiano::build_content() {
// keep the control anchored while the game window changes size or mode // keep the control anchored while the game window changes size or mode
ImGui::SetWindowPos( ImGui::SetWindowPos(
overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN), overlay::apply_scaling_to_vector(EDGE_MARGIN, EDGE_MARGIN),
ImGuiCond_Always); ImGuiCond_Always);
// stay above regular overlay windows, but never cover a blocking modal // stay above regular overlay windows, but never cover a blocking modal
ImGuiWindow *mode_window = ImGui::GetCurrentWindow(); ImGuiWindow *mode_window = ImGui::GetCurrentWindow();
if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) { if (ImGuiWindow *modal = ImGui::GetTopMostPopupModal()) {
ImGui::BringWindowToDisplayBehind(mode_window, modal); ImGui::BringWindowToDisplayBehind(mode_window, modal);
} else { } else {
ImGui::BringWindowToDisplayFront(mode_window); ImGui::BringWindowToDisplayFront(mode_window);
} }
const bool nav_mode = const bool nav_mode =
games::nost::touch_mode::current_mode() == games::nost::touch_mode::Mode::Nav; games::nost::touch_mode::current_mode() == games::nost::touch_mode::Mode::Nav;
const char *label = nav_mode ? "Nav Mode" : "Piano Mode"; const char *label = nav_mode ? "Nav Mode" : "Piano Mode";
// make the active routing mode recognizable without reading the label // make the active routing mode recognizable without reading the label
const auto &palette = nav_mode ? NAV_MODE_PALETTE : PIANO_MODE_PALETTE; const auto &palette = nav_mode ? NAV_MODE_PALETTE : PIANO_MODE_PALETTE;
ImGui::PushStyleColor(ImGuiCol_Button, palette.normal); ImGui::PushStyleColor(ImGuiCol_Button, palette.normal);
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette.hovered); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, palette.hovered);
ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette.active); ImGui::PushStyleColor(ImGuiCol_ButtonActive, palette.active);
ImGui::Button(label, overlay::apply_scaling_to_vector(BUTTON_WIDTH, BUTTON_HEIGHT)); ImGui::Button(label, overlay::apply_scaling_to_vector(BUTTON_WIDTH, BUTTON_HEIGHT));
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
const auto &io = ImGui::GetIO(); const auto &io = ImGui::GetIO();
RECT client_rect {}; RECT client_rect {};
if (io.DisplaySize.x > 0.f && io.DisplaySize.y > 0.f && if (io.DisplaySize.x > 0.f && io.DisplaySize.y > 0.f &&
GetClientRect(this->overlay->get_window(), &client_rect)) { GetClientRect(this->overlay->get_window(), &client_rect)) {
// convert the rendered imgui rectangle into the client coordinates // convert the rendered imgui rectangle into the client coordinates
// used by hardware touch publication and piano-key mapping // used by hardware touch publication and piano-key mapping
const auto item_min = ImGui::GetItemRectMin(); const auto item_min = ImGui::GetItemRectMin();
const auto item_max = ImGui::GetItemRectMax(); const auto item_max = ImGui::GetItemRectMax();
const auto client_width = client_rect.right - client_rect.left; const auto client_width = client_rect.right - client_rect.left;
const auto client_height = client_rect.bottom - client_rect.top; const auto client_height = client_rect.bottom - client_rect.top;
if (!nav_mode) { if (!nav_mode) {
draw_piano_keys( draw_piano_keys(
io.DisplaySize, io.DisplaySize,
client_width, client_width,
games::nost::touch_mode::piano_key_state()); games::nost::touch_mode::piano_key_state());
} }
RECT button_bounds { RECT button_bounds {
static_cast<LONG>(std::lround(item_min.x * client_width / io.DisplaySize.x)), static_cast<LONG>(std::lround(item_min.x * client_width / io.DisplaySize.x)),
static_cast<LONG>(std::lround(item_min.y * client_height / io.DisplaySize.y)), static_cast<LONG>(std::lround(item_min.y * client_height / io.DisplaySize.y)),
static_cast<LONG>(std::lround(item_max.x * client_width / io.DisplaySize.x)), static_cast<LONG>(std::lround(item_max.x * client_width / io.DisplaySize.x)),
static_cast<LONG>(std::lround(item_max.y * client_height / io.DisplaySize.y)), static_cast<LONG>(std::lround(item_max.y * client_height / io.DisplaySize.y)),
}; };
games::nost::touch_mode::publish_button_bounds( games::nost::touch_mode::publish_button_bounds(
this->overlay->get_window(), button_bounds); this->overlay->get_window(), button_bounds);
} }
} }
} }
@@ -1,15 +1,15 @@
#pragma once #pragma once
#include "overlay/window.h" #include "overlay/window.h"
namespace overlay::windows { namespace overlay::windows {
// persistent mode control rendered independently of the main overlay visibility // persistent mode control rendered independently of the main overlay visibility
class NostalgiaTouchPiano : public Window { class NostalgiaTouchPiano : public Window {
public: public:
explicit NostalgiaTouchPiano(SpiceOverlay *overlay); explicit NostalgiaTouchPiano(SpiceOverlay *overlay);
void calculate_initial_window() override; void calculate_initial_window() override;
void build_content() override; void build_content() override;
}; };
} }
+290 -290
View File
@@ -1,290 +1,290 @@
#include "obs.h" #include "obs.h"
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <cstdio> #include <cstdio>
#include "external/imgui/imgui.h" #include "external/imgui/imgui.h"
#include "games/io.h" #include "games/io.h"
#include "overlay/overlay.h" #include "overlay/overlay.h"
#include "overlay/imgui/extensions.h" #include "overlay/imgui/extensions.h"
using namespace std::chrono; using namespace std::chrono;
// OBS WebSocket protocol/worker thread lives in obs_websocket.cpp; this file // OBS WebSocket protocol/worker thread lives in obs_websocket.cpp; this file
// owns the ImGui control window and the connection lifecycle. // owns the ImGui control window and the connection lifecycle.
namespace { namespace {
// status text colors // status text colors
const ImVec4 COL_GREEN(0.40f, 0.85f, 0.40f, 1.0f); const ImVec4 COL_GREEN(0.40f, 0.85f, 0.40f, 1.0f);
const ImVec4 COL_RED(0.90f, 0.30f, 0.30f, 1.0f); const ImVec4 COL_RED(0.90f, 0.30f, 0.30f, 1.0f);
const ImVec4 COL_YELLOW(0.95f, 0.80f, 0.30f, 1.0f); const ImVec4 COL_YELLOW(0.95f, 0.80f, 0.30f, 1.0f);
const ImVec4 COL_GREY(0.60f, 0.60f, 0.60f, 1.0f); const ImVec4 COL_GREY(0.60f, 0.60f, 0.60f, 1.0f);
// muted action-button fills (start = green, stop = red, pause = yellow); the // muted action-button fills (start = green, stop = red, pause = yellow); the
// hovered/active shades are derived by brightening the base // hovered/active shades are derived by brightening the base
const ImVec4 COL_BTN_GREEN(0.20f, 0.45f, 0.24f, 1.0f); const ImVec4 COL_BTN_GREEN(0.20f, 0.45f, 0.24f, 1.0f);
const ImVec4 COL_BTN_RED(0.52f, 0.20f, 0.20f, 1.0f); const ImVec4 COL_BTN_RED(0.52f, 0.20f, 0.20f, 1.0f);
const ImVec4 COL_BTN_YELLOW(0.52f, 0.42f, 0.16f, 1.0f); const ImVec4 COL_BTN_YELLOW(0.52f, 0.42f, 0.16f, 1.0f);
// an in-flight request lingers for at most this long before the button frees // an in-flight request lingers for at most this long before the button frees
// itself, so a dropped state event can never wedge a control permanently // itself, so a dropped state event can never wedge a control permanently
const int64_t PENDING_TIMEOUT_MS = 5000; const int64_t PENDING_TIMEOUT_MS = 5000;
int64_t now_tick_ms() { int64_t now_tick_ms() {
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(); return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
} }
std::string format_duration(int64_t ms) { std::string format_duration(int64_t ms) {
if (ms < 0) { if (ms < 0) {
ms = 0; ms = 0;
} }
const int64_t total_seconds = ms / 1000; const int64_t total_seconds = ms / 1000;
const int64_t hours = total_seconds / 3600; const int64_t hours = total_seconds / 3600;
const int64_t minutes = (total_seconds % 3600) / 60; const int64_t minutes = (total_seconds % 3600) / 60;
const int64_t seconds = total_seconds % 60; const int64_t seconds = total_seconds % 60;
char buf[16]; char buf[16];
snprintf(buf, sizeof(buf), "%02lld:%02lld:%02lld", snprintf(buf, sizeof(buf), "%02lld:%02lld:%02lld",
static_cast<long long>(hours), static_cast<long long>(hours),
static_cast<long long>(minutes), static_cast<long long>(minutes),
static_cast<long long>(seconds)); static_cast<long long>(seconds));
return buf; return buf;
} }
} }
namespace overlay::windows { namespace overlay::windows {
OBSControl::OBSControl(SpiceOverlay *overlay) : Window(overlay) { OBSControl::OBSControl(SpiceOverlay *overlay) : Window(overlay) {
this->title = "OBS Control"; this->title = "OBS Control";
this->flags |= ImGuiWindowFlags_AlwaysAutoResize; this->flags |= ImGuiWindowFlags_AlwaysAutoResize;
this->init_pos = overlay::apply_scaling_to_vector(120, 120); this->init_pos = overlay::apply_scaling_to_vector(120, 120);
this->toggle_button = games::OverlayButtons::ToggleOBSControl; this->toggle_button = games::OverlayButtons::ToggleOBSControl;
this->worker_running.store(true); this->worker_running.store(true);
this->worker_thread = std::thread(&OBSControl::worker_main, this); this->worker_thread = std::thread(&OBSControl::worker_main, this);
} }
OBSControl::~OBSControl() { OBSControl::~OBSControl() {
// signal stop and wake any in-progress interruptible_sleep at once; the // signal stop and wake any in-progress interruptible_sleep at once; the
// lock around the store pairs with the wait predicate to avoid a lost wakeup // lock around the store pairs with the wait predicate to avoid a lost wakeup
{ {
std::lock_guard<std::mutex> lock(this->worker_mutex); std::lock_guard<std::mutex> lock(this->worker_mutex);
this->worker_running.store(false); this->worker_running.store(false);
} }
this->worker_cv.notify_all(); this->worker_cv.notify_all();
if (this->worker_thread.joinable()) { if (this->worker_thread.joinable()) {
// note: if the worker is mid-connect, WebSocket::from_url performs a // note: if the worker is mid-connect, WebSocket::from_url performs a
// blocking getaddrinfo/connect that does not observe worker_running, // blocking getaddrinfo/connect that does not observe worker_running,
// so this join can stall for the OS connect timeout. the default // so this join can stall for the OS connect timeout. the default
// 127.0.0.1 host fails fast (connection refused); only a misconfigured // 127.0.0.1 host fails fast (connection refused); only a misconfigured
// unreachable remote OBS_CONTROL_HOST would delay shutdown here. // unreachable remote OBS_CONTROL_HOST would delay shutdown here.
this->worker_thread.join(); this->worker_thread.join();
} }
} }
OBSStatus OBSControl::get_status() { OBSStatus OBSControl::get_status() {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
return this->status; return this->status;
} }
int64_t OBSControl::live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking) { int64_t OBSControl::live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking) {
if (!ticking) { if (!ticking) {
return (std::max<int64_t>)(base_ms, 0); return (std::max<int64_t>)(base_ms, 0);
} }
const int64_t now = const int64_t now =
duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(); duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
// clamp so a stale base tick / clock hiccup can never yield a negative // clamp so a stale base tick / clock hiccup can never yield a negative
// duration; callers (FPS rows, build_content) format this directly // duration; callers (FPS rows, build_content) format this directly
return (std::max<int64_t>)(base_ms + (now - base_tick), 0); return (std::max<int64_t>)(base_ms + (now - base_tick), 0);
} }
void OBSControl::build_content() { void OBSControl::build_content() {
const OBSStatus s = this->get_status(); const OBSStatus s = this->get_status();
// label + colored value on a single line // label + colored value on a single line
const auto status_line = [](const char *label, const ImVec4 &col, const char *value) { const auto status_line = [](const char *label, const ImVec4 &col, const char *value) {
ImGui::Text("%s", label); ImGui::Text("%s", label);
ImGui::SameLine(); ImGui::SameLine();
ImGui::TextColored(col, "%s", value); ImGui::TextColored(col, "%s", value);
}; };
if (!s.connected) { if (!s.connected) {
if (s.disabled) { if (s.disabled) {
ImGui::TextColored(COL_GREY, "%s", "OBS Control is disabled"); ImGui::TextColored(COL_GREY, "%s", "OBS Control is disabled");
return; return;
} }
if (s.identifying) { if (s.identifying) {
status_line("OBS WebSocket:", COL_YELLOW, "Connecting..."); status_line("OBS WebSocket:", COL_YELLOW, "Connecting...");
} else { } else {
status_line("OBS WebSocket:", COL_GREY, "Not connected"); status_line("OBS WebSocket:", COL_GREY, "Not connected");
} }
const std::string url = const std::string url =
"ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT); "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
status_line("Address:", COL_GREY, url.c_str()); status_line("Address:", COL_GREY, url.c_str());
if (!s.connection_error.empty()) { if (!s.connection_error.empty()) {
ImGui::TextColored(COL_RED, "%s", s.connection_error.c_str()); ImGui::TextColored(COL_RED, "%s", s.connection_error.c_str());
} }
return; return;
} }
status_line("OBS WebSocket:", COL_GREEN, "Connected"); status_line("OBS WebSocket:", COL_GREEN, "Connected");
// one fixed content width drives the whole panel so it never resizes as // one fixed content width drives the whole panel so it never resizes as
// the scene name or button labels change; every row is sized to fit it // the scene name or button labels change; every row is sized to fit it
const float spacing = ImGui::GetStyle().ItemSpacing.x; const float spacing = ImGui::GetStyle().ItemSpacing.x;
const float row_w = overlay::apply_scaling(240); const float row_w = overlay::apply_scaling(240);
if (s.current_scene.empty()) { if (s.current_scene.empty()) {
status_line("Scene:", COL_GREY, "(unknown)"); status_line("Scene:", COL_GREY, "(unknown)");
} else { } else {
ImGui::Text("Scene:"); ImGui::Text("Scene:");
ImGui::SameLine(); ImGui::SameLine();
// truncate to the remaining row width so "Scene:" + value together // truncate to the remaining row width so "Scene:" + value together
// never overflow and push the window wider // never overflow and push the window wider
const float label_w = ImGui::CalcTextSize("Scene:").x; const float label_w = ImGui::CalcTextSize("Scene:").x;
ImGui::PushStyleColor(ImGuiCol_Text, COL_GREY); ImGui::PushStyleColor(ImGuiCol_Text, COL_GREY);
ImGui::TextTruncated(s.current_scene, row_w - label_w - spacing); ImGui::TextTruncated(s.current_scene, row_w - label_w - spacing);
ImGui::PopStyleColor(); ImGui::PopStyleColor();
} }
ImGui::Separator(); ImGui::Separator();
const int64_t now = now_tick_ms(); const int64_t now = now_tick_ms();
// every button shares one fixed size; two side-by-side fill the row width, // every button shares one fixed size; two side-by-side fill the row width,
// single buttons keep that same size rather than stretching to fill // single buttons keep that same size rather than stretching to fill
const ImVec2 btn((row_w - spacing) * 0.5f, 0); const ImVec2 btn((row_w - spacing) * 0.5f, 0);
// has OBS reached the state a pending action was waiting for? // has OBS reached the state a pending action was waiting for?
const auto reached = [&](OBSAction a) { const auto reached = [&](OBSAction a) {
switch (a) { switch (a) {
case OBSAction::StreamStart: return s.streaming; case OBSAction::StreamStart: return s.streaming;
case OBSAction::StreamStop: return !s.streaming; case OBSAction::StreamStop: return !s.streaming;
case OBSAction::RecordStart: return s.recording; case OBSAction::RecordStart: return s.recording;
case OBSAction::RecordStop: return !s.recording; case OBSAction::RecordStop: return !s.recording;
case OBSAction::RecordPause: return s.record_paused; case OBSAction::RecordPause: return s.record_paused;
case OBSAction::RecordResume: return !s.record_paused; case OBSAction::RecordResume: return !s.record_paused;
default: return true; default: return true;
} }
}; };
// drop a pending action once OBS confirms the new state, or once the // drop a pending action once OBS confirms the new state, or once the
// safety deadline lapses (so a dropped event can't wedge the button) // safety deadline lapses (so a dropped event can't wedge the button)
const auto settle = [&](OBSAction &slot, int64_t deadline) { const auto settle = [&](OBSAction &slot, int64_t deadline) {
if (slot != OBSAction::None && (reached(slot) || now >= deadline)) { if (slot != OBSAction::None && (reached(slot) || now >= deadline)) {
slot = OBSAction::None; slot = OBSAction::None;
} }
}; };
settle(this->stream_pending, this->stream_pending_deadline); settle(this->stream_pending, this->stream_pending_deadline);
settle(this->record_pending, this->record_pending_deadline); settle(this->record_pending, this->record_pending_deadline);
// a colored button that fires a request and marks the output busy on click // a colored button that fires a request and marks the output busy on click
const auto action_button = const auto action_button =
[&](const char *label, [&](const char *label,
const ImVec4 &color, const ImVec4 &color,
const char *request, const char *request,
OBSAction &slot, OBSAction &slot,
int64_t &deadline, int64_t &deadline,
OBSAction action) { OBSAction action) {
if (ImGui::ColoredButton(label, color, btn)) { if (ImGui::ColoredButton(label, color, btn)) {
enqueue_request(request); enqueue_request(request);
slot = action; slot = action;
deadline = now + PENDING_TIMEOUT_MS; deadline = now + PENDING_TIMEOUT_MS;
} }
}; };
// streaming // streaming
{ {
const bool pending = this->stream_pending != OBSAction::None; const bool pending = this->stream_pending != OBSAction::None;
if (s.streaming) { if (s.streaming) {
const int64_t ms = live_duration_ms( const int64_t ms = live_duration_ms(
s.stream_duration_ms, s.stream_duration_base_tick, true); s.stream_duration_ms, s.stream_duration_base_tick, true);
status_line("Streaming:", COL_RED, ("LIVE " + format_duration(ms)).c_str()); status_line("Streaming:", COL_RED, ("LIVE " + format_duration(ms)).c_str());
} else { } else {
status_line("Streaming:", COL_GREY, pending ? "Starting..." : "Idle"); status_line("Streaming:", COL_GREY, pending ? "Starting..." : "Idle");
} }
ImGui::BeginDisabled(pending); ImGui::BeginDisabled(pending);
if (s.streaming) { if (s.streaming) {
action_button( action_button(
pending ? "Stopping...##stream" : "Stop Streaming##stream", pending ? "Stopping...##stream" : "Stop Streaming##stream",
COL_BTN_RED, COL_BTN_RED,
"StopStream", "StopStream",
this->stream_pending, this->stream_pending,
this->stream_pending_deadline, this->stream_pending_deadline,
OBSAction::StreamStop); OBSAction::StreamStop);
} else { } else {
action_button( action_button(
pending ? "Starting...##stream" : "Start Streaming##stream", pending ? "Starting...##stream" : "Start Streaming##stream",
COL_BTN_GREEN, COL_BTN_GREEN,
"StartStream", "StartStream",
this->stream_pending, this->stream_pending,
this->stream_pending_deadline, this->stream_pending_deadline,
OBSAction::StreamStart); OBSAction::StreamStart);
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
ImGui::Separator(); ImGui::Separator();
// recording // recording
{ {
const bool pending = this->record_pending != OBSAction::None; const bool pending = this->record_pending != OBSAction::None;
if (!s.recording) { if (!s.recording) {
status_line("Recording:", COL_GREY, pending ? "Starting..." : "Idle"); status_line("Recording:", COL_GREY, pending ? "Starting..." : "Idle");
ImGui::BeginDisabled(pending); ImGui::BeginDisabled(pending);
action_button( action_button(
pending ? "Starting...##record" : "Start Recording##record", pending ? "Starting...##record" : "Start Recording##record",
COL_BTN_GREEN, COL_BTN_GREEN,
"StartRecord", "StartRecord",
this->record_pending, this->record_pending,
this->record_pending_deadline, this->record_pending_deadline,
OBSAction::RecordStart); OBSAction::RecordStart);
ImGui::EndDisabled(); ImGui::EndDisabled();
return; return;
} }
const int64_t ms = live_duration_ms( const int64_t ms = live_duration_ms(
s.record_duration_ms, s.record_duration_base_tick, !s.record_paused); s.record_duration_ms, s.record_duration_base_tick, !s.record_paused);
if (s.record_paused) { if (s.record_paused) {
status_line("Recording:", COL_YELLOW, ("PAUSED " + format_duration(ms)).c_str()); status_line("Recording:", COL_YELLOW, ("PAUSED " + format_duration(ms)).c_str());
} else { } else {
status_line("Recording:", COL_RED, ("REC " + format_duration(ms)).c_str()); status_line("Recording:", COL_RED, ("REC " + format_duration(ms)).c_str());
} }
ImGui::BeginDisabled(pending); ImGui::BeginDisabled(pending);
action_button( action_button(
this->record_pending == OBSAction::RecordStop ? "Stopping...##record" : "Stop Recording##record", this->record_pending == OBSAction::RecordStop ? "Stopping...##record" : "Stop Recording##record",
COL_BTN_RED, COL_BTN_RED,
"StopRecord", "StopRecord",
this->record_pending, this->record_pending,
this->record_pending_deadline, OBSAction::RecordStop); this->record_pending_deadline, OBSAction::RecordStop);
ImGui::SameLine(); ImGui::SameLine();
if (s.record_paused) { if (s.record_paused) {
action_button( action_button(
this->record_pending == OBSAction::RecordResume ? "Resuming...##record_toggle" : "Resume##record_toggle", this->record_pending == OBSAction::RecordResume ? "Resuming...##record_toggle" : "Resume##record_toggle",
COL_BTN_GREEN, COL_BTN_GREEN,
"ResumeRecord", "ResumeRecord",
this->record_pending, this->record_pending,
this->record_pending_deadline, this->record_pending_deadline,
OBSAction::RecordResume); OBSAction::RecordResume);
} else { } else {
action_button( action_button(
this->record_pending == OBSAction::RecordPause ? "Pausing...##record_toggle" : "Pause##record_toggle", this->record_pending == OBSAction::RecordPause ? "Pausing...##record_toggle" : "Pause##record_toggle",
COL_BTN_YELLOW, COL_BTN_YELLOW,
"PauseRecord", "PauseRecord",
this->record_pending, this->record_pending,
this->record_pending_deadline, this->record_pending_deadline,
OBSAction::RecordPause); OBSAction::RecordPause);
} }
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
} }
} }
+131 -131
View File
@@ -1,131 +1,131 @@
#pragma once #pragma once
#include <atomic> #include <atomic>
#include <condition_variable> #include <condition_variable>
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <functional> #include <functional>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <thread> #include <thread>
#include "external/rapidjson/fwd.h" #include "external/rapidjson/fwd.h"
#include "overlay/window.h" #include "overlay/window.h"
namespace easywsclient { namespace easywsclient {
class WebSocket; class WebSocket;
} }
namespace overlay::windows { namespace overlay::windows {
// OBS WebSocket connection settings, resolved once at launch from the merged // OBS WebSocket connection settings, resolved once at launch from the merged
// launcher options (command line + saved config) following the same pattern // launcher options (command line + saved config) following the same pattern
// as the other global launch settings in launcher.cpp // as the other global launch settings in launcher.cpp
extern bool OBS_CONTROL_ENABLED; extern bool OBS_CONTROL_ENABLED;
extern std::string OBS_CONTROL_HOST; extern std::string OBS_CONTROL_HOST;
extern uint16_t OBS_CONTROL_PORT; extern uint16_t OBS_CONTROL_PORT;
extern std::string OBS_CONTROL_PASSWORD; extern std::string OBS_CONTROL_PASSWORD;
// when true, easywsclient's internal diagnostics are routed to the logger // when true, easywsclient's internal diagnostics are routed to the logger
extern bool OBS_CONTROL_DEBUG; extern bool OBS_CONTROL_DEBUG;
// status snapshot shared between the OBS worker thread and the render thread // status snapshot shared between the OBS worker thread and the render thread
struct OBSStatus { struct OBSStatus {
bool disabled = true; bool disabled = true;
bool connected = false; bool connected = false;
bool identifying = false; bool identifying = false;
std::string connection_error; std::string connection_error;
// name of the active program scene (read-only, from obs-websocket) // name of the active program scene (read-only, from obs-websocket)
std::string current_scene; std::string current_scene;
bool streaming = false; bool streaming = false;
bool recording = false; bool recording = false;
bool record_paused = false; bool record_paused = false;
// duration base values (milliseconds) and the local timestamp (ms since // duration base values (milliseconds) and the local timestamp (ms since
// steady epoch) at which they were last refreshed, so the UI can tick a // steady epoch) at which they were last refreshed, so the UI can tick a
// smooth timer between polls // smooth timer between polls
int64_t stream_duration_ms = 0; int64_t stream_duration_ms = 0;
int64_t record_duration_ms = 0; int64_t record_duration_ms = 0;
int64_t stream_duration_base_tick = 0; int64_t stream_duration_base_tick = 0;
int64_t record_duration_base_tick = 0; int64_t record_duration_base_tick = 0;
}; };
// in-flight user action used purely for UI feedback: when the user clicks a // in-flight user action used purely for UI feedback: when the user clicks a
// control we remember what we asked for so the button can show a transitional // control we remember what we asked for so the button can show a transitional
// label and stay disabled until the observed OBS state matches the request // label and stay disabled until the observed OBS state matches the request
// (or a short deadline lapses). owned solely by the render thread. // (or a short deadline lapses). owned solely by the render thread.
enum class OBSAction { enum class OBSAction {
None, None,
StreamStart, StreamStop, StreamStart, StreamStop,
RecordStart, RecordStop, RecordStart, RecordStop,
RecordPause, RecordResume, RecordPause, RecordResume,
}; };
class OBSControl : public Window { class OBSControl : public Window {
public: public:
OBSControl(SpiceOverlay *overlay); OBSControl(SpiceOverlay *overlay);
~OBSControl() override; ~OBSControl() override;
void build_content() override; void build_content() override;
// thread-safe snapshot of the current status for external widgets (e.g. FPS) // thread-safe snapshot of the current status for external widgets (e.g. FPS)
OBSStatus get_status(); OBSStatus get_status();
// live (ticked) duration in ms from a base value/tick captured at last poll // live (ticked) duration in ms from a base value/tick captured at last poll
static int64_t live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking); static int64_t live_duration_ms(int64_t base_ms, int64_t base_tick, bool ticking);
private: private:
// worker thread entry + helpers (implementation owns the WebSocket) // worker thread entry + helpers (implementation owns the WebSocket)
void worker_main(); void worker_main();
// run one connected session loop until the socket closes or we stop; // run one connected session loop until the socket closes or we stop;
// returns true if the obs-websocket handshake reached "Identified", false // returns true if the obs-websocket handshake reached "Identified", false
// if the socket closed first (e.g. OBS rejected our auth) // if the socket closed first (e.g. OBS rejected our auth)
bool run_session(easywsclient::WebSocket *ws, const std::string &password, bool run_session(easywsclient::WebSocket *ws, const std::string &password,
uint64_t &request_id); uint64_t &request_id);
// handle a single inbound obs-websocket message (parses + dispatches) // handle a single inbound obs-websocket message (parses + dispatches)
void handle_message(easywsclient::WebSocket *ws, const std::string &message, void handle_message(easywsclient::WebSocket *ws, const std::string &message,
const std::string &password, uint64_t &request_id, const std::string &password, uint64_t &request_id,
bool &identified); bool &identified);
// per-opcode handlers dispatched from handle_message // per-opcode handlers dispatched from handle_message
using request_fn = std::function<void(const char *request_type)>; using request_fn = std::function<void(const char *request_type)>;
void handle_identified(bool &identified, const request_fn &request); void handle_identified(bool &identified, const request_fn &request);
void handle_event(const rapidjson::Value &d, const request_fn &request); void handle_event(const rapidjson::Value &d, const request_fn &request);
void handle_response(const rapidjson::Value &d); void handle_response(const rapidjson::Value &d);
void enqueue_request(const std::string &request_type); void enqueue_request(const std::string &request_type);
// sleep up to total_ms, waking early if the worker is asked to stop // sleep up to total_ms, waking early if the worker is asked to stop
void interruptible_sleep(int total_ms); void interruptible_sleep(int total_ms);
// worker thread // worker thread
std::thread worker_thread; std::thread worker_thread;
std::atomic<bool> worker_running { false }; std::atomic<bool> worker_running { false };
// wakes interruptible_sleep immediately when worker_running is cleared, // wakes interruptible_sleep immediately when worker_running is cleared,
// so shutdown (and the reconnect backoff) never waits out a fixed delay // so shutdown (and the reconnect backoff) never waits out a fixed delay
std::mutex worker_mutex; std::mutex worker_mutex;
std::condition_variable worker_cv; std::condition_variable worker_cv;
// shared status (guarded by status_mutex) // shared status (guarded by status_mutex)
std::mutex status_mutex; std::mutex status_mutex;
OBSStatus status; OBSStatus status;
// outgoing user commands (guarded by command_mutex) // outgoing user commands (guarded by command_mutex)
std::mutex command_mutex; std::mutex command_mutex;
std::deque<std::string> command_queue; std::deque<std::string> command_queue;
// transient action feedback, touched only by the render thread (no sync): // transient action feedback, touched only by the render thread (no sync):
// remembers the last start/stop/pause request per output so the button can // remembers the last start/stop/pause request per output so the button can
// show a "Starting.../Stopping..." label and stay disabled until OBS reports // show a "Starting.../Stopping..." label and stay disabled until OBS reports
// the matching state, with *_deadline as a fallback if the update is missed // the matching state, with *_deadline as a fallback if the update is missed
OBSAction stream_pending = OBSAction::None; OBSAction stream_pending = OBSAction::None;
OBSAction record_pending = OBSAction::None; OBSAction record_pending = OBSAction::None;
int64_t stream_pending_deadline = 0; int64_t stream_pending_deadline = 0;
int64_t record_pending_deadline = 0; int64_t record_pending_deadline = 0;
}; };
} }
+434 -434
View File
@@ -1,434 +1,434 @@
#include <winsock2.h> #include <winsock2.h>
#include "obs.h" #include "obs.h"
#include <chrono> #include <chrono>
#include "external/easywsclient/easywsclient.hpp" #include "external/easywsclient/easywsclient.hpp"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "external/rapidjson/stringbuffer.h" #include "external/rapidjson/stringbuffer.h"
#include "external/rapidjson/writer.h" #include "external/rapidjson/writer.h"
#include "external/hash-library/sha256.h" #include "external/hash-library/sha256.h"
#include "overlay/notifications.h" #include "overlay/notifications.h"
#include "util/crypt.h" #include "util/crypt.h"
#include "util/logging.h" #include "util/logging.h"
// defined in easywsclient.cpp; gates its internal diagnostic output // defined in easywsclient.cpp; gates its internal diagnostic output
extern bool EASYWSCLIENT_LOGGING_ENABLED; extern bool EASYWSCLIENT_LOGGING_ENABLED;
using easywsclient::WebSocket; using easywsclient::WebSocket;
using namespace std::chrono; using namespace std::chrono;
// obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket): // obs-websocket v5 message flow (https://github.com/obsproject/obs-websocket):
// server -> op 0 Hello (may include an auth challenge) // server -> op 0 Hello (may include an auth challenge)
// client -> op 1 Identify (answers the challenge, picks rpcVersion) // client -> op 1 Identify (answers the challenge, picks rpcVersion)
// server -> op 2 Identified (handshake done; requests may now be sent) // server -> op 2 Identified (handshake done; requests may now be sent)
// server -> op 5 Event (state changes: stream/record/scene/...) // server -> op 5 Event (state changes: stream/record/scene/...)
// client -> op 6 Request (e.g. GetStreamStatus, StartRecord) // client -> op 6 Request (e.g. GetStreamStatus, StartRecord)
// server -> op 7 RequestResponse (reply to a Request, carries responseData) // server -> op 7 RequestResponse (reply to a Request, carries responseData)
// Every message is { "op": <int>, "d": { ... } }. Event fields are nested under // Every message is { "op": <int>, "d": { ... } }. Event fields are nested under
// d["eventData"] and request replies under d["responseData"], not in d directly. // d["eventData"] and request replies under d["responseData"], not in d directly.
namespace { namespace {
int64_t now_ms() { int64_t now_ms() {
return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count(); return duration_cast<milliseconds>(steady_clock::now().time_since_epoch()).count();
} }
// raw SHA256 digest -> base64 (obs-websocket v5 auth primitive) // raw SHA256 digest -> base64 (obs-websocket v5 auth primitive)
std::string sha256_base64(const std::string &input) { std::string sha256_base64(const std::string &input) {
SHA256 hasher; SHA256 hasher;
hasher.add(input.data(), input.size()); hasher.add(input.data(), input.size());
unsigned char digest[SHA256::HashBytes]; unsigned char digest[SHA256::HashBytes];
hasher.getHash(digest); hasher.getHash(digest);
return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes); return crypt::base64_encode(reinterpret_cast<const uint8_t *>(digest), SHA256::HashBytes);
} }
// auth = base64(sha256(base64(sha256(password + salt)) + challenge)) // auth = base64(sha256(base64(sha256(password + salt)) + challenge))
std::string compute_auth(const std::string &password, const std::string &salt, std::string compute_auth(const std::string &password, const std::string &salt,
const std::string &challenge) { const std::string &challenge) {
const std::string secret = sha256_base64(password + salt); const std::string secret = sha256_base64(password + salt);
return sha256_base64(secret + challenge); return sha256_base64(secret + challenge);
} }
std::string build_identify(int rpc_version, const std::string &authentication) { std::string build_identify(int rpc_version, const std::string &authentication) {
rapidjson::StringBuffer sb; rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb); rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject(); w.StartObject();
w.Key("op"); w.Int(1); w.Key("op"); w.Int(1);
w.Key("d"); w.Key("d");
w.StartObject(); w.StartObject();
w.Key("rpcVersion"); w.Int(rpc_version); w.Key("rpcVersion"); w.Int(rpc_version);
if (!authentication.empty()) { if (!authentication.empty()) {
w.Key("authentication"); w.String(authentication.c_str()); w.Key("authentication"); w.String(authentication.c_str());
} }
w.EndObject(); w.EndObject();
w.EndObject(); w.EndObject();
return sb.GetString(); return sb.GetString();
} }
std::string build_request(const std::string &request_type, uint64_t request_id) { std::string build_request(const std::string &request_type, uint64_t request_id) {
rapidjson::StringBuffer sb; rapidjson::StringBuffer sb;
rapidjson::Writer<rapidjson::StringBuffer> w(sb); rapidjson::Writer<rapidjson::StringBuffer> w(sb);
w.StartObject(); w.StartObject();
w.Key("op"); w.Int(6); w.Key("op"); w.Int(6);
w.Key("d"); w.Key("d");
w.StartObject(); w.StartObject();
w.Key("requestType"); w.String(request_type.c_str()); w.Key("requestType"); w.String(request_type.c_str());
w.Key("requestId"); w.String(std::to_string(request_id).c_str()); w.Key("requestId"); w.String(std::to_string(request_id).c_str());
w.EndObject(); w.EndObject();
w.EndObject(); w.EndObject();
return sb.GetString(); return sb.GetString();
} }
// read a numeric field as int64 ms (obs sends durations as integers/doubles) // read a numeric field as int64 ms (obs sends durations as integers/doubles)
int64_t json_number(const rapidjson::Value &obj, const char *key) { int64_t json_number(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsNumber()) { if (obj.HasMember(key) && obj[key].IsNumber()) {
return static_cast<int64_t>(obj[key].GetDouble()); return static_cast<int64_t>(obj[key].GetDouble());
} }
return 0; return 0;
} }
bool json_bool(const rapidjson::Value &obj, const char *key) { bool json_bool(const rapidjson::Value &obj, const char *key) {
return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool(); return obj.HasMember(key) && obj[key].IsBool() && obj[key].GetBool();
} }
std::string json_string(const rapidjson::Value &obj, const char *key) { std::string json_string(const rapidjson::Value &obj, const char *key) {
if (obj.HasMember(key) && obj[key].IsString()) { if (obj.HasMember(key) && obj[key].IsString()) {
return obj[key].GetString(); return obj[key].GetString();
} }
return ""; return "";
} }
// build the Identify (op 1) reply to a Hello (op 0), answering the auth // build the Identify (op 1) reply to a Hello (op 0), answering the auth
// challenge if the server requires one // challenge if the server requires one
std::string build_hello_response(const rapidjson::Value &d, const std::string &password) { std::string build_hello_response(const rapidjson::Value &d, const std::string &password) {
int rpc_version = 1; int rpc_version = 1;
if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) { if (d.HasMember("rpcVersion") && d["rpcVersion"].IsInt()) {
rpc_version = d["rpcVersion"].GetInt(); rpc_version = d["rpcVersion"].GetInt();
} }
std::string auth; std::string auth;
if (d.HasMember("authentication") && d["authentication"].IsObject()) { if (d.HasMember("authentication") && d["authentication"].IsObject()) {
const rapidjson::Value &a = d["authentication"]; const rapidjson::Value &a = d["authentication"];
const std::string challenge = json_string(a, "challenge"); const std::string challenge = json_string(a, "challenge");
const std::string salt = json_string(a, "salt"); const std::string salt = json_string(a, "salt");
if (!challenge.empty()) { if (!challenge.empty()) {
auth = compute_auth(password, salt, challenge); auth = compute_auth(password, salt, challenge);
} }
} }
return build_identify(rpc_version, auth); return build_identify(rpc_version, auth);
} }
// map an obs-websocket outputState to a user notification. `label` is the // map an obs-websocket outputState to a user notification. `label` is the
// output kind ("Streaming" or "Recording"). transitional states are ignored. // output kind ("Streaming" or "Recording"). transitional states are ignored.
void notify_output_state(const char *label, const std::string &state) { void notify_output_state(const char *label, const std::string &state) {
using overlay::notifications::Severity; using overlay::notifications::Severity;
struct StateToast { struct StateToast {
const char *state; const char *state;
Severity severity; Severity severity;
const char *verb; const char *verb;
}; };
static const StateToast TOASTS[] = { static const StateToast TOASTS[] = {
{ "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" }, { "OBS_WEBSOCKET_OUTPUT_STARTED", Severity::Success, "started" },
{ "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" }, { "OBS_WEBSOCKET_OUTPUT_STOPPED", Severity::Info, "stopped" },
{ "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" }, { "OBS_WEBSOCKET_OUTPUT_PAUSED", Severity::Warning, "paused" },
{ "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" }, { "OBS_WEBSOCKET_OUTPUT_RESUMED", Severity::Info, "resumed" },
}; };
for (const auto &toast : TOASTS) { for (const auto &toast : TOASTS) {
if (state == toast.state) { if (state == toast.state) {
overlay::notifications::add(toast.severity, overlay::notifications::add(toast.severity,
"OBS: " + std::string(label) + " " + toast.verb); "OBS: " + std::string(label) + " " + toast.verb);
return; return;
} }
} }
} }
} }
namespace overlay::windows { namespace overlay::windows {
// connection settings resolved at launch (see launcher.cpp) // connection settings resolved at launch (see launcher.cpp)
bool OBS_CONTROL_ENABLED = false; bool OBS_CONTROL_ENABLED = false;
std::string OBS_CONTROL_HOST = "127.0.0.1"; std::string OBS_CONTROL_HOST = "127.0.0.1";
uint16_t OBS_CONTROL_PORT = 4455; uint16_t OBS_CONTROL_PORT = 4455;
std::string OBS_CONTROL_PASSWORD; std::string OBS_CONTROL_PASSWORD;
bool OBS_CONTROL_DEBUG = false; bool OBS_CONTROL_DEBUG = false;
void OBSControl::enqueue_request(const std::string &request_type) { void OBSControl::enqueue_request(const std::string &request_type) {
std::lock_guard<std::mutex> lock(this->command_mutex); std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.push_back(request_type); this->command_queue.push_back(request_type);
} }
void OBSControl::interruptible_sleep(int total_ms) { void OBSControl::interruptible_sleep(int total_ms) {
std::unique_lock<std::mutex> lock(this->worker_mutex); std::unique_lock<std::mutex> lock(this->worker_mutex);
this->worker_cv.wait_for(lock, milliseconds(total_ms), this->worker_cv.wait_for(lock, milliseconds(total_ms),
[this] { return !this->worker_running.load(); }); [this] { return !this->worker_running.load(); });
} }
void OBSControl::handle_message(WebSocket *ws, const std::string &message, void OBSControl::handle_message(WebSocket *ws, const std::string &message,
const std::string &password, uint64_t &request_id, bool &identified) { const std::string &password, uint64_t &request_id, bool &identified) {
rapidjson::Document doc; rapidjson::Document doc;
if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) { if (doc.Parse(message.c_str()).HasParseError() || !doc.IsObject()) {
return; return;
} }
if (!doc.HasMember("op") || !doc["op"].IsInt() if (!doc.HasMember("op") || !doc["op"].IsInt()
|| !doc.HasMember("d") || !doc["d"].IsObject()) { || !doc.HasMember("d") || !doc["d"].IsObject()) {
return; return;
} }
const int op = doc["op"].GetInt(); const int op = doc["op"].GetInt();
const rapidjson::Value &d = doc["d"]; const rapidjson::Value &d = doc["d"];
// send an op 6 Request; each needs a unique id (we never match replies // send an op 6 Request; each needs a unique id (we never match replies
// back, so a simple incrementing counter is enough) // back, so a simple incrementing counter is enough)
const request_fn request = [&](const char *request_type) { const request_fn request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id)); ws->send(build_request(request_type, ++request_id));
}; };
switch (op) { switch (op) {
case 0: // Hello case 0: // Hello
// server greeted us: reply with Identify, solving the auth // server greeted us: reply with Identify, solving the auth
// challenge inline if the server set a password // challenge inline if the server set a password
ws->send(build_hello_response(d, password)); ws->send(build_hello_response(d, password));
break; break;
case 2: // Identified case 2: // Identified
this->handle_identified(identified, request); this->handle_identified(identified, request);
break; break;
case 5: // Event case 5: // Event
this->handle_event(d, request); this->handle_event(d, request);
break; break;
case 7: // RequestResponse case 7: // RequestResponse
this->handle_response(d); this->handle_response(d);
break; break;
default: default:
break; break;
} }
} }
void OBSControl::handle_identified(bool &identified, const request_fn &request) { void OBSControl::handle_identified(bool &identified, const request_fn &request) {
// handshake complete: the connection is now usable for requests // handshake complete: the connection is now usable for requests
identified = true; identified = true;
{ {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = true; this->status.connected = true;
this->status.identifying = false; this->status.identifying = false;
this->status.connection_error.clear(); this->status.connection_error.clear();
} }
log_info("obs", "connected and identified"); log_info("obs", "connected and identified");
// pull the current scene/stream/record state so the UI starts accurate // pull the current scene/stream/record state so the UI starts accurate
request("GetCurrentProgramScene"); request("GetCurrentProgramScene");
request("GetStreamStatus"); request("GetStreamStatus");
request("GetRecordStatus"); request("GetRecordStatus");
} }
void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) { void OBSControl::handle_event(const rapidjson::Value &d, const request_fn &request) {
const std::string type = json_string(d, "eventType"); const std::string type = json_string(d, "eventType");
const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject(); const bool has_data = d.HasMember("eventData") && d["eventData"].IsObject();
if (type == "StreamStateChanged") { if (type == "StreamStateChanged") {
if (has_data) { if (has_data) {
notify_output_state("Streaming", json_string(d["eventData"], "outputState")); notify_output_state("Streaming", json_string(d["eventData"], "outputState"));
} }
request("GetStreamStatus"); request("GetStreamStatus");
} else if (type == "RecordStateChanged") { } else if (type == "RecordStateChanged") {
if (has_data) { if (has_data) {
notify_output_state("Recording", json_string(d["eventData"], "outputState")); notify_output_state("Recording", json_string(d["eventData"], "outputState"));
} }
request("GetRecordStatus"); request("GetRecordStatus");
} else if (type == "CurrentProgramSceneChanged" && has_data) { } else if (type == "CurrentProgramSceneChanged" && has_data) {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.current_scene = json_string(d["eventData"], "sceneName"); this->status.current_scene = json_string(d["eventData"], "sceneName");
} }
} }
void OBSControl::handle_response(const rapidjson::Value &d) { void OBSControl::handle_response(const rapidjson::Value &d) {
const std::string type = json_string(d, "requestType"); const std::string type = json_string(d, "requestType");
if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) { if (type.empty() || !d.HasMember("responseData") || !d["responseData"].IsObject()) {
return; return;
} }
const rapidjson::Value &rd = d["responseData"]; const rapidjson::Value &rd = d["responseData"];
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
if (type == "GetCurrentProgramScene") { if (type == "GetCurrentProgramScene") {
// newer obs returns sceneName; older builds used the now-deprecated // newer obs returns sceneName; older builds used the now-deprecated
// currentProgramSceneName, so prefer it then fall back // currentProgramSceneName, so prefer it then fall back
std::string scene = json_string(rd, "currentProgramSceneName"); std::string scene = json_string(rd, "currentProgramSceneName");
if (scene.empty()) { if (scene.empty()) {
scene = json_string(rd, "sceneName"); scene = json_string(rd, "sceneName");
} }
this->status.current_scene = scene; this->status.current_scene = scene;
} else if (type == "GetStreamStatus") { } else if (type == "GetStreamStatus") {
this->status.streaming = json_bool(rd, "outputActive"); this->status.streaming = json_bool(rd, "outputActive");
this->status.stream_duration_ms = json_number(rd, "outputDuration"); this->status.stream_duration_ms = json_number(rd, "outputDuration");
this->status.stream_duration_base_tick = now_ms(); this->status.stream_duration_base_tick = now_ms();
} else if (type == "GetRecordStatus") { } else if (type == "GetRecordStatus") {
this->status.recording = json_bool(rd, "outputActive"); this->status.recording = json_bool(rd, "outputActive");
this->status.record_paused = json_bool(rd, "outputPaused"); this->status.record_paused = json_bool(rd, "outputPaused");
this->status.record_duration_ms = json_number(rd, "outputDuration"); this->status.record_duration_ms = json_number(rd, "outputDuration");
this->status.record_duration_base_tick = now_ms(); this->status.record_duration_base_tick = now_ms();
} }
} }
bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) { bool OBSControl::run_session(WebSocket *ws, const std::string &password, uint64_t &request_id) {
// one iteration of a live connection: pump socket I/O, dispatch any // one iteration of a live connection: pump socket I/O, dispatch any
// inbound messages, flush queued user commands, then refresh status // inbound messages, flush queued user commands, then refresh status
bool identified = false; bool identified = false;
// handle_identified() issues the first GetStreamStatus/GetRecordStatus on // handle_identified() issues the first GetStreamStatus/GetRecordStatus on
// identify, so the periodic poll below just maintains the ~1s cadence // identify, so the periodic poll below just maintains the ~1s cadence
auto last_status_poll = steady_clock::now(); auto last_status_poll = steady_clock::now();
// send a request with the next sequential id // send a request with the next sequential id
const auto request = [&](const char *request_type) { const auto request = [&](const char *request_type) {
ws->send(build_request(request_type, ++request_id)); ws->send(build_request(request_type, ++request_id));
}; };
while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) { while (this->worker_running.load() && ws->getReadyState() != WebSocket::CLOSED) {
ws->poll(100); ws->poll(100);
ws->dispatch([&](const std::string &message) { ws->dispatch([&](const std::string &message) {
this->handle_message(ws, message, password, request_id, identified); this->handle_message(ws, message, password, request_id, identified);
}); });
if (ws->getReadyState() == WebSocket::CLOSED) { if (ws->getReadyState() == WebSocket::CLOSED) {
break; break;
} }
// nothing may be sent until the op 2 Identified handshake completes // nothing may be sent until the op 2 Identified handshake completes
if (!identified) { if (!identified) {
continue; continue;
} }
// drain user commands // drain user commands
std::deque<std::string> pending; std::deque<std::string> pending;
{ {
std::lock_guard<std::mutex> lock(this->command_mutex); std::lock_guard<std::mutex> lock(this->command_mutex);
pending.swap(this->command_queue); pending.swap(this->command_queue);
} }
for (const auto &cmd : pending) { for (const auto &cmd : pending) {
ws->send(build_request(cmd, ++request_id)); ws->send(build_request(cmd, ++request_id));
} }
// periodic status refresh (~1s) for live duration // periodic status refresh (~1s) for live duration
const auto now = steady_clock::now(); const auto now = steady_clock::now();
if (now - last_status_poll >= milliseconds(1000)) { if (now - last_status_poll >= milliseconds(1000)) {
last_status_poll = now; last_status_poll = now;
request("GetStreamStatus"); request("GetStreamStatus");
request("GetRecordStatus"); request("GetRecordStatus");
} }
} }
return identified; return identified;
} }
void OBSControl::worker_main() { void OBSControl::worker_main() {
// connection settings are resolved once at launch into globals // connection settings are resolved once at launch into globals
// (launcher.cpp, from the merged command-line + saved config options) // (launcher.cpp, from the merged command-line + saved config options)
if (!OBS_CONTROL_ENABLED) { if (!OBS_CONTROL_ENABLED) {
log_info("obs", "disabled, not connecting"); log_info("obs", "disabled, not connecting");
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = true; this->status.disabled = true;
return; return;
} }
const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT); const std::string url = "ws://" + OBS_CONTROL_HOST + ":" + std::to_string(OBS_CONTROL_PORT);
const std::string password = OBS_CONTROL_PASSWORD; const std::string password = OBS_CONTROL_PASSWORD;
// opt easywsclient's internal diagnostics in/out per the debug option // opt easywsclient's internal diagnostics in/out per the debug option
EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG; EASYWSCLIENT_LOGGING_ENABLED = OBS_CONTROL_DEBUG;
// winsock is reference-counted: the app performs its own WSAStartup at // winsock is reference-counted: the app performs its own WSAStartup at
// launch (which outlives this worker), so this paired Startup/Cleanup only // launch (which outlives this worker), so this paired Startup/Cleanup only
// bumps the refcount and the WSACleanup below never tears down winsock for // bumps the refcount and the WSACleanup below never tears down winsock for
// the rest of the process // the rest of the process
WSADATA wsa_data; WSADATA wsa_data;
WSAStartup(MAKEWORD(2, 2), &wsa_data); WSAStartup(MAKEWORD(2, 2), &wsa_data);
log_info("obs", "enabled, connecting to {}", url); log_info("obs", "enabled, connecting to {}", url);
uint64_t request_id = 0; uint64_t request_id = 0;
// the reconnect loop retries every 5s; latch the auth-failure warning so a // the reconnect loop retries every 5s; latch the auth-failure warning so a
// wrong password logs once, not on every retry. reset after any identified // wrong password logs once, not on every retry. reset after any identified
// session so a later genuine failure is reported again // session so a later genuine failure is reported again
bool auth_warning_logged = false; bool auth_warning_logged = false;
// reconnect loop: keep a session alive while enabled, retrying on drop // reconnect loop: keep a session alive while enabled, retrying on drop
while (this->worker_running.load()) { while (this->worker_running.load()) {
// mark "connecting" for the UI before each attempt // mark "connecting" for the UI before each attempt
{ {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.disabled = false; this->status.disabled = false;
this->status.connected = false; this->status.connected = false;
this->status.identifying = true; this->status.identifying = true;
this->status.connection_error.clear(); this->status.connection_error.clear();
} }
// open the TCP socket and perform the WebSocket handshake; null means // open the TCP socket and perform the WebSocket handshake; null means
// OBS is unreachable (not running / wrong port / obs-websocket off) // OBS is unreachable (not running / wrong port / obs-websocket off)
WebSocket::pointer ws = WebSocket::from_url(url); WebSocket::pointer ws = WebSocket::from_url(url);
if (ws == nullptr) { if (ws == nullptr) {
{ {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.identifying = false; this->status.identifying = false;
this->status.connection_error = "Unable to connect"; this->status.connection_error = "Unable to connect";
} }
interruptible_sleep(5000); interruptible_sleep(5000);
continue; continue;
} }
// blocks here pumping the connection until it closes or we stop. // blocks here pumping the connection until it closes or we stop.
// a session that never reaches "Identified" was rejected by OBS, // a session that never reaches "Identified" was rejected by OBS,
// overwhelmingly because the password is wrong or missing // overwhelmingly because the password is wrong or missing
const bool identified = this->run_session(ws, password, request_id); const bool identified = this->run_session(ws, password, request_id);
// session ended: close the socket cleanly and free it // session ended: close the socket cleanly and free it
ws->close(); ws->close();
ws->poll(); ws->poll();
delete ws; delete ws;
if (!identified && this->worker_running.load()) { if (!identified && this->worker_running.load()) {
if (!auth_warning_logged) { if (!auth_warning_logged) {
log_warning("obs", "connection closed before identify; " log_warning("obs", "connection closed before identify; "
"OBS likely rejected authentication (check the password)"); "OBS likely rejected authentication (check the password)");
auth_warning_logged = true; auth_warning_logged = true;
} }
} else if (identified) { } else if (identified) {
// a good session resets the latch so a future failure logs again // a good session resets the latch so a future failure logs again
auth_warning_logged = false; auth_warning_logged = false;
} }
// connection dropped: clear live state so the UI doesn't show stale // connection dropped: clear live state so the UI doesn't show stale
// scene/stream/record info while disconnected // scene/stream/record info while disconnected
{ {
std::lock_guard<std::mutex> lock(this->status_mutex); std::lock_guard<std::mutex> lock(this->status_mutex);
this->status.connected = false; this->status.connected = false;
this->status.identifying = false; this->status.identifying = false;
if (this->status.connection_error.empty()) { if (this->status.connection_error.empty()) {
this->status.connection_error = this->status.connection_error =
identified ? "Disconnected" : "Auth failed (check password)"; identified ? "Disconnected" : "Auth failed (check password)";
} }
this->status.streaming = false; this->status.streaming = false;
this->status.recording = false; this->status.recording = false;
this->status.record_paused = false; this->status.record_paused = false;
this->status.current_scene.clear(); this->status.current_scene.clear();
} }
// clear any commands queued while disconnected // clear any commands queued while disconnected
{ {
std::lock_guard<std::mutex> lock(this->command_mutex); std::lock_guard<std::mutex> lock(this->command_mutex);
this->command_queue.clear(); this->command_queue.clear();
} }
// wait before reconnecting (interruptible) // wait before reconnecting (interruptible)
interruptible_sleep(5000); interruptible_sleep(5000);
} }
WSACleanup(); WSACleanup();
log_info("obs", "OBS overlay worker stopped"); log_info("obs", "OBS overlay worker stopped");
} }
} }
+179 -179
View File
@@ -1,179 +1,179 @@
#include "internal.h" #include "internal.h"
#include <cstring> #include <cstring>
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
namespace patcher { namespace patcher {
static std::pair<std::string, std::string> make_patch_group_key( static std::pair<std::string, std::string> make_patch_group_key(
const std::string& game_code, const std::string& game_code,
const std::string& group_id) { const std::string& group_id) {
return {strtolower(game_code), group_id}; return {strtolower(game_code), group_id};
} }
static bool has_embedded_null(const rapidjson::Value& value) { static bool has_embedded_null(const rapidjson::Value& value) {
return strlen(value.GetString()) != value.GetStringLength(); return strlen(value.GetString()) != value.GetStringLength();
} }
bool is_patch_group_definition(const rapidjson::Value& patch) { bool is_patch_group_definition(const rapidjson::Value& patch) {
if (!patch.IsObject()) { if (!patch.IsObject()) {
return false; return false;
} }
const auto type_it = patch.FindMember("type"); const auto type_it = patch.FindMember("type");
return type_it != patch.MemberEnd() return type_it != patch.MemberEnd()
&& type_it->value.IsString() && type_it->value.IsString()
&& type_it->value.GetStringLength() == strlen("group") && type_it->value.GetStringLength() == strlen("group")
&& !_stricmp(type_it->value.GetString(), "group"); && !_stricmp(type_it->value.GetString(), "group");
} }
std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions( std::map<std::pair<std::string, std::string>, PatchGroup> parse_patch_group_definitions(
const rapidjson::Document& doc) { const rapidjson::Document& doc) {
std::map<std::pair<std::string, std::string>, PatchGroup> groups; std::map<std::pair<std::string, std::string>, PatchGroup> groups;
for (const auto& patch : doc.GetArray()) { for (const auto& patch : doc.GetArray()) {
if (!is_patch_group_definition(patch)) { if (!is_patch_group_definition(patch)) {
continue; continue;
} }
const auto id_it = patch.FindMember("id"); const auto id_it = patch.FindMember("id");
const auto game_code_it = patch.FindMember("gameCode"); const auto game_code_it = patch.FindMember("gameCode");
const auto name_it = patch.FindMember("name"); const auto name_it = patch.FindMember("name");
if (id_it == patch.MemberEnd() || !id_it->value.IsString() if (id_it == patch.MemberEnd() || !id_it->value.IsString()
|| id_it->value.GetStringLength() == 0 || id_it->value.GetStringLength() == 0
|| has_embedded_null(id_it->value) || has_embedded_null(id_it->value)
|| game_code_it == patch.MemberEnd() || !game_code_it->value.IsString() || game_code_it == patch.MemberEnd() || !game_code_it->value.IsString()
|| game_code_it->value.GetStringLength() == 0 || game_code_it->value.GetStringLength() == 0
|| has_embedded_null(game_code_it->value) || has_embedded_null(game_code_it->value)
|| name_it == patch.MemberEnd() || !name_it->value.IsString() || name_it == patch.MemberEnd() || !name_it->value.IsString()
|| name_it->value.GetStringLength() == 0 || name_it->value.GetStringLength() == 0
|| has_embedded_null(name_it->value)) { || has_embedded_null(name_it->value)) {
log_warning("patchmanager", "invalid patch group definition"); log_warning("patchmanager", "invalid patch group definition");
continue; continue;
} }
PatchGroup group; PatchGroup group;
group.name.assign(name_it->value.GetString(), name_it->value.GetStringLength()); group.name.assign(name_it->value.GetString(), name_it->value.GetStringLength());
group.name_in_lower_case = strtolower(group.name); group.name_in_lower_case = strtolower(group.name);
const std::string group_id( const std::string group_id(
id_it->value.GetString(), id_it->value.GetString(),
id_it->value.GetStringLength()); id_it->value.GetStringLength());
const auto description_it = patch.FindMember("description"); const auto description_it = patch.FindMember("description");
if (description_it != patch.MemberEnd()) { if (description_it != patch.MemberEnd()) {
if (!description_it->value.IsString() if (!description_it->value.IsString()
|| has_embedded_null(description_it->value)) { || has_embedded_null(description_it->value)) {
log_warning("patchmanager", "invalid description for patch group {}", group_id); log_warning("patchmanager", "invalid description for patch group {}", group_id);
continue; continue;
} }
group.description.assign( group.description.assign(
description_it->value.GetString(), description_it->value.GetString(),
description_it->value.GetStringLength()); description_it->value.GetStringLength());
} }
const auto caution_it = patch.FindMember("caution"); const auto caution_it = patch.FindMember("caution");
if (caution_it != patch.MemberEnd()) { if (caution_it != patch.MemberEnd()) {
if (!caution_it->value.IsString() || has_embedded_null(caution_it->value)) { if (!caution_it->value.IsString() || has_embedded_null(caution_it->value)) {
log_warning("patchmanager", "invalid caution for patch group {}", group_id); log_warning("patchmanager", "invalid caution for patch group {}", group_id);
continue; continue;
} }
group.caution.assign( group.caution.assign(
caution_it->value.GetString(), caution_it->value.GetString(),
caution_it->value.GetStringLength()); caution_it->value.GetStringLength());
} }
const std::string game_code( const std::string game_code(
game_code_it->value.GetString(), game_code_it->value.GetString(),
game_code_it->value.GetStringLength()); game_code_it->value.GetStringLength());
if (!groups.emplace( if (!groups.emplace(
make_patch_group_key(game_code, group_id), make_patch_group_key(game_code, group_id),
std::move(group)).second) { std::move(group)).second) {
log_warning( log_warning(
"patchmanager", "patchmanager",
"duplicate patch group definition for {}/{}, ignoring duplicate", "duplicate patch group definition for {}/{}, ignoring duplicate",
game_code, game_code,
group_id); group_id);
} }
} }
return groups; return groups;
} }
static const PatchGroup* find_patch_group( static const PatchGroup* find_patch_group(
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups, const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
const std::string& game_code, const std::string& game_code,
const std::string& group_id) { const std::string& group_id) {
const auto group = groups.find(make_patch_group_key(game_code, group_id)); const auto group = groups.find(make_patch_group_key(game_code, group_id));
return group == groups.end() ? nullptr : &group->second; return group == groups.end() ? nullptr : &group->second;
} }
const PatchGroup* find_patch_group(const PatchData& patch) { const PatchGroup* find_patch_group(const PatchData& patch) {
return find_patch_group(patch_groups, patch.game_code, patch.group_id); return find_patch_group(patch_groups, patch.game_code, patch.group_id);
} }
std::string resolve_patch_group_id( std::string resolve_patch_group_id(
const rapidjson::Value& patch, const rapidjson::Value& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& groups, const std::map<std::pair<std::string, std::string>, PatchGroup>& groups,
const std::string& game_code, const std::string& game_code,
const char *patch_name) { const char *patch_name) {
const auto group_it = patch.FindMember("group"); const auto group_it = patch.FindMember("group");
if (group_it == patch.MemberEnd()) { if (group_it == patch.MemberEnd()) {
return ""; return "";
} }
if (!group_it->value.IsString() if (!group_it->value.IsString()
|| group_it->value.GetStringLength() == 0 || group_it->value.GetStringLength() == 0
|| has_embedded_null(group_it->value)) { || has_embedded_null(group_it->value)) {
log_warning("patchmanager", "invalid group reference for {}", patch_name); log_warning("patchmanager", "invalid group reference for {}", patch_name);
return ""; return "";
} }
const std::string group_id( const std::string group_id(
group_it->value.GetString(), group_it->value.GetString(),
group_it->value.GetStringLength()); group_it->value.GetStringLength());
if (!find_patch_group(groups, game_code, group_id)) { if (!find_patch_group(groups, game_code, group_id)) {
log_warning( log_warning(
"patchmanager", "patchmanager",
"unknown patch group {}/{} referenced by {}", "unknown patch group {}/{} referenced by {}",
game_code, game_code,
group_id, group_id,
patch_name); patch_name);
return ""; return "";
} }
return group_id; return group_id;
} }
void register_patch_group( void register_patch_group(
PatchData& patch, PatchData& patch,
const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions) { const std::map<std::pair<std::string, std::string>, PatchGroup>& definitions) {
if (patch.group_id.empty()) { if (patch.group_id.empty()) {
return; return;
} }
const auto *definition = find_patch_group( const auto *definition = find_patch_group(
definitions, definitions,
patch.game_code, patch.game_code,
patch.group_id); patch.group_id);
if (!definition) { if (!definition) {
patch.group_id.clear(); patch.group_id.clear();
return; return;
} }
const auto key = make_patch_group_key(patch.game_code, patch.group_id); const auto key = make_patch_group_key(patch.game_code, patch.group_id);
const auto [existing, inserted] = patch_groups.emplace(key, *definition); const auto [existing, inserted] = patch_groups.emplace(key, *definition);
if (!inserted if (!inserted
&& (existing->second.name != definition->name && (existing->second.name != definition->name
|| existing->second.description != definition->description || existing->second.description != definition->description
|| existing->second.caution != definition->caution)) { || existing->second.caution != definition->caution)) {
log_warning( log_warning(
"patchmanager", "patchmanager",
"conflicting group metadata for {}/{}, ignoring group on {}", "conflicting group metadata for {}/{}, ignoring group on {}",
patch.game_code, patch.game_code,
patch.group_id, patch.group_id,
patch.name); patch.name);
patch.group_id.clear(); patch.group_id.clear();
} }
} }
} }

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