fix line endings
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
# 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
|
||||||
+31
-31
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
+11023
-11023
File diff suppressed because one or more lines are too long
+552
-552
File diff suppressed because it is too large
Load Diff
+73
-73
@@ -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 */
|
||||||
|
|||||||
+835
-835
File diff suppressed because it is too large
Load Diff
+171
-171
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+1067
-1067
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,343 +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 "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// overlay render bridge
|
// overlay render bridge
|
||||||
|
|
||||||
namespace overlay::d3d11 {
|
namespace overlay::d3d11 {
|
||||||
|
|
||||||
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
|
// sRGB backbuffers need a UNORM view: ImGui vertex colors are already
|
||||||
// sRGB-encoded, so an extra linear->sRGB conversion would wash the
|
// sRGB-encoded, so an extra linear->sRGB conversion would wash the
|
||||||
// overlay out white.
|
// overlay out white.
|
||||||
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
|
static DXGI_FORMAT to_unorm_view(DXGI_FORMAT fmt) {
|
||||||
switch (fmt) {
|
switch (fmt) {
|
||||||
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
||||||
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||||
default: return fmt;
|
default: return fmt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ensure_rtv(ID3D11Device *device,
|
static void ensure_rtv(ID3D11Device *device,
|
||||||
IDXGISwapChain *swapchain,
|
IDXGISwapChain *swapchain,
|
||||||
ID3D11RenderTargetView **rtv)
|
ID3D11RenderTargetView **rtv)
|
||||||
{
|
{
|
||||||
if (*rtv || !device || !swapchain) {
|
if (*rtv || !device || !swapchain) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ID3D11Texture2D *backbuffer = nullptr;
|
ID3D11Texture2D *backbuffer = nullptr;
|
||||||
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
|
if (FAILED(swapchain->GetBuffer(0, IID_PPV_ARGS(&backbuffer))) || !backbuffer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
D3D11_TEXTURE2D_DESC td {};
|
D3D11_TEXTURE2D_DESC td {};
|
||||||
backbuffer->GetDesc(&td);
|
backbuffer->GetDesc(&td);
|
||||||
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
|
const DXGI_FORMAT view_fmt = to_unorm_view(td.Format);
|
||||||
if (view_fmt != td.Format) {
|
if (view_fmt != td.Format) {
|
||||||
D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
|
D3D11_RENDER_TARGET_VIEW_DESC rtvd {};
|
||||||
rtvd.Format = view_fmt;
|
rtvd.Format = view_fmt;
|
||||||
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
|
rtvd.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
|
||||||
device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
|
device->CreateRenderTargetView(backbuffer, &rtvd, rtv);
|
||||||
} else {
|
} else {
|
||||||
device->CreateRenderTargetView(backbuffer, nullptr, rtv);
|
device->CreateRenderTargetView(backbuffer, nullptr, rtv);
|
||||||
}
|
}
|
||||||
backbuffer->Release();
|
backbuffer->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// bind the backbuffer (lazily creating the RTV) and draw the imgui
|
// bind the backbuffer (lazily creating the RTV) and draw the imgui
|
||||||
// frame on top. reset_invalidate releases *rtv on ResizeBuffers.
|
// frame on top. reset_invalidate releases *rtv on ResizeBuffers.
|
||||||
void render(ID3D11Device *device,
|
void render(ID3D11Device *device,
|
||||||
ID3D11DeviceContext *context,
|
ID3D11DeviceContext *context,
|
||||||
IDXGISwapChain *swapchain,
|
IDXGISwapChain *swapchain,
|
||||||
ID3D11RenderTargetView **rtv)
|
ID3D11RenderTargetView **rtv)
|
||||||
{
|
{
|
||||||
ensure_rtv(device, swapchain, rtv);
|
ensure_rtv(device, swapchain, rtv);
|
||||||
if (!*rtv || !context) {
|
if (!*rtv || !context) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// present happens immediately after, so no need to save the previous
|
// present happens immediately after, so no need to save the previous
|
||||||
// RT binding (flip-model resets it anyway).
|
// RT binding (flip-model resets it anyway).
|
||||||
context->OMSetRenderTargets(1, rtv, nullptr);
|
context->OMSetRenderTargets(1, rtv, nullptr);
|
||||||
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// file-local state + per-frame helpers
|
// file-local state + per-frame helpers
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
|
using Present_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
IDXGISwapChain *, UINT, UINT);
|
IDXGISwapChain *, UINT, UINT);
|
||||||
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
|
using ResizeBuffers_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
|
IDXGISwapChain *, UINT, UINT, UINT, DXGI_FORMAT, UINT);
|
||||||
using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
|
using Present1_t = HRESULT(STDMETHODCALLTYPE *)(
|
||||||
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
|
IDXGISwapChain1 *, UINT, UINT, const DXGI_PRESENT_PARAMETERS *);
|
||||||
|
|
||||||
Present_t Present_orig = nullptr;
|
Present_t Present_orig = nullptr;
|
||||||
ResizeBuffers_t ResizeBuffers_orig = nullptr;
|
ResizeBuffers_t ResizeBuffers_orig = nullptr;
|
||||||
Present1_t Present1_orig = nullptr;
|
Present1_t Present1_orig = nullptr;
|
||||||
|
|
||||||
bool g_swapchain_hooked = false;
|
bool g_swapchain_hooked = false;
|
||||||
bool g_swapchain1_hooked = false;
|
bool g_swapchain1_hooked = false;
|
||||||
|
|
||||||
// sub-screens / IME helpers are usually child or zero-sized windows.
|
// sub-screens / IME helpers are usually child or zero-sized windows.
|
||||||
// visibility isn't checked - the game may present before showing the window.
|
// visibility isn't checked - the game may present before showing the window.
|
||||||
bool looks_like_game_window(HWND hwnd) {
|
bool looks_like_game_window(HWND hwnd) {
|
||||||
RECT client {};
|
RECT client {};
|
||||||
return GetAncestor(hwnd, GA_ROOT) == hwnd
|
return GetAncestor(hwnd, GA_ROOT) == hwnd
|
||||||
&& GetClientRect(hwnd, &client)
|
&& GetClientRect(hwnd, &client)
|
||||||
&& client.right > client.left
|
&& client.right > client.left
|
||||||
&& client.bottom > client.top;
|
&& client.bottom > client.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only the main game window; ignore sub-screens / IME helpers.
|
// only the main game window; ignore sub-screens / IME helpers.
|
||||||
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
|
||||||
DXGI_SWAP_CHAIN_DESC desc {};
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND main = d3d11_hooks::main_hwnd();
|
HWND main = d3d11_hooks::main_hwnd();
|
||||||
if (!main) {
|
if (!main) {
|
||||||
// no creation hook recorded a window, so fall back to the presenting one;
|
// no creation hook recorded a window, so fall back to the presenting one;
|
||||||
// the choice is permanent, so require a plausible game window
|
// the choice is permanent, so require a plausible game window
|
||||||
if (!looks_like_game_window(desc.OutputWindow)) {
|
if (!looks_like_game_window(desc.OutputWindow)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics::d3d11",
|
"graphics::d3d11",
|
||||||
"try to notemain hwnd from swapchain present: 0x{:x}",
|
"try to notemain hwnd from swapchain present: 0x{:x}",
|
||||||
(uintptr_t)desc.OutputWindow);
|
(uintptr_t)desc.OutputWindow);
|
||||||
|
|
||||||
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
|
||||||
|
|
||||||
// it may have been ignored, or another thread may have won the slot
|
// it may have been ignored, or another thread may have won the slot
|
||||||
main = d3d11_hooks::main_hwnd();
|
main = d3d11_hooks::main_hwnd();
|
||||||
}
|
}
|
||||||
return desc.OutputWindow == main;
|
return desc.OutputWindow == main;
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks are ordered cheapest first, since this runs on every present
|
// checks are ordered cheapest first, since this runs on every present
|
||||||
void try_create_overlay(IDXGISwapChain *swapchain) {
|
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||||
if (!swapchain) {
|
if (!swapchain) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// overlay is disabled by user
|
// overlay is disabled by user
|
||||||
if (!overlay::ENABLED) {
|
if (!overlay::ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// overlay is already enabled and attached
|
// overlay is already enabled and attached
|
||||||
if (overlay::OVERLAY) {
|
if (overlay::OVERLAY) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore sub windows
|
// ignore sub windows
|
||||||
if (!is_main_game_swapchain(swapchain)) {
|
if (!is_main_game_swapchain(swapchain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DXGI_SWAP_CHAIN_DESC desc {};
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// theme the native title bar; first present is the only reliable point for
|
// theme the native title bar; first present is the only reliable point for
|
||||||
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
|
||||||
set_window_dark_titlebar(desc.OutputWindow);
|
set_window_dark_titlebar(desc.OutputWindow);
|
||||||
|
|
||||||
ID3D11Device *device = nullptr;
|
ID3D11Device *device = nullptr;
|
||||||
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
|
if (FAILED(swapchain->GetDevice(IID_PPV_ARGS(&device))) || !device) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ID3D11DeviceContext *context = nullptr;
|
ID3D11DeviceContext *context = nullptr;
|
||||||
device->GetImmediateContext(&context);
|
device->GetImmediateContext(&context);
|
||||||
|
|
||||||
if (context) {
|
if (context) {
|
||||||
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
|
overlay::create_d3d11(desc.OutputWindow, device, context, swapchain);
|
||||||
RECT cr {};
|
RECT cr {};
|
||||||
::GetClientRect(desc.OutputWindow, &cr);
|
::GetClientRect(desc.OutputWindow, &cr);
|
||||||
log_info("graphics::d3d11",
|
log_info("graphics::d3d11",
|
||||||
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
|
"attached overlay to swapchain hwnd=0x{:x} backbuffer={}x{} client={}x{}",
|
||||||
(uintptr_t) desc.OutputWindow,
|
(uintptr_t) desc.OutputWindow,
|
||||||
desc.BufferDesc.Width, desc.BufferDesc.Height,
|
desc.BufferDesc.Width, desc.BufferDesc.Height,
|
||||||
cr.right - cr.left, cr.bottom - cr.top);
|
cr.right - cr.left, cr.bottom - cr.top);
|
||||||
context->Release();
|
context->Release();
|
||||||
}
|
}
|
||||||
device->Release();
|
device->Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
// screenshots have to keep working with the overlay disabled, so they are not gated on it
|
// screenshots have to keep working with the overlay disabled, so they are not gated on it
|
||||||
void pump_frame(IDXGISwapChain *swapchain) {
|
void pump_frame(IDXGISwapChain *swapchain) {
|
||||||
const bool has_overlay =
|
const bool has_overlay =
|
||||||
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||||
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
graphics_poll_screenshot_hotkey();
|
graphics_poll_screenshot_hotkey();
|
||||||
|
|
||||||
// before the overlay render so the screenshot excludes it
|
// before the overlay render so the screenshot excludes it
|
||||||
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
d3d11_hooks::try_screenshot(swapchain);
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (has_overlay) {
|
if (has_overlay) {
|
||||||
|
|
||||||
// size imgui to the backbuffer (not window client). dxgi may upscale
|
// size imgui to the backbuffer (not window client). dxgi may upscale
|
||||||
// a small backbuffer into a larger client rect; without this override
|
// a small backbuffer into a larger client rect; without this override
|
||||||
// imgui would draw past the RTV and the mouse mapping would be off.
|
// imgui would draw past the RTV and the mouse mapping would be off.
|
||||||
DXGI_SWAP_CHAIN_DESC desc {};
|
DXGI_SWAP_CHAIN_DESC desc {};
|
||||||
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
|
||||||
ImGui_ImplSpice_SetDisplaySizeOverride(
|
ImGui_ImplSpice_SetDisplaySizeOverride(
|
||||||
(float) desc.BufferDesc.Width,
|
(float) desc.BufferDesc.Width,
|
||||||
(float) desc.BufferDesc.Height);
|
(float) desc.BufferDesc.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay::OVERLAY->update();
|
overlay::OVERLAY->update();
|
||||||
overlay::OVERLAY->new_frame();
|
overlay::OVERLAY->new_frame();
|
||||||
overlay::OVERLAY->render();
|
overlay::OVERLAY->render();
|
||||||
}
|
}
|
||||||
|
|
||||||
// after the overlay render so the screenshot includes toasts / menus
|
// after the overlay render so the screenshot includes toasts / menus
|
||||||
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
d3d11_hooks::try_screenshot(swapchain);
|
d3d11_hooks::try_screenshot(swapchain);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
// swapchain method hooks
|
// swapchain method hooks
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE Present_hook(
|
HRESULT STDMETHODCALLTYPE Present_hook(
|
||||||
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
|
||||||
{
|
{
|
||||||
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
try_create_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
pump_frame(swapchain);
|
pump_frame(swapchain);
|
||||||
}
|
}
|
||||||
return Present_orig(swapchain, SyncInterval, Flags);
|
return Present_orig(swapchain, SyncInterval, Flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE Present1_hook(
|
HRESULT STDMETHODCALLTYPE Present1_hook(
|
||||||
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
|
||||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||||
{
|
{
|
||||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
try_create_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
pump_frame(swapchain);
|
pump_frame(swapchain);
|
||||||
}
|
}
|
||||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
|
HRESULT STDMETHODCALLTYPE ResizeBuffers_hook(
|
||||||
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
|
IDXGISwapChain *swapchain, UINT BufferCount, UINT Width, UINT Height,
|
||||||
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
|
DXGI_FORMAT NewFormat, UINT SwapChainFlags)
|
||||||
{
|
{
|
||||||
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
const bool ours = overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
||||||
if (ours) {
|
if (ours) {
|
||||||
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
|
log_info("graphics::d3d11", "ResizeBuffers {}x{} fmt={}",
|
||||||
Width, Height, (int32_t) NewFormat);
|
Width, Height, (int32_t) NewFormat);
|
||||||
overlay::OVERLAY->reset_invalidate();
|
overlay::OVERLAY->reset_invalidate();
|
||||||
}
|
}
|
||||||
HRESULT res = ResizeBuffers_orig(
|
HRESULT res = ResizeBuffers_orig(
|
||||||
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
|
swapchain, BufferCount, Width, Height, NewFormat, SwapChainFlags);
|
||||||
if (ours && SUCCEEDED(res)) {
|
if (ours && SUCCEEDED(res)) {
|
||||||
overlay::OVERLAY->reset_recreate();
|
overlay::OVERLAY->reset_recreate();
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// --------------------------------------------------------------------------
|
// --------------------------------------------------------------------------
|
||||||
// d3d11_hooks public surface: main-window tracking + vtable install.
|
// d3d11_hooks public surface: main-window tracking + vtable install.
|
||||||
|
|
||||||
namespace d3d11_hooks {
|
namespace d3d11_hooks {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
std::atomic<HWND> g_main_hwnd { nullptr };
|
std::atomic<HWND> g_main_hwnd { nullptr };
|
||||||
std::atomic<HWND> g_ignored_hwnd { nullptr };
|
std::atomic<HWND> g_ignored_hwnd { nullptr };
|
||||||
}
|
}
|
||||||
|
|
||||||
void note_main_hwnd(HWND hwnd) {
|
void note_main_hwnd(HWND hwnd) {
|
||||||
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
HWND expected = nullptr;
|
HWND expected = nullptr;
|
||||||
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
|
if (g_main_hwnd.compare_exchange_strong(expected, hwnd)) {
|
||||||
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
|
log_info("graphics::d3d11", "main hwnd recorded: 0x{:x}",
|
||||||
(uintptr_t) hwnd);
|
(uintptr_t) hwnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HWND main_hwnd() {
|
HWND main_hwnd() {
|
||||||
return g_main_hwnd.load();
|
return g_main_hwnd.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ignore_hwnd(HWND hwnd) {
|
void ignore_hwnd(HWND hwnd) {
|
||||||
g_ignored_hwnd.store(hwnd);
|
g_ignored_hwnd.store(hwnd);
|
||||||
}
|
}
|
||||||
|
|
||||||
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
|
// patch IDXGISwapChain::Present + ResizeBuffers and (if implemented)
|
||||||
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
|
// IDXGISwapChain1::Present1. idempotent; flag is set only after success
|
||||||
// so failed attempts can be retried on the next swapchain.
|
// so failed attempts can be retried on the next swapchain.
|
||||||
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
||||||
if (!swapchain) {
|
if (!swapchain) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
static std::mutex s_hook_mutex;
|
static std::mutex s_hook_mutex;
|
||||||
std::lock_guard<std::mutex> lock(s_hook_mutex);
|
std::lock_guard<std::mutex> lock(s_hook_mutex);
|
||||||
|
|
||||||
if (!g_swapchain_hooked) {
|
if (!g_swapchain_hooked) {
|
||||||
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
|
const bool a = hook_vtbl(swapchain, 8, (void *) Present_hook,
|
||||||
(void **) &Present_orig, "IDXGISwapChain::Present");
|
(void **) &Present_orig, "IDXGISwapChain::Present");
|
||||||
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
|
const bool b = hook_vtbl(swapchain, 13, (void *) ResizeBuffers_hook,
|
||||||
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
|
(void **) &ResizeBuffers_orig, "IDXGISwapChain::ResizeBuffers");
|
||||||
if (a && b) {
|
if (a && b) {
|
||||||
g_swapchain_hooked = true;
|
g_swapchain_hooked = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!g_swapchain1_hooked) {
|
if (!g_swapchain1_hooked) {
|
||||||
IDXGISwapChain1 *sc1 = nullptr;
|
IDXGISwapChain1 *sc1 = nullptr;
|
||||||
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
|
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
|
||||||
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
|
if (hook_vtbl(sc1, 22, (void *) Present1_hook,
|
||||||
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
|
(void **) &Present1_orig, "IDXGISwapChain1::Present1")) {
|
||||||
g_swapchain1_hooked = true;
|
g_swapchain1_hooked = true;
|
||||||
}
|
}
|
||||||
sc1->Release();
|
sc1->Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // SPICE_D3D11
|
#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,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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+154
-154
@@ -1,154 +1,154 @@
|
|||||||
#include "hotkeys.h"
|
#include "hotkeys.h"
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <stop_token>
|
#include <stop_token>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
#include "launcher/superexit.h"
|
#include "launcher/superexit.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "rawinput/rawinput.h"
|
#include "rawinput/rawinput.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace hotkeys {
|
namespace hotkeys {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
|
// 8 ms targets short screenshot pulses; sleep_for may use a coarser scheduler
|
||||||
// interval during early boot or when process timer adjustments are disabled
|
// interval during early boot or when process timer adjustments are disabled
|
||||||
constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
|
constexpr auto MIN_SAMPLE_INTERVAL = std::chrono::milliseconds(8);
|
||||||
|
|
||||||
std::atomic_bool SCREENSHOT_PENDING {false};
|
std::atomic_bool SCREENSHOT_PENDING {false};
|
||||||
std::mutex INPUT_MUTEX;
|
std::mutex INPUT_MUTEX;
|
||||||
bool INPUT_ENABLED = false;
|
bool INPUT_ENABLED = false;
|
||||||
bool RAW_INPUT_ENABLED = false;
|
bool RAW_INPUT_ENABLED = false;
|
||||||
std::jthread WORKER;
|
std::jthread WORKER;
|
||||||
|
|
||||||
bool read_button(std::vector<Button> *buttons, size_t index) {
|
bool read_button(std::vector<Button> *buttons, size_t index) {
|
||||||
// getState retains each binding's focus, modifier, inversion, and debounce policy
|
// getState retains each binding's focus, modifier, inversion, and debounce policy
|
||||||
return RI_MGR && buttons && index < buttons->size() &&
|
return RI_MGR && buttons && index < buttons->size() &&
|
||||||
GameAPI::Buttons::getState(RI_MGR, buttons->at(index));
|
GameAPI::Buttons::getState(RI_MGR, buttons->at(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
bool read_alt_f4() {
|
bool read_alt_f4() {
|
||||||
// preserve both legacy detection paths whenever raw input is available
|
// preserve both legacy detection paths whenever raw input is available
|
||||||
bool pressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
bool pressed = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0 &&
|
||||||
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
(GetAsyncKeyState(VK_F4) & 0x8000) != 0;
|
||||||
if (!RAW_INPUT_ENABLED || !RI_MGR) {
|
if (!RAW_INPUT_ENABLED || !RI_MGR) {
|
||||||
return pressed;
|
return pressed;
|
||||||
}
|
}
|
||||||
return pressed || RI_MGR->keyboard_combo_pressed(VK_MENU, VK_F4);
|
return pressed || RI_MGR->keyboard_combo_pressed(VK_MENU, VK_F4);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool rising_edge(bool current, bool &previous) {
|
bool rising_edge(bool current, bool &previous) {
|
||||||
const bool edge = current && !previous;
|
const bool edge = current && !previous;
|
||||||
previous = current;
|
previous = current;
|
||||||
return edge;
|
return edge;
|
||||||
}
|
}
|
||||||
|
|
||||||
void run(std::stop_token stop_token) {
|
void run(std::stop_token stop_token) {
|
||||||
bool screenshot_previous = false;
|
bool screenshot_previous = false;
|
||||||
bool coin_previous = false;
|
bool coin_previous = false;
|
||||||
|
|
||||||
while (!stop_token.stop_requested()) {
|
while (!stop_token.stop_requested()) {
|
||||||
bool coin_edge = false;
|
bool coin_edge = false;
|
||||||
bool super_exit_current = false;
|
bool super_exit_current = false;
|
||||||
bool alt_f4_current = false;
|
bool alt_f4_current = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
// lifecycle functions hold this mutex until raw-input polling is complete
|
// lifecycle functions hold this mutex until raw-input polling is complete
|
||||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
if (INPUT_ENABLED || RAW_INPUT_ENABLED) {
|
if (INPUT_ENABLED || RAW_INPUT_ENABLED) {
|
||||||
auto *buttons = games::get_buttons_overlay(eamuse_get_game());
|
auto *buttons = games::get_buttons_overlay(eamuse_get_game());
|
||||||
const bool screenshot_down = INPUT_ENABLED && read_button(
|
const bool screenshot_down = INPUT_ENABLED && read_button(
|
||||||
buttons, games::OverlayButtons::Screenshot);
|
buttons, games::OverlayButtons::Screenshot);
|
||||||
const bool coin_current = INPUT_ENABLED && read_button(
|
const bool coin_current = INPUT_ENABLED && read_button(
|
||||||
buttons, games::OverlayButtons::InsertCoin);
|
buttons, games::OverlayButtons::InsertCoin);
|
||||||
const bool super_exit_down = RAW_INPUT_ENABLED && read_button(
|
const bool super_exit_down = RAW_INPUT_ENABLED && read_button(
|
||||||
buttons, games::OverlayButtons::SuperExit);
|
buttons, games::OverlayButtons::SuperExit);
|
||||||
|
|
||||||
// global_hotkeys_triggered takes OVERLAY_MUTEX, then the overlay's
|
// global_hotkeys_triggered takes OVERLAY_MUTEX, then the overlay's
|
||||||
// hotkeys_mutex; its button reads may then take device mutexes. polling
|
// hotkeys_mutex; its button reads may then take device mutexes. polling
|
||||||
// every mapped-input tick is intentional so HotkeyToggle releases cannot
|
// every mapped-input tick is intentional so HotkeyToggle releases cannot
|
||||||
// be missed when the render thread stalls.
|
// be missed when the render thread stalls.
|
||||||
const bool gate_active = overlay::global_hotkeys_triggered();
|
const bool gate_active = overlay::global_hotkeys_triggered();
|
||||||
const bool screenshot_current = screenshot_down && gate_active;
|
const bool screenshot_current = screenshot_down && gate_active;
|
||||||
super_exit_current = super_exit_down && gate_active;
|
super_exit_current = super_exit_down && gate_active;
|
||||||
|
|
||||||
if (rising_edge(screenshot_current, screenshot_previous)) {
|
if (rising_edge(screenshot_current, screenshot_previous)) {
|
||||||
SCREENSHOT_PENDING.store(true, std::memory_order_relaxed);
|
SCREENSHOT_PENDING.store(true, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
coin_edge = rising_edge(coin_current, coin_previous);
|
coin_edge = rising_edge(coin_current, coin_previous);
|
||||||
} else {
|
} else {
|
||||||
screenshot_previous = false;
|
screenshot_previous = false;
|
||||||
coin_previous = false;
|
coin_previous = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
alt_f4_current = read_alt_f4();
|
alt_f4_current = read_alt_f4();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coin_edge) {
|
if (coin_edge) {
|
||||||
eamuse_coin_insert();
|
eamuse_coin_insert();
|
||||||
}
|
}
|
||||||
|
|
||||||
// pass held state so returning focus can exit without another key press
|
// pass held state so returning focus can exit without another key press
|
||||||
superexit::handle_hotkeys(alt_f4_current, super_exit_current);
|
superexit::handle_hotkeys(alt_f4_current, super_exit_current);
|
||||||
|
|
||||||
std::this_thread::sleep_for(MIN_SAMPLE_INTERVAL);
|
std::this_thread::sleep_for(MIN_SAMPLE_INTERVAL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
if (WORKER.joinable()) {
|
if (WORKER.joinable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
WORKER = std::jthread(run);
|
WORKER = std::jthread(run);
|
||||||
log_info("hotkeys", "sampler started");
|
log_info("hotkeys", "sampler started");
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable_raw_input() {
|
void enable_raw_input() {
|
||||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
RAW_INPUT_ENABLED = true;
|
RAW_INPUT_ENABLED = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void enable_input() {
|
void enable_input() {
|
||||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
INPUT_ENABLED = true;
|
INPUT_ENABLED = true;
|
||||||
log_info("hotkeys", "configured input enabled");
|
log_info("hotkeys", "configured input enabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
void disable_input() {
|
void disable_input() {
|
||||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
INPUT_ENABLED = false;
|
INPUT_ENABLED = false;
|
||||||
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
SCREENSHOT_PENDING.store(false, std::memory_order_relaxed);
|
||||||
log_info("hotkeys", "configured input disabled");
|
log_info("hotkeys", "configured input disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
void disable_raw_input() {
|
void disable_raw_input() {
|
||||||
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
std::lock_guard<std::mutex> lock(INPUT_MUTEX);
|
||||||
RAW_INPUT_ENABLED = false;
|
RAW_INPUT_ENABLED = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stop() {
|
void stop() {
|
||||||
if (!WORKER.joinable()) {
|
if (!WORKER.joinable()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WORKER.request_stop();
|
WORKER.request_stop();
|
||||||
WORKER.join();
|
WORKER.join();
|
||||||
log_info("hotkeys", "sampler stopped");
|
log_info("hotkeys", "sampler stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool consume_screenshot() {
|
bool consume_screenshot() {
|
||||||
return SCREENSHOT_PENDING.exchange(false, std::memory_order_relaxed);
|
return SCREENSHOT_PENDING.exchange(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -1,13 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace hotkeys {
|
namespace hotkeys {
|
||||||
|
|
||||||
// ALT+F4 monitoring spans boot and teardown; configured actions are enabled separately
|
// ALT+F4 monitoring spans boot and teardown; configured actions are enabled separately
|
||||||
void start();
|
void start();
|
||||||
void enable_raw_input();
|
void enable_raw_input();
|
||||||
void enable_input();
|
void enable_input();
|
||||||
void disable_input();
|
void disable_input();
|
||||||
void disable_raw_input();
|
void disable_raw_input();
|
||||||
void stop();
|
void stop();
|
||||||
bool consume_screenshot();
|
bool consume_screenshot();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+726
-726
File diff suppressed because it is too large
Load Diff
+293
-293
@@ -1,293 +1,293 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#ifndef SPICE_SDK_H
|
#ifndef SPICE_SDK_H
|
||||||
#define SPICE_SDK_H
|
#define SPICE_SDK_H
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
|
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
|
||||||
#else
|
#else
|
||||||
#define SPICE_SDK_ENTRY_POINT __declspec(dllexport) int __cdecl
|
#define SPICE_SDK_ENTRY_POINT __declspec(dllexport) int __cdecl
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
typedef enum SPICE_SDK_STATUS_CODE {
|
typedef enum SPICE_SDK_STATUS_CODE {
|
||||||
SPICE_SDK_STATUS_SUCCESS = 0,
|
SPICE_SDK_STATUS_SUCCESS = 0,
|
||||||
|
|
||||||
// 000: generic
|
// 000: generic
|
||||||
SPICE_SDK_STATUS_GENERIC_ERROR = 1,
|
SPICE_SDK_STATUS_GENERIC_ERROR = 1,
|
||||||
SPICE_SDK_STATUS_NOT_INITIALIZED = 2,
|
SPICE_SDK_STATUS_NOT_INITIALIZED = 2,
|
||||||
SPICE_SDK_STATUS_NOT_SUPPORTED = 3,
|
SPICE_SDK_STATUS_NOT_SUPPORTED = 3,
|
||||||
SPICE_SDK_STATUS_TOO_SMALL = 4,
|
SPICE_SDK_STATUS_TOO_SMALL = 4,
|
||||||
SPICE_SDK_STATUS_TOO_LATE = 5,
|
SPICE_SDK_STATUS_TOO_LATE = 5,
|
||||||
|
|
||||||
// 1000: invalid args
|
// 1000: invalid args
|
||||||
SPICE_SDK_STATUS_INVALID_ARGUMENT_1 = 1001,
|
SPICE_SDK_STATUS_INVALID_ARGUMENT_1 = 1001,
|
||||||
SPICE_SDK_STATUS_INVALID_ARGUMENT_2 = 1002,
|
SPICE_SDK_STATUS_INVALID_ARGUMENT_2 = 1002,
|
||||||
SPICE_SDK_STATUS_INVALID_ARGUMENT_3 = 1003,
|
SPICE_SDK_STATUS_INVALID_ARGUMENT_3 = 1003,
|
||||||
SPICE_SDK_STATUS_INVALID_ARGUMENT_4 = 1004,
|
SPICE_SDK_STATUS_INVALID_ARGUMENT_4 = 1004,
|
||||||
SPICE_SDK_STATUS_INVALID_ARGUMENT_5 = 1005,
|
SPICE_SDK_STATUS_INVALID_ARGUMENT_5 = 1005,
|
||||||
|
|
||||||
} SPICE_SDK_STATUS_CODE;
|
} SPICE_SDK_STATUS_CODE;
|
||||||
|
|
||||||
typedef enum SPICE_SDK_LOG_LEVEL {
|
typedef enum SPICE_SDK_LOG_LEVEL {
|
||||||
SPICE_SDK_LOG_LEVEL_MISC = 0,
|
SPICE_SDK_LOG_LEVEL_MISC = 0,
|
||||||
SPICE_SDK_LOG_LEVEL_INFO = 1,
|
SPICE_SDK_LOG_LEVEL_INFO = 1,
|
||||||
SPICE_SDK_LOG_LEVEL_WARNING = 2,
|
SPICE_SDK_LOG_LEVEL_WARNING = 2,
|
||||||
SPICE_SDK_LOG_LEVEL_FATAL = 3,
|
SPICE_SDK_LOG_LEVEL_FATAL = 3,
|
||||||
} SPICE_SDK_LOG_LEVEL;
|
} SPICE_SDK_LOG_LEVEL;
|
||||||
|
|
||||||
typedef enum SPICE_SDK_TOAST_SEVERITY {
|
typedef enum SPICE_SDK_TOAST_SEVERITY {
|
||||||
SPICE_SDK_TOAST_LEVEL_INFO = 0,
|
SPICE_SDK_TOAST_LEVEL_INFO = 0,
|
||||||
SPICE_SDK_TOAST_LEVEL_SUCCESS = 1,
|
SPICE_SDK_TOAST_LEVEL_SUCCESS = 1,
|
||||||
SPICE_SDK_TOAST_LEVEL_WARNING = 2,
|
SPICE_SDK_TOAST_LEVEL_WARNING = 2,
|
||||||
SPICE_SDK_TOAST_LEVEL_ERROR = 3,
|
SPICE_SDK_TOAST_LEVEL_ERROR = 3,
|
||||||
} SPICE_SDK_TOAST_SEVERITY;
|
} SPICE_SDK_TOAST_SEVERITY;
|
||||||
|
|
||||||
typedef struct SPICE_SDK_TOUCH_POINT {
|
typedef struct SPICE_SDK_TOUCH_POINT {
|
||||||
uint32_t id;
|
uint32_t id;
|
||||||
int x;
|
int x;
|
||||||
int y;
|
int y;
|
||||||
} SPICE_SDK_TOUCH_POINT;
|
} SPICE_SDK_TOUCH_POINT;
|
||||||
|
|
||||||
typedef struct SPICE_SDK_GAME_INFO {
|
typedef struct SPICE_SDK_GAME_INFO {
|
||||||
char name[64]; // null-terminated
|
char name[64]; // null-terminated
|
||||||
} SPICE_SDK_GAME_INFO;
|
} SPICE_SDK_GAME_INFO;
|
||||||
|
|
||||||
typedef struct SPICE_SDK_AVS_INFO {
|
typedef struct SPICE_SDK_AVS_INFO {
|
||||||
char model[4]; // "MDX", null-terminated
|
char model[4]; // "MDX", null-terminated
|
||||||
char dest; // J
|
char dest; // J
|
||||||
char spec; // A
|
char spec; // A
|
||||||
char rev; // A
|
char rev; // A
|
||||||
char ext[11]; // "2025061002", null-terminated
|
char ext[11]; // "2025061002", null-terminated
|
||||||
} SPICE_SDK_AVS_INFO;
|
} SPICE_SDK_AVS_INFO;
|
||||||
|
|
||||||
// get_game_info (v0.1 and up)
|
// get_game_info (v0.1 and up)
|
||||||
//
|
//
|
||||||
// get info about the currently running game
|
// get info about the currently running game
|
||||||
//
|
//
|
||||||
// info: receives game info
|
// info: receives game info
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_game_info_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_game_info_func)(
|
||||||
SPICE_SDK_GAME_INFO *info
|
SPICE_SDK_GAME_INFO *info
|
||||||
);
|
);
|
||||||
|
|
||||||
// get_avs_info (v0.1 and up)
|
// get_avs_info (v0.1 and up)
|
||||||
//
|
//
|
||||||
// get AVS info (model, dest, spec, rev, ext)
|
// get AVS info (model, dest, spec, rev, ext)
|
||||||
//
|
//
|
||||||
// info: receives AVS info
|
// info: receives AVS info
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_avs_info_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_avs_info_func)(
|
||||||
SPICE_SDK_AVS_INFO *info
|
SPICE_SDK_AVS_INFO *info
|
||||||
);
|
);
|
||||||
|
|
||||||
// log (v0.1 and up)
|
// log (v0.1 and up)
|
||||||
// logs a message to the log
|
// logs a message to the log
|
||||||
// writing a FATAL message will terminate spice, only use in catastrophic failure
|
// writing a FATAL message will terminate spice, only use in catastrophic failure
|
||||||
//
|
//
|
||||||
// level: see log level enum
|
// level: see log level enum
|
||||||
// module: short string that identifies the facility / module / submodule
|
// module: short string that identifies the facility / module / submodule
|
||||||
// message: the message to log
|
// message: the message to log
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_log_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_log_func)(
|
||||||
SPICE_SDK_LOG_LEVEL level,
|
SPICE_SDK_LOG_LEVEL level,
|
||||||
const char *module,
|
const char *module,
|
||||||
const char *message
|
const char *message
|
||||||
);
|
);
|
||||||
|
|
||||||
// get_button (v0.1 and up)
|
// get_button (v0.1 and up)
|
||||||
// gets the button state
|
// gets the button state
|
||||||
//
|
//
|
||||||
// button_id: ID of the button; see spicesdk_io.h for named values
|
// button_id: ID of the button; see spicesdk_io.h for named values
|
||||||
// pressed: (optional) is the button pressed?
|
// pressed: (optional) is the button pressed?
|
||||||
// velocity: (optional) MIDI velocity of the button
|
// velocity: (optional) MIDI velocity of the button
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_button_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_button_func)(
|
||||||
uint32_t button_id,
|
uint32_t button_id,
|
||||||
bool *pressed,
|
bool *pressed,
|
||||||
float *velocity
|
float *velocity
|
||||||
);
|
);
|
||||||
|
|
||||||
// set_button (v0.1 and up)
|
// set_button (v0.1 and up)
|
||||||
// sets or clears the button override
|
// sets or clears the button override
|
||||||
//
|
//
|
||||||
// make sure to hold the button long enough for the game's I/O engine to pick up
|
// make sure to hold the button long enough for the game's I/O engine to pick up
|
||||||
// usually, one or two frames
|
// usually, one or two frames
|
||||||
//
|
//
|
||||||
// button_id: ID of the button; see spicesdk_io.h for named values
|
// button_id: ID of the button; see spicesdk_io.h for named values
|
||||||
// pressed: true to set the button override (permanently set the button to be ON until cleared),
|
// pressed: true to set the button override (permanently set the button to be ON until cleared),
|
||||||
// false to clear the override (allow user's controller to provide input again)
|
// false to clear the override (allow user's controller to provide input again)
|
||||||
// velocity: MIDI velocity of the button; only valid when pressed is true
|
// velocity: MIDI velocity of the button; only valid when pressed is true
|
||||||
// can be between 0.0 and 1.0, inclusive
|
// can be between 0.0 and 1.0, inclusive
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_button_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_button_func)(
|
||||||
uint32_t button_id,
|
uint32_t button_id,
|
||||||
bool pressed,
|
bool pressed,
|
||||||
float velocity
|
float velocity
|
||||||
);
|
);
|
||||||
|
|
||||||
// get_analog (v0.1 and up)
|
// get_analog (v0.1 and up)
|
||||||
// gets the analog state
|
// gets the analog state
|
||||||
//
|
//
|
||||||
// button_id: ID of the button; see spicesdk_io.h for named values
|
// button_id: ID of the button; see spicesdk_io.h for named values
|
||||||
// value: receives the state of the analog
|
// value: receives the state of the analog
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_analog_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_analog_func)(
|
||||||
uint32_t analog_id,
|
uint32_t analog_id,
|
||||||
float *value
|
float *value
|
||||||
);
|
);
|
||||||
|
|
||||||
// set_analog (v0.1 and up)
|
// set_analog (v0.1 and up)
|
||||||
// sets or clears the analog override
|
// sets or clears the analog override
|
||||||
//
|
//
|
||||||
// analog_id: ID of the analog; see spicesdk_io.h for named values
|
// analog_id: ID of the analog; see spicesdk_io.h for named values
|
||||||
// override_active: true to set override (gain exclusive control)
|
// override_active: true to set override (gain exclusive control)
|
||||||
// false to clear it (allow user's controller provide input again)
|
// false to clear it (allow user's controller provide input again)
|
||||||
// value: value of the analog; only valid when override_active is true
|
// value: value of the analog; only valid when override_active is true
|
||||||
// can be between 0.0 and 1.0, inclusive
|
// can be between 0.0 and 1.0, inclusive
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_analog_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_analog_func)(
|
||||||
uint32_t analog_id,
|
uint32_t analog_id,
|
||||||
bool override_active,
|
bool override_active,
|
||||||
float value
|
float value
|
||||||
);
|
);
|
||||||
|
|
||||||
// get_light (v0.1 and up)
|
// get_light (v0.1 and up)
|
||||||
// gets the last observed value of a light
|
// gets the last observed value of a light
|
||||||
//
|
//
|
||||||
// light_id: ID of the light; see spicesdk_io.h for named values
|
// light_id: ID of the light; see spicesdk_io.h for named values
|
||||||
// value: output parameter for the light value; 0.0 to 1.0
|
// value: output parameter for the light value; 0.0 to 1.0
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_light_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_light_func)(
|
||||||
uint32_t light_id,
|
uint32_t light_id,
|
||||||
float *value
|
float *value
|
||||||
);
|
);
|
||||||
|
|
||||||
// set_light (v0.1 and up)
|
// set_light (v0.1 and up)
|
||||||
// sets or clears the light override
|
// sets or clears the light override
|
||||||
//
|
//
|
||||||
// light_id: ID of the light; see spicesdk_io.h for named values
|
// light_id: ID of the light; see spicesdk_io.h for named values
|
||||||
// light_value: output parameter for the light value; 0.0 to 1.0
|
// light_value: output parameter for the light value; 0.0 to 1.0
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_light_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_light_func)(
|
||||||
uint32_t light_id,
|
uint32_t light_id,
|
||||||
bool override_active,
|
bool override_active,
|
||||||
float light_value
|
float light_value
|
||||||
);
|
);
|
||||||
|
|
||||||
// set_touch (v0.1 and up)
|
// set_touch (v0.1 and up)
|
||||||
// adds or updates touch points
|
// adds or updates touch points
|
||||||
//
|
//
|
||||||
// points: array of touch points to add or update
|
// points: array of touch points to add or update
|
||||||
// count: number of touch points in the array
|
// count: number of touch points in the array
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_touch_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_touch_func)(
|
||||||
const SPICE_SDK_TOUCH_POINT *points,
|
const SPICE_SDK_TOUCH_POINT *points,
|
||||||
uint32_t count
|
uint32_t count
|
||||||
);
|
);
|
||||||
|
|
||||||
// clear_touch (v0.1 and up)
|
// clear_touch (v0.1 and up)
|
||||||
// clears touch points (i.e., no longer being touched)
|
// clears touch points (i.e., no longer being touched)
|
||||||
//
|
//
|
||||||
// ids: array of touch point IDs to clear
|
// ids: array of touch point IDs to clear
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_clear_touch_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_clear_touch_func)(
|
||||||
const uint32_t *ids,
|
const uint32_t *ids,
|
||||||
uint32_t count
|
uint32_t count
|
||||||
);
|
);
|
||||||
|
|
||||||
// insert_card (v0.1 and up)
|
// insert_card (v0.1 and up)
|
||||||
// simulates inserting an e-amuse card with the given ID
|
// simulates inserting an e-amuse card with the given ID
|
||||||
//
|
//
|
||||||
// unit: 0 for player 1, 1 for player 2
|
// unit: 0 for player 1, 1 for player 2
|
||||||
// card_id: null-terminated string of the card ID
|
// card_id: null-terminated string of the card ID
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_card_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_card_func)(
|
||||||
uint8_t unit,
|
uint8_t unit,
|
||||||
const char *card_id
|
const char *card_id
|
||||||
);
|
);
|
||||||
|
|
||||||
// set_keypad (v0.1 and up)
|
// set_keypad (v0.1 and up)
|
||||||
// sets keypad state
|
// sets keypad state
|
||||||
//
|
//
|
||||||
// make sure to hold the button long enough for the game to pick up
|
// make sure to hold the button long enough for the game to pick up
|
||||||
// 70ms is usually sufficient, except for DDR which needs 150ms
|
// 70ms is usually sufficient, except for DDR which needs 150ms
|
||||||
//
|
//
|
||||||
// unit: 0 for player 1, 1 for player 2
|
// unit: 0 for player 1, 1 for player 2
|
||||||
// key: '0' to '9' for numbers, 'A' for 00, 'D' for decimal point, 0 or '\0' to release all keys
|
// key: '0' to '9' for numbers, 'A' for 00, 'D' for decimal point, 0 or '\0' to release all keys
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_keypad_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_keypad_func)(
|
||||||
uint8_t unit,
|
uint8_t unit,
|
||||||
char key
|
char key
|
||||||
);
|
);
|
||||||
|
|
||||||
// add_toast (v0.2 and up)
|
// add_toast (v0.2 and up)
|
||||||
// adds an overlay toast notification
|
// adds an overlay toast notification
|
||||||
//
|
//
|
||||||
// severity: see SPICE_SDK_TOAST_SEVERITY; controls the accent color (purely cosmetic)
|
// severity: see SPICE_SDK_TOAST_SEVERITY; controls the accent color (purely cosmetic)
|
||||||
// text: null-terminated UTF-8 message to display; wraps inside the toast
|
// text: null-terminated UTF-8 message to display; wraps inside the toast
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
|
||||||
SPICE_SDK_TOAST_SEVERITY severity,
|
SPICE_SDK_TOAST_SEVERITY severity,
|
||||||
const char *text
|
const char *text
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef struct SPICE_SDK_V0 {
|
typedef struct SPICE_SDK_V0 {
|
||||||
uint32_t size;
|
uint32_t size;
|
||||||
|
|
||||||
spice_sdk_log_func *log;
|
spice_sdk_log_func *log;
|
||||||
|
|
||||||
spice_sdk_get_game_info_func *get_game_info;
|
spice_sdk_get_game_info_func *get_game_info;
|
||||||
spice_sdk_get_avs_info_func *get_avs_info;
|
spice_sdk_get_avs_info_func *get_avs_info;
|
||||||
|
|
||||||
spice_sdk_get_button_func *get_button;
|
spice_sdk_get_button_func *get_button;
|
||||||
spice_sdk_set_button_func *set_button;
|
spice_sdk_set_button_func *set_button;
|
||||||
|
|
||||||
spice_sdk_get_analog_func *get_analog;
|
spice_sdk_get_analog_func *get_analog;
|
||||||
spice_sdk_set_analog_func *set_analog;
|
spice_sdk_set_analog_func *set_analog;
|
||||||
|
|
||||||
spice_sdk_get_light_func *get_light;
|
spice_sdk_get_light_func *get_light;
|
||||||
spice_sdk_set_light_func *set_light;
|
spice_sdk_set_light_func *set_light;
|
||||||
|
|
||||||
spice_sdk_set_touch_func *set_touch;
|
spice_sdk_set_touch_func *set_touch;
|
||||||
spice_sdk_clear_touch_func *clear_touch;
|
spice_sdk_clear_touch_func *clear_touch;
|
||||||
|
|
||||||
spice_sdk_insert_card_func *insert_card;
|
spice_sdk_insert_card_func *insert_card;
|
||||||
spice_sdk_set_keypad_func *set_keypad;
|
spice_sdk_set_keypad_func *set_keypad;
|
||||||
|
|
||||||
spice_sdk_add_toast_func *add_toast;
|
spice_sdk_add_toast_func *add_toast;
|
||||||
|
|
||||||
} SPICE_SDK_V0;
|
} SPICE_SDK_V0;
|
||||||
|
|
||||||
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
||||||
void
|
void
|
||||||
);
|
);
|
||||||
|
|
||||||
// init (v0.1 and up)
|
// init (v0.1 and up)
|
||||||
//
|
//
|
||||||
// version: supply 0
|
// version: supply 0
|
||||||
// destroy_callback: supply a function pointer that will be called when spice
|
// destroy_callback: supply a function pointer that will be called when spice
|
||||||
// is shutting down
|
// is shutting down
|
||||||
// sdk_functions: supply a pointer to SPICE_SDK_V0; ensure size field is initialized
|
// sdk_functions: supply a pointer to SPICE_SDK_V0; ensure size field is initialized
|
||||||
// to sizeof(SPICE_SDK_V0) before calling this function
|
// to sizeof(SPICE_SDK_V0) before calling this function
|
||||||
|
|
||||||
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_init_func)(
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_init_func)(
|
||||||
uint32_t version,
|
uint32_t version,
|
||||||
spice_sdk_destroy_callback_func *destroy_callback,
|
spice_sdk_destroy_callback_func *destroy_callback,
|
||||||
void *sdk_functions
|
void *sdk_functions
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef int (__cdecl spice_sdk_entry_point_func)(
|
typedef int (__cdecl spice_sdk_entry_point_func)(
|
||||||
spice_sdk_init_func *init
|
spice_sdk_init_func *init
|
||||||
);
|
);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif // SPICE_SDK_H
|
#endif // SPICE_SDK_H
|
||||||
|
|||||||
+1870
-1870
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
LIBRARY sdk_sample_v0_cpp
|
LIBRARY sdk_sample_v0_cpp
|
||||||
|
|
||||||
EXPORTS
|
EXPORTS
|
||||||
spice_sdk_entry_point
|
spice_sdk_entry_point
|
||||||
@@ -1,318 +1,318 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <process.h>
|
#include <process.h>
|
||||||
|
|
||||||
#include "sdk/include/spicesdk.h"
|
#include "sdk/include/spicesdk.h"
|
||||||
#include "sdk/include/spicesdk_io.h"
|
#include "sdk/include/spicesdk_io.h"
|
||||||
|
|
||||||
static SPICE_SDK_V0 spice;
|
static SPICE_SDK_V0 spice;
|
||||||
|
|
||||||
static spice_sdk_destroy_callback_func destroy_callback;
|
static spice_sdk_destroy_callback_func destroy_callback;
|
||||||
|
|
||||||
static void test_logging();
|
static void test_logging();
|
||||||
static void test_game_info();
|
static void test_game_info();
|
||||||
static void test_avs_info();
|
static void test_avs_info();
|
||||||
|
|
||||||
static void get_buttons();
|
static void get_buttons();
|
||||||
static void set_buttons();
|
static void set_buttons();
|
||||||
static void clear_buttons();
|
static void clear_buttons();
|
||||||
|
|
||||||
static void get_analogs();
|
static void get_analogs();
|
||||||
static void set_analogs();
|
static void set_analogs();
|
||||||
static void clear_analogs();
|
static void clear_analogs();
|
||||||
|
|
||||||
static void get_lights();
|
static void get_lights();
|
||||||
static void set_lights();
|
static void set_lights();
|
||||||
static void clear_lights();
|
static void clear_lights();
|
||||||
|
|
||||||
static void set_touch();
|
static void set_touch();
|
||||||
static void clear_touch();
|
static void clear_touch();
|
||||||
static void insert_card();
|
static void insert_card();
|
||||||
static void set_keypad();
|
static void set_keypad();
|
||||||
static void clear_keypad();
|
static void clear_keypad();
|
||||||
|
|
||||||
static HANDLE worker_stop_event;
|
static HANDLE worker_stop_event;
|
||||||
static HANDLE worker_handle;
|
static HANDLE worker_handle;
|
||||||
static unsigned __stdcall worker_thread(void *arg);
|
static unsigned __stdcall worker_thread(void *arg);
|
||||||
|
|
||||||
// this sample assumes that the game is IIDX, but it doesn't check for it.
|
// this sample assumes that the game is IIDX, but it doesn't check for it.
|
||||||
|
|
||||||
SPICE_SDK_ENTRY_POINT
|
SPICE_SDK_ENTRY_POINT
|
||||||
spice_sdk_entry_point(
|
spice_sdk_entry_point(
|
||||||
spice_sdk_init_func *init
|
spice_sdk_init_func *init
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
SPICE_SDK_STATUS_CODE status;
|
SPICE_SDK_STATUS_CODE status;
|
||||||
|
|
||||||
memset(&spice, 0, sizeof(spice));
|
memset(&spice, 0, sizeof(spice));
|
||||||
spice.size = sizeof(spice);
|
spice.size = sizeof(spice);
|
||||||
status = init(0, destroy_callback, &spice);
|
status = init(0, destroy_callback, &spice);
|
||||||
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin loaded");
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin loaded");
|
||||||
test_logging();
|
test_logging();
|
||||||
test_game_info();
|
test_game_info();
|
||||||
test_avs_info();
|
test_avs_info();
|
||||||
|
|
||||||
worker_stop_event = CreateEventA(NULL, TRUE, FALSE, NULL);
|
worker_stop_event = CreateEventA(NULL, TRUE, FALSE, NULL);
|
||||||
if (!worker_stop_event) {
|
if (!worker_stop_event) {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker stop event");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker stop event");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
worker_handle = (HANDLE)_beginthreadex(
|
worker_handle = (HANDLE)_beginthreadex(
|
||||||
NULL, // security
|
NULL, // security
|
||||||
0, // stack size
|
0, // stack size
|
||||||
worker_thread, // function
|
worker_thread, // function
|
||||||
NULL, // argument
|
NULL, // argument
|
||||||
0, // flags
|
0, // flags
|
||||||
NULL // thread id
|
NULL // thread id
|
||||||
);
|
);
|
||||||
if (!worker_handle) {
|
if (!worker_handle) {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker thread");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "failed to create worker thread");
|
||||||
CloseHandle(worker_stop_event);
|
CloseHandle(worker_stop_event);
|
||||||
worker_stop_event = NULL;
|
worker_stop_event = NULL;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
__cdecl
|
__cdecl
|
||||||
destroy_callback(
|
destroy_callback(
|
||||||
void
|
void
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin unloaded");
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "plugin unloaded");
|
||||||
if (worker_stop_event) {
|
if (worker_stop_event) {
|
||||||
SetEvent(worker_stop_event);
|
SetEvent(worker_stop_event);
|
||||||
}
|
}
|
||||||
if (worker_handle) {
|
if (worker_handle) {
|
||||||
WaitForSingleObject(worker_handle, INFINITE);
|
WaitForSingleObject(worker_handle, INFINITE);
|
||||||
CloseHandle(worker_handle);
|
CloseHandle(worker_handle);
|
||||||
worker_handle = NULL;
|
worker_handle = NULL;
|
||||||
}
|
}
|
||||||
if (worker_stop_event) {
|
if (worker_stop_event) {
|
||||||
CloseHandle(worker_stop_event);
|
CloseHandle(worker_stop_event);
|
||||||
worker_stop_event = NULL;
|
worker_stop_event = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static unsigned __stdcall worker_thread(void *arg) {
|
static unsigned __stdcall worker_thread(void *arg) {
|
||||||
int phase = 0;
|
int phase = 0;
|
||||||
while (WaitForSingleObject(worker_stop_event, 0) == WAIT_TIMEOUT) {
|
while (WaitForSingleObject(worker_stop_event, 0) == WAIT_TIMEOUT) {
|
||||||
phase += 1;
|
phase += 1;
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case 1:
|
case 1:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get buttons...");
|
||||||
get_buttons();
|
get_buttons();
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set buttons...");
|
||||||
set_buttons();
|
set_buttons();
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear buttons...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear buttons...");
|
||||||
clear_buttons();
|
clear_buttons();
|
||||||
break;
|
break;
|
||||||
case 4:
|
case 4:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get analogs...");
|
||||||
get_analogs();
|
get_analogs();
|
||||||
break;
|
break;
|
||||||
case 5:
|
case 5:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set analogs...");
|
||||||
set_analogs();
|
set_analogs();
|
||||||
break;
|
break;
|
||||||
case 6:
|
case 6:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear analogs...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear analogs...");
|
||||||
clear_analogs();
|
clear_analogs();
|
||||||
break;
|
break;
|
||||||
case 7:
|
case 7:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "get lights...");
|
||||||
get_lights();
|
get_lights();
|
||||||
break;
|
break;
|
||||||
case 8:
|
case 8:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_WARNING, "set lights...");
|
||||||
set_lights();
|
set_lights();
|
||||||
break;
|
break;
|
||||||
case 9:
|
case 9:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear lights...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear lights...");
|
||||||
clear_lights();
|
clear_lights();
|
||||||
break;
|
break;
|
||||||
case 10:
|
case 10:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "set touch...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_INFO, "set touch...");
|
||||||
set_touch();
|
set_touch();
|
||||||
break;
|
break;
|
||||||
case 11:
|
case 11:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear touch...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear touch...");
|
||||||
clear_touch();
|
clear_touch();
|
||||||
break;
|
break;
|
||||||
case 12:
|
case 12:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "insert card...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "insert card...");
|
||||||
insert_card();
|
insert_card();
|
||||||
break;
|
break;
|
||||||
case 13:
|
case 13:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "set keypad...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, "set keypad...");
|
||||||
set_keypad();
|
set_keypad();
|
||||||
break;
|
break;
|
||||||
case 14:
|
case 14:
|
||||||
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear keypad...");
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_ERROR, "clear keypad...");
|
||||||
clear_keypad();
|
clear_keypad();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
phase = 0;
|
phase = 0;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (phase != 0) {
|
if (phase != 0) {
|
||||||
WaitForSingleObject(worker_stop_event, 3000);
|
WaitForSingleObject(worker_stop_event, 3000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_logging() {
|
static void test_logging() {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_MISC, "sample_v0", "this is a misc message");
|
spice.log(SPICE_SDK_LOG_LEVEL_MISC, "sample_v0", "this is a misc message");
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "this is an info message");
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", "this is an info message");
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "this is a warning message");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "this is a warning message");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_game_info() {
|
static void test_game_info() {
|
||||||
SPICE_SDK_GAME_INFO info;
|
SPICE_SDK_GAME_INFO info;
|
||||||
SPICE_SDK_STATUS_CODE status;
|
SPICE_SDK_STATUS_CODE status;
|
||||||
|
|
||||||
status = spice.get_game_info(&info);
|
status = spice.get_game_info(&info);
|
||||||
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_game_info failed");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_game_info failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
char log_message[128];
|
char log_message[128];
|
||||||
snprintf(log_message, sizeof(log_message), "game info - name: %s", info.name);
|
snprintf(log_message, sizeof(log_message), "game info - name: %s", info.name);
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_avs_info() {
|
static void test_avs_info() {
|
||||||
SPICE_SDK_AVS_INFO info;
|
SPICE_SDK_AVS_INFO info;
|
||||||
SPICE_SDK_STATUS_CODE status;
|
SPICE_SDK_STATUS_CODE status;
|
||||||
|
|
||||||
status = spice.get_avs_info(&info);
|
status = spice.get_avs_info(&info);
|
||||||
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_avs_info failed");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_avs_info failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char log_message[128];
|
char log_message[128];
|
||||||
snprintf(
|
snprintf(
|
||||||
log_message,
|
log_message,
|
||||||
sizeof(log_message),
|
sizeof(log_message),
|
||||||
"avs - model: %s, dest: %c, spec: %c, rev: %c, ext: %s",
|
"avs - model: %s, dest: %c, spec: %c, rev: %c, ext: %s",
|
||||||
info.model, info.dest, info.spec, info.rev, info.ext);
|
info.model, info.dest, info.spec, info.rev, info.ext);
|
||||||
|
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void get_buttons() {
|
static void get_buttons() {
|
||||||
bool pressed;
|
bool pressed;
|
||||||
float velocity;
|
float velocity;
|
||||||
spice.get_button(IIDX_Button_P1_Headphone, &pressed, &velocity);
|
spice.get_button(IIDX_Button_P1_Headphone, &pressed, &velocity);
|
||||||
|
|
||||||
char log_message[128];
|
char log_message[128];
|
||||||
snprintf(
|
snprintf(
|
||||||
log_message,
|
log_message,
|
||||||
sizeof(log_message),
|
sizeof(log_message),
|
||||||
"button P1_Headphone pressed: %s, velocity: %.2f",
|
"button P1_Headphone pressed: %s, velocity: %.2f",
|
||||||
pressed ? "ON" : "off", velocity);
|
pressed ? "ON" : "off", velocity);
|
||||||
|
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_buttons() {
|
static void set_buttons() {
|
||||||
spice.set_button(IIDX_Button_P1_1, true, 0.3f);
|
spice.set_button(IIDX_Button_P1_1, true, 0.3f);
|
||||||
spice.set_button(IIDX_Button_P1_3, true, 0.5f);
|
spice.set_button(IIDX_Button_P1_3, true, 0.5f);
|
||||||
spice.set_button(IIDX_Button_P1_5, true, 0.7f);
|
spice.set_button(IIDX_Button_P1_5, true, 0.7f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_buttons() {
|
static void clear_buttons() {
|
||||||
spice.set_button(IIDX_Button_P1_1, false, 0.f);
|
spice.set_button(IIDX_Button_P1_1, false, 0.f);
|
||||||
spice.set_button(IIDX_Button_P1_3, false, 0.f);
|
spice.set_button(IIDX_Button_P1_3, false, 0.f);
|
||||||
spice.set_button(IIDX_Button_P1_5, false, 0.f);
|
spice.set_button(IIDX_Button_P1_5, false, 0.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void get_analogs() {
|
static void get_analogs() {
|
||||||
float value;
|
float value;
|
||||||
spice.get_analog(IIDX_Analog_TT_P1, &value);
|
spice.get_analog(IIDX_Analog_TT_P1, &value);
|
||||||
|
|
||||||
char log_message[128];
|
char log_message[128];
|
||||||
snprintf(
|
snprintf(
|
||||||
log_message,
|
log_message,
|
||||||
sizeof(log_message),
|
sizeof(log_message),
|
||||||
"analog TT_P1: %.2f",
|
"analog TT_P1: %.2f",
|
||||||
value);
|
value);
|
||||||
|
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_analogs() {
|
static void set_analogs() {
|
||||||
spice.set_analog(IIDX_Analog_TT_P1, true, 0.25f);
|
spice.set_analog(IIDX_Analog_TT_P1, true, 0.25f);
|
||||||
spice.set_analog(IIDX_Analog_TT_P2, true, 0.75f);
|
spice.set_analog(IIDX_Analog_TT_P2, true, 0.75f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_analogs() {
|
static void clear_analogs() {
|
||||||
spice.set_analog(IIDX_Analog_TT_P1, false, 0.f);
|
spice.set_analog(IIDX_Analog_TT_P1, false, 0.f);
|
||||||
spice.set_analog(IIDX_Analog_TT_P2, false, 0.f);
|
spice.set_analog(IIDX_Analog_TT_P2, false, 0.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void get_lights() {
|
static void get_lights() {
|
||||||
float value;
|
float value;
|
||||||
SPICE_SDK_STATUS_CODE status;
|
SPICE_SDK_STATUS_CODE status;
|
||||||
|
|
||||||
status = spice.get_light(IIDX_Light_TT_P1_Resistance, &value);
|
status = spice.get_light(IIDX_Light_TT_P1_Resistance, &value);
|
||||||
if (status == SPICE_SDK_STATUS_SUCCESS) {
|
if (status == SPICE_SDK_STATUS_SUCCESS) {
|
||||||
char log_message[64];
|
char log_message[64];
|
||||||
snprintf(log_message, sizeof(log_message), "P1 TT resistance value: %.2f", value);
|
snprintf(log_message, sizeof(log_message), "P1 TT resistance value: %.2f", value);
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_INFO, "sample_v0", log_message);
|
||||||
} else {
|
} else {
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_light failed");
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", "get_light failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_lights() {
|
static void set_lights() {
|
||||||
spice.set_light(IIDX_Light_P1_Start, true, 1.f);
|
spice.set_light(IIDX_Light_P1_Start, true, 1.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_lights() {
|
static void clear_lights() {
|
||||||
spice.set_light(IIDX_Light_P1_Start, false, 0.f);
|
spice.set_light(IIDX_Light_P1_Start, false, 0.f);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_touch() {
|
static void set_touch() {
|
||||||
SPICE_SDK_TOUCH_POINT points[2] = {
|
SPICE_SDK_TOUCH_POINT points[2] = {
|
||||||
{ .id = 1, .x = 100, .y = 200 },
|
{ .id = 1, .x = 100, .y = 200 },
|
||||||
{ .id = 2, .x = 300, .y = 400 },
|
{ .id = 2, .x = 300, .y = 400 },
|
||||||
};
|
};
|
||||||
spice.set_touch(points, 2);
|
spice.set_touch(points, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_touch() {
|
static void clear_touch() {
|
||||||
uint32_t ids[2] = { 1, 2 };
|
uint32_t ids[2] = { 1, 2 };
|
||||||
spice.clear_touch(ids, 2);
|
spice.clear_touch(ids, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void insert_card() {
|
static void insert_card() {
|
||||||
spice.insert_card(0, "E004010000001234");
|
spice.insert_card(0, "E004010000001234");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void set_keypad() {
|
static void set_keypad() {
|
||||||
SPICE_SDK_STATUS_CODE ret = spice.set_keypad(0, '3');
|
SPICE_SDK_STATUS_CODE ret = spice.set_keypad(0, '3');
|
||||||
if (ret != SPICE_SDK_STATUS_SUCCESS) {
|
if (ret != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
char log_message[64];
|
char log_message[64];
|
||||||
snprintf(log_message, sizeof(log_message), "set_keypad failed: %d", ret);
|
snprintf(log_message, sizeof(log_message), "set_keypad failed: %d", ret);
|
||||||
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", log_message);
|
spice.log(SPICE_SDK_LOG_LEVEL_WARNING, "sample_v0", log_message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void clear_keypad() {
|
static void clear_keypad() {
|
||||||
spice.set_keypad(0, 0);
|
spice.set_keypad(0, 0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
LIBRARY sdk_sample_v0_flat_c
|
LIBRARY sdk_sample_v0_flat_c
|
||||||
|
|
||||||
EXPORTS
|
EXPORTS
|
||||||
spice_sdk_entry_point
|
spice_sdk_entry_point
|
||||||
+643
-643
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -1,12 +1,12 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace sdk {
|
namespace sdk {
|
||||||
|
|
||||||
void register_sdk_hooks(std::string dll, HINSTANCE module);
|
void register_sdk_hooks(std::string dll, HINSTANCE module);
|
||||||
void init_sdk_modules();
|
void init_sdk_modules();
|
||||||
void fini_sdk_modules();
|
void fini_sdk_modules();
|
||||||
|
|
||||||
}
|
}
|
||||||
+213
-213
@@ -1,213 +1,213 @@
|
|||||||
#include "gdi_overlay.h"
|
#include "gdi_overlay.h"
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
// the back buffer matches the window DC; the software buffer has a fixed 32-bit layout
|
||||||
enum class BufferType {
|
enum class BufferType {
|
||||||
TargetCompatible,
|
TargetCompatible,
|
||||||
Bgra32,
|
Bgra32,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct GdiBuffer {
|
struct GdiBuffer {
|
||||||
HDC dc = nullptr;
|
HDC dc = nullptr;
|
||||||
HBITMAP bitmap = nullptr;
|
HBITMAP bitmap = nullptr;
|
||||||
// bitmap originally selected into the memory DC, restored before cleanup
|
// bitmap originally selected into the memory DC, restored before cleanup
|
||||||
HGDIOBJ old_bitmap = nullptr;
|
HGDIOBJ old_bitmap = nullptr;
|
||||||
int width = 0;
|
int width = 0;
|
||||||
int height = 0;
|
int height = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
// back buffer holds the complete frame; overlay buffer holds ImGui software pixels
|
||||||
GdiBuffer BACK_BUFFER;
|
GdiBuffer BACK_BUFFER;
|
||||||
GdiBuffer OVERLAY_BUFFER;
|
GdiBuffer OVERLAY_BUFFER;
|
||||||
|
|
||||||
void release_buffer(GdiBuffer &buffer) {
|
void release_buffer(GdiBuffer &buffer) {
|
||||||
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
// destroy the DC before the bitmap so cleanup is safe even if restoration fails
|
||||||
if (buffer.dc != nullptr) {
|
if (buffer.dc != nullptr) {
|
||||||
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
if (buffer.old_bitmap != nullptr && buffer.old_bitmap != HGDI_ERROR) {
|
||||||
SelectObject(buffer.dc, buffer.old_bitmap);
|
SelectObject(buffer.dc, buffer.old_bitmap);
|
||||||
}
|
}
|
||||||
DeleteDC(buffer.dc);
|
DeleteDC(buffer.dc);
|
||||||
}
|
}
|
||||||
if (buffer.bitmap != nullptr) {
|
if (buffer.bitmap != nullptr) {
|
||||||
DeleteObject(buffer.bitmap);
|
DeleteObject(buffer.bitmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer = {};
|
buffer = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
// ensures the buffer has a memory DC with a bitmap of the requested size and type
|
||||||
// selected into it. a matching allocation is reused; otherwise the old resources are
|
// selected into it. a matching allocation is reused; otherwise the old resources are
|
||||||
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
// released and recreated. returns false if the dimensions or any GDI operation fail.
|
||||||
bool ensure_buffer(
|
bool ensure_buffer(
|
||||||
GdiBuffer &buffer,
|
GdiBuffer &buffer,
|
||||||
HDC target_dc,
|
HDC target_dc,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
BufferType type,
|
BufferType type,
|
||||||
const char *name) {
|
const char *name) {
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
if (buffer.dc != nullptr && buffer.bitmap != nullptr &&
|
||||||
buffer.width == width && buffer.height == height) {
|
buffer.width == width && buffer.height == height) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep allocations across frames and recreate only after a size change
|
// keep allocations across frames and recreate only after a size change
|
||||||
release_buffer(buffer);
|
release_buffer(buffer);
|
||||||
buffer.dc = CreateCompatibleDC(target_dc);
|
buffer.dc = CreateCompatibleDC(target_dc);
|
||||||
if (buffer.dc == nullptr) {
|
if (buffer.dc == nullptr) {
|
||||||
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
log_warning("touch", "failed to create {} DC: {}", name, GetLastError());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
// compatible bitmaps are fast presentation targets; BGRA bitmaps accept raw pixels
|
||||||
if (type == BufferType::TargetCompatible) {
|
if (type == BufferType::TargetCompatible) {
|
||||||
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
buffer.bitmap = CreateCompatibleBitmap(target_dc, width, height);
|
||||||
} else {
|
} else {
|
||||||
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
buffer.bitmap = CreateBitmap(width, height, 1, sizeof(uint32_t) * 8, nullptr);
|
||||||
}
|
}
|
||||||
if (buffer.bitmap == nullptr) {
|
if (buffer.bitmap == nullptr) {
|
||||||
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
log_warning("touch", "failed to create {} bitmap: {}", name, GetLastError());
|
||||||
release_buffer(buffer);
|
release_buffer(buffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
buffer.old_bitmap = SelectObject(buffer.dc, buffer.bitmap);
|
||||||
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
if (buffer.old_bitmap == nullptr || buffer.old_bitmap == HGDI_ERROR) {
|
||||||
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
log_warning("touch", "failed to select {} bitmap: {}", name, GetLastError());
|
||||||
release_buffer(buffer);
|
release_buffer(buffer);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.width = width;
|
buffer.width = width;
|
||||||
buffer.height = height;
|
buffer.height = height;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool update_overlay_buffer(
|
bool update_overlay_buffer(
|
||||||
HDC target_dc,
|
HDC target_dc,
|
||||||
const uint32_t *pixels,
|
const uint32_t *pixels,
|
||||||
bool pixels_dirty,
|
bool pixels_dirty,
|
||||||
int width,
|
int width,
|
||||||
int height) {
|
int height) {
|
||||||
if (pixels == nullptr) {
|
if (pixels == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
bool needs_update = pixels_dirty || OVERLAY_BUFFER.bitmap == nullptr ||
|
||||||
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
OVERLAY_BUFFER.width != width || OVERLAY_BUFFER.height != height;
|
||||||
if (!ensure_buffer(
|
if (!ensure_buffer(
|
||||||
OVERLAY_BUFFER,
|
OVERLAY_BUFFER,
|
||||||
target_dc,
|
target_dc,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
BufferType::Bgra32,
|
BufferType::Bgra32,
|
||||||
"software overlay")) {
|
"software overlay")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!needs_update) {
|
if (!needs_update) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDIBits requires the destination bitmap not to be selected into a DC
|
// SetDIBits requires the destination bitmap not to be selected into a DC
|
||||||
HGDIOBJ overlay_bitmap =
|
HGDIOBJ overlay_bitmap =
|
||||||
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.old_bitmap);
|
||||||
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
if (overlay_bitmap == nullptr || overlay_bitmap == HGDI_ERROR) {
|
||||||
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
log_warning("touch", "failed to deselect software overlay bitmap: {}", GetLastError());
|
||||||
release_buffer(OVERLAY_BUFFER);
|
release_buffer(OVERLAY_BUFFER);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
BITMAPINFO bitmap_info {};
|
BITMAPINFO bitmap_info {};
|
||||||
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
bitmap_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
|
||||||
bitmap_info.bmiHeader.biWidth = width;
|
bitmap_info.bmiHeader.biWidth = width;
|
||||||
bitmap_info.bmiHeader.biHeight = -height;
|
bitmap_info.bmiHeader.biHeight = -height;
|
||||||
bitmap_info.bmiHeader.biPlanes = 1;
|
bitmap_info.bmiHeader.biPlanes = 1;
|
||||||
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
bitmap_info.bmiHeader.biBitCount = sizeof(uint32_t) * 8;
|
||||||
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
bitmap_info.bmiHeader.biCompression = BI_RGB;
|
||||||
|
|
||||||
int copied_lines = SetDIBits(
|
int copied_lines = SetDIBits(
|
||||||
target_dc,
|
target_dc,
|
||||||
OVERLAY_BUFFER.bitmap,
|
OVERLAY_BUFFER.bitmap,
|
||||||
0,
|
0,
|
||||||
height,
|
height,
|
||||||
pixels,
|
pixels,
|
||||||
&bitmap_info,
|
&bitmap_info,
|
||||||
DIB_RGB_COLORS);
|
DIB_RGB_COLORS);
|
||||||
|
|
||||||
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
HGDIOBJ old_bitmap = SelectObject(OVERLAY_BUFFER.dc, OVERLAY_BUFFER.bitmap);
|
||||||
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) {
|
||||||
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
log_warning("touch", "failed to reselect software overlay bitmap: {}", GetLastError());
|
||||||
release_buffer(OVERLAY_BUFFER);
|
release_buffer(OVERLAY_BUFFER);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
OVERLAY_BUFFER.old_bitmap = old_bitmap;
|
||||||
|
|
||||||
if (copied_lines != height) {
|
if (copied_lines != height) {
|
||||||
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
log_warning("touch", "failed to update software overlay bitmap: {} of {} lines copied",
|
||||||
copied_lines, height);
|
copied_lines, height);
|
||||||
release_buffer(OVERLAY_BUFFER);
|
release_buffer(OVERLAY_BUFFER);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HDC touch_gdi_overlay_begin_frame(
|
HDC touch_gdi_overlay_begin_frame(
|
||||||
HDC target_dc,
|
HDC target_dc,
|
||||||
HBRUSH background_brush,
|
HBRUSH background_brush,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
const uint32_t *overlay_pixels,
|
const uint32_t *overlay_pixels,
|
||||||
bool overlay_pixels_dirty,
|
bool overlay_pixels_dirty,
|
||||||
int overlay_width,
|
int overlay_width,
|
||||||
int overlay_height) {
|
int overlay_height) {
|
||||||
if (!ensure_buffer(
|
if (!ensure_buffer(
|
||||||
BACK_BUFFER,
|
BACK_BUFFER,
|
||||||
target_dc,
|
target_dc,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
BufferType::TargetCompatible,
|
BufferType::TargetCompatible,
|
||||||
"overlay back buffer")) {
|
"overlay back buffer")) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
HDC draw_dc = BACK_BUFFER.dc;
|
HDC draw_dc = BACK_BUFFER.dc;
|
||||||
SetBkMode(draw_dc, TRANSPARENT);
|
SetBkMode(draw_dc, TRANSPARENT);
|
||||||
|
|
||||||
// start each frame from the transparent color-key background
|
// start each frame from the transparent color-key background
|
||||||
RECT buffer_rect {0, 0, width, height};
|
RECT buffer_rect {0, 0, width, height};
|
||||||
FillRect(draw_dc, &buffer_rect, background_brush);
|
FillRect(draw_dc, &buffer_rect, background_brush);
|
||||||
|
|
||||||
if (update_overlay_buffer(
|
if (update_overlay_buffer(
|
||||||
target_dc,
|
target_dc,
|
||||||
overlay_pixels,
|
overlay_pixels,
|
||||||
overlay_pixels_dirty,
|
overlay_pixels_dirty,
|
||||||
overlay_width,
|
overlay_width,
|
||||||
overlay_height) &&
|
overlay_height) &&
|
||||||
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
!BitBlt(draw_dc, 0, 0, overlay_width, overlay_height,
|
||||||
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
OVERLAY_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||||
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
log_warning("touch", "failed to draw software overlay bitmap: {}", GetLastError());
|
||||||
}
|
}
|
||||||
|
|
||||||
return draw_dc;
|
return draw_dc;
|
||||||
}
|
}
|
||||||
|
|
||||||
void touch_gdi_overlay_present(HDC target_dc) {
|
void touch_gdi_overlay_present(HDC target_dc) {
|
||||||
// one full-window blit exposes the completed frame without an intermediate erase
|
// one full-window blit exposes the completed frame without an intermediate erase
|
||||||
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
if (!BitBlt(target_dc, 0, 0, BACK_BUFFER.width, BACK_BUFFER.height,
|
||||||
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
BACK_BUFFER.dc, 0, 0, SRCCOPY)) {
|
||||||
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
log_warning("touch", "failed to present overlay back buffer: {}", GetLastError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void touch_gdi_overlay_release() {
|
void touch_gdi_overlay_release() {
|
||||||
release_buffer(BACK_BUFFER);
|
release_buffer(BACK_BUFFER);
|
||||||
release_buffer(OVERLAY_BUFFER);
|
release_buffer(OVERLAY_BUFFER);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
// prepares a complete offscreen frame and returns its drawing DC; returns null on failure
|
||||||
HDC touch_gdi_overlay_begin_frame(
|
HDC touch_gdi_overlay_begin_frame(
|
||||||
HDC target_dc,
|
HDC target_dc,
|
||||||
HBRUSH background_brush,
|
HBRUSH background_brush,
|
||||||
int width,
|
int width,
|
||||||
int height,
|
int height,
|
||||||
const uint32_t *overlay_pixels,
|
const uint32_t *overlay_pixels,
|
||||||
bool overlay_pixels_dirty,
|
bool overlay_pixels_dirty,
|
||||||
int overlay_width,
|
int overlay_width,
|
||||||
int overlay_height);
|
int overlay_height);
|
||||||
|
|
||||||
// presents the frame prepared by the most recent successful begin call
|
// presents the frame prepared by the most recent successful begin call
|
||||||
void touch_gdi_overlay_present(HDC target_dc);
|
void touch_gdi_overlay_present(HDC target_dc);
|
||||||
|
|
||||||
// releases all cached GDI resources
|
// releases all cached GDI resources
|
||||||
void touch_gdi_overlay_release();
|
void touch_gdi_overlay_release();
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace nativetouch::inject {
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
enum class ContactOwner {
|
enum class ContactOwner {
|
||||||
None,
|
None,
|
||||||
Mouse,
|
Mouse,
|
||||||
Synthetic,
|
Synthetic,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool initialize_touch_injection();
|
bool initialize_touch_injection();
|
||||||
void initialize_synthetic_touch();
|
void initialize_synthetic_touch();
|
||||||
void refresh_contact_lifetime();
|
void refresh_contact_lifetime();
|
||||||
|
|
||||||
bool contact_is_active();
|
bool contact_is_active();
|
||||||
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
bool contact_is_owned_by(ContactOwner owner, HWND window);
|
||||||
bool begin_contact(
|
bool begin_contact(
|
||||||
ContactOwner owner,
|
ContactOwner owner,
|
||||||
HWND window,
|
HWND window,
|
||||||
POINT position,
|
POINT position,
|
||||||
bool transform_returned_coordinates);
|
bool transform_returned_coordinates);
|
||||||
bool update_contact(ContactOwner owner, HWND window, POINT position);
|
bool update_contact(ContactOwner owner, HWND window, POINT position);
|
||||||
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
|
void set_contact_timer(ContactOwner owner, HWND window, UINT_PTR timer_id);
|
||||||
bool release_active_contact();
|
bool release_active_contact();
|
||||||
|
|
||||||
HWND get_injection_window();
|
HWND get_injection_window();
|
||||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
|
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param);
|
||||||
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
|
bool handle_synthetic_message(HWND window, UINT message, WPARAM w_param, LPARAM l_param);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +1,146 @@
|
|||||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||||
#define _WIN32_WINNT 0x0602
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include "inject_internal.h"
|
#include "inject_internal.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace nativetouch::inject {
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
|
constexpr UINT CONTACT_TIMER_INTERVAL_MS = 16;
|
||||||
|
|
||||||
static int mouse_contact_timer_token;
|
static int mouse_contact_timer_token;
|
||||||
|
|
||||||
struct PrimaryMouseButton {
|
struct PrimaryMouseButton {
|
||||||
UINT down_message;
|
UINT down_message;
|
||||||
UINT double_click_message;
|
UINT double_click_message;
|
||||||
UINT up_message;
|
UINT up_message;
|
||||||
WPARAM state_mask;
|
WPARAM state_mask;
|
||||||
};
|
};
|
||||||
|
|
||||||
// honor the user's swapped-button setting when choosing the primary button
|
// honor the user's swapped-button setting when choosing the primary button
|
||||||
static PrimaryMouseButton get_primary_mouse_button() {
|
static PrimaryMouseButton get_primary_mouse_button() {
|
||||||
if (GetSystemMetrics(SM_SWAPBUTTON)) {
|
if (GetSystemMetrics(SM_SWAPBUTTON)) {
|
||||||
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
|
return { WM_RBUTTONDOWN, WM_RBUTTONDBLCLK, WM_RBUTTONUP, MK_RBUTTON };
|
||||||
}
|
}
|
||||||
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
|
return { WM_LBUTTONDOWN, WM_LBUTTONDBLCLK, WM_LBUTTONUP, MK_LBUTTON };
|
||||||
}
|
}
|
||||||
|
|
||||||
// use the current physical cursor but reject points outside the subscreen
|
// use the current physical cursor but reject points outside the subscreen
|
||||||
static bool get_mouse_injection_position(HWND window, POINT *position) {
|
static bool get_mouse_injection_position(HWND window, POINT *position) {
|
||||||
|
|
||||||
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
|
// queued WM_MOUSEMOVE coordinates can lag behind the cursor; injecting them makes
|
||||||
// Windows move its primary pointer back to stale positions during a drag.
|
// Windows move its primary pointer back to stale positions during a drag.
|
||||||
if (!GetCursorPos(position)) {
|
if (!GetCursorPos(position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT transformed = *position;
|
POINT transformed = *position;
|
||||||
return transform::mouse_to_game(window, &transformed);
|
return transform::mouse_to_game(window, &transformed);
|
||||||
}
|
}
|
||||||
|
|
||||||
// release the active injected contact and its window capture
|
// release the active injected contact and its window capture
|
||||||
static void end_mouse_contact(HWND window) {
|
static void end_mouse_contact(HWND window) {
|
||||||
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
|
if (contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
release_active_contact();
|
release_active_contact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin a contact at the physical cursor position and capture future mouse input
|
// begin a contact at the physical cursor position and capture future mouse input
|
||||||
static void begin_mouse_contact(HWND window) {
|
static void begin_mouse_contact(HWND window) {
|
||||||
if (contact_is_active()) {
|
if (contact_is_active()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT position;
|
POINT position;
|
||||||
if (!get_mouse_injection_position(window, &position) ||
|
if (!get_mouse_injection_position(window, &position) ||
|
||||||
!begin_contact(ContactOwner::Mouse, window, position, true)) {
|
!begin_contact(ContactOwner::Mouse, window, position, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep receiving drag messages after the cursor leaves the client area
|
// keep receiving drag messages after the cursor leaves the client area
|
||||||
SetCapture(window);
|
SetCapture(window);
|
||||||
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
if (!settings::REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP) {
|
||||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
const auto timer_id = reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token);
|
||||||
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
if (SetTimer(window, timer_id, CONTACT_TIMER_INTERVAL_MS, nullptr)) {
|
||||||
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
set_contact_timer(ContactOwner::Mouse, window, timer_id);
|
||||||
} else {
|
} else {
|
||||||
log_warning("touch::native", "failed to start mouse touch injection timer");
|
log_warning("touch::native", "failed to start mouse touch injection timer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the contact while the primary button remains held
|
// update the contact while the primary button remains held
|
||||||
static void move_mouse_contact(
|
static void move_mouse_contact(
|
||||||
HWND window, WPARAM w_param, WPARAM primary_button_state) {
|
HWND window, WPARAM w_param, WPARAM primary_button_state) {
|
||||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT position;
|
POINT position;
|
||||||
if (!get_mouse_injection_position(window, &position)) {
|
if (!get_mouse_injection_position(window, &position)) {
|
||||||
end_mouse_contact(window);
|
end_mouse_contact(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((w_param & primary_button_state) == 0) {
|
if ((w_param & primary_button_state) == 0) {
|
||||||
end_mouse_contact(window);
|
end_mouse_contact(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_contact(ContactOwner::Mouse, window, position);
|
update_contact(ContactOwner::Mouse, window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// emit stationary update frames so Windows keeps the contact alive
|
// emit stationary update frames so Windows keeps the contact alive
|
||||||
static void refresh_mouse_contact(HWND window) {
|
static void refresh_mouse_contact(HWND window) {
|
||||||
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
if (!contact_is_owned_by(ContactOwner::Mouse, window)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT position {};
|
POINT position {};
|
||||||
if (!GetCursorPos(&position)) {
|
if (!GetCursorPos(&position)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
POINT transformed = position;
|
POINT transformed = position;
|
||||||
if (!transform::mouse_to_game(window, &transformed)) {
|
if (!transform::mouse_to_game(window, &transformed)) {
|
||||||
end_mouse_contact(window);
|
end_mouse_contact(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
update_contact(ContactOwner::Mouse, window, position);
|
update_contact(ContactOwner::Mouse, window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
|
bool handle_mouse_message(HWND window, UINT message, WPARAM w_param) {
|
||||||
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
|
if (message >= WM_MOUSEFIRST && message <= WM_MOUSELAST &&
|
||||||
is_mouse_message_from_touchscreen()) {
|
is_mouse_message_from_touchscreen()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message == WM_TIMER &&
|
if (message == WM_TIMER &&
|
||||||
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
|
w_param == reinterpret_cast<UINT_PTR>(&mouse_contact_timer_token)) {
|
||||||
refresh_mouse_contact(window);
|
refresh_mouse_contact(window);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto primary_button = get_primary_mouse_button();
|
const auto primary_button = get_primary_mouse_button();
|
||||||
if (message == primary_button.down_message ||
|
if (message == primary_button.down_message ||
|
||||||
message == primary_button.double_click_message) {
|
message == primary_button.double_click_message) {
|
||||||
begin_mouse_contact(window);
|
begin_mouse_contact(window);
|
||||||
} else if (message == WM_MOUSEMOVE) {
|
} else if (message == WM_MOUSEMOVE) {
|
||||||
move_mouse_contact(window, w_param, primary_button.state_mask);
|
move_mouse_contact(window, w_param, primary_button.state_mask);
|
||||||
} else if (message == primary_button.up_message) {
|
} else if (message == primary_button.up_message) {
|
||||||
end_mouse_contact(window);
|
end_mouse_contact(window);
|
||||||
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
|
} else if (message == WM_CANCELMODE || message == WM_KILLFOCUS ||
|
||||||
message == WM_CAPTURECHANGED) {
|
message == WM_CAPTURECHANGED) {
|
||||||
end_mouse_contact(window);
|
end_mouse_contact(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,175 +1,175 @@
|
|||||||
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
// enable Windows 8 touch injection types; the functions are loaded dynamically
|
||||||
#define _WIN32_WINNT 0x0602
|
#define _WIN32_WINNT 0x0602
|
||||||
|
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <windowsx.h>
|
#include <windowsx.h>
|
||||||
|
|
||||||
#include "inject.h"
|
#include "inject.h"
|
||||||
#include "inject_internal.h"
|
#include "inject_internal.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace nativetouch::inject {
|
namespace nativetouch::inject {
|
||||||
|
|
||||||
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
constexpr UINT SYNTHETIC_CONTACT_TIMEOUT_MS = 100;
|
||||||
|
|
||||||
enum class SyntheticTouchMessage : WPARAM {
|
enum class SyntheticTouchMessage : WPARAM {
|
||||||
Up, // used by callers releasing a contact
|
Up, // used by callers releasing a contact
|
||||||
DownGameSpace, // coordinates are relative to the game's logical touch surface
|
DownGameSpace, // coordinates are relative to the game's logical touch surface
|
||||||
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
|
DownScreenSpace, // coordinates are absolute pixels in Windows desktop coordinates
|
||||||
};
|
};
|
||||||
|
|
||||||
static std::once_flag synthetic_initialization_once;
|
static std::once_flag synthetic_initialization_once;
|
||||||
static int synthetic_contact_timer_token;
|
static int synthetic_contact_timer_token;
|
||||||
static UINT synthetic_touch_message;
|
static UINT synthetic_touch_message;
|
||||||
|
|
||||||
void initialize_synthetic_touch() {
|
void initialize_synthetic_touch() {
|
||||||
std::call_once(synthetic_initialization_once, [] {
|
std::call_once(synthetic_initialization_once, [] {
|
||||||
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
|
synthetic_touch_message = RegisterWindowMessageW(L"spice2x.native_touch.inject");
|
||||||
if (synthetic_touch_message == 0) {
|
if (synthetic_touch_message == 0) {
|
||||||
log_warning(
|
log_warning(
|
||||||
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
|
"touch::native", "failed to register synthetic touch message: {}", GetLastError());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// synthetic touches preempt the mouse and keep it disabled until release or timeout
|
// synthetic touches preempt the mouse and keep it disabled until release or timeout
|
||||||
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
|
static void begin_synthetic_contact(HWND window, POINT position, bool screen_space) {
|
||||||
// remember when Windows-returned coordinates must map back into game space
|
// remember when Windows-returned coordinates must map back into game space
|
||||||
const auto transform_returned_coordinates =
|
const auto transform_returned_coordinates =
|
||||||
transform::is_tdj_dedicated_subscreen(window) ||
|
transform::is_tdj_dedicated_subscreen(window) ||
|
||||||
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||||
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
if (!screen_space && !transform::game_to_screen(window, &position)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
const auto timer_id = reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token);
|
||||||
|
|
||||||
// when this producer already owns the contact, move it instead of releasing and
|
// when this producer already owns the contact, move it instead of releasing and
|
||||||
// re-pressing so continuous input (such as the API surface) drags smoothly
|
// re-pressing so continuous input (such as the API surface) drags smoothly
|
||||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||||
if (update_contact(ContactOwner::Synthetic, window, position)) {
|
if (update_contact(ContactOwner::Synthetic, window, position)) {
|
||||||
// refresh the safety timeout while updates keep arriving
|
// refresh the safety timeout while updates keep arriving
|
||||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!release_active_contact()) {
|
if (!release_active_contact()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!begin_contact(
|
if (!begin_contact(
|
||||||
ContactOwner::Synthetic,
|
ContactOwner::Synthetic,
|
||||||
window,
|
window,
|
||||||
position,
|
position,
|
||||||
transform_returned_coordinates)) {
|
transform_returned_coordinates)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
if (SetTimer(window, timer_id, SYNTHETIC_CONTACT_TIMEOUT_MS, nullptr)) {
|
||||||
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
set_contact_timer(ContactOwner::Synthetic, window, timer_id);
|
||||||
} else {
|
} else {
|
||||||
log_warning("touch::native", "failed to start synthetic touch timeout timer");
|
log_warning("touch::native", "failed to start synthetic touch timeout timer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void end_synthetic_contact(HWND window) {
|
static void end_synthetic_contact(HWND window) {
|
||||||
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
if (contact_is_owned_by(ContactOwner::Synthetic, window)) {
|
||||||
release_active_contact();
|
release_active_contact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool handle_synthetic_message(
|
bool handle_synthetic_message(
|
||||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||||
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
if (synthetic_touch_message != 0 && message == synthetic_touch_message) {
|
||||||
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
POINT position { GET_X_LPARAM(l_param), GET_Y_LPARAM(l_param) };
|
||||||
switch (static_cast<SyntheticTouchMessage>(w_param)) {
|
switch (static_cast<SyntheticTouchMessage>(w_param)) {
|
||||||
case SyntheticTouchMessage::DownGameSpace:
|
case SyntheticTouchMessage::DownGameSpace:
|
||||||
begin_synthetic_contact(window, position, false);
|
begin_synthetic_contact(window, position, false);
|
||||||
break;
|
break;
|
||||||
case SyntheticTouchMessage::DownScreenSpace:
|
case SyntheticTouchMessage::DownScreenSpace:
|
||||||
begin_synthetic_contact(window, position, true);
|
begin_synthetic_contact(window, position, true);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
end_synthetic_contact(window);
|
end_synthetic_contact(window);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message == WM_TIMER &&
|
if (message == WM_TIMER &&
|
||||||
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
|
w_param == reinterpret_cast<UINT_PTR>(&synthetic_contact_timer_token)) {
|
||||||
end_synthetic_contact(window);
|
end_synthetic_contact(window);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
static HWND prepare_synthetic_touch() {
|
static HWND prepare_synthetic_touch() {
|
||||||
if (!initialize_touch_injection()) {
|
if (!initialize_touch_injection()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto window = get_injection_window();
|
const auto window = get_injection_window();
|
||||||
if (window == nullptr || synthetic_touch_message == 0) {
|
if (window == nullptr || synthetic_touch_message == 0) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool post_synthetic_touch(
|
static bool post_synthetic_touch(
|
||||||
HWND window, POINT position, SyntheticTouchMessage message) {
|
HWND window, POINT position, SyntheticTouchMessage message) {
|
||||||
return PostMessageW(
|
return PostMessageW(
|
||||||
window,
|
window,
|
||||||
synthetic_touch_message,
|
synthetic_touch_message,
|
||||||
static_cast<WPARAM>(message),
|
static_cast<WPARAM>(message),
|
||||||
MAKELPARAM(position.x, position.y)) != FALSE;
|
MAKELPARAM(position.x, position.y)) != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// inject a point expressed in the game's synthetic touch coordinate space
|
// inject a point expressed in the game's synthetic touch coordinate space
|
||||||
bool inject_synthetic_touch(POINT position, bool down) {
|
bool inject_synthetic_touch(POINT position, bool down) {
|
||||||
const auto window = prepare_synthetic_touch();
|
const auto window = prepare_synthetic_touch();
|
||||||
if (window == nullptr) {
|
if (window == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto message = down
|
const auto message = down
|
||||||
? SyntheticTouchMessage::DownGameSpace
|
? SyntheticTouchMessage::DownGameSpace
|
||||||
: SyntheticTouchMessage::Up;
|
: SyntheticTouchMessage::Up;
|
||||||
return post_synthetic_touch(window, position, message);
|
return post_synthetic_touch(window, position, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// map a logical canvas point onto the live injection window before injecting it
|
// map a logical canvas point onto the live injection window before injecting it
|
||||||
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
|
bool inject_synthetic_touch_from_canvas(POINT position, SIZE canvas, bool down) {
|
||||||
const auto window = prepare_synthetic_touch();
|
const auto window = prepare_synthetic_touch();
|
||||||
if (window == nullptr) {
|
if (window == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!down) {
|
if (!down) {
|
||||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
|
return post_synthetic_touch(window, position, SyntheticTouchMessage::Up);
|
||||||
}
|
}
|
||||||
|
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
if (canvas.cx <= 0 || canvas.cy <= 0 ||
|
if (canvas.cx <= 0 || canvas.cy <= 0 ||
|
||||||
!GetClientRect(window, &client_rect) ||
|
!GetClientRect(window, &client_rect) ||
|
||||||
client_rect.right <= 0 || client_rect.bottom <= 0) {
|
client_rect.right <= 0 || client_rect.bottom <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
|
position.x = MulDiv(position.x, client_rect.right, canvas.cx);
|
||||||
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
|
position.y = MulDiv(position.y, client_rect.bottom, canvas.cy);
|
||||||
if (!ClientToScreen(window, &position)) {
|
if (!ClientToScreen(window, &position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
|
return post_synthetic_touch(window, position, SyntheticTouchMessage::DownScreenSpace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace nativetouch::settings {
|
namespace nativetouch::settings {
|
||||||
extern bool EMULATE_DIGITIZER;
|
extern bool EMULATE_DIGITIZER;
|
||||||
extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP;
|
extern bool REFRESH_CONTACT_LIFETIME_FROM_GAME_LOOP;
|
||||||
extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
extern bool SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES;
|
||||||
}
|
}
|
||||||
@@ -1,215 +1,215 @@
|
|||||||
#include "transform.h"
|
#include "transform.h"
|
||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
#include "settings.h"
|
#include "settings.h"
|
||||||
#include "touch/touch.h"
|
#include "touch/touch.h"
|
||||||
|
|
||||||
namespace nativetouch::transform {
|
namespace nativetouch::transform {
|
||||||
|
|
||||||
static bool game_client_to_screen(HWND window, POINT *position) {
|
static bool game_client_to_screen(HWND window, POINT *position) {
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
if (window == nullptr ||
|
if (window == nullptr ||
|
||||||
!GetClientRect(window, &client_rect) ||
|
!GetClientRect(window, &client_rect) ||
|
||||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
!PtInRect(&client_rect, *position)) {
|
!PtInRect(&client_rect, *position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ClientToScreen(window, position) != FALSE;
|
return ClientToScreen(window, position) != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool screen_to_game_client(HWND window, POINT *position) {
|
static bool screen_to_game_client(HWND window, POINT *position) {
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
if (window == nullptr ||
|
if (window == nullptr ||
|
||||||
!GetClientRect(window, &client_rect) ||
|
!GetClientRect(window, &client_rect) ||
|
||||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
!ScreenToClient(window, position) ||
|
!ScreenToClient(window, position) ||
|
||||||
!PtInRect(&client_rect, *position)) {
|
!PtInRect(&client_rect, *position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_tdj_dedicated_subscreen(HWND window) {
|
bool is_tdj_dedicated_subscreen(HWND window) {
|
||||||
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
return window != nullptr && GRAPHICS_WINDOWED && GRAPHICS_IIDX_WSUB &&
|
||||||
window == TDJ_SUBSCREEN_WINDOW;
|
window == TDJ_SUBSCREEN_WINDOW;
|
||||||
}
|
}
|
||||||
|
|
||||||
// mouse-as-touch only applies while the cursor is over the target window
|
// mouse-as-touch only applies while the cursor is over the target window
|
||||||
static bool is_cursor_over_window(HWND window, POINT position) {
|
static bool is_cursor_over_window(HWND window, POINT position) {
|
||||||
return screen_to_game_client(window, &position);
|
return screen_to_game_client(window, &position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert game touch coordinates to Windows desktop coordinates
|
// convert game touch coordinates to Windows desktop coordinates
|
||||||
bool game_to_screen(HWND window, POINT *position) {
|
bool game_to_screen(HWND window, POINT *position) {
|
||||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||||
return game_client_to_screen(window, position);
|
return game_client_to_screen(window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_tdj_dedicated_subscreen(window)) {
|
if (!is_tdj_dedicated_subscreen(window)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
if (!GetClientRect(window, &client_rect) ||
|
if (!GetClientRect(window, &client_rect) ||
|
||||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
position->x = MulDiv(
|
position->x = MulDiv(
|
||||||
position->x - SPICETOUCH_TOUCH_X,
|
position->x - SPICETOUCH_TOUCH_X,
|
||||||
client_rect.right,
|
client_rect.right,
|
||||||
SPICETOUCH_TOUCH_WIDTH);
|
SPICETOUCH_TOUCH_WIDTH);
|
||||||
position->y = MulDiv(
|
position->y = MulDiv(
|
||||||
position->y - SPICETOUCH_TOUCH_Y,
|
position->y - SPICETOUCH_TOUCH_Y,
|
||||||
client_rect.bottom,
|
client_rect.bottom,
|
||||||
SPICETOUCH_TOUCH_HEIGHT);
|
SPICETOUCH_TOUCH_HEIGHT);
|
||||||
return ClientToScreen(window, position) != FALSE;
|
return ClientToScreen(window, position) != FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool overlay_owns_touch_input() {
|
static bool overlay_owns_touch_input() {
|
||||||
// the arena SMALL window is the touch surface whenever it exists, so the
|
// the arena SMALL window is the touch surface whenever it exists, so the
|
||||||
// subscreen overlay must not claim touch input in those window modes
|
// subscreen overlay must not claim touch input in those window modes
|
||||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return overlay::OVERLAY != nullptr &&
|
return overlay::OVERLAY != nullptr &&
|
||||||
overlay::OVERLAY->get_active() &&
|
overlay::OVERLAY->get_active() &&
|
||||||
overlay::OVERLAY->has_subscreen_touch_transform();
|
overlay::OVERLAY->has_subscreen_touch_transform();
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool transform_overlay_touch_position(POINT *position) {
|
static bool transform_overlay_touch_position(POINT *position) {
|
||||||
// convert physical screen coordinates to the window-relative coordinates the overlay expects
|
// convert physical screen coordinates to the window-relative coordinates the overlay expects
|
||||||
if (GRAPHICS_WINDOWED) {
|
if (GRAPHICS_WINDOWED) {
|
||||||
position->x -= SPICETOUCH_TOUCH_X;
|
position->x -= SPICETOUCH_TOUCH_X;
|
||||||
position->y -= SPICETOUCH_TOUCH_Y;
|
position->y -= SPICETOUCH_TOUCH_Y;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask the overlay to do the game-specific translation
|
// ask the overlay to do the game-specific translation
|
||||||
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
return overlay::OVERLAY->transform_touch_point(&position->x, &position->y);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SDVX still expects portrait coordinates when its image is rendered in landscape:
|
// SDVX still expects portrait coordinates when its image is rendered in landscape:
|
||||||
// (x, y) -> (width * (1 - y / height), height * x / width).
|
// (x, y) -> (width * (1 - y / height), height * x / width).
|
||||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) {
|
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height) {
|
||||||
if (width <= 0 || height <= 0) {
|
if (width <= 0 || height <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto input_x = position->x;
|
const auto input_x = position->x;
|
||||||
position->x = width - MulDiv(position->y, width, height);
|
position->x = width - MulDiv(position->y, width, height);
|
||||||
position->y = MulDiv(input_x, height, width);
|
position->y = MulDiv(input_x, height, width);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// the digitizer is mapped to the zero-based primary display, so the contact is already
|
// the digitizer is mapped to the zero-based primary display, so the contact is already
|
||||||
// in the effective landscape resolution the rotation is based on
|
// in the effective landscape resolution the rotation is based on
|
||||||
static bool transform_sdvx_landscape_touch_position(POINT *position) {
|
static bool transform_sdvx_landscape_touch_position(POINT *position) {
|
||||||
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
const auto landscape_width = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first : GRAPHICS_FS_ORIGINAL_HEIGHT);
|
||||||
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
const auto landscape_height = static_cast<LONG>(GRAPHICS_FS_CUSTOM_RESOLUTION.has_value() ?
|
||||||
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH);
|
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second : GRAPHICS_FS_ORIGINAL_WIDTH);
|
||||||
|
|
||||||
return sdvx_landscape_rotate(position, landscape_width, landscape_height);
|
return sdvx_landscape_rotate(position, landscape_width, landscape_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert physical screen coordinates to game touch coordinates for a known target
|
// convert physical screen coordinates to game touch coordinates for a known target
|
||||||
bool screen_to_game(HWND window, POINT *position) {
|
bool screen_to_game(HWND window, POINT *position) {
|
||||||
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
if (settings::SYNTHETIC_TOUCH_USES_CLIENT_COORDINATES) {
|
||||||
return screen_to_game_client(window, position);
|
return screen_to_game_client(window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
// scale the resized IIDX subscreen client area into the game's touch-display coordinates
|
||||||
if (is_tdj_dedicated_subscreen(window)) {
|
if (is_tdj_dedicated_subscreen(window)) {
|
||||||
RECT client_rect {};
|
RECT client_rect {};
|
||||||
if (!GetClientRect(window, &client_rect) ||
|
if (!GetClientRect(window, &client_rect) ||
|
||||||
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
client_rect.right <= 0 || client_rect.bottom <= 0 ||
|
||||||
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
SPICETOUCH_TOUCH_WIDTH <= 0 || SPICETOUCH_TOUCH_HEIGHT <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ScreenToClient(window, position)) {
|
if (!ScreenToClient(window, position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!PtInRect(&client_rect, *position)) {
|
if (!PtInRect(&client_rect, *position)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
position->x = SPICETOUCH_TOUCH_X +
|
position->x = SPICETOUCH_TOUCH_X +
|
||||||
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
|
MulDiv(position->x, SPICETOUCH_TOUCH_WIDTH, client_rect.right);
|
||||||
position->y = SPICETOUCH_TOUCH_Y +
|
position->y = SPICETOUCH_TOUCH_Y +
|
||||||
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
|
MulDiv(position->y, SPICETOUCH_TOUCH_HEIGHT, client_rect.bottom);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if subscreen overlay is active and can transform the touch point;
|
// check if subscreen overlay is active and can transform the touch point;
|
||||||
// if not, the touch point is valid as-is
|
// if not, the touch point is valid as-is
|
||||||
if (!overlay_owns_touch_input()) {
|
if (!overlay_owns_touch_input()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ask the overlay to transform the touch point into game coordinates
|
// ask the overlay to transform the touch point into game coordinates
|
||||||
return transform_overlay_touch_position(position);
|
return transform_overlay_touch_position(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool mouse_to_game(HWND window, POINT *position) {
|
bool mouse_to_game(HWND window, POINT *position) {
|
||||||
|
|
||||||
// exception: iidx tdj dedicated subscreen window is allowed
|
// exception: iidx tdj dedicated subscreen window is allowed
|
||||||
if (is_tdj_dedicated_subscreen(window)) {
|
if (is_tdj_dedicated_subscreen(window)) {
|
||||||
return screen_to_game(window, position);
|
return screen_to_game(window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
|
// exception: sdvx windowed subscreen does not use the subscreen overlay transform
|
||||||
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
|
if (GRAPHICS_WINDOWED && window == SDVX_SUBSCREEN_WINDOW) {
|
||||||
return is_cursor_over_window(window, *position);
|
return is_cursor_over_window(window, *position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// exception: the arena SMALL window is the touch panel, so accept the mouse there
|
// exception: the arena SMALL window is the touch panel, so accept the mouse there
|
||||||
// (and only there) with the coordinates a real contact on it would produce
|
// (and only there) with the coordinates a real contact on it would produce
|
||||||
if (graphics_gitadora_has_dedicated_subscreen()) {
|
if (graphics_gitadora_has_dedicated_subscreen()) {
|
||||||
return window == GFDM_SUBSCREEN_WINDOW &&
|
return window == GFDM_SUBSCREEN_WINDOW &&
|
||||||
is_cursor_over_window(window, *position);
|
is_cursor_over_window(window, *position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this game has a subscreen overlay that can transform touch input
|
// if this game has a subscreen overlay that can transform touch input
|
||||||
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
// but the window is hidden or not under the cursor, reject mouse-as-touch
|
||||||
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
// (e.g., iidx/sdvx are rejected here, but nostalgia is allowed)
|
||||||
if (overlay::OVERLAY != nullptr &&
|
if (overlay::OVERLAY != nullptr &&
|
||||||
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
overlay::OVERLAY->has_subscreen_touch_transform() &&
|
||||||
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
!overlay::OVERLAY->accepts_subscreen_mouse_input()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return screen_to_game(window, position);
|
return screen_to_game(window, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
// route hardware screen coordinates through dedicated or overlay mapping and report the result
|
||||||
Result hardware_to_game(POINT *position) {
|
Result hardware_to_game(POINT *position) {
|
||||||
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
const auto dedicated_subscreen = is_tdj_dedicated_subscreen(TDJ_SUBSCREEN_WINDOW);
|
||||||
const auto active_overlay = overlay_owns_touch_input();
|
const auto active_overlay = overlay_owns_touch_input();
|
||||||
|
|
||||||
// special case for SDVX landscape mode
|
// special case for SDVX landscape mode
|
||||||
if (!dedicated_subscreen && !active_overlay &&
|
if (!dedicated_subscreen && !active_overlay &&
|
||||||
GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) {
|
GRAPHICS_FS_ORIENTATION_SWAP && avs::game::is_model("KFC")) {
|
||||||
return transform_sdvx_landscape_touch_position(position) ?
|
return transform_sdvx_landscape_touch_position(position) ?
|
||||||
Result::Transformed : Result::Rejected;
|
Result::Transformed : Result::Rejected;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
// no dedicated subscreen or active overlay mapping; pass the point through unchanged
|
||||||
if (!dedicated_subscreen && !active_overlay) {
|
if (!dedicated_subscreen && !active_overlay) {
|
||||||
return Result::Unchanged;
|
return Result::Unchanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
// route through the dedicated subscreen when active, otherwise through the overlay
|
// route through the dedicated subscreen when active, otherwise through the overlay
|
||||||
const auto valid = screen_to_game(
|
const auto valid = screen_to_game(
|
||||||
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
|
dedicated_subscreen ? TDJ_SUBSCREEN_WINDOW : nullptr,
|
||||||
position);
|
position);
|
||||||
|
|
||||||
// reject out-of-bounds points and any coordinate conversion failure
|
// reject out-of-bounds points and any coordinate conversion failure
|
||||||
return valid ? Result::Transformed : Result::Rejected;
|
return valid ? Result::Transformed : Result::Rejected;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
namespace nativetouch::transform {
|
namespace nativetouch::transform {
|
||||||
enum class Result {
|
enum class Result {
|
||||||
Unchanged,
|
Unchanged,
|
||||||
Transformed,
|
Transformed,
|
||||||
Rejected,
|
Rejected,
|
||||||
};
|
};
|
||||||
|
|
||||||
bool is_tdj_dedicated_subscreen(HWND window);
|
bool is_tdj_dedicated_subscreen(HWND window);
|
||||||
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height);
|
bool sdvx_landscape_rotate(POINT *position, LONG width, LONG height);
|
||||||
bool game_to_screen(HWND window, POINT *position);
|
bool game_to_screen(HWND window, POINT *position);
|
||||||
bool screen_to_game(HWND window, POINT *position);
|
bool screen_to_game(HWND window, POINT *position);
|
||||||
bool mouse_to_game(HWND window, POINT *position);
|
bool mouse_to_game(HWND window, POINT *position);
|
||||||
Result hardware_to_game(POINT *position);
|
Result hardware_to_game(POINT *position);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,66 @@
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
# --- configuration ----------------------------------------------------------
|
# --- configuration ----------------------------------------------------------
|
||||||
$scriptDir = if ($env:SPICE_DIR) { $env:SPICE_DIR } elseif ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
$scriptDir = if ($env:SPICE_DIR) { $env:SPICE_DIR } elseif ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
||||||
$repo = 'spice2x/spice2x.github.io'
|
$repo = 'spice2x/spice2x.github.io'
|
||||||
$targets = @('spice.exe', 'spice64.exe', 'spicecfg.exe')
|
$targets = @('spice.exe', 'spice64.exe', 'spicecfg.exe')
|
||||||
$beta = ($env:SPICE_CHANNEL -match 'beta')
|
$beta = ($env:SPICE_CHANNEL -match 'beta')
|
||||||
$rc = 0
|
$rc = 0
|
||||||
|
|
||||||
$title = if ($beta) { '=== spice2x updater (beta channel) ===' } else { '=== spice2x updater ===' }
|
$title = if ($beta) { '=== spice2x updater (beta channel) ===' } else { '=== spice2x updater ===' }
|
||||||
Write-Host "`n$title"
|
Write-Host "`n$title"
|
||||||
Write-Host "Target folder: $scriptDir`n"
|
Write-Host "Target folder: $scriptDir`n"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
$headers = @{ 'User-Agent' = 'spice2x-updater'; 'Accept' = 'application/vnd.github+json' }
|
$headers = @{ 'User-Agent' = 'spice2x-updater'; 'Accept' = 'application/vnd.github+json' }
|
||||||
|
|
||||||
# --- find the newest release (beta = include pre-releases) --------------
|
# --- find the newest release (beta = include pre-releases) --------------
|
||||||
Write-Host 'Querying latest release...'
|
Write-Host 'Querying latest release...'
|
||||||
$url = if ($beta) { "https://api.github.com/repos/$repo/releases?per_page=1" } else { "https://api.github.com/repos/$repo/releases/latest" }
|
$url = if ($beta) { "https://api.github.com/repos/$repo/releases?per_page=1" } else { "https://api.github.com/repos/$repo/releases/latest" }
|
||||||
$rel = Invoke-RestMethod -Headers $headers -Uri $url | Select-Object -First 1
|
$rel = Invoke-RestMethod -Headers $headers -Uri $url | Select-Object -First 1
|
||||||
if (-not $rel) { throw 'No releases found.' }
|
if (-not $rel) { throw 'No releases found.' }
|
||||||
|
|
||||||
# --- pick the distribution zip (spice2x-<date>.zip, not the -full one) --
|
# --- pick the distribution zip (spice2x-<date>.zip, not the -full one) --
|
||||||
$asset = $rel.assets | Where-Object { $_.name -like 'spice2x-*.zip' -and $_.name -notlike '*-full.zip' } | Select-Object -First 1
|
$asset = $rel.assets | Where-Object { $_.name -like 'spice2x-*.zip' -and $_.name -notlike '*-full.zip' } | Select-Object -First 1
|
||||||
if (-not $asset) { throw 'No .zip asset found in the release.' }
|
if (-not $asset) { throw 'No .zip asset found in the release.' }
|
||||||
Write-Host "Latest release: $($rel.tag_name)$(if ($rel.prerelease) { ' [pre-release]' }) (asset: $($asset.name))"
|
Write-Host "Latest release: $($rel.tag_name)$(if ($rel.prerelease) { ' [pre-release]' }) (asset: $($asset.name))"
|
||||||
|
|
||||||
# --- download the zip into memory ---------------------------------------
|
# --- download the zip into memory ---------------------------------------
|
||||||
# note: on Windows PowerShell 5.1 .Content is a String (empty for binary
|
# note: on Windows PowerShell 5.1 .Content is a String (empty for binary
|
||||||
# responses), so read the raw byte stream instead
|
# responses), so read the raw byte stream instead
|
||||||
Write-Host 'Downloading...'
|
Write-Host 'Downloading...'
|
||||||
$bytes = (Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -UseBasicParsing).RawContentStream.ToArray()
|
$bytes = (Invoke-WebRequest -Headers $headers -Uri $asset.browser_download_url -UseBasicParsing).RawContentStream.ToArray()
|
||||||
|
|
||||||
# --- extract just the three executables straight into this folder -------
|
# --- extract just the three executables straight into this folder -------
|
||||||
# PS 5.1 needs these assemblies loaded; PS 7 already has the types (and the
|
# PS 5.1 needs these assemblies loaded; PS 7 already has the types (and the
|
||||||
# FileSystem assembly name no longer resolves there), so only load if missing
|
# FileSystem assembly name no longer resolves there), so only load if missing
|
||||||
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
|
if (-not ('System.IO.Compression.ZipFile' -as [type])) {
|
||||||
Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
|
Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
|
||||||
}
|
}
|
||||||
$zip = [IO.Compression.ZipArchive]::new([IO.MemoryStream]::new([byte[]]$bytes))
|
$zip = [IO.Compression.ZipArchive]::new([IO.MemoryStream]::new([byte[]]$bytes))
|
||||||
try {
|
try {
|
||||||
$updated = 0
|
$updated = 0
|
||||||
foreach ($name in $targets) {
|
foreach ($name in $targets) {
|
||||||
$entry = $zip.Entries | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
$entry = $zip.Entries | Where-Object { $_.Name -eq $name } | Select-Object -First 1
|
||||||
if (-not $entry) { Write-Warning " $name not found in the archive"; continue }
|
if (-not $entry) { Write-Warning " $name not found in the archive"; continue }
|
||||||
try {
|
try {
|
||||||
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $scriptDir $name), $true)
|
[IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $scriptDir $name), $true)
|
||||||
Write-Host " updated $name"; $updated++
|
Write-Host " updated $name"; $updated++
|
||||||
} catch {
|
} catch {
|
||||||
Write-Warning " FAILED to write $name (running / read-only?): $($_.Exception.Message)"
|
Write-Warning " FAILED to write $name (running / read-only?): $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally { $zip.Dispose() }
|
} finally { $zip.Dispose() }
|
||||||
|
|
||||||
Write-Host "`nDone. $updated of $($targets.Count) executables updated to $($rel.tag_name)."
|
Write-Host "`nDone. $updated of $($targets.Count) executables updated to $($rel.tag_name)."
|
||||||
Write-Host "Only the .exe files are updated; if you copied any DLL stubs, they were not changed."
|
Write-Host "Only the .exe files are updated; if you copied any DLL stubs, they were not changed."
|
||||||
if ($updated -ne $targets.Count) { $rc = 1 }
|
if ($updated -ne $targets.Count) { $rc = 1 }
|
||||||
} catch {
|
} catch {
|
||||||
Write-Host ''; Write-Error $_.Exception.Message; $rc = 1
|
Write-Host ''; Write-Error $_.Exception.Message; $rc = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- result ------------------------------------------------------------------
|
# --- result ------------------------------------------------------------------
|
||||||
Write-Host $(if ($rc) { "`nUpdate FAILED. See the error above." } else { "`nUpdate finished successfully." })
|
Write-Host $(if ($rc) { "`nUpdate FAILED. See the error above." } else { "`nUpdate finished successfully." })
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
exit $rc
|
exit $rc
|
||||||
|
|||||||
Reference in New Issue
Block a user