Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4652dc5c4a | ||
|
|
357e6c86f1 | ||
|
|
67963389ef | ||
|
|
4009bb8a84 | ||
|
|
8fb6d94000 | ||
|
|
8d3685415d | ||
|
|
50b38b856f | ||
|
|
dad88caf01 | ||
|
|
699659d4bf | ||
|
|
21e7d24ed3 | ||
|
|
1cdb7be810 | ||
|
|
09314f1fd3 | ||
|
|
e7b90271ca | ||
|
|
95f10ed733 | ||
|
|
d51de976b1 | ||
|
|
8acd433ec6 | ||
|
|
b9c8afbbc1 | ||
|
|
7d5f528e1a | ||
|
|
786a4254d2 |
@@ -22,6 +22,7 @@ To contribute, fork the repo (just the main branch), make changes in your fork,
|
|||||||
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
* Any additions to built-in patches (modifications / removals are OK if we have a good reason).
|
||||||
* Changes to the software license terms.
|
* Changes to the software license terms.
|
||||||
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
* Localization / translation for UI text. It becomes too difficult to manage by the maintainers.
|
||||||
|
* Bundling third party DLL binary; e.g., Discord Social SDK, PC light control integration, and so on. These should be built as hook DLLs instead, optionally with [Spice SDK](https://github.com/spice2x/spice2x.github.io/wiki/Spice-SDK).
|
||||||
|
|
||||||
### Avoiding regressions
|
### Avoiding regressions
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
|
|||||||
|
|
||||||
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
|
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
|
||||||
|
|
||||||
|
### AI-Use Policy
|
||||||
|
|
||||||
|
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
|
||||||
|
|
||||||
## Additional information
|
## Additional information
|
||||||
|
|
||||||
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL)
|
|||||||
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
|
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
|
||||||
|
add_subdirectory(external/wslay EXCLUDE_FROM_ALL)
|
||||||
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
|
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
|
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
|
||||||
@@ -530,6 +531,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/scotto/io.cpp
|
games/scotto/io.cpp
|
||||||
games/drs/drs.cpp
|
games/drs/drs.cpp
|
||||||
games/drs/io.cpp
|
games/drs/io.cpp
|
||||||
|
games/drs/motion_cam.cpp
|
||||||
games/drs/rgb_cam.cpp
|
games/drs/rgb_cam.cpp
|
||||||
games/we/we.cpp
|
games/we/we.cpp
|
||||||
games/we/io.cpp
|
games/we/io.cpp
|
||||||
@@ -541,6 +543,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/otoca/p4io.cpp
|
games/otoca/p4io.cpp
|
||||||
games/silentscope/silentscope.cpp
|
games/silentscope/silentscope.cpp
|
||||||
games/silentscope/io.cpp
|
games/silentscope/io.cpp
|
||||||
|
games/silentscope/projector.cpp
|
||||||
games/pcm/pcm.cpp
|
games/pcm/pcm.cpp
|
||||||
games/pcm/io.cpp
|
games/pcm/io.cpp
|
||||||
games/onpara/onpara.cpp
|
games/onpara/onpara.cpp
|
||||||
@@ -813,7 +816,7 @@ endfunction()
|
|||||||
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
|
||||||
target_link_libraries(spicetools_spice_objs
|
target_link_libraries(spicetools_spice_objs
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
|
||||||
|
|
||||||
if(NOT MSVC)
|
if(NOT MSVC)
|
||||||
@@ -853,7 +856,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
|
|||||||
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
target_link_libraries(spicetools_spice_linux
|
target_link_libraries(spicetools_spice_linux
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
|
||||||
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
|
||||||
@@ -871,7 +874,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
|
|||||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||||
target_link_libraries(spicetools_spice64
|
target_link_libraries(spicetools_spice64
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
target_link_libraries(spicetools_spice64 PUBLIC winscard)
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
|
||||||
@@ -894,7 +897,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
|
|||||||
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
|
||||||
target_link_libraries(spicetools_spice64_linux
|
target_link_libraries(spicetools_spice64_linux
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
|
||||||
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
|
||||||
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
|
||||||
@@ -916,7 +919,8 @@ add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
|||||||
# the configurator serves neither the API nor the video stream, so it needs no codecs
|
# the configurator serves neither the API nor the video stream, so it needs no codecs
|
||||||
target_link_libraries(spicetools_cfg
|
target_link_libraries(spicetools_cfg
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||||
|
target_link_libraries(spicetools_cfg PUBLIC winscard)
|
||||||
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
|
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
|
||||||
target_compile_definitions(spicetools_cfg PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
target_compile_definitions(spicetools_cfg PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
||||||
@@ -933,7 +937,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
|
|||||||
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
|
||||||
target_link_libraries(spicetools_cfg_linux
|
target_link_libraries(spicetools_cfg_linux
|
||||||
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
|
||||||
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
|
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
|
||||||
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
|
||||||
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
|
||||||
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
|
||||||
|
|||||||
+31
-3
@@ -127,7 +127,30 @@ doesn't matter since the TCP protocol doesn't allow for out of order data,
|
|||||||
however this may change when/if support for UDP is being introduced. The only
|
however this may change when/if support for UDP is being introduced. The only
|
||||||
restriction is that the ID has to be a valid 64-bit unsigned integer.
|
restriction is that the ID has to be a valid 64-bit unsigned integer.
|
||||||
|
|
||||||
|
#### Capture
|
||||||
|
- get_screens()
|
||||||
|
- returns the screen numbers the game has registered for capture
|
||||||
|
- get_jpg(screen: uint, quality: uint, divide: uint)
|
||||||
|
- returns the timestamp, width, height and base64 encoded JPEG of one screen
|
||||||
|
- all parameters are optional and default to screen 0, quality 70, divide 1
|
||||||
|
- divide shrinks the image by that factor before encoding
|
||||||
|
- get_streams()
|
||||||
|
- returns a dict describing the HTTP video stream, or no data at all when
|
||||||
|
`-apistream` is not enabled and there is nothing to describe
|
||||||
|
- `port` is the stream server port
|
||||||
|
- `formats` lists the wire formats this build serves, each with a `name`
|
||||||
|
(`h264` or `mjpeg`) and the `path` to request them on
|
||||||
|
- `screens` lists every capturable screen with its `width`, `height`, and
|
||||||
|
`busy`
|
||||||
|
|
||||||
#### Card
|
#### Card
|
||||||
|
- get_cards()
|
||||||
|
- returns the current card ID and source for each active card reader
|
||||||
|
- each entry contains `index`, `card_id`, and `source`
|
||||||
|
- `source` is `file` or `override`
|
||||||
|
- file entries also contain `file_name`, without the full configured path
|
||||||
|
- `-card0` and `-card1` overrides do not contain `file_name`
|
||||||
|
- this function only works when an API password is configured
|
||||||
- insert(index: uint, card_id: hex)
|
- insert(index: uint, card_id: hex)
|
||||||
- inserts a card which gets read by the emulated card readers for the game
|
- inserts a card which gets read by the emulated card readers for the game
|
||||||
- index has to be either 0 (for P1) or 1 (for P2)
|
- index has to be either 0 (for P1) or 1 (for P2)
|
||||||
@@ -298,6 +321,10 @@ stream over plain HTTP. Enable it with `-apistream`. It listens on the API port
|
|||||||
plus two, in the same way the WebSocket server uses the API port plus one, so
|
plus two, in the same way the WebSocket server uses the API port plus one, so
|
||||||
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
|
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
|
||||||
|
|
||||||
|
Rather than working the port out, clients should ask the JSON API for it with
|
||||||
|
`capture.get_streams()`, which also reports which of the formats below this
|
||||||
|
build serves, the size of each screen and whether one is already taken.
|
||||||
|
|
||||||
Two formats are served:
|
Two formats are served:
|
||||||
|
|
||||||
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
|
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
|
||||||
@@ -323,9 +350,10 @@ The stream is view only. Touch and other input still go through the JSON API,
|
|||||||
so a companion app needs both. There is no authentication on the stream port -
|
so a companion app needs both. There is no authentication on the stream port -
|
||||||
anyone who can reach it can watch the screen.
|
anyone who can reach it can watch the screen.
|
||||||
|
|
||||||
WinXP builds have no video stream. Neither encoder is compiled in, so every
|
WinXP builds have no video stream. Neither encoder is compiled in, so nothing
|
||||||
endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for
|
listens on the stream port even with `-apistream`, `capture.get_streams()`
|
||||||
the same reason.
|
returns no data, and the JSON API's JPEG screen capture is unavailable for the
|
||||||
|
same reason.
|
||||||
|
|
||||||
## Native wrapper libraries
|
## Native wrapper libraries
|
||||||
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "bmpu.h"
|
#include "bmpu.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "acio/icca/icca.h"
|
#include "acio/icca/icca.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "cfg/api.h"
|
#include "cfg/api.h"
|
||||||
@@ -9,6 +11,7 @@
|
|||||||
#include "games/ftt/io.h"
|
#include "games/ftt/io.h"
|
||||||
#include "games/museca/io.h"
|
#include "games/museca/io.h"
|
||||||
#include "games/silentscope/io.h"
|
#include "games/silentscope/io.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
|
||||||
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
|
|||||||
STATUS_BUFFER[4] |= 0x20;
|
STATUS_BUFFER[4] |= 0x20;
|
||||||
}
|
}
|
||||||
|
|
||||||
// joy stick raw input
|
// gun position, sent big endian; the game keeps the top 12 bits of each pair
|
||||||
auto &analogs = games::silentscope::get_analogs();
|
auto &analogs = games::silentscope::get_analogs();
|
||||||
|
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
|
||||||
|
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
|
||||||
unsigned short joy_x = 0x7FFF;
|
unsigned short joy_x = 0x7FFF;
|
||||||
unsigned short joy_y = 0x7FFF;
|
unsigned short joy_y = 0x7FFF;
|
||||||
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
|
|
||||||
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
|
if (gun_x.isSet() || gun_y.isSet()) {
|
||||||
|
if (gun_x.isSet()) {
|
||||||
|
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
|
||||||
|
}
|
||||||
|
if (gun_y.isSet()) {
|
||||||
|
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// the gun reports where it is aimed, which a relative pointer cannot express - the
|
||||||
|
// calibration screen asks for the screen centre and two corners, so read the cursor
|
||||||
|
POINT cursor {};
|
||||||
|
RECT client {};
|
||||||
|
POINT origin {};
|
||||||
|
if (NDD_MAIN_WINDOW != nullptr &&
|
||||||
|
GetCursorPos(&cursor) &&
|
||||||
|
GetClientRect(NDD_MAIN_WINDOW, &client) &&
|
||||||
|
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
|
||||||
|
client.right > 1 && client.bottom > 1)
|
||||||
|
{
|
||||||
|
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
|
||||||
|
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
|
||||||
|
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
|
||||||
|
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
|
||||||
}
|
}
|
||||||
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
|
|
||||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// invert X axis
|
// invert X axis
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "capture_pump.h"
|
#include "capture_pump.h"
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
@@ -11,8 +12,8 @@ namespace api::capture_pump {
|
|||||||
|
|
||||||
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
|
std::array<std::mutex, GRAPHICS_CAPTURE_SCREEN_NO> CONSUMER_M;
|
||||||
|
|
||||||
std::mutex CLAIMED_M;
|
// read once per capture from the present thread, so it stays lock free
|
||||||
std::array<bool, GRAPHICS_CAPTURE_SCREEN_NO> CLAIMED {};
|
std::atomic<bool> CLAIMED[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
|
|
||||||
bool valid_screen(int screen) {
|
bool valid_screen(int screen) {
|
||||||
return 0 <= screen && screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO);
|
return 0 <= screen && screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO);
|
||||||
@@ -37,14 +38,7 @@ namespace api::capture_pump {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(CLAIMED_M);
|
return !CLAIMED[screen].exchange(true);
|
||||||
|
|
||||||
if (CLAIMED[screen]) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
CLAIMED[screen] = true;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void release_screen(int screen) {
|
void release_screen(int screen) {
|
||||||
@@ -52,7 +46,14 @@ namespace api::capture_pump {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(CLAIMED_M);
|
|
||||||
CLAIMED[screen] = false;
|
CLAIMED[screen] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool screen_claimed(int screen) {
|
||||||
|
if (!valid_screen(screen)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CLAIMED[screen];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,4 +21,7 @@ namespace api::capture_pump {
|
|||||||
// a screen carries one stream at a time; false when another connection already holds it
|
// a screen carries one stream at a time; false when another connection already holds it
|
||||||
bool claim_screen(int screen);
|
bool claim_screen(int screen);
|
||||||
void release_screen(int screen);
|
void release_screen(int screen);
|
||||||
|
|
||||||
|
// true while a video stream client holds this screen
|
||||||
|
bool screen_claimed(int screen);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,9 +337,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
|
|||||||
if (module->name == request.module) {
|
if (module->name == request.module) {
|
||||||
module_found = true;
|
module_found = true;
|
||||||
|
|
||||||
// check password force
|
// check password requirement
|
||||||
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
|
if (module->requires_password(request.function)
|
||||||
Value err("Module requires the password to be set.");
|
&& this->password.empty()
|
||||||
|
&& request.function != "session_refresh") {
|
||||||
|
Value err("Function requires the password to be set.");
|
||||||
response.add_error(err);
|
response.add_error(err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,13 @@ namespace api {
|
|||||||
param.i_height = height;
|
param.i_height = height;
|
||||||
param.i_fps_num = this->fps;
|
param.i_fps_num = this->fps;
|
||||||
param.i_fps_den = 1;
|
param.i_fps_den = 1;
|
||||||
param.i_threads = 1;
|
|
||||||
|
// sliced threading, which zerolatency already selected, so a frame is split
|
||||||
|
// across workers rather than held back to be reordered. deliberately not the
|
||||||
|
// automatic count: this shares a machine with the game it is capturing, and
|
||||||
|
// taking every core to encode would win back frames at the game's expense
|
||||||
|
param.i_threads = 4;
|
||||||
|
|
||||||
param.b_annexb = 1;
|
param.b_annexb = 1;
|
||||||
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
||||||
param.b_repeat_headers = 1;
|
param.b_repeat_headers = 1;
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ namespace api {
|
|||||||
this->password_force = password_force;
|
this->password_force = password_force;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Module::require_password(const std::string &function) {
|
||||||
|
this->password_force_functions.emplace(function);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Module::requires_password(const std::string &function) const {
|
||||||
|
return this->password_force || this->password_force_functions.contains(function);
|
||||||
|
}
|
||||||
|
|
||||||
void Module::handle(Request &req, Response &res) {
|
void Module::handle(Request &req, Response &res) {
|
||||||
|
|
||||||
// log module access
|
// log module access
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <unordered_set>
|
||||||
#include <external/robin_hood.h>
|
#include <external/robin_hood.h>
|
||||||
|
|
||||||
#include "response.h"
|
#include "response.h"
|
||||||
@@ -26,6 +27,13 @@ namespace api {
|
|||||||
// default constructor
|
// default constructor
|
||||||
explicit Module(std::string name, bool password_force=false);
|
explicit Module(std::string name, bool password_force=false);
|
||||||
|
|
||||||
|
void require_password(const std::string &function);
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
// functions which expose sensitive data or actions
|
||||||
|
std::unordered_set<std::string> password_force_functions;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
// virtual deconstructor
|
// virtual deconstructor
|
||||||
@@ -35,6 +43,8 @@ namespace api {
|
|||||||
std::string name;
|
std::string name;
|
||||||
bool password_force;
|
bool password_force;
|
||||||
|
|
||||||
|
bool requires_password(const std::string &function) const;
|
||||||
|
|
||||||
// the magic
|
// the magic
|
||||||
void handle(Request &req, Response &res);
|
void handle(Request &req, Response &res);
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#include "capture.h"
|
#include "capture.h"
|
||||||
|
#include <atomic>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include "api/capture_pump.h"
|
#include "api/capture_pump.h"
|
||||||
|
#include "api/stream_format.h"
|
||||||
|
#include "api/stream_server.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "hooks/graphics/jpeg_encoder.h"
|
#include "hooks/graphics/jpeg_encoder.h"
|
||||||
@@ -71,6 +74,7 @@ namespace api::modules {
|
|||||||
Capture::Capture() : Module("capture") {
|
Capture::Capture() : Module("capture") {
|
||||||
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
|
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
|
||||||
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
|
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
|
||||||
|
functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,4 +145,98 @@ namespace api::modules {
|
|||||||
CAPTURE_BUFFER.clear();
|
CAPTURE_BUFFER.clear();
|
||||||
try_cached_response(screen, res);
|
try_cached_response(screen, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get_streams()
|
||||||
|
*/
|
||||||
|
void Capture::get_streams(Request &req, Response &res) {
|
||||||
|
|
||||||
|
auto &alloc = res.doc()->GetAllocator();
|
||||||
|
|
||||||
|
// nothing is listening without -apistream, so there is no stream to describe
|
||||||
|
const unsigned short port = stream_server_port();
|
||||||
|
if (port == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value formats(kArrayType);
|
||||||
|
for (const auto &[name, path] : stream_formats()) {
|
||||||
|
Value entry(kObjectType);
|
||||||
|
entry.AddMember("name", Value(name.c_str(), alloc), alloc);
|
||||||
|
entry.AddMember("path", Value(path.c_str(), alloc), alloc);
|
||||||
|
formats.PushBack(entry, alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int> screen_numbers;
|
||||||
|
graphics_screens_get(screen_numbers);
|
||||||
|
|
||||||
|
// measuring a screen nobody has captured yet waits for the game to present, which can
|
||||||
|
// take as long as the whole request is allowed, so only one screen is measured per
|
||||||
|
// call and the rest are reported null until a later one settles them. which screen
|
||||||
|
// gets the attempt rotates, otherwise one that never presents would take every
|
||||||
|
// request and the screens behind it would stay unmeasured forever
|
||||||
|
int probe_screen = -1;
|
||||||
|
{
|
||||||
|
std::vector<int> unmeasured;
|
||||||
|
for (const auto screen : screen_numbers) {
|
||||||
|
if (screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
|
||||||
|
&& !graphics_capture_last_size(screen, nullptr, nullptr)
|
||||||
|
&& !capture_pump::screen_claimed(screen)) {
|
||||||
|
unmeasured.push_back(screen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!unmeasured.empty()) {
|
||||||
|
static std::atomic<unsigned> probe_cursor { 0 };
|
||||||
|
probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value screens(kArrayType);
|
||||||
|
for (const auto screen : screen_numbers) {
|
||||||
|
if (screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
bool known = graphics_capture_last_size(screen, &width, &height);
|
||||||
|
|
||||||
|
// a probe holds the screen for as long as it waits, so a second caller arriving
|
||||||
|
// during one would queue behind it and then take a wait of its own; let it report
|
||||||
|
// the screen as unmeasured instead and pick the size up once the first is done
|
||||||
|
static std::atomic<bool> probe_running { false };
|
||||||
|
if (!known && screen == probe_screen && !probe_running.exchange(true)) {
|
||||||
|
std::shared_ptr<uint8_t[]> pixels;
|
||||||
|
known = capture_pump::capture_direct(
|
||||||
|
screen, pixels, 1, nullptr, &width, &height);
|
||||||
|
probe_running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a screen of unknown size cannot be described, and a client told about it could
|
||||||
|
// not size its decoder anyway; leaving it out until it has been measured beats
|
||||||
|
// handing over an entry that has to be treated as absent
|
||||||
|
if (!known) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value entry(kObjectType);
|
||||||
|
entry.AddMember("screen", screen, alloc);
|
||||||
|
entry.AddMember("width", width, alloc);
|
||||||
|
entry.AddMember("height", height, alloc);
|
||||||
|
|
||||||
|
// a screen carries one viewer at a time, so this is what decides whether a client
|
||||||
|
// can connect at all; still racy by the time it does, only more honest than not
|
||||||
|
entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc);
|
||||||
|
|
||||||
|
screens.PushBack(entry, alloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Value info(kObjectType);
|
||||||
|
info.AddMember("port", port, alloc);
|
||||||
|
info.AddMember("formats", formats, alloc);
|
||||||
|
info.AddMember("screens", screens, alloc);
|
||||||
|
|
||||||
|
res.add_data(info);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,5 +19,6 @@ namespace api::modules {
|
|||||||
// function definitions
|
// function definitions
|
||||||
void get_screens(Request &req, Response &res);
|
void get_screens(Request &req, Response &res);
|
||||||
void get_jpg(Request &req, Response &res);
|
void get_jpg(Request &req, Response &res);
|
||||||
|
void get_streams(Request &req, Response &res);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "card.h"
|
#include "card.h"
|
||||||
|
#include <fstream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -11,8 +12,71 @@ using namespace rapidjson;
|
|||||||
|
|
||||||
namespace api::modules {
|
namespace api::modules {
|
||||||
|
|
||||||
|
static bool normalize_card_id(const std::string &value, std::string &card_id) {
|
||||||
|
if (value.size() != 16) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t card_bin[8] {};
|
||||||
|
if (!hex2bin(value.c_str(), card_bin)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
card_id = bin2hex(card_bin, std::size(card_bin));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
|
||||||
|
std::ifstream file(path);
|
||||||
|
char buffer[16] {};
|
||||||
|
if (!file.read(buffer, std::size(buffer))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
|
||||||
|
}
|
||||||
|
|
||||||
Card::Card() : Module("card") {
|
Card::Card() : Module("card") {
|
||||||
|
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
|
||||||
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
|
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
|
||||||
|
require_password("get_cards");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get_cards()
|
||||||
|
*/
|
||||||
|
void Card::get_cards(Request &req, Response &res) {
|
||||||
|
auto &alloc = res.doc()->GetAllocator();
|
||||||
|
|
||||||
|
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
|
||||||
|
std::string card_id;
|
||||||
|
std::string filename;
|
||||||
|
const auto card_override = eamuse_get_card_override(index);
|
||||||
|
const bool has_override = !card_override.empty();
|
||||||
|
|
||||||
|
if (has_override) {
|
||||||
|
if (!normalize_card_id(card_override, card_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const auto path = eamuse_get_card_path(index);
|
||||||
|
if (!read_card_id(path, card_id)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto filename_u8 = path.filename().u8string();
|
||||||
|
filename.assign(filename_u8.begin(), filename_u8.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value card(kObjectType);
|
||||||
|
card.AddMember("index", index, alloc);
|
||||||
|
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
|
||||||
|
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
|
||||||
|
if (!has_override) {
|
||||||
|
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
|
||||||
|
}
|
||||||
|
res.add_data(card);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
|||||||
private:
|
private:
|
||||||
|
|
||||||
// function definitions
|
// function definitions
|
||||||
|
void get_cards(Request &req, Response &res);
|
||||||
void insert(Request &req, Response &res);
|
void insert(Request &req, Response &res);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ part "src/exceptions.dart";
|
|||||||
part "src/rc4.dart";
|
part "src/rc4.dart";
|
||||||
part "src/wrappers/analogs.dart";
|
part "src/wrappers/analogs.dart";
|
||||||
part "src/wrappers/buttons.dart";
|
part "src/wrappers/buttons.dart";
|
||||||
|
part "src/wrappers/capture.dart";
|
||||||
part "src/wrappers/card.dart";
|
part "src/wrappers/card.dart";
|
||||||
part "src/wrappers/coin.dart";
|
part "src/wrappers/coin.dart";
|
||||||
part "src/wrappers/control.dart";
|
part "src/wrappers/control.dart";
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ part of spiceapi;
|
|||||||
class Connection {
|
class Connection {
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
static const _TIMEOUT = Duration(seconds: 2);
|
static const _TIMEOUT = Duration(seconds: 3);
|
||||||
static const _BUFFER_SIZE = 1024 * 8;
|
static const _BUFFER_SIZE = 1024 * 1024 * 8;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
final String host, pass;
|
final String host, pass;
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
part of spiceapi;
|
||||||
|
|
||||||
|
class CaptureData {
|
||||||
|
int timestamp;
|
||||||
|
int width, height;
|
||||||
|
Uint8List data;
|
||||||
|
}
|
||||||
|
|
||||||
|
var _base64DecoderInstance = Base64Decoder();
|
||||||
|
|
||||||
|
Future<List> captureGetScreens(Connection con) {
|
||||||
|
var req = Request("capture", "get_screens");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
return res.getData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<CaptureData> captureGetJPG(Connection con, {
|
||||||
|
int screen = 0,
|
||||||
|
int quality = 70,
|
||||||
|
int divide = 1,
|
||||||
|
}) {
|
||||||
|
var req = Request("capture", "get_jpg");
|
||||||
|
req.addParam(screen);
|
||||||
|
req.addParam(quality);
|
||||||
|
req.addParam(divide);
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
var captureData = CaptureData();
|
||||||
|
var data = res.getData();
|
||||||
|
if (data.length > 0) captureData.timestamp = data[0];
|
||||||
|
if (data.length > 1) captureData.width = data[1];
|
||||||
|
if (data.length > 2) captureData.height = data[2];
|
||||||
|
if (data.length > 3) {
|
||||||
|
captureData.data = _base64DecoderInstance.convert(data[3]);
|
||||||
|
}
|
||||||
|
return captureData;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||||
|
Future<Map> captureGetStreams(Connection con) {
|
||||||
|
var req = Request("capture", "get_streams");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
var data = res.getData();
|
||||||
|
return data.length > 0 ? data[0] : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
part of spiceapi;
|
||||||
|
|
||||||
|
class CardInfo {
|
||||||
|
final int index;
|
||||||
|
final String cardID;
|
||||||
|
final String source;
|
||||||
|
final String? fileName;
|
||||||
|
|
||||||
|
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||||
|
var req = Request("card", "get_cards");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
List<CardInfo> cards = [];
|
||||||
|
for (var value in res.getData()) {
|
||||||
|
cards.add(
|
||||||
|
CardInfo(
|
||||||
|
value["index"],
|
||||||
|
value["card_id"],
|
||||||
|
value["source"],
|
||||||
|
value["file_name"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ Future<List> captureGetScreens(Connection con) {
|
|||||||
|
|
||||||
Future<CaptureData> captureGetJPG(Connection con, {
|
Future<CaptureData> captureGetJPG(Connection con, {
|
||||||
int screen = 0,
|
int screen = 0,
|
||||||
int quality = 60,
|
int quality = 70,
|
||||||
int divide = 1,
|
int divide = 1,
|
||||||
}) {
|
}) {
|
||||||
var req = Request("capture", "get_jpg");
|
var req = Request("capture", "get_jpg");
|
||||||
@@ -36,3 +36,12 @@ Future<CaptureData> captureGetJPG(Connection con, {
|
|||||||
return captureData;
|
return captureData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describes the HTTP video stream, or null when this spice2x serves none.
|
||||||
|
Future<Map> captureGetStreams(Connection con) {
|
||||||
|
var req = Request("capture", "get_streams");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
var data = res.getData();
|
||||||
|
return data.length > 0 ? data[0] : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
part of spiceapi;
|
||||||
|
|
||||||
|
class CardInfo {
|
||||||
|
final int index;
|
||||||
|
final String cardID;
|
||||||
|
final String source;
|
||||||
|
final String? fileName;
|
||||||
|
|
||||||
|
CardInfo(this.index, this.cardID, this.source, this.fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<CardInfo>> cardGetCards(Connection con) {
|
||||||
|
var req = Request("card", "get_cards");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
List<CardInfo> cards = [];
|
||||||
|
for (var value in res.getData()) {
|
||||||
|
cards.add(
|
||||||
|
CardInfo(
|
||||||
|
value["index"],
|
||||||
|
value["card_id"],
|
||||||
|
value["source"],
|
||||||
|
value["file_name"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from .connection import Connection
|
|||||||
from .request import Request
|
from .request import Request
|
||||||
from .analogs import *
|
from .analogs import *
|
||||||
from .buttons import *
|
from .buttons import *
|
||||||
|
from .capture import *
|
||||||
from .card import *
|
from .card import *
|
||||||
from .coin import *
|
from .coin import *
|
||||||
from .control import *
|
from .control import *
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import base64
|
||||||
|
|
||||||
|
from .connection import Connection
|
||||||
|
from .request import Request
|
||||||
|
|
||||||
|
|
||||||
|
def capture_get_screens(con: Connection):
|
||||||
|
res = con.request(Request("capture", "get_screens"))
|
||||||
|
return res.get_data()
|
||||||
|
|
||||||
|
|
||||||
|
def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1):
|
||||||
|
req = Request("capture", "get_jpg")
|
||||||
|
req.add_param(screen)
|
||||||
|
req.add_param(quality)
|
||||||
|
req.add_param(divide)
|
||||||
|
data = con.request(req).get_data()
|
||||||
|
|
||||||
|
if len(data) < 4:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"timestamp": data[0],
|
||||||
|
"width": data[1],
|
||||||
|
"height": data[2],
|
||||||
|
"data": base64.b64decode(data[3]),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def capture_get_streams(con: Connection):
|
||||||
|
"""Describes the HTTP video stream, or None when this spice2x serves none."""
|
||||||
|
data = con.request(Request("capture", "get_streams")).get_data()
|
||||||
|
return data[0] if data else None
|
||||||
@@ -2,6 +2,10 @@ from .connection import Connection
|
|||||||
from .request import Request
|
from .request import Request
|
||||||
|
|
||||||
|
|
||||||
|
def card_get_cards(con: Connection):
|
||||||
|
return con.request(Request("card", "get_cards")).get_data()
|
||||||
|
|
||||||
|
|
||||||
def card_insert(con: Connection, unit: int, card_id: str):
|
def card_insert(con: Connection, unit: int, card_id: str):
|
||||||
req = Request("card", "insert")
|
req = Request("card", "insert")
|
||||||
req.add_param(unit)
|
req.add_param(unit)
|
||||||
|
|||||||
@@ -67,4 +67,18 @@ namespace api {
|
|||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> stream_formats() {
|
||||||
|
std::vector<std::pair<std::string, std::string>> formats;
|
||||||
|
|
||||||
|
#ifdef SPICE_JPEG
|
||||||
|
formats.emplace_back("mjpeg", "/stream.mjpg");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef SPICE_H264
|
||||||
|
formats.emplace_back("h264", "/stream.h264");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return formats;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "capture_pump.h"
|
#include "capture_pump.h"
|
||||||
|
|
||||||
@@ -35,4 +37,7 @@ namespace api {
|
|||||||
// null when the path does not name a format this build supports
|
// null when the path does not name a format this build supports
|
||||||
std::unique_ptr<StreamWriter> make_stream_writer(
|
std::unique_ptr<StreamWriter> make_stream_writer(
|
||||||
const std::string &path, int quality, int fps);
|
const std::string &path, int quality, int fps);
|
||||||
|
|
||||||
|
// name and path of every format compiled into this build, for clients to pick from
|
||||||
|
std::vector<std::pair<std::string, std::string>> stream_formats();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "capture_pump.h"
|
#include "capture_pump.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
#include "stream_format.h"
|
#include "stream_format.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -178,6 +179,13 @@ namespace api {
|
|||||||
// carry it too, or the client sees an opaque failure instead of the status.
|
// carry it too, or the client sees an opaque failure instead of the status.
|
||||||
constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n";
|
constexpr const char *cors_header = "Access-Control-Allow-Origin: *\r\n";
|
||||||
|
|
||||||
|
// the port is unauthenticated, so a scanner hammering a busy/missing screen could
|
||||||
|
// otherwise flood the overlay; throttle failure toasts per distinct cause. kept under
|
||||||
|
// a second so it only swallows that, not a legitimate reconnect - substream itself
|
||||||
|
// switches screens with a 300ms gap, and only backs off to a full second once a
|
||||||
|
// retry has actually failed
|
||||||
|
constexpr double notification_throttle_seconds = 0.5;
|
||||||
|
|
||||||
void send_error(SOCKET socket, const char *status) {
|
void send_error(SOCKET socket, const char *status) {
|
||||||
const std::string response =
|
const std::string response =
|
||||||
std::string("HTTP/1.0 ") + status + "\r\n"
|
std::string("HTTP/1.0 ") + status + "\r\n"
|
||||||
@@ -187,11 +195,25 @@ namespace api {
|
|||||||
"\r\n";
|
"\r\n";
|
||||||
send_all(socket, response);
|
send_all(socket, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::atomic<unsigned short> LISTENING_PORT { 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned short stream_server_port() {
|
||||||
|
return LISTENING_PORT.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamServer::StreamServer(unsigned short port)
|
StreamServer::StreamServer(unsigned short port)
|
||||||
: port(port)
|
: port(port)
|
||||||
{
|
{
|
||||||
|
// WinXP builds compile in neither encoder, so there would be nothing to serve and
|
||||||
|
// every request would 404; taking the port instead only invites confused clients
|
||||||
|
if (stream_formats().empty()) {
|
||||||
|
log_warning("api::stream",
|
||||||
|
"this build has no video encoders, the video stream is unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this->open_listener()) {
|
if (!this->open_listener()) {
|
||||||
// the stream was asked for explicitly, so say plainly that it is not there
|
// the stream was asked for explicitly, so say plainly that it is not there
|
||||||
log_warning("api::stream",
|
log_warning("api::stream",
|
||||||
@@ -204,6 +226,8 @@ namespace api {
|
|||||||
this->accept_worker();
|
this->accept_worker();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
LISTENING_PORT = this->port;
|
||||||
|
|
||||||
// deliberately not logging a full URL; local IPs would leak into shared logs
|
// deliberately not logging a full URL; local IPs would leak into shared logs
|
||||||
log_info("api::stream", "video stream is listening on port: {}", this->port);
|
log_info("api::stream", "video stream is listening on port: {}", this->port);
|
||||||
log_warning("api::stream",
|
log_warning("api::stream",
|
||||||
@@ -261,6 +285,7 @@ namespace api {
|
|||||||
StreamServer::~StreamServer() {
|
StreamServer::~StreamServer() {
|
||||||
|
|
||||||
this->running = false;
|
this->running = false;
|
||||||
|
LISTENING_PORT = 0;
|
||||||
|
|
||||||
if (this->listener != INVALID_SOCKET) {
|
if (this->listener != INVALID_SOCKET) {
|
||||||
closesocket(this->listener);
|
closesocket(this->listener);
|
||||||
@@ -334,6 +359,11 @@ namespace api {
|
|||||||
|
|
||||||
if (slot < 0) {
|
if (slot < 0) {
|
||||||
log_warning("api::stream", "client limit of {} hit", client_limit);
|
log_warning("api::stream", "client limit of {} hit", client_limit);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Warning,
|
||||||
|
"api::stream.client_limit",
|
||||||
|
notification_throttle_seconds,
|
||||||
|
fmt::format("Video stream refused: client limit reached ({})", address));
|
||||||
send_error(client, "503 Service Unavailable");
|
send_error(client, "503 Service Unavailable");
|
||||||
closesocket(client);
|
closesocket(client);
|
||||||
continue;
|
continue;
|
||||||
@@ -410,16 +440,32 @@ namespace api {
|
|||||||
if (!streamable(screen)) {
|
if (!streamable(screen)) {
|
||||||
log_warning("api::stream",
|
log_warning("api::stream",
|
||||||
"screen {} is not available, refusing {}", screen, address);
|
"screen {} is not available, refusing {}", screen, address);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Warning,
|
||||||
|
fmt::format("api::stream.screen_unavailable.{}", screen),
|
||||||
|
notification_throttle_seconds,
|
||||||
|
fmt::format("Video stream refused: screen {} not available ({})",
|
||||||
|
screen, address));
|
||||||
send_error(socket, "404 Not Found");
|
send_error(socket, "404 Not Found");
|
||||||
} else if (!capture_pump::claim_screen(screen)) {
|
} else if (!capture_pump::claim_screen(screen)) {
|
||||||
log_warning("api::stream",
|
log_warning("api::stream",
|
||||||
"screen {} is already being streamed, refusing {}",
|
"screen {} is already being streamed, refusing {}",
|
||||||
screen, address);
|
screen, address);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Warning,
|
||||||
|
fmt::format("api::stream.screen_claimed.{}", screen),
|
||||||
|
notification_throttle_seconds,
|
||||||
|
fmt::format("Video stream refused: screen {} already streaming ({})",
|
||||||
|
screen, address));
|
||||||
send_error(socket, "503 Service Unavailable");
|
send_error(socket, "503 Service Unavailable");
|
||||||
} else {
|
} else {
|
||||||
log_info("api::stream",
|
log_info("api::stream",
|
||||||
"client connected: {} ({}, screen={}, fps={}, quality={})",
|
"client connected: {} ({}, screen={}, fps={}, quality={})",
|
||||||
address, request.path, screen, fps, quality);
|
address, request.path, screen, fps, quality);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("Video stream client connected ({}, screen {})",
|
||||||
|
address, screen));
|
||||||
|
|
||||||
const std::string header =
|
const std::string header =
|
||||||
"HTTP/1.0 200 OK\r\n"
|
"HTTP/1.0 200 OK\r\n"
|
||||||
@@ -460,6 +506,9 @@ namespace api {
|
|||||||
|
|
||||||
capture_pump::release_screen(screen);
|
capture_pump::release_screen(screen);
|
||||||
log_info("api::stream", "client disconnected: {}", address);
|
log_info("api::stream", "client disconnected: {}", address);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("Video stream client disconnected ({})", address));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
|
// 0 while no stream server is listening, so the API can tell clients not to look for one
|
||||||
|
unsigned short stream_server_port();
|
||||||
|
|
||||||
class StreamServer {
|
class StreamServer {
|
||||||
public:
|
public:
|
||||||
|
|
||||||
|
|||||||
+536
-162
@@ -1,205 +1,579 @@
|
|||||||
#define HEADSOCKET_IMPLEMENTATION
|
#include <winsock2.h>
|
||||||
#include "external/headsocket.h"
|
#include <ws2tcpip.h>
|
||||||
|
|
||||||
#include "websocket.h"
|
#include "websocket.h"
|
||||||
#include "util/utils.h"
|
|
||||||
#include "util/rc4.h"
|
|
||||||
#include "util/logging.h"
|
|
||||||
#include "overlay/notifications.h"
|
|
||||||
#include "controller.h"
|
|
||||||
|
|
||||||
using namespace headsocket;
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
|
#include <cctype>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "external/wslay/msvc_compat.h"
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
#include "controller.h"
|
||||||
|
#include "external/hash-library/sha1.h"
|
||||||
|
#include "overlay/notifications.h"
|
||||||
|
#include "util/crypt.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
namespace api {
|
namespace api {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// how long a single handshake read may stall before the connection is dropped;
|
constexpr int server_backlog = 4;
|
||||||
// headsocket reads the request a byte at a time, so this is an idle timeout between
|
constexpr size_t client_limit = 8;
|
||||||
// bytes rather than a deadline for the whole handshake
|
|
||||||
constexpr int handshake_timeout_ms = 5000;
|
|
||||||
|
|
||||||
void set_recv_timeout(connection &conn, int milliseconds) {
|
// a peer that connects and then says nothing must not hold a slot forever
|
||||||
DWORD timeout = static_cast<DWORD>(milliseconds);
|
constexpr int handshake_timeout_ms = 5000;
|
||||||
setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO,
|
constexpr size_t request_size_limit = 8 * 1024;
|
||||||
|
constexpr uint64_t message_size_limit = 64 * 1024;
|
||||||
|
|
||||||
|
// how long a quiet connection waits before the loop rechecks whether we are stopping
|
||||||
|
constexpr int idle_poll_ms = 500;
|
||||||
|
|
||||||
|
// RFC 6455 appends this to the client key before hashing
|
||||||
|
constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||||
|
|
||||||
|
constexpr double notification_throttle_seconds = 0.5;
|
||||||
|
|
||||||
|
std::string trim(const std::string &text) {
|
||||||
|
const auto begin = text.find_first_not_of(" \t");
|
||||||
|
if (begin == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return text.substr(begin, text.find_last_not_of(" \t") - begin + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string to_lower(std::string text) {
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
|
||||||
|
return static_cast<char>(std::tolower(c));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool contains_token(const std::string &value, const std::string &expected) {
|
||||||
|
size_t pos = 0;
|
||||||
|
while (pos < value.size()) {
|
||||||
|
const size_t end = value.find(',', pos);
|
||||||
|
if (to_lower(trim(value.substr(pos, end - pos))) == expected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (end == std::string::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos = end + 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool valid_websocket_key(const std::string &key) {
|
||||||
|
if (key.size() != 24 || key[22] != '=' || key[23] != '=') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string alphabet =
|
||||||
|
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
for (size_t i = 0; i < 22; i++) {
|
||||||
|
if (alphabet.find(key[i]) == std::string::npos) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool send_all(SOCKET socket, const std::string &text) {
|
||||||
|
size_t remaining = text.size();
|
||||||
|
const char *cursor = text.data();
|
||||||
|
|
||||||
|
while (remaining > 0) {
|
||||||
|
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
|
||||||
|
if (sent <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cursor += sent;
|
||||||
|
remaining -= static_cast<size_t>(sent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_recv_timeout(SOCKET socket, int milliseconds) {
|
||||||
|
const DWORD timeout = static_cast<DWORD>(milliseconds);
|
||||||
|
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
|
||||||
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string accept_key(const std::string &client_key) {
|
||||||
|
SHA1 sha1;
|
||||||
|
sha1.add(client_key.data(), client_key.size());
|
||||||
|
sha1.add(websocket_guid, strlen(websocket_guid));
|
||||||
|
|
||||||
|
unsigned char digest[SHA1::HashBytes] {};
|
||||||
|
sha1.getHash(digest);
|
||||||
|
|
||||||
|
return crypt::base64_encode(digest, sizeof(digest));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
bool handshake(SOCKET socket) {
|
||||||
* Client class declaration
|
std::string request;
|
||||||
*/
|
char byte = 0;
|
||||||
class WebSocketClient : public web_socket_client {
|
bool complete = false;
|
||||||
|
|
||||||
// required class header
|
while (request.size() < request_size_limit) {
|
||||||
HEADSOCKET_CLIENT(WebSocketClient, web_socket_client);
|
const int read = recv(socket, &byte, 1, 0);
|
||||||
|
if (read <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
request.push_back(byte);
|
||||||
|
if (request.size() >= 4
|
||||||
|
&& request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) {
|
||||||
|
complete = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string key;
|
||||||
|
std::string version;
|
||||||
|
bool connection_upgrade = false;
|
||||||
|
bool upgrade = false;
|
||||||
|
const size_t request_line_end = request.find("\r\n");
|
||||||
|
const std::string request_line = request.substr(0, request_line_end);
|
||||||
|
const size_t target_end = request_line.find(' ', 4);
|
||||||
|
const bool valid_request_line = request_line_end != std::string::npos
|
||||||
|
&& request_line.compare(0, 4, "GET ") == 0
|
||||||
|
&& target_end != std::string::npos
|
||||||
|
&& target_end > 4
|
||||||
|
&& target_end == request_line.rfind(' ')
|
||||||
|
&& request_line.substr(target_end + 1) == "HTTP/1.1";
|
||||||
|
size_t pos = request_line_end;
|
||||||
|
|
||||||
|
while (pos != std::string::npos) {
|
||||||
|
const size_t end = request.find("\r\n", pos + 2);
|
||||||
|
if (end == std::string::npos || end == pos + 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string line = request.substr(pos + 2, end - pos - 2);
|
||||||
|
const size_t colon = line.find(':');
|
||||||
|
if (colon != std::string::npos) {
|
||||||
|
const std::string name = to_lower(trim(line.substr(0, colon)));
|
||||||
|
const std::string value = trim(line.substr(colon + 1));
|
||||||
|
|
||||||
|
if (name == "sec-websocket-key") {
|
||||||
|
key = value;
|
||||||
|
} else if (name == "sec-websocket-version") {
|
||||||
|
version = value;
|
||||||
|
} else if (name == "connection") {
|
||||||
|
connection_upgrade = connection_upgrade
|
||||||
|
|| contains_token(value, "upgrade");
|
||||||
|
} else if (name == "upgrade") {
|
||||||
|
upgrade = upgrade || contains_token(value, "websocket");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pos = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!complete || !valid_request_line || !connection_upgrade || !upgrade
|
||||||
|
|| version != "13" || !valid_websocket_key(key)) {
|
||||||
|
send_all(socket,
|
||||||
|
"HTTP/1.1 400 Bad Request\r\n"
|
||||||
|
"Connection: close\r\n"
|
||||||
|
"Content-Length: 0\r\n"
|
||||||
|
"\r\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deliberately echoes back no extension or subprotocol: naming one the client did
|
||||||
|
// not offer is a handshake failure, and none of them are wanted here
|
||||||
|
return send_all(socket,
|
||||||
|
"HTTP/1.1 101 Switching Protocols\r\n"
|
||||||
|
"Upgrade: websocket\r\n"
|
||||||
|
"Connection: Upgrade\r\n"
|
||||||
|
"Sec-WebSocket-Accept: " + accept_key(key) + "\r\n"
|
||||||
|
"\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything one connection needs; wslay hands this back to the callbacks
|
||||||
|
struct Session {
|
||||||
|
SOCKET socket = INVALID_SOCKET;
|
||||||
|
Controller *controller = nullptr;
|
||||||
ClientState *state = nullptr;
|
ClientState *state = nullptr;
|
||||||
|
bool failed = false;
|
||||||
protected:
|
|
||||||
bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override;
|
|
||||||
|
|
||||||
void on_accept() override;
|
|
||||||
void on_disconnect() override;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length,
|
||||||
* Server class declaration
|
int flags, void *user_data) {
|
||||||
*/
|
|
||||||
class WebSocketServer : public web_socket_server<WebSocketClient> {
|
|
||||||
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
|
|
||||||
public:
|
|
||||||
WebSocketController *websocket;
|
|
||||||
|
|
||||||
protected:
|
(void) flags;
|
||||||
bool handshake(connection &conn) override {
|
|
||||||
|
|
||||||
// headsocket runs the handshake on its single accept thread with a blocking
|
auto *session = static_cast<Session *>(user_data);
|
||||||
// recv, so a peer that connects and then says nothing would park that thread and
|
const int read = recv(session->socket, reinterpret_cast<char *>(buffer),
|
||||||
// leave every later connection sitting unaccepted in the backlog
|
static_cast<int>(length), 0);
|
||||||
set_recv_timeout(conn, handshake_timeout_ms);
|
|
||||||
const bool accepted = base_t::handshake(conn);
|
|
||||||
|
|
||||||
// from here the client thread owns the socket and wants to block on reads
|
if (read > 0) {
|
||||||
set_recv_timeout(conn, 0);
|
return read;
|
||||||
|
|
||||||
return accepted;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
void api::WebSocketServer::init() {}
|
// the socket is non-blocking, so an empty one has to read as "nothing yet"
|
||||||
|
// rather than as a dead peer
|
||||||
|
if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||||
|
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wslay_event_set_error(ctx,
|
||||||
|
read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length,
|
||||||
|
int flags, void *user_data) {
|
||||||
|
|
||||||
|
(void) flags;
|
||||||
|
|
||||||
|
auto *session = static_cast<Session *>(user_data);
|
||||||
|
const int sent = send(session->socket, reinterpret_cast<const char *>(data),
|
||||||
|
static_cast<int>(length), 0);
|
||||||
|
|
||||||
|
if (sent > 0) {
|
||||||
|
return sent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
|
||||||
|
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void on_msg_recv(wslay_event_context_ptr ctx,
|
||||||
|
const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
|
||||||
|
|
||||||
|
auto *session = static_cast<Session *>(user_data);
|
||||||
|
|
||||||
|
// pings and closes are wslay's business, it answers them itself
|
||||||
|
if (wslay_is_ctrl_frame(arg->opcode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg->opcode != WSLAY_BINARY_FRAME) {
|
||||||
|
log_warning("api::websocket", "ignoring a non-binary message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<char> in(arg->msg, arg->msg + arg->msg_length);
|
||||||
|
std::vector<char> out;
|
||||||
|
|
||||||
|
if (session->state->cipher) {
|
||||||
|
session->state->cipher->crypt(
|
||||||
|
reinterpret_cast<uint8_t *>(in.data()), in.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
session->controller->process_request(session->state, &in, &out);
|
||||||
|
|
||||||
|
if (session->state->cipher) {
|
||||||
|
session->state->cipher->crypt(
|
||||||
|
reinterpret_cast<uint8_t *>(out.data()), out.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
wslay_event_msg reply {};
|
||||||
|
reply.opcode = WSLAY_BINARY_FRAME;
|
||||||
|
reply.msg = reinterpret_cast<const uint8_t *>(out.data());
|
||||||
|
reply.msg_length = out.size();
|
||||||
|
|
||||||
|
if (wslay_event_queue_msg(ctx, &reply) != 0) {
|
||||||
|
session->failed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller::process_password_change(session->state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Controller state so we don't have to import headsocket stuff in our header
|
|
||||||
*/
|
|
||||||
struct WebSocketControllerState {
|
struct WebSocketControllerState {
|
||||||
std::shared_ptr<WebSocketServer> server;
|
struct Client {
|
||||||
|
std::thread thread;
|
||||||
|
SOCKET socket = INVALID_SOCKET;
|
||||||
|
bool active = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Controller *controller = nullptr;
|
||||||
|
unsigned short port = 0;
|
||||||
|
SOCKET listener = INVALID_SOCKET;
|
||||||
|
bool wsa_started = false;
|
||||||
|
std::atomic_bool running { false };
|
||||||
|
std::thread acceptor;
|
||||||
|
std::mutex clients_m;
|
||||||
|
std::array<Client, client_limit> clients;
|
||||||
|
|
||||||
|
bool open_listener();
|
||||||
|
void accept_worker();
|
||||||
|
void client_worker(int slot, SOCKET socket, std::string address);
|
||||||
|
void stop();
|
||||||
|
};
|
||||||
|
|
||||||
|
bool WebSocketControllerState::open_listener() {
|
||||||
|
WSADATA wsa_data;
|
||||||
|
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
|
||||||
|
if (error != 0) {
|
||||||
|
log_warning("api::websocket", "WSAStartup() returned {}", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->wsa_started = true;
|
||||||
|
|
||||||
|
this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
|
||||||
|
if (this->listener == INVALID_SOCKET) {
|
||||||
|
log_warning("api::websocket", "socket() returned {}", WSAGetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sockaddr_in address {};
|
||||||
|
address.sin_family = AF_INET;
|
||||||
|
address.sin_addr.s_addr = INADDR_ANY;
|
||||||
|
address.sin_port = htons(this->port);
|
||||||
|
|
||||||
|
if (bind(this->listener, reinterpret_cast<sockaddr *>(&address), sizeof(address))
|
||||||
|
== SOCKET_ERROR) {
|
||||||
|
log_warning("api::websocket", "bind() returned {}", WSAGetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listen(this->listener, server_backlog) == SOCKET_ERROR) {
|
||||||
|
log_warning("api::websocket", "listen() returned {}", WSAGetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketControllerState::accept_worker() {
|
||||||
|
|
||||||
|
while (this->running) {
|
||||||
|
sockaddr_in client_address {};
|
||||||
|
int client_address_size = sizeof(sockaddr_in);
|
||||||
|
|
||||||
|
const SOCKET client = accept(
|
||||||
|
this->listener, reinterpret_cast<sockaddr *>(&client_address),
|
||||||
|
&client_address_size);
|
||||||
|
|
||||||
|
if (client == INVALID_SOCKET) {
|
||||||
|
// on shutdown the listener is closed under us; otherwise do not spin
|
||||||
|
if (this->running) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->running) {
|
||||||
|
closesocket(client);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatted by hand rather than with inet_ntop, which needs a newer Windows than
|
||||||
|
// the XP toolchain targets, or inet_ntoa, which answers from a shared buffer
|
||||||
|
const uint32_t raw = ntohl(client_address.sin_addr.s_addr);
|
||||||
|
const std::string address = fmt::format("{}.{}.{}.{}",
|
||||||
|
(raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
|
||||||
|
|
||||||
|
int slot = -1;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||||
|
for (size_t i = 0; i < this->clients.size(); i++) {
|
||||||
|
if (!this->clients[i].active) {
|
||||||
|
this->clients[i].active = true;
|
||||||
|
this->clients[i].socket = client;
|
||||||
|
slot = static_cast<int>(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot < 0) {
|
||||||
|
log_warning("api::websocket", "client limit of {} hit", client_limit);
|
||||||
|
overlay::notifications::add_throttled(
|
||||||
|
overlay::notifications::Severity::Warning,
|
||||||
|
"api::websocket.client_limit",
|
||||||
|
notification_throttle_seconds,
|
||||||
|
fmt::format("API websocket refused: client limit reached ({})", address));
|
||||||
|
closesocket(client);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this thread is the only one that touches the thread objects, so the slot's
|
||||||
|
// previous occupant gets reaped here rather than being detached
|
||||||
|
if (this->clients[slot].thread.joinable()) {
|
||||||
|
this->clients[slot].thread.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// the handshake runs on the client thread on purpose: doing it here would put
|
||||||
|
// every later connection behind whatever this one is waiting for
|
||||||
|
this->clients[slot].thread = std::thread([this, slot, client, address] {
|
||||||
|
this->client_worker(slot, client, address);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) {
|
||||||
|
|
||||||
|
// wslay writes a frame header and its payload as separate sends, so leaving Nagle on
|
||||||
|
// holds the payload back until the peer acknowledges the header, costing a delayed
|
||||||
|
// ack per message; requests here are small and latency sensitive
|
||||||
|
int nodelay = 1;
|
||||||
|
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
|
||||||
|
reinterpret_cast<const char *>(&nodelay), sizeof(nodelay));
|
||||||
|
|
||||||
|
set_recv_timeout(socket, handshake_timeout_ms);
|
||||||
|
|
||||||
|
if (handshake(socket)) {
|
||||||
|
// wslay reads until the socket would block, so leaving it blocking would make
|
||||||
|
// every reply wait out a receive timeout before the send got a turn
|
||||||
|
u_long non_blocking = 1;
|
||||||
|
ioctlsocket(socket, FIONBIO, &non_blocking);
|
||||||
|
|
||||||
|
Session session;
|
||||||
|
session.socket = socket;
|
||||||
|
session.controller = this->controller;
|
||||||
|
session.state = new ClientState();
|
||||||
|
this->controller->init_state(session.state);
|
||||||
|
|
||||||
|
log_info("api::websocket", "client connected: {}", address);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Success,
|
||||||
|
fmt::format("API websocket client connected ({})", address));
|
||||||
|
|
||||||
|
wslay_event_callbacks callbacks {};
|
||||||
|
callbacks.recv_callback = recv_callback;
|
||||||
|
callbacks.send_callback = send_callback;
|
||||||
|
callbacks.on_msg_recv_callback = on_msg_recv;
|
||||||
|
|
||||||
|
wslay_event_context_ptr ctx = nullptr;
|
||||||
|
if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) {
|
||||||
|
wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit);
|
||||||
|
while (this->running && !session.failed
|
||||||
|
&& (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) {
|
||||||
|
|
||||||
|
fd_set read_set;
|
||||||
|
fd_set write_set;
|
||||||
|
FD_ZERO(&read_set);
|
||||||
|
FD_ZERO(&write_set);
|
||||||
|
|
||||||
|
if (wslay_event_want_read(ctx)) {
|
||||||
|
FD_SET(socket, &read_set);
|
||||||
|
}
|
||||||
|
if (wslay_event_want_write(ctx)) {
|
||||||
|
FD_SET(socket, &write_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
// bounded so a silent connection still notices us shutting down
|
||||||
|
timeval timeout {};
|
||||||
|
timeout.tv_usec = idle_poll_ms * 1000;
|
||||||
|
|
||||||
|
if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unconditional, so a reply queued by the read above goes out now
|
||||||
|
if (wslay_event_send(ctx) != 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wslay_event_context_free(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
Controller::free_state(session.state);
|
||||||
|
delete session.state;
|
||||||
|
|
||||||
|
log_info("api::websocket", "client disconnected: {}", address);
|
||||||
|
overlay::notifications::add(
|
||||||
|
overlay::notifications::Severity::Info,
|
||||||
|
fmt::format("API websocket client disconnected ({})", address));
|
||||||
|
}
|
||||||
|
|
||||||
|
closesocket(socket);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||||
|
this->clients[slot].socket = INVALID_SOCKET;
|
||||||
|
this->clients[slot].active = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WebSocketControllerState::stop() {
|
||||||
|
this->running = false;
|
||||||
|
|
||||||
|
if (this->listener != INVALID_SOCKET) {
|
||||||
|
closesocket(this->listener);
|
||||||
|
this->listener = INVALID_SOCKET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// drops the client threads out of their blocking send/recv
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->clients_m);
|
||||||
|
for (auto &client : this->clients) {
|
||||||
|
if (client.socket != INVALID_SOCKET) {
|
||||||
|
::shutdown(client.socket, SD_BOTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->acceptor.joinable()) {
|
||||||
|
this->acceptor.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// joining is what guarantees no client thread outlives this object
|
||||||
|
for (auto &client : this->clients) {
|
||||||
|
if (client.thread.joinable()) {
|
||||||
|
client.thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->wsa_started) {
|
||||||
|
WSACleanup();
|
||||||
|
this->wsa_started = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
|
WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
|
||||||
this->controller = controller;
|
this->controller = controller;
|
||||||
|
|
||||||
// create state
|
|
||||||
this->state = new WebSocketControllerState();
|
this->state = new WebSocketControllerState();
|
||||||
|
this->state->controller = controller;
|
||||||
|
this->state->port = port;
|
||||||
|
|
||||||
// start server
|
if (!this->state->open_listener()) {
|
||||||
this->state->server = WebSocketServer::create(port);
|
|
||||||
this->state->server->websocket = this;
|
|
||||||
if (this->state->server->is_running()) {
|
|
||||||
log_info("api::websocket", "server listening on port: {}", port);
|
|
||||||
} else {
|
|
||||||
log_warning("api::websocket", "server failed to listen on port: {}", port);
|
log_warning("api::websocket", "server failed to listen on port: {}", port);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this->state->running = true;
|
||||||
|
this->state->acceptor = std::thread([this] {
|
||||||
|
this->state->accept_worker();
|
||||||
|
});
|
||||||
|
|
||||||
|
log_info("api::websocket", "server listening on port: {}", port);
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocketController::~WebSocketController() {
|
WebSocketController::~WebSocketController() {
|
||||||
|
this->state->stop();
|
||||||
// stop server
|
|
||||||
this->state->server->stop();
|
|
||||||
|
|
||||||
// delete state
|
|
||||||
delete this->state;
|
delete this->state;
|
||||||
|
this->state = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void WebSocketController::free_socket() {
|
void WebSocketController::free_socket() {
|
||||||
this->state->server->stop();
|
this->state->stop();
|
||||||
}
|
|
||||||
|
|
||||||
void WebSocketClient::on_accept() {
|
|
||||||
web_socket_client::on_accept();
|
|
||||||
|
|
||||||
// get pointer to server
|
|
||||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
|
||||||
if (!srv || !srv->websocket) {
|
|
||||||
log_fatal("api::websocket", "on_accept has no server");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for init
|
|
||||||
state = new ClientState();
|
|
||||||
srv->websocket->controller->init_state(state);
|
|
||||||
|
|
||||||
// log connection
|
|
||||||
log_info("api::websocket", "client connected");
|
|
||||||
overlay::notifications::add(
|
|
||||||
overlay::notifications::Severity::Success,
|
|
||||||
"API websocket client connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
void WebSocketClient::on_disconnect() {
|
|
||||||
|
|
||||||
// log disconnection
|
|
||||||
log_info("api::websocket", "client disconnected");
|
|
||||||
overlay::notifications::add(
|
|
||||||
overlay::notifications::Severity::Info,
|
|
||||||
"API websocket client disconnected");
|
|
||||||
|
|
||||||
// get pointer to server
|
|
||||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
|
||||||
if (!srv || !srv->websocket) {
|
|
||||||
log_fatal("api::websocket", "on_disconnect has no server");
|
|
||||||
}
|
|
||||||
|
|
||||||
// clean up state
|
|
||||||
srv->websocket->controller->free_state(state);
|
|
||||||
delete state;
|
|
||||||
state = nullptr;
|
|
||||||
|
|
||||||
// call super
|
|
||||||
web_socket_client::on_disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is where business actually happens, gets called on every datablock receive
|
|
||||||
*/
|
|
||||||
bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) {
|
|
||||||
|
|
||||||
// get pointer to server
|
|
||||||
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
|
|
||||||
if (!srv || !srv->websocket) {
|
|
||||||
log_fatal("api::websocket", "received datablock without server");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check state
|
|
||||||
if (!state) {
|
|
||||||
log_fatal("api::websocket", "client with no state received datablock");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check datablock type
|
|
||||||
switch (db.op) {
|
|
||||||
case opcode::binary: {
|
|
||||||
|
|
||||||
// allocate buffers
|
|
||||||
std::vector<char> in(ptr, ptr + length);
|
|
||||||
std::vector<char> out;
|
|
||||||
|
|
||||||
// crypt in-data
|
|
||||||
if (state->cipher) {
|
|
||||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(in.data()), in.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// process request
|
|
||||||
srv->websocket->controller->process_request(state, &in, &out);
|
|
||||||
|
|
||||||
// crypt out-data
|
|
||||||
if (state->cipher) {
|
|
||||||
state->cipher->crypt(reinterpret_cast<uint8_t *>(out.data()), out.size());
|
|
||||||
}
|
|
||||||
|
|
||||||
// send answer
|
|
||||||
push(out.data(), out.size());
|
|
||||||
|
|
||||||
// check for password change
|
|
||||||
srv->websocket->controller->process_password_change(state);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
log_warning("api::websocket", "datablock received with non-binary type");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// always consume the datablock, nomnom
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ RUN pacman --noconfirm -Syu git \
|
|||||||
cmake \
|
cmake \
|
||||||
unzip \
|
unzip \
|
||||||
nasm \
|
nasm \
|
||||||
|
wget \
|
||||||
mingw-w64-crt \
|
mingw-w64-crt \
|
||||||
mingw-w64-winpthreads \
|
mingw-w64-winpthreads \
|
||||||
mingw-w64-gcc \
|
mingw-w64-gcc \
|
||||||
|
|||||||
Vendored
-2192
File diff suppressed because it is too large
Load Diff
+33
@@ -0,0 +1,33 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.12)
|
||||||
|
project(wslay C)
|
||||||
|
|
||||||
|
set(WSLAY_HEADERS
|
||||||
|
msvc_compat.h
|
||||||
|
wslay_event.h
|
||||||
|
wslay_frame.h
|
||||||
|
wslay_macro.h
|
||||||
|
wslay_net.h
|
||||||
|
wslay_queue.h
|
||||||
|
includes/wslay/wslay.h
|
||||||
|
includes/wslay/wslayver.h
|
||||||
|
)
|
||||||
|
set(WSLAY_SOURCES
|
||||||
|
wslay_event.c
|
||||||
|
wslay_frame.c
|
||||||
|
wslay_net.c
|
||||||
|
wslay_queue.c
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(wslay STATIC ${WSLAY_HEADERS} ${WSLAY_SOURCES})
|
||||||
|
|
||||||
|
# config.h here is checked in rather than generated, see the note in it
|
||||||
|
target_compile_definitions(wslay PRIVATE HAVE_CONFIG_H)
|
||||||
|
target_include_directories(wslay PRIVATE ${PROJECT_SOURCE_DIR})
|
||||||
|
target_include_directories(wslay PUBLIC ${PROJECT_SOURCE_DIR}/includes)
|
||||||
|
|
||||||
|
if(MSVC)
|
||||||
|
target_compile_options(wslay PRIVATE "/FI${PROJECT_SOURCE_DIR}/msvc_compat.h")
|
||||||
|
else()
|
||||||
|
# upstream code, and not ours to keep warning clean
|
||||||
|
target_compile_options(wslay PRIVATE -w)
|
||||||
|
endif()
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/* Hand written stand-in for the autotools/CMake generated config.h. Spice only
|
||||||
|
builds this for Windows on x86, so the probes have single known answers. */
|
||||||
|
#ifndef CONFIG_H
|
||||||
|
#define CONFIG_H
|
||||||
|
|
||||||
|
#define HAVE_WINSOCK2_H
|
||||||
|
/* x86 and x86_64 are little endian, so WORDS_BIGENDIAN stays undefined */
|
||||||
|
|
||||||
|
#endif /* CONFIG_H */
|
||||||
+841
@@ -0,0 +1,841 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_H
|
||||||
|
#define WSLAY_H
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* wslay/wslayver.h is generated from wslay/wslayver.h.in by
|
||||||
|
* configure. The projects which do not use autotools can set
|
||||||
|
* WSLAY_VERSION macro from outside to avoid to generating wslayver.h
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_VERSION
|
||||||
|
# include <wslay/wslayver.h>
|
||||||
|
#endif /* WSLAY_VERSION */
|
||||||
|
|
||||||
|
enum wslay_error {
|
||||||
|
WSLAY_ERR_WANT_READ = -100,
|
||||||
|
WSLAY_ERR_WANT_WRITE = -101,
|
||||||
|
WSLAY_ERR_PROTO = -200,
|
||||||
|
WSLAY_ERR_INVALID_ARGUMENT = -300,
|
||||||
|
WSLAY_ERR_INVALID_CALLBACK = -301,
|
||||||
|
WSLAY_ERR_NO_MORE_MSG = -302,
|
||||||
|
WSLAY_ERR_CALLBACK_FAILURE = -400,
|
||||||
|
WSLAY_ERR_WOULDBLOCK = -401,
|
||||||
|
WSLAY_ERR_NOMEM = -500
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Status codes defined in RFC6455
|
||||||
|
*/
|
||||||
|
enum wslay_status_code {
|
||||||
|
WSLAY_CODE_NORMAL_CLOSURE = 1000,
|
||||||
|
WSLAY_CODE_GOING_AWAY = 1001,
|
||||||
|
WSLAY_CODE_PROTOCOL_ERROR = 1002,
|
||||||
|
WSLAY_CODE_UNSUPPORTED_DATA = 1003,
|
||||||
|
WSLAY_CODE_NO_STATUS_RCVD = 1005,
|
||||||
|
WSLAY_CODE_ABNORMAL_CLOSURE = 1006,
|
||||||
|
WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA = 1007,
|
||||||
|
WSLAY_CODE_POLICY_VIOLATION = 1008,
|
||||||
|
WSLAY_CODE_MESSAGE_TOO_BIG = 1009,
|
||||||
|
WSLAY_CODE_MANDATORY_EXT = 1010,
|
||||||
|
WSLAY_CODE_INTERNAL_SERVER_ERROR = 1011,
|
||||||
|
WSLAY_CODE_TLS_HANDSHAKE = 1015
|
||||||
|
};
|
||||||
|
|
||||||
|
enum wslay_io_flags {
|
||||||
|
/*
|
||||||
|
* There is more data to send.
|
||||||
|
*/
|
||||||
|
WSLAY_MSG_MORE = 1
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function used by wslay_frame_send() function when it needs
|
||||||
|
* to send data. The implementation of this function must send at most
|
||||||
|
* len bytes of data in data. flags is the bitwise OR of zero or more
|
||||||
|
* of the following flag:
|
||||||
|
*
|
||||||
|
* WSLAY_MSG_MORE
|
||||||
|
* There is more data to send
|
||||||
|
*
|
||||||
|
* It provides some hints to tune performance and behaviour. user_data
|
||||||
|
* is one given in wslay_frame_context_init() function. The
|
||||||
|
* implementation of this function must return the number of bytes
|
||||||
|
* sent. If there is an error, return -1. The return value 0 is also
|
||||||
|
* treated an error by the library.
|
||||||
|
*/
|
||||||
|
typedef ssize_t (*wslay_frame_send_callback)(const uint8_t *data, size_t len,
|
||||||
|
int flags, void *user_data);
|
||||||
|
/*
|
||||||
|
* Callback function used by wslay_frame_recv() function when it needs
|
||||||
|
* more data. The implementation of this function must fill at most
|
||||||
|
* len bytes of data into buf. The memory area of buf is allocated by
|
||||||
|
* library and not be freed by the application code. flags is always 0
|
||||||
|
* in this version. user_data is one given in
|
||||||
|
* wslay_frame_context_init() function. The implementation of this
|
||||||
|
* function must return the number of bytes filled. If there is an
|
||||||
|
* error, return -1. The return value 0 is also treated an error by
|
||||||
|
* the library.
|
||||||
|
*/
|
||||||
|
typedef ssize_t (*wslay_frame_recv_callback)(uint8_t *buf, size_t len,
|
||||||
|
int flags, void *user_data);
|
||||||
|
/*
|
||||||
|
* Callback function used by wslay_frame_send() function when it needs
|
||||||
|
* new mask key. The implementation of this function must write
|
||||||
|
* exactly len bytes of mask key to buf. user_data is one given in
|
||||||
|
* wslay_frame_context_init() function. The implementation of this
|
||||||
|
* function return 0 on success. If there is an error, return -1.
|
||||||
|
*/
|
||||||
|
typedef int (*wslay_frame_genmask_callback)(uint8_t *buf, size_t len,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
struct wslay_frame_callbacks {
|
||||||
|
wslay_frame_send_callback send_callback;
|
||||||
|
wslay_frame_recv_callback recv_callback;
|
||||||
|
wslay_frame_genmask_callback genmask_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The opcode defined in RFC6455.
|
||||||
|
*/
|
||||||
|
enum wslay_opcode {
|
||||||
|
WSLAY_CONTINUATION_FRAME = 0x0u,
|
||||||
|
WSLAY_TEXT_FRAME = 0x1u,
|
||||||
|
WSLAY_BINARY_FRAME = 0x2u,
|
||||||
|
WSLAY_CONNECTION_CLOSE = 0x8u,
|
||||||
|
WSLAY_PING = 0x9u,
|
||||||
|
WSLAY_PONG = 0xau
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Macro that returns 1 if opcode is control frame opcode, otherwise
|
||||||
|
* returns 0.
|
||||||
|
*/
|
||||||
|
#define wslay_is_ctrl_frame(opcode) ((opcode >> 3) & 1)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Macros that represent and return reserved bits: RSV1, RSV2, RSV3.
|
||||||
|
* These macros assume that rsv is constructed by ((RSV1 << 2) |
|
||||||
|
* (RSV2 << 1) | RSV3)
|
||||||
|
*/
|
||||||
|
#define WSLAY_RSV_NONE ((uint8_t)0)
|
||||||
|
#define WSLAY_RSV1_BIT (((uint8_t)1) << 2)
|
||||||
|
#define WSLAY_RSV2_BIT (((uint8_t)1) << 1)
|
||||||
|
#define WSLAY_RSV3_BIT (((uint8_t)1) << 0)
|
||||||
|
|
||||||
|
#define wslay_get_rsv1(rsv) ((rsv >> 2) & 1)
|
||||||
|
#define wslay_get_rsv2(rsv) ((rsv >> 1) & 1)
|
||||||
|
#define wslay_get_rsv3(rsv) (rsv & 1)
|
||||||
|
|
||||||
|
struct wslay_frame_iocb {
|
||||||
|
/* 1 for fragmented final frame, 0 for otherwise */
|
||||||
|
uint8_t fin;
|
||||||
|
/*
|
||||||
|
* reserved 3 bits. rsv = ((RSV1 << 2) | (RSV << 1) | RSV3).
|
||||||
|
* RFC6455 requires 0 unless extensions are negotiated.
|
||||||
|
*/
|
||||||
|
uint8_t rsv;
|
||||||
|
/* 4 bit opcode */
|
||||||
|
uint8_t opcode;
|
||||||
|
/* payload length [0, 2**63-1] */
|
||||||
|
uint64_t payload_length;
|
||||||
|
/* 1 for masked frame, 0 for unmasked */
|
||||||
|
uint8_t mask;
|
||||||
|
/* part of payload data */
|
||||||
|
const uint8_t *data;
|
||||||
|
/* bytes of data defined above */
|
||||||
|
size_t data_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_frame_context;
|
||||||
|
typedef struct wslay_frame_context *wslay_frame_context_ptr;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initializes ctx using given callbacks and user_data. This function
|
||||||
|
* allocates memory for struct wslay_frame_context and stores the
|
||||||
|
* result to *ctx. The callback functions specified in callbacks are
|
||||||
|
* copied to ctx. user_data is stored in ctx and it will be passed to
|
||||||
|
* callback functions. When the user code finished using ctx, it must
|
||||||
|
* call wslay_frame_context_free to deallocate memory.
|
||||||
|
*/
|
||||||
|
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
|
||||||
|
const struct wslay_frame_callbacks *callbacks,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Deallocates memory pointed by ctx.
|
||||||
|
*/
|
||||||
|
void wslay_frame_context_free(wslay_frame_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Send WebSocket frame specified in iocb. ctx must be initialized
|
||||||
|
* using wslay_frame_context_init() function. iocb->fin must be 1 if
|
||||||
|
* this is a fin frame, otherwise 0. iocb->rsv is reserved bits.
|
||||||
|
* iocb->opcode must be the opcode of this frame. iocb->mask must be
|
||||||
|
* 1 if this is masked frame, otherwise 0. iocb->payload_length is
|
||||||
|
* the payload_length of this frame. iocb->data must point to the
|
||||||
|
* payload data to be sent. iocb->data_length must be the length of
|
||||||
|
* the data. This function calls send_callback function if it needs
|
||||||
|
* to send bytes. This function calls gen_mask_callback function if
|
||||||
|
* it needs new mask key. This function returns the number of payload
|
||||||
|
* bytes sent. Please note that it does not include any number of
|
||||||
|
* header bytes. If it cannot send any single bytes of payload, it
|
||||||
|
* returns WSLAY_ERR_WANT_WRITE. If the library detects error in iocb,
|
||||||
|
* this function returns WSLAY_ERR_INVALID_ARGUMENT. If callback
|
||||||
|
* functions report a failure, this function returns
|
||||||
|
* WSLAY_ERR_INVALID_CALLBACK. This function does not always send all
|
||||||
|
* given data in iocb. If there are remaining data to be sent, adjust
|
||||||
|
* data and data_length in iocb accordingly and call this function
|
||||||
|
* again.
|
||||||
|
*/
|
||||||
|
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Write WebSocket frame specified in iocb to buf of length
|
||||||
|
* buflen. ctx must be initialized using wslay_frame_context_init()
|
||||||
|
* function. iocb->fin must be 1 if this is a fin frame, otherwise 0.
|
||||||
|
* iocb->rsv is reserved bits. iocb->opcode must be the opcode of
|
||||||
|
* this frame. iocb->mask must be 1 if this is masked frame,
|
||||||
|
* otherwise 0. iocb->payload_length is the payload_length of this
|
||||||
|
* frame. iocb->data must point to the payload data to be
|
||||||
|
* sent. iocb->data_length must be the length of the data. Unlike
|
||||||
|
* wslay_frame_send, this function does not call send_callback
|
||||||
|
* function. This function calls gen_mask_callback function if it
|
||||||
|
* needs new mask key. This function returns the number of bytes
|
||||||
|
* written to a buffer. Unlike wslay_frame_send, it includes the
|
||||||
|
* number of header bytes. Instead, the number of payload bytes
|
||||||
|
* written is assigned to *pwpayloadlen if this function succeeds. If
|
||||||
|
* there is not enough space left in a buffer, it returns 0. If the
|
||||||
|
* library detects error in iocb, this function returns
|
||||||
|
* WSLAY_ERR_INVALID_ARGUMENT. If callback functions report a
|
||||||
|
* failure, this function returns WSLAY_ERR_INVALID_CALLBACK. This
|
||||||
|
* function does not always send all given data in iocb. If there are
|
||||||
|
* remaining data to be sent, adjust data and data_length in iocb
|
||||||
|
* accordingly and call this function again.
|
||||||
|
*/
|
||||||
|
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb, uint8_t *buf,
|
||||||
|
size_t buflen, size_t *pwpayloadlen);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Receives WebSocket frame and stores it in iocb. This function
|
||||||
|
* returns the number of payload bytes received. This does not
|
||||||
|
* include header bytes. In this case, iocb will be populated as
|
||||||
|
* follows: iocb->fin is 1 if received frame is fin frame, otherwise
|
||||||
|
* 0. iocb->rsv is reserved bits of received frame. iocb->opcode is
|
||||||
|
* opcode of received frame. iocb->mask is 1 if received frame is
|
||||||
|
* masked, otherwise 0. iocb->payload_length is the payload length of
|
||||||
|
* received frame. iocb->data is pointed to the buffer containing
|
||||||
|
* received payload data. This buffer is allocated by the library and
|
||||||
|
* must be read-only. iocb->data_length is the number of payload
|
||||||
|
* bytes recieved. This function calls recv_callback if it needs to
|
||||||
|
* receive additional bytes. If it cannot receive any single bytes of
|
||||||
|
* payload, it returns WSLAY_ERR_WANT_READ. If the library detects
|
||||||
|
* protocol violation in a received frame, this function returns
|
||||||
|
* WSLAY_ERR_PROTO. If callback functions report a failure, this
|
||||||
|
* function returns WSLAY_ERR_INVALID_CALLBACK. This function does
|
||||||
|
* not always receive whole frame in a single call. If there are
|
||||||
|
* remaining data to be received, call this function again. This
|
||||||
|
* function ensures frame alignment.
|
||||||
|
*/
|
||||||
|
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb);
|
||||||
|
|
||||||
|
struct wslay_event_context;
|
||||||
|
/* Pointer to the event-based API context */
|
||||||
|
typedef struct wslay_event_context *wslay_event_context_ptr;
|
||||||
|
|
||||||
|
struct wslay_event_on_msg_recv_arg {
|
||||||
|
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
|
||||||
|
uint8_t rsv;
|
||||||
|
/* opcode */
|
||||||
|
uint8_t opcode;
|
||||||
|
/* received message */
|
||||||
|
const uint8_t *msg;
|
||||||
|
/* message length */
|
||||||
|
size_t msg_length;
|
||||||
|
/*
|
||||||
|
* Status code iff opcode == WSLAY_CONNECTION_CLOSE. If no status
|
||||||
|
* code is included in the close control frame, it is set to 0.
|
||||||
|
*/
|
||||||
|
uint16_t status_code;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_recv() when a message is
|
||||||
|
* completely received.
|
||||||
|
*/
|
||||||
|
typedef void (*wslay_event_on_msg_recv_callback)(
|
||||||
|
wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
struct wslay_event_on_frame_recv_start_arg {
|
||||||
|
/* fin bit; 1 for final frame, or 0. */
|
||||||
|
uint8_t fin;
|
||||||
|
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
|
||||||
|
uint8_t rsv;
|
||||||
|
/* opcode of the frame */
|
||||||
|
uint8_t opcode;
|
||||||
|
/* payload length of ths frame */
|
||||||
|
uint64_t payload_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_recv() when a new frame
|
||||||
|
* starts to be received. This callback function is only invoked once
|
||||||
|
* for each frame.
|
||||||
|
*/
|
||||||
|
typedef void (*wslay_event_on_frame_recv_start_callback)(
|
||||||
|
wslay_event_context_ptr ctx,
|
||||||
|
const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data);
|
||||||
|
|
||||||
|
struct wslay_event_on_frame_recv_chunk_arg {
|
||||||
|
/* chunk of payload data */
|
||||||
|
const uint8_t *data;
|
||||||
|
/* length of data */
|
||||||
|
size_t data_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_recv() when a chunk of
|
||||||
|
* frame payload is received.
|
||||||
|
*/
|
||||||
|
typedef void (*wslay_event_on_frame_recv_chunk_callback)(
|
||||||
|
wslay_event_context_ptr ctx,
|
||||||
|
const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_recv() when a frame is
|
||||||
|
* completely received.
|
||||||
|
*/
|
||||||
|
typedef void (*wslay_event_on_frame_recv_end_callback)(
|
||||||
|
wslay_event_context_ptr ctx, void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_recv() when it wants to
|
||||||
|
* receive more data from peer. The implementation of this callback
|
||||||
|
* function must read data at most len bytes from peer and store them
|
||||||
|
* in buf and return the number of bytes read. flags is always 0 in
|
||||||
|
* this version.
|
||||||
|
*
|
||||||
|
* If there is an error, return -1 and set error code
|
||||||
|
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
|
||||||
|
* event-based API on the whole assumes non-blocking I/O. If the cause
|
||||||
|
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
|
||||||
|
* instead. This is important because it tells wslay_event_recv() to
|
||||||
|
* stop receiving further data and return.
|
||||||
|
*/
|
||||||
|
typedef ssize_t (*wslay_event_recv_callback)(wslay_event_context_ptr ctx,
|
||||||
|
uint8_t *buf, size_t len,
|
||||||
|
int flags, void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_send() when it wants to
|
||||||
|
* send more data to peer. The implementation of this callback
|
||||||
|
* function must send data at most len bytes to peer and return the
|
||||||
|
* number of bytes sent. flags is the bitwise OR of zero or more of
|
||||||
|
* the following flag:
|
||||||
|
*
|
||||||
|
* WSLAY_MSG_MORE
|
||||||
|
* There is more data to send
|
||||||
|
*
|
||||||
|
* It provides some hints to tune performance and behaviour.
|
||||||
|
*
|
||||||
|
* If there is an error, return -1 and set error code
|
||||||
|
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
|
||||||
|
* event-based API on the whole assumes non-blocking I/O. If the cause
|
||||||
|
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
|
||||||
|
* instead. This is important because it tells wslay_event_send() to
|
||||||
|
* stop sending data and return.
|
||||||
|
*/
|
||||||
|
typedef ssize_t (*wslay_event_send_callback)(wslay_event_context_ptr ctx,
|
||||||
|
const uint8_t *data, size_t len,
|
||||||
|
int flags, void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function invoked by wslay_event_send() when it wants new
|
||||||
|
* mask key. As described in RFC6455, only the traffic from WebSocket
|
||||||
|
* client is masked, so this callback function is only needed if an
|
||||||
|
* event-based API is initialized for WebSocket client use.
|
||||||
|
*/
|
||||||
|
typedef int (*wslay_event_genmask_callback)(wslay_event_context_ptr ctx,
|
||||||
|
uint8_t *buf, size_t len,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
struct wslay_event_callbacks {
|
||||||
|
wslay_event_recv_callback recv_callback;
|
||||||
|
wslay_event_send_callback send_callback;
|
||||||
|
wslay_event_genmask_callback genmask_callback;
|
||||||
|
wslay_event_on_frame_recv_start_callback on_frame_recv_start_callback;
|
||||||
|
wslay_event_on_frame_recv_chunk_callback on_frame_recv_chunk_callback;
|
||||||
|
wslay_event_on_frame_recv_end_callback on_frame_recv_end_callback;
|
||||||
|
wslay_event_on_msg_recv_callback on_msg_recv_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initializes ctx as WebSocket Server. user_data is an arbitrary
|
||||||
|
* pointer, which is directly passed to each callback functions as
|
||||||
|
* user_data argument.
|
||||||
|
*
|
||||||
|
* On success, returns 0. On error, returns one of following negative
|
||||||
|
* values:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*/
|
||||||
|
int wslay_event_context_server_init(
|
||||||
|
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Initializes ctx as WebSocket client. user_data is an arbitrary
|
||||||
|
* pointer, which is directly passed to each callback functions as
|
||||||
|
* user_data argument.
|
||||||
|
*
|
||||||
|
* On success, returns 0. On error, returns one of following negative
|
||||||
|
* values:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*/
|
||||||
|
int wslay_event_context_client_init(
|
||||||
|
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Releases allocated resources for ctx.
|
||||||
|
*/
|
||||||
|
void wslay_event_context_free(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sets a bit mask of allowed reserved bits.
|
||||||
|
* Currently only permitted values are WSLAY_RSV1_BIT to allow PMCE
|
||||||
|
* extension (see RFC-7692) or WSLAY_RSV_NONE to disable.
|
||||||
|
*
|
||||||
|
* Default: WSLAY_RSV_NONE
|
||||||
|
*/
|
||||||
|
void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx,
|
||||||
|
uint8_t rsv);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enables or disables buffering of an entire message for non-control
|
||||||
|
* frames. If val is 0, buffering is enabled. Otherwise, buffering is
|
||||||
|
* disabled. If wslay_event_on_msg_recv_callback is invoked when
|
||||||
|
* buffering is disabled, the msg_length member of struct
|
||||||
|
* wslay_event_on_msg_recv_arg is set to 0.
|
||||||
|
*
|
||||||
|
* The control frames are always buffered regardless of this function call.
|
||||||
|
*
|
||||||
|
* This function must not be used after the first invocation of
|
||||||
|
* wslay_event_recv() function.
|
||||||
|
*/
|
||||||
|
void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sets maximum length of a message that can be received. The length
|
||||||
|
* of message is checked by wslay_event_recv() function. If the length
|
||||||
|
* of a message is larger than this value, reading operation is
|
||||||
|
* disabled (same effect with wslay_event_shutdown_read() call) and
|
||||||
|
* close control frame with WSLAY_CODE_MESSAGE_TOO_BIG is queued. If
|
||||||
|
* buffering for non-control frames is disabled, the library checks
|
||||||
|
* each frame payload length and does not check length of entire
|
||||||
|
* message.
|
||||||
|
*
|
||||||
|
* The default value is (1u << 31)-1.
|
||||||
|
*/
|
||||||
|
void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx,
|
||||||
|
uint64_t val);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sets callbacks to ctx. The callbacks previously set by this function
|
||||||
|
* or wslay_event_context_server_init() or
|
||||||
|
* wslay_event_context_client_init() are replaced with callbacks.
|
||||||
|
*/
|
||||||
|
void wslay_event_config_set_callbacks(
|
||||||
|
wslay_event_context_ptr ctx, const struct wslay_event_callbacks *callbacks);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Receives messages from peer. When receiving
|
||||||
|
* messages, it uses wslay_event_recv_callback function. Single call
|
||||||
|
* of this function receives multiple messages until
|
||||||
|
* wslay_event_recv_callback function sets error code
|
||||||
|
* WSLAY_ERR_WOULDBLOCK.
|
||||||
|
*
|
||||||
|
* When close control frame is received, this function automatically
|
||||||
|
* queues close control frame. Also this function calls
|
||||||
|
* wslay_event_set_read_enabled() with second argument 0 to disable
|
||||||
|
* further read from peer.
|
||||||
|
*
|
||||||
|
* When ping control frame is received, this function automatically
|
||||||
|
* queues pong control frame.
|
||||||
|
*
|
||||||
|
* In case of a fatal errror which leads to negative return code, this
|
||||||
|
* function calls wslay_event_set_read_enabled() with second argument
|
||||||
|
* 0 to disable further read from peer.
|
||||||
|
*
|
||||||
|
* wslay_event_recv() returns 0 if it succeeds, or one of the
|
||||||
|
* following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_CALLBACK_FAILURE
|
||||||
|
* User defined callback function is failed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*
|
||||||
|
* When negative error code is returned, application must not make any
|
||||||
|
* further call of wslay_event_recv() and must close WebSocket
|
||||||
|
* connection.
|
||||||
|
*/
|
||||||
|
int wslay_event_recv(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sends queued messages to peer. When sending a
|
||||||
|
* message, it uses wslay_event_send_callback function. Single call of
|
||||||
|
* wslay_event_send() sends multiple messages until
|
||||||
|
* wslay_event_send_callback sets error code WSLAY_ERR_WOULDBLOCK.
|
||||||
|
*
|
||||||
|
* If ctx is initialized for WebSocket client use, wslay_event_send()
|
||||||
|
* uses wslay_event_genmask_callback to get new mask key.
|
||||||
|
*
|
||||||
|
* When a message queued using wslay_event_queue_fragmented_msg() is
|
||||||
|
* sent, wslay_event_send() invokes
|
||||||
|
* wslay_event_fragmented_msg_callback for that message.
|
||||||
|
*
|
||||||
|
* After close control frame is sent, this function calls
|
||||||
|
* wslay_event_set_write_enabled() with second argument 0 to disable
|
||||||
|
* further transmission to peer.
|
||||||
|
*
|
||||||
|
* If there are any pending messages, wslay_event_want_write() returns
|
||||||
|
* 1, otherwise returns 0.
|
||||||
|
*
|
||||||
|
* In case of a fatal errror which leads to negative return code, this
|
||||||
|
* function calls wslay_event_set_write_enabled() with second argument
|
||||||
|
* 0 to disable further transmission to peer.
|
||||||
|
*
|
||||||
|
* wslay_event_send() returns 0 if it succeeds, or one of the
|
||||||
|
* following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_CALLBACK_FAILURE
|
||||||
|
* User defined callback function is failed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*
|
||||||
|
* When negative error code is returned, application must not make any
|
||||||
|
* further call of wslay_event_send() and must close WebSocket
|
||||||
|
* connection.
|
||||||
|
*/
|
||||||
|
int wslay_event_send(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Writes queued messages to a buffer. Unlike wslay_event_send(), this
|
||||||
|
* function writes messages into the given buffer. It does not use
|
||||||
|
* wslay_event_send_callback function. Single call of
|
||||||
|
* wslay_event_write() writes multiple messages until there is not
|
||||||
|
* enough space left in a buffer.
|
||||||
|
*
|
||||||
|
* If ctx is initialized for WebSocket client use, wslay_event_write()
|
||||||
|
* uses wslay_event_genmask_callback to get new mask key.
|
||||||
|
*
|
||||||
|
* buf is a pointer to buffer and its capacity is given in buflen. It
|
||||||
|
* should have at least 14 bytes.
|
||||||
|
*
|
||||||
|
* When a message queued using wslay_event_queue_fragmented_msg() is
|
||||||
|
* sent, wslay_event_write() invokes
|
||||||
|
* wslay_event_fragmented_msg_callback for that message.
|
||||||
|
*
|
||||||
|
* After close control frame is sent, this function calls
|
||||||
|
* wslay_event_set_write_enabled() with second argument 0 to disable
|
||||||
|
* further transmission to peer.
|
||||||
|
*
|
||||||
|
* If there are any pending messages, wslay_event_want_write() returns
|
||||||
|
* 1, otherwise returns 0.
|
||||||
|
*
|
||||||
|
* In case of a fatal errror which leads to negative return code, this
|
||||||
|
* function calls wslay_event_set_write_enabled() with second argument
|
||||||
|
* 0 to disable further transmission to peer.
|
||||||
|
*
|
||||||
|
* wslay_event_write() returns the number of bytes written to a buffer
|
||||||
|
* if it succeeds, or one of the following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_CALLBACK_FAILURE
|
||||||
|
* User defined callback function is failed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*
|
||||||
|
* When negative error code is returned, application must not make any
|
||||||
|
* further call of wslay_event_write() and must close WebSocket
|
||||||
|
* connection.
|
||||||
|
*/
|
||||||
|
ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf,
|
||||||
|
size_t buflen);
|
||||||
|
|
||||||
|
struct wslay_event_msg {
|
||||||
|
uint8_t opcode;
|
||||||
|
const uint8_t *msg;
|
||||||
|
size_t msg_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Queues message specified in arg.
|
||||||
|
*
|
||||||
|
* This function supports both control and non-control messages and
|
||||||
|
* the given message is sent without fragmentation. If fragmentation
|
||||||
|
* is needed, use wslay_event_queue_fragmented_msg() function instead.
|
||||||
|
*
|
||||||
|
* This function just queues a message and does not send
|
||||||
|
* it. wslay_event_send() function call sends these queued messages.
|
||||||
|
*
|
||||||
|
* wslay_event_queue_msg() returns 0 if it succeeds, or returns the
|
||||||
|
* following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NO_MORE_MSG
|
||||||
|
* Could not queue given message. The one of possible reason is that
|
||||||
|
* close control frame has been queued/sent and no further queueing
|
||||||
|
* message is not allowed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_INVALID_ARGUMENT
|
||||||
|
* The given message is invalid.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*/
|
||||||
|
int wslay_event_queue_msg(wslay_event_context_ptr ctx,
|
||||||
|
const struct wslay_event_msg *arg);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extended version of wslay_event_queue_msg which allows to set reserved bits.
|
||||||
|
*/
|
||||||
|
int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx,
|
||||||
|
const struct wslay_event_msg *arg, uint8_t rsv);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Specify "source" to generate message.
|
||||||
|
*/
|
||||||
|
union wslay_event_msg_source {
|
||||||
|
int fd;
|
||||||
|
void *data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Callback function called by wslay_event_send() to read message data
|
||||||
|
* from source. The implementation of
|
||||||
|
* wslay_event_fragmented_msg_callback must store at most len bytes of
|
||||||
|
* data to buf and return the number of stored bytes. If all data is
|
||||||
|
* read (i.e., EOF), set *eof to 1. If no data can be generated at the
|
||||||
|
* moment, return 0. If there is an error, return -1 and set error
|
||||||
|
* code WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error().
|
||||||
|
*/
|
||||||
|
typedef ssize_t (*wslay_event_fragmented_msg_callback)(
|
||||||
|
wslay_event_context_ptr ctx, uint8_t *buf, size_t len,
|
||||||
|
const union wslay_event_msg_source *source, int *eof, void *user_data);
|
||||||
|
|
||||||
|
struct wslay_event_fragmented_msg {
|
||||||
|
/* opcode */
|
||||||
|
uint8_t opcode;
|
||||||
|
/* "source" to generate message data */
|
||||||
|
union wslay_event_msg_source source;
|
||||||
|
/* Callback function to read message data from source. */
|
||||||
|
wslay_event_fragmented_msg_callback read_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Queues a fragmented message specified in arg.
|
||||||
|
*
|
||||||
|
* This function supports non-control messages only. For control frames,
|
||||||
|
* use wslay_event_queue_msg() or wslay_event_queue_close().
|
||||||
|
*
|
||||||
|
* This function just queues a message and does not send
|
||||||
|
* it. wslay_event_send() function call sends these queued messages.
|
||||||
|
*
|
||||||
|
* wslay_event_queue_fragmented_msg() returns 0 if it succeeds, or
|
||||||
|
* returns the following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NO_MORE_MSG
|
||||||
|
* Could not queue given message. The one of possible reason is that
|
||||||
|
* close control frame has been queued/sent and no further queueing
|
||||||
|
* message is not allowed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_INVALID_ARGUMENT
|
||||||
|
* The given message is invalid.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*/
|
||||||
|
int wslay_event_queue_fragmented_msg(
|
||||||
|
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extended version of wslay_event_queue_fragmented_msg which allows to set
|
||||||
|
* reserved bits.
|
||||||
|
*/
|
||||||
|
int wslay_event_queue_fragmented_msg_ex(
|
||||||
|
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg,
|
||||||
|
uint8_t rsv);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Queues close control frame. This function is provided just for
|
||||||
|
* convenience. wslay_event_queue_msg() can queue a close control
|
||||||
|
* frame as well. status_code is the status code of close control
|
||||||
|
* frame. reason is the close reason encoded in UTF-8. reason_length
|
||||||
|
* is the length of reason in bytes. reason_length must be less than
|
||||||
|
* 123 bytes.
|
||||||
|
*
|
||||||
|
* If status_code is 0, reason and reason_length is not used and close
|
||||||
|
* control frame with zero-length payload will be queued.
|
||||||
|
*
|
||||||
|
* This function just queues a message and does not send
|
||||||
|
* it. wslay_event_send() function call sends these queued messages.
|
||||||
|
*
|
||||||
|
* wslay_event_queue_close() returns 0 if it succeeds, or returns the
|
||||||
|
* following negative error codes:
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NO_MORE_MSG
|
||||||
|
* Could not queue given message. The one of possible reason is that
|
||||||
|
* close control frame has been queued/sent and no further queueing
|
||||||
|
* message is not allowed.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_INVALID_ARGUMENT
|
||||||
|
* The given message is invalid.
|
||||||
|
*
|
||||||
|
* WSLAY_ERR_NOMEM
|
||||||
|
* Out of memory.
|
||||||
|
*/
|
||||||
|
int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code,
|
||||||
|
const uint8_t *reason, size_t reason_length);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sets error code to tell the library there is an error. This
|
||||||
|
* function is typically used in user defined callback functions. See
|
||||||
|
* the description of callback function to know which error code
|
||||||
|
* should be used.
|
||||||
|
*/
|
||||||
|
void wslay_event_set_error(wslay_event_context_ptr ctx, int val);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Query whehter the library want to read more data from peer.
|
||||||
|
*
|
||||||
|
* wslay_event_want_read() returns 1 if the library want to read more
|
||||||
|
* data from peer, or returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_want_read(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Query whehter the library want to send more data to peer.
|
||||||
|
*
|
||||||
|
* wslay_event_want_write() returns 1 if the library want to send more
|
||||||
|
* data to peer, or returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_want_write(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevents the event-based API context from reading any further data
|
||||||
|
* from peer.
|
||||||
|
*
|
||||||
|
* This function may be used with wslay_event_queue_close() if the
|
||||||
|
* application detects error in the data received and wants to fail
|
||||||
|
* WebSocket connection.
|
||||||
|
*/
|
||||||
|
void wslay_event_shutdown_read(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Prevents the event-based API context from sending any further data
|
||||||
|
* to peer.
|
||||||
|
*/
|
||||||
|
void wslay_event_shutdown_write(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns 1 if the event-based API context allows read operation, or
|
||||||
|
* return 0.
|
||||||
|
*
|
||||||
|
* After wslay_event_shutdown_read() is called,
|
||||||
|
* wslay_event_get_read_enabled() returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_get_read_enabled(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns 1 if the event-based API context allows write operation, or
|
||||||
|
* return 0.
|
||||||
|
*
|
||||||
|
* After wslay_event_shutdown_write() is called,
|
||||||
|
* wslay_event_get_write_enabled() returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_get_write_enabled(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns 1 if a close control frame has been received from peer, or
|
||||||
|
* returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_get_close_received(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns 1 if a close control frame has been sent to peer, or
|
||||||
|
* returns 0.
|
||||||
|
*/
|
||||||
|
int wslay_event_get_close_sent(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns status code received in close control frame. If no close
|
||||||
|
* control frame has not been received, returns
|
||||||
|
* WSLAY_CODE_ABNORMAL_CLOSURE. If received close control frame has no
|
||||||
|
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
|
||||||
|
*/
|
||||||
|
uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns status code sent in close control frame. If no close
|
||||||
|
* control frame has not been sent, returns
|
||||||
|
* WSLAY_CODE_ABNORMAL_CLOSURE. If sent close control frame has no
|
||||||
|
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
|
||||||
|
*/
|
||||||
|
uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the number of queued messages.
|
||||||
|
*/
|
||||||
|
size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Returns the sum of queued message length. It only counts the
|
||||||
|
* message length queued using wslay_event_queue_msg() or
|
||||||
|
* wslay_event_queue_close().
|
||||||
|
*/
|
||||||
|
size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* WSLAY_H */
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAYVER_H
|
||||||
|
#define WSLAYVER_H
|
||||||
|
|
||||||
|
/* normally substituted by the build system; pinned to the vendored release */
|
||||||
|
#define WSLAY_VERSION "1.1.1"
|
||||||
|
|
||||||
|
#endif /* WSLAYVER_H */
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef _MSC_VER
|
||||||
|
#include <BaseTsd.h>
|
||||||
|
#ifndef _SSIZE_T_DEFINED
|
||||||
|
#define _SSIZE_T_DEFINED
|
||||||
|
typedef SSIZE_T ssize_t;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
+1082
File diff suppressed because it is too large
Load Diff
+138
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_EVENT_H
|
||||||
|
#define WSLAY_EVENT_H
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include <config.h>
|
||||||
|
#endif /* HAVE_CONFIG_H */
|
||||||
|
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
#include "wslay_queue.h"
|
||||||
|
|
||||||
|
struct wslay_event_byte_chunk {
|
||||||
|
struct wslay_queue_entry qe;
|
||||||
|
uint8_t *data;
|
||||||
|
size_t data_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_event_imsg {
|
||||||
|
uint8_t fin;
|
||||||
|
uint8_t rsv;
|
||||||
|
uint8_t opcode;
|
||||||
|
uint32_t utf8state;
|
||||||
|
struct wslay_queue chunks;
|
||||||
|
size_t msg_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum wslay_event_msg_type { WSLAY_NON_FRAGMENTED, WSLAY_FRAGMENTED };
|
||||||
|
|
||||||
|
struct wslay_event_omsg {
|
||||||
|
struct wslay_queue_entry qe;
|
||||||
|
uint8_t fin;
|
||||||
|
uint8_t opcode;
|
||||||
|
uint8_t rsv;
|
||||||
|
enum wslay_event_msg_type type;
|
||||||
|
|
||||||
|
uint8_t *data;
|
||||||
|
size_t data_length;
|
||||||
|
|
||||||
|
union wslay_event_msg_source source;
|
||||||
|
wslay_event_fragmented_msg_callback read_callback;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_event_frame_user_data {
|
||||||
|
wslay_event_context_ptr ctx;
|
||||||
|
void *user_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum wslay_event_close_status {
|
||||||
|
WSLAY_CLOSE_RECEIVED = 1 << 0,
|
||||||
|
WSLAY_CLOSE_QUEUED = 1 << 1,
|
||||||
|
WSLAY_CLOSE_SENT = 1 << 2
|
||||||
|
};
|
||||||
|
|
||||||
|
enum wslay_event_config { WSLAY_CONFIG_NO_BUFFERING = 1 << 0 };
|
||||||
|
|
||||||
|
struct wslay_event_context {
|
||||||
|
/* config status, bitwise OR of enum wslay_event_config values*/
|
||||||
|
uint32_t config;
|
||||||
|
/* maximum message length that can be received */
|
||||||
|
uint64_t max_recv_msg_length;
|
||||||
|
/* 1 if initialized for server, otherwise 0 */
|
||||||
|
uint8_t server;
|
||||||
|
/* bitwise OR of enum wslay_event_close_status values */
|
||||||
|
uint8_t close_status;
|
||||||
|
/* status code in received close control frame */
|
||||||
|
uint16_t status_code_recv;
|
||||||
|
/* status code in sent close control frame */
|
||||||
|
uint16_t status_code_sent;
|
||||||
|
wslay_frame_context_ptr frame_ctx;
|
||||||
|
/* 1 if reading is enabled, otherwise 0. Upon receiving close
|
||||||
|
control frame this value set to 0. If any errors in read
|
||||||
|
operation will also set this value to 0. */
|
||||||
|
uint8_t read_enabled;
|
||||||
|
/* 1 if writing is enabled, otherwise 0 Upon completing sending
|
||||||
|
close control frame, this value set to 0. If any errors in write
|
||||||
|
opration will also set this value to 0. */
|
||||||
|
uint8_t write_enabled;
|
||||||
|
/* imsg buffer to allow interleaved control frame between
|
||||||
|
non-control frames. */
|
||||||
|
struct wslay_event_imsg imsgs[2];
|
||||||
|
/* Pointer to imsgs to indicate current used buffer. */
|
||||||
|
struct wslay_event_imsg *imsg;
|
||||||
|
/* payload length of frame currently being received. */
|
||||||
|
uint64_t ipayloadlen;
|
||||||
|
/* next byte offset of payload currently being received. */
|
||||||
|
uint64_t ipayloadoff;
|
||||||
|
/* error value set by user callback */
|
||||||
|
int error;
|
||||||
|
/* Pointer to the message currently being sent. NULL if no message
|
||||||
|
is currently sent. */
|
||||||
|
struct wslay_event_omsg *omsg;
|
||||||
|
/* Queue for non-control frames */
|
||||||
|
struct wslay_queue /*<wslay_omsg*>*/ send_queue;
|
||||||
|
/* Queue for control frames */
|
||||||
|
struct wslay_queue /*<wslay_omsg*>*/ send_ctrl_queue;
|
||||||
|
/* Size of send_queue + size of send_ctrl_queue */
|
||||||
|
size_t queued_msg_count;
|
||||||
|
/* The sum of message length in send_queue */
|
||||||
|
size_t queued_msg_length;
|
||||||
|
/* Buffer used for fragmented messages */
|
||||||
|
uint8_t obuf[4096];
|
||||||
|
uint8_t *obuflimit;
|
||||||
|
uint8_t *obufmark;
|
||||||
|
/* payload length of frame currently being sent. */
|
||||||
|
uint64_t opayloadlen;
|
||||||
|
/* next byte offset of payload currently being sent. */
|
||||||
|
uint64_t opayloadoff;
|
||||||
|
struct wslay_event_callbacks callbacks;
|
||||||
|
struct wslay_event_frame_user_data frame_user_data;
|
||||||
|
void *user_data;
|
||||||
|
uint8_t allowed_rsv_bits;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* WSLAY_EVENT_H */
|
||||||
+438
@@ -0,0 +1,438 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#include "wslay_frame.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include "wslay_net.h"
|
||||||
|
|
||||||
|
#define wslay_min(A, B) (((A) < (B)) ? (A) : (B))
|
||||||
|
|
||||||
|
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
|
||||||
|
const struct wslay_frame_callbacks *callbacks,
|
||||||
|
void *user_data) {
|
||||||
|
*ctx = malloc(sizeof(struct wslay_frame_context));
|
||||||
|
if (*ctx == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memset(*ctx, 0, sizeof(struct wslay_frame_context));
|
||||||
|
(*ctx)->istate = RECV_HEADER1;
|
||||||
|
(*ctx)->ireqread = 2;
|
||||||
|
(*ctx)->ostate = PREP_HEADER;
|
||||||
|
(*ctx)->user_data = user_data;
|
||||||
|
(*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf;
|
||||||
|
(*ctx)->callbacks = *callbacks;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wslay_frame_context_free(wslay_frame_context_ptr ctx) { free(ctx); }
|
||||||
|
|
||||||
|
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb) {
|
||||||
|
if (iocb->data_length > iocb->payload_length) {
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (ctx->ostate == PREP_HEADER) {
|
||||||
|
uint8_t *hdptr = ctx->oheader;
|
||||||
|
memset(ctx->oheader, 0, sizeof(ctx->oheader));
|
||||||
|
*hdptr |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
|
||||||
|
*hdptr |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
|
||||||
|
/* Suppress stubborn gcc-10 warning */
|
||||||
|
*hdptr |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
|
||||||
|
++hdptr;
|
||||||
|
*hdptr |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
|
||||||
|
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (iocb->payload_length < 126) {
|
||||||
|
*hdptr |= (uint8_t)iocb->payload_length;
|
||||||
|
++hdptr;
|
||||||
|
} else if (iocb->payload_length < (1 << 16)) {
|
||||||
|
uint16_t len = htons((uint16_t)iocb->payload_length);
|
||||||
|
*hdptr |= 126;
|
||||||
|
++hdptr;
|
||||||
|
memcpy(hdptr, &len, 2);
|
||||||
|
hdptr += 2;
|
||||||
|
} else if (iocb->payload_length < (1ull << 63)) {
|
||||||
|
uint64_t len = hton64(iocb->payload_length);
|
||||||
|
*hdptr |= 127;
|
||||||
|
++hdptr;
|
||||||
|
memcpy(hdptr, &len, 8);
|
||||||
|
hdptr += 8;
|
||||||
|
} else {
|
||||||
|
/* Too large payload length */
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (iocb->mask) {
|
||||||
|
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
|
||||||
|
0) {
|
||||||
|
return WSLAY_ERR_INVALID_CALLBACK;
|
||||||
|
} else {
|
||||||
|
ctx->omask = 1;
|
||||||
|
memcpy(hdptr, ctx->omaskkey, 4);
|
||||||
|
hdptr += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx->ostate = SEND_HEADER;
|
||||||
|
ctx->oheadermark = ctx->oheader;
|
||||||
|
ctx->oheaderlimit = hdptr;
|
||||||
|
ctx->opayloadlen = iocb->payload_length;
|
||||||
|
ctx->opayloadoff = 0;
|
||||||
|
}
|
||||||
|
if (ctx->ostate == SEND_HEADER) {
|
||||||
|
ptrdiff_t len = ctx->oheaderlimit - ctx->oheadermark;
|
||||||
|
ssize_t r;
|
||||||
|
int flags = 0;
|
||||||
|
if (iocb->data_length > 0) {
|
||||||
|
flags |= WSLAY_MSG_MORE;
|
||||||
|
}
|
||||||
|
r = ctx->callbacks.send_callback(ctx->oheadermark, (size_t)len, flags,
|
||||||
|
ctx->user_data);
|
||||||
|
if (r > 0) {
|
||||||
|
if (r > len) {
|
||||||
|
return WSLAY_ERR_INVALID_CALLBACK;
|
||||||
|
} else {
|
||||||
|
ctx->oheadermark += r;
|
||||||
|
if (ctx->oheadermark == ctx->oheaderlimit) {
|
||||||
|
ctx->ostate = SEND_PAYLOAD;
|
||||||
|
} else {
|
||||||
|
return WSLAY_ERR_WANT_WRITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return WSLAY_ERR_WANT_WRITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->ostate == SEND_PAYLOAD) {
|
||||||
|
size_t totallen = 0;
|
||||||
|
if (iocb->data_length > 0) {
|
||||||
|
if (ctx->omask) {
|
||||||
|
uint8_t temp[4096];
|
||||||
|
const uint8_t *datamark = iocb->data,
|
||||||
|
*datalimit = iocb->data + iocb->data_length;
|
||||||
|
while (datamark < datalimit) {
|
||||||
|
size_t datalen = (size_t)(datalimit - datamark);
|
||||||
|
const uint8_t *writelimit =
|
||||||
|
datamark + wslay_min(sizeof(temp), datalen);
|
||||||
|
size_t writelen = (size_t)(writelimit - datamark);
|
||||||
|
ssize_t r;
|
||||||
|
size_t i;
|
||||||
|
for (i = 0; i < writelen; ++i) {
|
||||||
|
temp[i] = datamark[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
|
||||||
|
}
|
||||||
|
r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data);
|
||||||
|
if (r > 0) {
|
||||||
|
if ((size_t)r > writelen) {
|
||||||
|
return WSLAY_ERR_INVALID_CALLBACK;
|
||||||
|
} else {
|
||||||
|
datamark += r;
|
||||||
|
ctx->opayloadoff += (uint64_t)r;
|
||||||
|
totallen += (size_t)r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (totallen > 0) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
return WSLAY_ERR_WANT_WRITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ssize_t r;
|
||||||
|
r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0,
|
||||||
|
ctx->user_data);
|
||||||
|
if (r > 0) {
|
||||||
|
if ((size_t)r > iocb->data_length) {
|
||||||
|
return WSLAY_ERR_INVALID_CALLBACK;
|
||||||
|
} else {
|
||||||
|
ctx->opayloadoff += (uint64_t)r;
|
||||||
|
totallen = (size_t)r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return WSLAY_ERR_WANT_WRITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->opayloadoff == ctx->opayloadlen) {
|
||||||
|
ctx->ostate = PREP_HEADER;
|
||||||
|
}
|
||||||
|
return (ssize_t)totallen;
|
||||||
|
}
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb, uint8_t *buf,
|
||||||
|
size_t buflen, size_t *pwpayloadlen) {
|
||||||
|
uint8_t *buf_last = buf;
|
||||||
|
size_t i;
|
||||||
|
size_t hdlen;
|
||||||
|
|
||||||
|
*pwpayloadlen = 0;
|
||||||
|
|
||||||
|
if (iocb->data_length > iocb->payload_length) {
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ctx->ostate) {
|
||||||
|
case PREP_HEADER:
|
||||||
|
case PREP_HEADER_NOBUF:
|
||||||
|
hdlen = 2;
|
||||||
|
if (iocb->payload_length < 126) {
|
||||||
|
/* nothing to do */
|
||||||
|
} else if (iocb->payload_length < (1 << 16)) {
|
||||||
|
hdlen += 2;
|
||||||
|
} else if (iocb->payload_length < (1ull << 63)) {
|
||||||
|
hdlen += 8;
|
||||||
|
}
|
||||||
|
if (iocb->mask) {
|
||||||
|
hdlen += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buflen < hdlen) {
|
||||||
|
ctx->ostate = PREP_HEADER_NOBUF;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(buf_last, 0, hdlen);
|
||||||
|
*buf_last |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
|
||||||
|
*buf_last |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
|
||||||
|
/* Suppress stubborn gcc-10 warning */
|
||||||
|
*buf_last |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
|
||||||
|
++buf_last;
|
||||||
|
*buf_last |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
|
||||||
|
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (iocb->payload_length < 126) {
|
||||||
|
*buf_last |= (uint8_t)iocb->payload_length;
|
||||||
|
++buf_last;
|
||||||
|
} else if (iocb->payload_length < (1 << 16)) {
|
||||||
|
uint16_t len = htons((uint16_t)iocb->payload_length);
|
||||||
|
*buf_last |= 126;
|
||||||
|
++buf_last;
|
||||||
|
memcpy(buf_last, &len, 2);
|
||||||
|
buf_last += 2;
|
||||||
|
} else if (iocb->payload_length < (1ull << 63)) {
|
||||||
|
uint64_t len = hton64(iocb->payload_length);
|
||||||
|
*buf_last |= 127;
|
||||||
|
++buf_last;
|
||||||
|
memcpy(buf_last, &len, 8);
|
||||||
|
buf_last += 8;
|
||||||
|
} else {
|
||||||
|
/* Too large payload length */
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
if (iocb->mask) {
|
||||||
|
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
|
||||||
|
0) {
|
||||||
|
return WSLAY_ERR_INVALID_CALLBACK;
|
||||||
|
} else {
|
||||||
|
ctx->omask = 1;
|
||||||
|
memcpy(buf_last, ctx->omaskkey, 4);
|
||||||
|
buf_last += 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx->ostate = SEND_PAYLOAD;
|
||||||
|
ctx->opayloadlen = iocb->payload_length;
|
||||||
|
ctx->opayloadoff = 0;
|
||||||
|
|
||||||
|
buflen -= (size_t)(buf_last - buf);
|
||||||
|
/* fall through */
|
||||||
|
case SEND_PAYLOAD:
|
||||||
|
if (iocb->data_length > 0) {
|
||||||
|
size_t writelen = wslay_min(buflen, iocb->data_length);
|
||||||
|
|
||||||
|
if (ctx->omask) {
|
||||||
|
for (i = 0; i < writelen; ++i) {
|
||||||
|
*buf_last++ =
|
||||||
|
iocb->data[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memcpy(buf_last, iocb->data, writelen);
|
||||||
|
buf_last += writelen;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->opayloadoff += writelen;
|
||||||
|
*pwpayloadlen = writelen;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->opayloadoff == ctx->opayloadlen) {
|
||||||
|
ctx->ostate = PREP_HEADER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf_last - buf;
|
||||||
|
default:
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) {
|
||||||
|
ptrdiff_t len = ctx->ibuflimit - ctx->ibufmark;
|
||||||
|
memmove(ctx->ibuf, ctx->ibufmark, (size_t)len);
|
||||||
|
ctx->ibuflimit = ctx->ibuf + len;
|
||||||
|
ctx->ibufmark = ctx->ibuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ssize_t wslay_recv(wslay_frame_context_ptr ctx) {
|
||||||
|
ssize_t r;
|
||||||
|
if (ctx->ibufmark != ctx->ibuf) {
|
||||||
|
wslay_shift_ibuf(ctx);
|
||||||
|
}
|
||||||
|
r = ctx->callbacks.recv_callback(
|
||||||
|
ctx->ibuflimit, (size_t)(ctx->ibuf + sizeof(ctx->ibuf) - ctx->ibuflimit),
|
||||||
|
0, ctx->user_data);
|
||||||
|
if (r > 0) {
|
||||||
|
ctx->ibuflimit += r;
|
||||||
|
} else {
|
||||||
|
r = WSLAY_ERR_WANT_READ;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark))
|
||||||
|
|
||||||
|
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
|
||||||
|
struct wslay_frame_iocb *iocb) {
|
||||||
|
ssize_t r;
|
||||||
|
if (ctx->istate == RECV_HEADER1) {
|
||||||
|
uint8_t fin, opcode, rsv, payloadlen;
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
if ((r = wslay_recv(ctx)) <= 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
return WSLAY_ERR_WANT_READ;
|
||||||
|
}
|
||||||
|
fin = (ctx->ibufmark[0] >> 7) & 1;
|
||||||
|
rsv = (ctx->ibufmark[0] >> 4) & 7;
|
||||||
|
opcode = ctx->ibufmark[0] & 0xfu;
|
||||||
|
ctx->iom.opcode = opcode;
|
||||||
|
ctx->iom.fin = fin;
|
||||||
|
ctx->iom.rsv = rsv;
|
||||||
|
++ctx->ibufmark;
|
||||||
|
ctx->imask = (ctx->ibufmark[0] >> 7) & 1;
|
||||||
|
payloadlen = ctx->ibufmark[0] & 0x7fu;
|
||||||
|
++ctx->ibufmark;
|
||||||
|
if (wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) {
|
||||||
|
return WSLAY_ERR_PROTO;
|
||||||
|
}
|
||||||
|
if (payloadlen == 126) {
|
||||||
|
ctx->istate = RECV_EXT_PAYLOADLEN;
|
||||||
|
ctx->ireqread = 2;
|
||||||
|
} else if (payloadlen == 127) {
|
||||||
|
ctx->istate = RECV_EXT_PAYLOADLEN;
|
||||||
|
ctx->ireqread = 8;
|
||||||
|
} else {
|
||||||
|
ctx->ipayloadlen = payloadlen;
|
||||||
|
ctx->ipayloadoff = 0;
|
||||||
|
if (ctx->imask) {
|
||||||
|
ctx->istate = RECV_MASKKEY;
|
||||||
|
ctx->ireqread = 4;
|
||||||
|
} else {
|
||||||
|
ctx->istate = RECV_PAYLOAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->istate == RECV_EXT_PAYLOADLEN) {
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
if ((r = wslay_recv(ctx)) <= 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
return WSLAY_ERR_WANT_READ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx->ipayloadlen = 0;
|
||||||
|
ctx->ipayloadoff = 0;
|
||||||
|
memcpy((uint8_t *)&ctx->ipayloadlen + (8 - ctx->ireqread), ctx->ibufmark,
|
||||||
|
ctx->ireqread);
|
||||||
|
ctx->ipayloadlen = ntoh64(ctx->ipayloadlen);
|
||||||
|
ctx->ibufmark += ctx->ireqread;
|
||||||
|
if (ctx->ireqread == 8) {
|
||||||
|
if (ctx->ipayloadlen < (1 << 16) || ctx->ipayloadlen & (1ull << 63)) {
|
||||||
|
return WSLAY_ERR_PROTO;
|
||||||
|
}
|
||||||
|
} else if (ctx->ipayloadlen < 126) {
|
||||||
|
return WSLAY_ERR_PROTO;
|
||||||
|
}
|
||||||
|
if (ctx->imask) {
|
||||||
|
ctx->istate = RECV_MASKKEY;
|
||||||
|
ctx->ireqread = 4;
|
||||||
|
} else {
|
||||||
|
ctx->istate = RECV_PAYLOAD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ctx->istate == RECV_MASKKEY) {
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
if ((r = wslay_recv(ctx)) <= 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
|
||||||
|
return WSLAY_ERR_WANT_READ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
memcpy(ctx->imaskkey, ctx->ibufmark, 4);
|
||||||
|
ctx->ibufmark += 4;
|
||||||
|
ctx->istate = RECV_PAYLOAD;
|
||||||
|
}
|
||||||
|
if (ctx->istate == RECV_PAYLOAD) {
|
||||||
|
uint8_t *readlimit, *readmark;
|
||||||
|
uint64_t rempayloadlen = ctx->ipayloadlen - ctx->ipayloadoff;
|
||||||
|
if (WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) {
|
||||||
|
if ((r = wslay_recv(ctx)) <= 0) {
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readmark = ctx->ibufmark;
|
||||||
|
readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen
|
||||||
|
? ctx->ibuflimit
|
||||||
|
: ctx->ibufmark + rempayloadlen;
|
||||||
|
if (ctx->imask) {
|
||||||
|
for (; ctx->ibufmark != readlimit; ++ctx->ibufmark, ++ctx->ipayloadoff) {
|
||||||
|
ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx->ibufmark = readlimit;
|
||||||
|
ctx->ipayloadoff += (uint64_t)(readlimit - readmark);
|
||||||
|
}
|
||||||
|
iocb->fin = ctx->iom.fin;
|
||||||
|
iocb->rsv = ctx->iom.rsv;
|
||||||
|
iocb->opcode = ctx->iom.opcode;
|
||||||
|
iocb->payload_length = ctx->ipayloadlen;
|
||||||
|
iocb->mask = ctx->imask;
|
||||||
|
iocb->data = readmark;
|
||||||
|
iocb->data_length = (size_t)(ctx->ibufmark - readmark);
|
||||||
|
if (ctx->ipayloadlen == ctx->ipayloadoff) {
|
||||||
|
ctx->istate = RECV_HEADER1;
|
||||||
|
ctx->ireqread = 2;
|
||||||
|
}
|
||||||
|
return (ssize_t)iocb->data_length;
|
||||||
|
}
|
||||||
|
return WSLAY_ERR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_FRAME_H
|
||||||
|
#define WSLAY_FRAME_H
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include <config.h>
|
||||||
|
#endif /* HAVE_CONFIG_H */
|
||||||
|
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
enum wslay_frame_state {
|
||||||
|
PREP_HEADER,
|
||||||
|
PREP_HEADER_NOBUF,
|
||||||
|
SEND_HEADER,
|
||||||
|
SEND_PAYLOAD,
|
||||||
|
RECV_HEADER1,
|
||||||
|
RECV_PAYLOADLEN,
|
||||||
|
RECV_EXT_PAYLOADLEN,
|
||||||
|
RECV_MASKKEY,
|
||||||
|
RECV_PAYLOAD
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_frame_opcode_memo {
|
||||||
|
uint8_t fin;
|
||||||
|
uint8_t opcode;
|
||||||
|
uint8_t rsv;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_frame_context {
|
||||||
|
uint8_t ibuf[4096];
|
||||||
|
uint8_t *ibufmark;
|
||||||
|
uint8_t *ibuflimit;
|
||||||
|
struct wslay_frame_opcode_memo iom;
|
||||||
|
uint64_t ipayloadlen;
|
||||||
|
uint64_t ipayloadoff;
|
||||||
|
uint8_t imask;
|
||||||
|
uint8_t imaskkey[4];
|
||||||
|
enum wslay_frame_state istate;
|
||||||
|
size_t ireqread;
|
||||||
|
|
||||||
|
uint8_t oheader[14];
|
||||||
|
uint8_t *oheadermark;
|
||||||
|
uint8_t *oheaderlimit;
|
||||||
|
uint64_t opayloadlen;
|
||||||
|
uint64_t opayloadoff;
|
||||||
|
uint8_t omask;
|
||||||
|
uint8_t omaskkey[4];
|
||||||
|
enum wslay_frame_state ostate;
|
||||||
|
|
||||||
|
struct wslay_frame_callbacks callbacks;
|
||||||
|
void *user_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* WSLAY_FRAME_H */
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2020 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_MACRO_H
|
||||||
|
#define WSLAY_MACRO_H
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include "config.h"
|
||||||
|
#endif /* HAVE_CONFIG_H */
|
||||||
|
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#define wslay_struct_of(ptr, type, member) \
|
||||||
|
((type *)(void *)((char *)(ptr)-offsetof(type, member)))
|
||||||
|
|
||||||
|
#endif /* WSLAY_MACRO_H */
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#include "wslay_net.h"
|
||||||
|
|
||||||
|
#ifndef WORDS_BIGENDIAN
|
||||||
|
|
||||||
|
uint64_t wslay_byteswap64(uint64_t x) {
|
||||||
|
uint64_t u = ntohl(x & 0xffffffffllu);
|
||||||
|
uint64_t l = ntohl((uint32_t)(x >> 32));
|
||||||
|
return (u << 32) | l;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif /* !WORDS_BIGENDIAN */
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_NET_H
|
||||||
|
#define WSLAY_NET_H
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include <config.h>
|
||||||
|
#endif /* HAVE_CONFIG_H */
|
||||||
|
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
#ifdef HAVE_ARPA_INET_H
|
||||||
|
# include <arpa/inet.h>
|
||||||
|
#endif /* HAVE_ARPA_INET_H */
|
||||||
|
#ifdef HAVE_NETINET_IN_H
|
||||||
|
# include <netinet/in.h>
|
||||||
|
#endif /* HAVE_NETINET_IN_H */
|
||||||
|
/* For Mingw build */
|
||||||
|
#ifdef HAVE_WINSOCK2_H
|
||||||
|
# include <winsock2.h>
|
||||||
|
#endif /* HAVE_WINSOCK2_H */
|
||||||
|
|
||||||
|
#ifdef WORDS_BIGENDIAN
|
||||||
|
# define ntoh64(x) (x)
|
||||||
|
# define hton64(x) (x)
|
||||||
|
#else /* !WORDS_BIGENDIAN */
|
||||||
|
uint64_t wslay_byteswap64(uint64_t x);
|
||||||
|
# define ntoh64(x) wslay_byteswap64(x)
|
||||||
|
# define hton64(x) wslay_byteswap64(x)
|
||||||
|
#endif /* !WORDS_BIGENDIAN */
|
||||||
|
|
||||||
|
#endif /* WSLAY_NET_H */
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#include "wslay_queue.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include "wslay_macro.h"
|
||||||
|
|
||||||
|
void wslay_queue_init(struct wslay_queue *queue) {
|
||||||
|
queue->top = NULL;
|
||||||
|
queue->tail = &queue->top;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wslay_queue_deinit(struct wslay_queue *queue) { (void)queue; }
|
||||||
|
|
||||||
|
void wslay_queue_push(struct wslay_queue *queue,
|
||||||
|
struct wslay_queue_entry *ent) {
|
||||||
|
ent->next = NULL;
|
||||||
|
*queue->tail = ent;
|
||||||
|
queue->tail = &ent->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
void wslay_queue_push_front(struct wslay_queue *queue,
|
||||||
|
struct wslay_queue_entry *ent) {
|
||||||
|
ent->next = queue->top;
|
||||||
|
queue->top = ent;
|
||||||
|
|
||||||
|
if (ent->next == NULL) {
|
||||||
|
queue->tail = &ent->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void wslay_queue_pop(struct wslay_queue *queue) {
|
||||||
|
assert(queue->top);
|
||||||
|
queue->top = queue->top->next;
|
||||||
|
if (queue->top == NULL) {
|
||||||
|
queue->tail = &queue->top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue) {
|
||||||
|
assert(queue->top);
|
||||||
|
return queue->top;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue) {
|
||||||
|
assert(queue->top);
|
||||||
|
return wslay_struct_of(queue->tail, struct wslay_queue_entry, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
int wslay_queue_empty(struct wslay_queue *queue) {
|
||||||
|
assert(queue->top || queue->tail == &queue->top);
|
||||||
|
return queue->top == NULL;
|
||||||
|
}
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Wslay - The WebSocket Library
|
||||||
|
*
|
||||||
|
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
* a copy of this software and associated documentation files (the
|
||||||
|
* "Software"), to deal in the Software without restriction, including
|
||||||
|
* without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
* permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
* the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be
|
||||||
|
* included in all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
#ifndef WSLAY_QUEUE_H
|
||||||
|
#define WSLAY_QUEUE_H
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
# include "config.h"
|
||||||
|
#endif /* HAVE_CONFIG_H */
|
||||||
|
|
||||||
|
#include <wslay/wslay.h>
|
||||||
|
|
||||||
|
struct wslay_queue_entry {
|
||||||
|
struct wslay_queue_entry *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct wslay_queue {
|
||||||
|
struct wslay_queue_entry *top;
|
||||||
|
struct wslay_queue_entry **tail;
|
||||||
|
};
|
||||||
|
|
||||||
|
void wslay_queue_init(struct wslay_queue *queue);
|
||||||
|
void wslay_queue_deinit(struct wslay_queue *queue);
|
||||||
|
void wslay_queue_push(struct wslay_queue *queue, struct wslay_queue_entry *ent);
|
||||||
|
void wslay_queue_push_front(struct wslay_queue *queue,
|
||||||
|
struct wslay_queue_entry *ent);
|
||||||
|
void wslay_queue_pop(struct wslay_queue *queue);
|
||||||
|
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue);
|
||||||
|
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue);
|
||||||
|
int wslay_queue_empty(struct wslay_queue *queue);
|
||||||
|
|
||||||
|
#endif /* WSLAY_QUEUE_H */
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
#include "util/precise_timer.h"
|
||||||
#include "util/memutils.h"
|
#include "util/memutils.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "motion_cam.h"
|
||||||
#include "rgb_cam.h"
|
#include "rgb_cam.h"
|
||||||
|
|
||||||
#pragma pack(push)
|
#pragma pack(push)
|
||||||
@@ -276,6 +278,7 @@ namespace games::drs {
|
|||||||
void DRSGame::attach() {
|
void DRSGame::attach() {
|
||||||
Game::attach();
|
Game::attach();
|
||||||
|
|
||||||
|
if (!DISABLE_TOUCH) {
|
||||||
// TouchSDK hooks
|
// TouchSDK hooks
|
||||||
detour::iat("??0TouchSDK@@QEAA@XZ",
|
detour::iat("??0TouchSDK@@QEAA@XZ",
|
||||||
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
|
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
|
||||||
@@ -312,12 +315,13 @@ namespace games::drs {
|
|||||||
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
|
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
|
||||||
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
|
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
|
||||||
|
|
||||||
if (!DISABLE_TOUCH) {
|
|
||||||
start_touch();
|
start_touch();
|
||||||
} else {
|
} else {
|
||||||
log_info("drs", "no native input method detected");
|
log_info("drs", "touch input for dance floor disabled");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init_down_motion_hook();
|
||||||
|
|
||||||
if (RGB_CAMERA_HOOK) {
|
if (RGB_CAMERA_HOOK) {
|
||||||
init_rgb_camera_hook();
|
init_rgb_camera_hook();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
|
|||||||
"P2 Up",
|
"P2 Up",
|
||||||
"P2 Down",
|
"P2 Down",
|
||||||
"P2 Left",
|
"P2 Left",
|
||||||
"P2 Right"
|
"P2 Right",
|
||||||
|
"Down Motion"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +31,8 @@ std::vector<Button> &games::drs::get_buttons() {
|
|||||||
std::string games::drs::get_buttons_help() {
|
std::string games::drs::get_buttons_help() {
|
||||||
// keep to max 100 characters wide
|
// keep to max 100 characters wide
|
||||||
return
|
return
|
||||||
"Motion camera required for DOWN movement.\n"
|
"Touchscreen supported for dance floor.\n"
|
||||||
"Touchscreen supported for dance floor."
|
"Down Motion applies to both players."
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ namespace games::drs {
|
|||||||
P2_Up,
|
P2_Up,
|
||||||
P2_Down,
|
P2_Down,
|
||||||
P2_Left,
|
P2_Left,
|
||||||
P2_Right
|
P2_Right,
|
||||||
|
DownMotion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#include "games/drs/motion_cam.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
|
#include "util/detour.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/sigscan.h"
|
||||||
|
#include "io.h"
|
||||||
|
|
||||||
|
namespace games::drs {
|
||||||
|
|
||||||
|
// the game reads down movement from a depth camera whose driver is compiled into the game DLL,
|
||||||
|
// so there is no import to replace.
|
||||||
|
//
|
||||||
|
// with no camera the tracked height never changes, so the motion state never leaves "still"
|
||||||
|
// and DOWN notes can never be hit.
|
||||||
|
//
|
||||||
|
// rather than answer CInputManager::IsDown, the button drives the same state pair the camera
|
||||||
|
// would; that leaves the game's own edge test untouched and keeps every note in a frame seeing
|
||||||
|
// one answer.
|
||||||
|
static int32_t *MOTION_STATE = nullptr;
|
||||||
|
static int32_t *MOTION_STATE_PREV = nullptr;
|
||||||
|
static constexpr int32_t MOTION_STATE_STILL = 1;
|
||||||
|
static constexpr int32_t MOTION_STATE_DOWN = 2;
|
||||||
|
|
||||||
|
static void (*UpdateMotion_orig)() = nullptr;
|
||||||
|
static bool DOWN_MOTION_HELD = false;
|
||||||
|
|
||||||
|
static void InputManager_UpdateMotion() {
|
||||||
|
UpdateMotion_orig();
|
||||||
|
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
const bool held = GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::DownMotion));
|
||||||
|
|
||||||
|
// a state of DOWN only counts as movement when the frame before it was something else
|
||||||
|
if (held) {
|
||||||
|
*MOTION_STATE_PREV = DOWN_MOTION_HELD ? MOTION_STATE_DOWN : MOTION_STATE_STILL;
|
||||||
|
*MOTION_STATE = MOTION_STATE_DOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
DOWN_MOTION_HELD = held;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_down_motion_hook() {
|
||||||
|
// only hook motion camera if the user has down motion button bound to something
|
||||||
|
auto &buttons = get_buttons();
|
||||||
|
if (!buttons.at(Buttons::DownMotion).isSet()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CInputManager::IsDown - read the state pair out of its two operands.
|
||||||
|
// Identical in game versions 2020121400 and 2022121400 / 2024120300:
|
||||||
|
//
|
||||||
|
// 83 3D xx xx xx xx 02 cmp dword [rip+prev], 2 ; state one frame ago
|
||||||
|
// 74 0C je FALSE ; already down, so no new edge
|
||||||
|
// 83 3D xx xx xx xx 02 cmp dword [rip+cur], 2 ; state this frame
|
||||||
|
// 75 03 jne FALSE ; not down
|
||||||
|
// B0 01 mov al, 1 ; went down on this frame
|
||||||
|
// C3 ret
|
||||||
|
// 32 C0 FALSE: xor al, al
|
||||||
|
// C3 ret
|
||||||
|
auto is_down = reinterpret_cast<uint8_t *>(find_pattern(
|
||||||
|
avs::game::DLL_INSTANCE,
|
||||||
|
"833D0000000002740C833D00000000027503B001C332C0C3",
|
||||||
|
"XX????XXXXX????XXXXXXXXX",
|
||||||
|
0, 0));
|
||||||
|
|
||||||
|
if (is_down == nullptr) {
|
||||||
|
log_warning("drs", "motion sensor state not found, DOWN motion button unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rip-relative displacements resolve against the end of their own instruction, and each
|
||||||
|
// cmp above is 7 bytes with its displacement 4 bytes in: the first ends at +7 with its
|
||||||
|
// displacement at +2, the second starts at +9 so it ends at +16 with its own at +11.
|
||||||
|
// read signed, since a target sitting below the instruction encodes as negative.
|
||||||
|
MOTION_STATE_PREV = (int32_t *) (is_down + 7 + *(int32_t *) (is_down + 2));
|
||||||
|
MOTION_STATE = (int32_t *) (is_down + 16 + *(int32_t *) (is_down + 11));
|
||||||
|
|
||||||
|
// CInputManager::UpdateMotion is hooked for the sole purpose of reading the Down Motion
|
||||||
|
// button mapping frequently enough so that the rising edge can be detected.
|
||||||
|
//
|
||||||
|
// Matched over the prologue up to the camera read; the wildcards are a float and a callee:
|
||||||
|
//
|
||||||
|
// 48 83 EC 38 sub rsp, 0x38
|
||||||
|
// 0F 29 74 24 20 movaps [rsp+0x20], xmm6
|
||||||
|
// F3 0F 10 35 xx xx xx xx movss xmm6, [rip+height] ; reading as of last frame
|
||||||
|
// E8 xx xx xx xx call GetPlayVideoProcess
|
||||||
|
// 48 8B C8 mov rcx, rax
|
||||||
|
// 48 8B 10 mov rdx, [rax]
|
||||||
|
// FF 52 60 call [rdx+0x60] ; reading off the newest frame
|
||||||
|
auto update = reinterpret_cast<void *>(find_pattern(
|
||||||
|
avs::game::DLL_INSTANCE,
|
||||||
|
"4883EC380F29742420F30F103500000000E800000000488BC8488B10FF5260",
|
||||||
|
"XXXXXXXXXXXXX????X????XXXXXXXXX",
|
||||||
|
0, 0));
|
||||||
|
|
||||||
|
if (update == nullptr ||
|
||||||
|
!detour::trampoline_try(
|
||||||
|
update, (void *) InputManager_UpdateMotion, (void **) &UpdateMotion_orig)) {
|
||||||
|
log_warning("drs", "motion update not hooked, DOWN motion button unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("drs", "hooked DOWN motion at +{:#x}",
|
||||||
|
(uintptr_t) ((uint8_t *) update - (uint8_t *) avs::game::DLL_INSTANCE));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::drs {
|
||||||
|
void init_down_motion_hook();
|
||||||
|
}
|
||||||
@@ -39,10 +39,38 @@ namespace games::gitadora {
|
|||||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||||
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||||
|
bool ARENA_SUBSCREEN_LANDSCAPE = false;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
bool ALLOW_REALTEK_AUDIO = false;
|
bool ALLOW_REALTEK_AUDIO = false;
|
||||||
bool NATIVE_TOUCH = false;
|
bool NATIVE_TOUCH = false;
|
||||||
|
|
||||||
|
std::pair<UINT, UINT> arena_subscreen_host_size() {
|
||||||
|
if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||||
|
return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value();
|
||||||
|
}
|
||||||
|
if (ARENA_SUBSCREEN_LANDSCAPE) {
|
||||||
|
return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT };
|
||||||
|
}
|
||||||
|
return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT };
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height) {
|
||||||
|
if (host_width <= 0 || host_height <= 0) {
|
||||||
|
return RECT {};
|
||||||
|
}
|
||||||
|
|
||||||
|
LONG width = MulDiv(host_height, ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT);
|
||||||
|
LONG height = host_height;
|
||||||
|
if (width > host_width) {
|
||||||
|
width = host_width;
|
||||||
|
height = MulDiv(host_width, ARENA_SUBSCREEN_HEIGHT, ARENA_SUBSCREEN_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
const LONG left = (host_width - width) / 2;
|
||||||
|
const LONG top = (host_height - height) / 2;
|
||||||
|
return RECT { left, top, left + width, top + height };
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Prevent GitaDora from creating folders on F drive
|
* Prevent GitaDora from creating folders on F drive
|
||||||
*/
|
*/
|
||||||
@@ -343,6 +371,23 @@ namespace games::gitadora {
|
|||||||
"gitadora",
|
"gitadora",
|
||||||
"arena model: unsupported window count: {}", count);
|
"arena model: unsupported window count: {}", count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ARENA_SUBSCREEN_LANDSCAPE && !ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||||
|
log_warning(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: landscape subscreen needs full screen two-window mode, ignoring");
|
||||||
|
ARENA_SUBSCREEN_LANDSCAPE = false;
|
||||||
|
}
|
||||||
|
if (ARENA_TWO_HEAD_EXCLUSIVE) {
|
||||||
|
const auto [host_width, host_height] = arena_subscreen_host_size();
|
||||||
|
if (host_width != ARENA_SUBSCREEN_WIDTH
|
||||||
|
|| host_height != ARENA_SUBSCREEN_HEIGHT) {
|
||||||
|
log_info(
|
||||||
|
"gitadora",
|
||||||
|
"arena model: SMALL head runs at {}x{}, subscreen is scaled to fit",
|
||||||
|
host_width, host_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <mmreg.h>
|
#include <mmreg.h>
|
||||||
@@ -21,6 +22,7 @@ namespace games::gitadora {
|
|||||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||||
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||||
|
extern bool ARENA_SUBSCREEN_LANDSCAPE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern bool ALLOW_REALTEK_AUDIO;
|
extern bool ALLOW_REALTEK_AUDIO;
|
||||||
extern bool NATIVE_TOUCH;
|
extern bool NATIVE_TOUCH;
|
||||||
@@ -29,6 +31,18 @@ namespace games::gitadora {
|
|||||||
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||||
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
||||||
|
|
||||||
|
// used when a landscape monitor drives the SMALL head and -forceressub is unset
|
||||||
|
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_WIDTH = 1920;
|
||||||
|
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_HEIGHT = 1080;
|
||||||
|
|
||||||
|
// resolution the SMALL head actually runs at; the portrait panel size unless
|
||||||
|
// -forceressub or the landscape option overrides it
|
||||||
|
std::pair<UINT, UINT> arena_subscreen_host_size();
|
||||||
|
|
||||||
|
// area of the host the portrait subscreen occupies, centered and aspect-preserved;
|
||||||
|
// whatever is left over on either side stays black
|
||||||
|
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height);
|
||||||
|
|
||||||
class GitaDoraGame : public games::Game {
|
class GitaDoraGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
GitaDoraGame();
|
GitaDoraGame();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -141,10 +143,100 @@ namespace games::popn {
|
|||||||
static AIO_SCI_COMM *aioSciComm;
|
static AIO_SCI_COMM *aioSciComm;
|
||||||
static AIO_IOB5_BI3A *aioIob5Bi3a;
|
static AIO_IOB5_BI3A *aioIob5Bi3a;
|
||||||
|
|
||||||
|
static constexpr size_t PIKA_BUTTON_COUNT = 9;
|
||||||
|
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
|
||||||
|
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
|
||||||
|
static bool pika_button_staging_dirty = false;
|
||||||
|
static std::mutex pika_button_staging_mutex;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* implementations
|
* implementations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
static void publish_pika_button_staging() {
|
||||||
|
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
|
if (!pika_button_staging_dirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data = pika_button_staging;
|
||||||
|
pika_button_staging_dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr Lights::popn_lights_t legacy_button_lights[] = {
|
||||||
|
Lights::popn_lights_t::Button1,
|
||||||
|
Lights::popn_lights_t::Button2,
|
||||||
|
Lights::popn_lights_t::Button3,
|
||||||
|
Lights::popn_lights_t::Button4,
|
||||||
|
Lights::popn_lights_t::Button5,
|
||||||
|
Lights::popn_lights_t::Button6,
|
||||||
|
Lights::popn_lights_t::Button7,
|
||||||
|
Lights::popn_lights_t::Button8,
|
||||||
|
Lights::popn_lights_t::Button9
|
||||||
|
};
|
||||||
|
constexpr Lights::popn_lights_t button_lights[] = {
|
||||||
|
Lights::popn_lights_t::PikaButton1_R,
|
||||||
|
Lights::popn_lights_t::PikaButton1_G,
|
||||||
|
Lights::popn_lights_t::PikaButton1_B,
|
||||||
|
Lights::popn_lights_t::PikaButton2_R,
|
||||||
|
Lights::popn_lights_t::PikaButton2_G,
|
||||||
|
Lights::popn_lights_t::PikaButton2_B,
|
||||||
|
Lights::popn_lights_t::PikaButton3_R,
|
||||||
|
Lights::popn_lights_t::PikaButton3_G,
|
||||||
|
Lights::popn_lights_t::PikaButton3_B,
|
||||||
|
Lights::popn_lights_t::PikaButton4_R,
|
||||||
|
Lights::popn_lights_t::PikaButton4_G,
|
||||||
|
Lights::popn_lights_t::PikaButton4_B,
|
||||||
|
Lights::popn_lights_t::PikaButton5_R,
|
||||||
|
Lights::popn_lights_t::PikaButton5_G,
|
||||||
|
Lights::popn_lights_t::PikaButton5_B,
|
||||||
|
Lights::popn_lights_t::PikaButton6_R,
|
||||||
|
Lights::popn_lights_t::PikaButton6_G,
|
||||||
|
Lights::popn_lights_t::PikaButton6_B,
|
||||||
|
Lights::popn_lights_t::PikaButton7_R,
|
||||||
|
Lights::popn_lights_t::PikaButton7_G,
|
||||||
|
Lights::popn_lights_t::PikaButton7_B,
|
||||||
|
Lights::popn_lights_t::PikaButton8_R,
|
||||||
|
Lights::popn_lights_t::PikaButton8_G,
|
||||||
|
Lights::popn_lights_t::PikaButton8_B,
|
||||||
|
Lights::popn_lights_t::PikaButton9_R,
|
||||||
|
Lights::popn_lights_t::PikaButton9_G,
|
||||||
|
Lights::popn_lights_t::PikaButton9_B
|
||||||
|
};
|
||||||
|
|
||||||
|
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
|
||||||
|
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
|
||||||
|
|
||||||
|
auto &lights = get_lights();
|
||||||
|
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
|
||||||
|
const auto data_offset = button * 3;
|
||||||
|
// special handling converting RGB data to non-RGB lights
|
||||||
|
// take the max(R, G, B) and use it to write the light value
|
||||||
|
const auto light_value = std::max({
|
||||||
|
data[data_offset],
|
||||||
|
data[data_offset + 1],
|
||||||
|
data[data_offset + 2]
|
||||||
|
});
|
||||||
|
|
||||||
|
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
|
||||||
|
// at all times, even when "off". When translating to legacy on/off lights,
|
||||||
|
// treat only values above about 60% brightness as fully on to preserve the
|
||||||
|
// dim "off" state.
|
||||||
|
GameAPI::Lights::writeLight(
|
||||||
|
RI_MGR,
|
||||||
|
lights.at(legacy_button_lights[button]),
|
||||||
|
light_value > 150 ? 1.f : 0.f);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t light = 0; light < data.size(); light++) {
|
||||||
|
GameAPI::Lights::writeLight(
|
||||||
|
RI_MGR,
|
||||||
|
lights.at(button_lights[light]),
|
||||||
|
data[light] / 255.f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// libaio.dll
|
// libaio.dll
|
||||||
|
|
||||||
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
|
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
|
||||||
@@ -260,6 +352,11 @@ namespace games::popn {
|
|||||||
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
|
||||||
|
|
||||||
if (i_pNodeMgr == aioNmgrIob5) {
|
if (i_pNodeMgr == aioNmgrIob5) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
|
pika_button_staging.fill(0);
|
||||||
|
pika_button_staging_dirty = false;
|
||||||
|
}
|
||||||
aioIob5Bi3a = new AIO_IOB5_BI3A;
|
aioIob5Bi3a = new AIO_IOB5_BI3A;
|
||||||
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
||||||
return aioIob5Bi3a;
|
return aioIob5Bi3a;
|
||||||
@@ -269,11 +366,15 @@ namespace games::popn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
|
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
|
||||||
RI_MGR->devices_flush_output();
|
|
||||||
if (i_pNodeCtl != aioIob5Bi3a) {
|
if (i_pNodeCtl != aioIob5Bi3a) {
|
||||||
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
|
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// snapshot the current state of lights and flush the output
|
||||||
|
// this closely mirrors what the game does with real I/O
|
||||||
|
publish_pika_button_staging();
|
||||||
|
RI_MGR->devices_flush_output();
|
||||||
|
|
||||||
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
|
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
|
||||||
auto &buttons = get_buttons();
|
auto &buttons = get_buttons();
|
||||||
|
|
||||||
@@ -413,61 +514,15 @@ namespace games::popn {
|
|||||||
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
|
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &lights = get_lights();
|
|
||||||
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
|
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
|
||||||
// special handling converting to non-RGB lights
|
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
|
||||||
// take the max(R, G, B) and use it to write the light value
|
// clears the buffer (set to 0 / lights off) between animations but they are not meant
|
||||||
uint8_t light_value[9] = { 0 };
|
// to be seen by the I/O
|
||||||
for (uint32_t i = 0; i < number_of_leds; i += 3) {
|
// therefore, instead of writing the lights out directly here, we stage them in a buffer
|
||||||
light_value[i / 3] =
|
// and wait for GetDeviceStatus to be called
|
||||||
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
|
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
|
||||||
}
|
memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
|
||||||
|
pika_button_staging_dirty = true;
|
||||||
constexpr Lights::popn_lights_t legacy_button_lights[] = {
|
|
||||||
Lights::popn_lights_t::Button1,
|
|
||||||
Lights::popn_lights_t::Button2,
|
|
||||||
Lights::popn_lights_t::Button3,
|
|
||||||
Lights::popn_lights_t::Button4,
|
|
||||||
Lights::popn_lights_t::Button5,
|
|
||||||
Lights::popn_lights_t::Button6,
|
|
||||||
Lights::popn_lights_t::Button7,
|
|
||||||
Lights::popn_lights_t::Button8,
|
|
||||||
Lights::popn_lights_t::Button9
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(std::size(legacy_button_lights) == 9);
|
|
||||||
|
|
||||||
for (size_t light = 0; light < 9; light++) {
|
|
||||||
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
|
|
||||||
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
|
|
||||||
// otherwise off, to avoid dimly lit "off" state
|
|
||||||
GameAPI::Lights::writeLight(
|
|
||||||
RI_MGR,
|
|
||||||
lights.at(legacy_button_lights[light]),
|
|
||||||
light_value[light] > 150 ? 1.f : 0.f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// color buttons
|
|
||||||
constexpr Lights::popn_lights_t button_lights[] = {
|
|
||||||
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
|
|
||||||
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
|
|
||||||
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
|
|
||||||
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
|
|
||||||
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
|
|
||||||
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
|
|
||||||
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
|
|
||||||
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
|
|
||||||
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
|
|
||||||
};
|
|
||||||
|
|
||||||
static_assert(std::size(button_lights) == 9 * 3);
|
|
||||||
|
|
||||||
for (size_t light = 0; light < number_of_leds; light++) {
|
|
||||||
GameAPI::Lights::writeLight(
|
|
||||||
RI_MGR,
|
|
||||||
lights.at(button_lights[light]),
|
|
||||||
((uint8_t *)i_pData)[light] / 255.f);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
#include "projector.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <cwchar>
|
||||||
|
|
||||||
|
#include "util/logging.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// request/response layout: identifier, command, two zero bytes, payload length,
|
||||||
|
// the payload itself and a checksum over everything before it
|
||||||
|
constexpr size_t HEADER_SIZE = 5;
|
||||||
|
constexpr uint8_t RESPONSE_ID = 0x23;
|
||||||
|
|
||||||
|
constexpr uint8_t COMMAND_COMMON_DATA = 0x8A;
|
||||||
|
constexpr uint8_t COMMAND_TEMPERATURE = 0x99;
|
||||||
|
constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B;
|
||||||
|
|
||||||
|
// shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT
|
||||||
|
constexpr uint32_t LAMP_SECONDS = 0;
|
||||||
|
constexpr uint32_t TEMPERATURE_INTAKE = 25;
|
||||||
|
constexpr uint32_t TEMPERATURE_EXHAUST = 35;
|
||||||
|
|
||||||
|
// anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact
|
||||||
|
// value was already recorded in /projecter/current on an earlier boot
|
||||||
|
constexpr uint32_t LAMP_CURRENT = 1000;
|
||||||
|
|
||||||
|
void put32(uint8_t *dest, uint32_t value) {
|
||||||
|
dest[0] = (uint8_t) (value & 0xFF);
|
||||||
|
dest[1] = (uint8_t) ((value >> 8) & 0xFF);
|
||||||
|
dest[2] = (uint8_t) ((value >> 16) & 0xFF);
|
||||||
|
dest[3] = (uint8_t) ((value >> 24) & 0xFF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) {
|
||||||
|
if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("silentscope", "Opened COM2 (projector)");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
|
||||||
|
|
||||||
|
DWORD bytes_read = 0;
|
||||||
|
while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) {
|
||||||
|
buffer[bytes_read++] = this->response.front();
|
||||||
|
this->response.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) bytes_read;
|
||||||
|
}
|
||||||
|
|
||||||
|
int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
|
||||||
|
this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite);
|
||||||
|
|
||||||
|
while (this->request.size() >= HEADER_SIZE) {
|
||||||
|
const size_t packet_size = HEADER_SIZE + this->request[4] + 1;
|
||||||
|
if (this->request.size() < packet_size) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->process_request(this->request.data());
|
||||||
|
this->request.erase(this->request.begin(), this->request.begin() + packet_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) nNumberOfBytesToWrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t games::silentscope::ProjectorHandle::bytes_available() {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
return this->response.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool games::silentscope::ProjectorHandle::close() {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
this->request.clear();
|
||||||
|
this->response.clear();
|
||||||
|
|
||||||
|
log_info("silentscope", "Closed COM2 (projector)");
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) {
|
||||||
|
const uint8_t command = packet[1];
|
||||||
|
const uint8_t length = packet[4];
|
||||||
|
const uint8_t *data = packet + HEADER_SIZE;
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case COMMAND_COMMON_DATA: {
|
||||||
|
|
||||||
|
// the game only looks at the lamp usage time near the end of the block
|
||||||
|
std::vector<uint8_t> payload(98, 0);
|
||||||
|
put32(&payload[94], LAMP_SECONDS);
|
||||||
|
this->reply(command, payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case COMMAND_TEMPERATURE: {
|
||||||
|
|
||||||
|
// the requested sensor is echoed back along with its reading
|
||||||
|
const uint8_t sensor = length > 0 ? data[0] : 0;
|
||||||
|
std::vector<uint8_t> payload(5, 0);
|
||||||
|
payload[0] = sensor;
|
||||||
|
put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST);
|
||||||
|
this->reply(command, payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case COMMAND_LAMP_CURRENT: {
|
||||||
|
|
||||||
|
// the three byte item selector is echoed back along with the measurement
|
||||||
|
std::vector<uint8_t> payload(7, 0);
|
||||||
|
memcpy(payload.data(), data, std::min<size_t>(length, 3));
|
||||||
|
put32(&payload[3], LAMP_CURRENT);
|
||||||
|
this->reply(command, payload);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
log_misc("silentscope", "unknown projector command {:#04x}", command);
|
||||||
|
this->reply(command, {});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector<uint8_t> &data) {
|
||||||
|
std::vector<uint8_t> packet {
|
||||||
|
RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size()
|
||||||
|
};
|
||||||
|
packet.insert(packet.end(), data.begin(), data.end());
|
||||||
|
|
||||||
|
uint8_t checksum = 0;
|
||||||
|
for (auto byte : packet) {
|
||||||
|
checksum += byte;
|
||||||
|
}
|
||||||
|
packet.push_back(checksum);
|
||||||
|
|
||||||
|
this->response.insert(this->response.end(), packet.begin(), packet.end());
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "hooks/devicehook.h"
|
||||||
|
|
||||||
|
namespace games::silentscope {
|
||||||
|
|
||||||
|
// The cabinet talks to its projector over COM2. Without an answer the game stops at
|
||||||
|
// I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly").
|
||||||
|
class ProjectorHandle : public CustomHandle {
|
||||||
|
public:
|
||||||
|
bool open(LPCWSTR lpFileName) override;
|
||||||
|
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
|
||||||
|
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
|
||||||
|
size_t bytes_available() override;
|
||||||
|
bool close() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mutex;
|
||||||
|
std::vector<uint8_t> request;
|
||||||
|
std::deque<uint8_t> response;
|
||||||
|
|
||||||
|
void process_request(const uint8_t *packet);
|
||||||
|
void reply(uint8_t command, const std::vector<uint8_t> &data);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
#include "silentscope.h"
|
#include "silentscope.h"
|
||||||
|
|
||||||
#include "acioemu/handle.h"
|
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
|
|
||||||
|
#include "projector.h"
|
||||||
|
|
||||||
namespace games::silentscope {
|
namespace games::silentscope {
|
||||||
|
|
||||||
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
|
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
|
||||||
@@ -16,9 +17,8 @@ namespace games::silentscope {
|
|||||||
// load the game DLL so hooks apply
|
// load the game DLL so hooks apply
|
||||||
libutils::try_library("gamendd.dll");
|
libutils::try_library("gamendd.dll");
|
||||||
|
|
||||||
// TODO(felix): implement stuff on this port
|
|
||||||
devicehook_init();
|
devicehook_init();
|
||||||
devicehook_add(new acioemu::ACIOHandle(L"COM2"));
|
devicehook_add(new ProjectorHandle());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SilentScopeGame::detach() {
|
void SilentScopeGame::detach() {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {}
|
|||||||
#include <dxgi.h>
|
#include <dxgi.h>
|
||||||
#include <dxgi1_2.h>
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
#include "d3d11_internal.h"
|
#include "d3d11_internal.h"
|
||||||
#include "util/nt_loader.h"
|
#include "util/nt_loader.h"
|
||||||
|
|
||||||
@@ -237,10 +238,11 @@ bool d3dcompiler_available() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void graphics_d3d11_init() {
|
void graphics_d3d11_init() {
|
||||||
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
|
// dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska
|
||||||
// their startup path completely untouched (no exports patched, no poll
|
// engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their
|
||||||
// thread, no LDR callback).
|
// startup path completely untouched (no exports patched, no poll thread, no
|
||||||
if (!GetModuleHandleW(L"execexe.dll")) {
|
// LDR callback).
|
||||||
|
if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -944,9 +944,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
*ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface);
|
||||||
|
|
||||||
|
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
|
||||||
|
*ppReturnedDeviceInterface = wrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return result
|
// return result
|
||||||
@@ -1030,14 +1033,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
|
||||||
log_warning(
|
|
||||||
"graphics::d3d9",
|
|
||||||
"-forceressub is unavailable; SMALL must remain {}x{}",
|
|
||||||
GFDM_SMALL_WIDTH,
|
|
||||||
GFDM_SMALL_HEIGHT);
|
|
||||||
return D3DERR_INVALIDCALL;
|
|
||||||
}
|
|
||||||
|
|
||||||
DWORD orig_behavior_flags = BehaviorFlags;
|
DWORD orig_behavior_flags = BehaviorFlags;
|
||||||
size_t num_adapters = 1;
|
size_t num_adapters = 1;
|
||||||
@@ -1093,7 +1088,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
params->BackBufferHeight, params->BackBufferWidth);
|
params->BackBufferHeight, params->BackBufferWidth);
|
||||||
std::swap(params->BackBufferWidth, params->BackBufferHeight);
|
std::swap(params->BackBufferWidth, params->BackBufferHeight);
|
||||||
}
|
}
|
||||||
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
} else if (i == 1 && !gfdm_two_head_exclusive()
|
||||||
|
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"use custom sub resolution {}x{} => {}x{}",
|
"use custom sub resolution {}x{} => {}x{}",
|
||||||
@@ -1137,7 +1133,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||||
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
|
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
|
||||||
}
|
}
|
||||||
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
} else if (i == 1 && !gfdm_two_head_exclusive()
|
||||||
|
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
|
||||||
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
|
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
|
||||||
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
|
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
|
||||||
}
|
}
|
||||||
@@ -1287,14 +1284,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
ppReturnedDeviceInterface);
|
ppReturnedDeviceInterface);
|
||||||
|
|
||||||
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
||||||
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
gfdm_publish_two_head_parameters(
|
||||||
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
pPresentationParameters,
|
||||||
gfdm_parameters.presentation_parameters[1];
|
pFullscreenDisplayMode,
|
||||||
if (pFullscreenDisplayMode != nullptr) {
|
gfdm_parameters);
|
||||||
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
|
||||||
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
|
||||||
gfdm_parameters.fullscreen_display_modes[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for error
|
// check for error
|
||||||
@@ -1307,13 +1300,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
*ppReturnedDeviceInterface,
|
*ppReturnedDeviceInterface,
|
||||||
gfdm_parameters.logical_small_swapchain,
|
gfdm_parameters.logical_small_swapchain,
|
||||||
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
||||||
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
|
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
|
||||||
|
|
||||||
|
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
|
||||||
|
*ppReturnedDeviceInterface = wrapped;
|
||||||
|
|
||||||
// initialize sub screen if the game requested a multi-head context
|
// initialize sub screen if the game requested a multi-head context
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
|
|||||||
@@ -157,6 +157,9 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// holds a reference on the device, so it has to go before the counts are compared
|
||||||
|
this->gfdm_small_head.release();
|
||||||
|
|
||||||
d3d9_readback::release_device_resources(this->pReal);
|
d3d9_readback::release_device_resources(this->pReal);
|
||||||
|
|
||||||
if (overlay::ENABLED) {
|
if (overlay::ENABLED) {
|
||||||
@@ -634,6 +637,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
|||||||
overlay::OVERLAY->reset_invalidate();
|
overlay::OVERLAY->reset_invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfdm_small_head.release();
|
||||||
|
|
||||||
|
// Reset refuses to run while any default pool resource is outstanding
|
||||||
|
d3d9_readback::discard_snapshot_targets(pReal);
|
||||||
|
|
||||||
HRESULT res = pReal->Reset(pPresentationParameters);
|
HRESULT res = pReal->Reset(pPresentationParameters);
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
@@ -659,6 +667,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
|
|||||||
|
|
||||||
graphics_d3d9_on_present(hFocusWindow, pReal, this);
|
graphics_d3d9_on_present(hFocusWindow, pReal, this);
|
||||||
|
|
||||||
|
// an adapter group device presents every head at once, so the SMALL head has to be
|
||||||
|
// composed here as well as in its own swap chain
|
||||||
|
if (gfdm_small_head.scaled()) {
|
||||||
|
gfdm_small_head.compose(pReal);
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
|
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +694,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
|||||||
if (is_gfdm_two_head_exclusive()
|
if (is_gfdm_two_head_exclusive()
|
||||||
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
||||||
{
|
{
|
||||||
|
if (gfdm_small_head.scaled()
|
||||||
|
&& iBackBuffer == 0
|
||||||
|
&& Type == D3DBACKBUFFER_TYPE_MONO)
|
||||||
|
{
|
||||||
|
CHECK_RESULT(gfdm_small_head.backbuffer(pReal, ppBackBuffer));
|
||||||
|
}
|
||||||
CHECK_RESULT(pReal->GetBackBuffer(
|
CHECK_RESULT(pReal->GetBackBuffer(
|
||||||
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
iBackBuffer,
|
iBackBuffer,
|
||||||
@@ -2321,6 +2341,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
overlay::OVERLAY->reset_invalidate();
|
overlay::OVERLAY->reset_invalidate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gfdm_small_head.release();
|
||||||
|
|
||||||
|
// ResetEx refuses to run while any default pool resource is outstanding
|
||||||
|
d3d9_readback::discard_snapshot_targets(pReal);
|
||||||
|
|
||||||
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
||||||
gfdm_parameters.presentation_parameters,
|
gfdm_parameters.presentation_parameters,
|
||||||
gfdm_parameters.fullscreen_display_modes);
|
gfdm_parameters.fullscreen_display_modes);
|
||||||
@@ -2335,14 +2360,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
}
|
}
|
||||||
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
||||||
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
||||||
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
gfdm_publish_two_head_parameters(
|
||||||
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
pPresentationParameters,
|
||||||
gfdm_parameters.presentation_parameters[1];
|
pFullscreenDisplayMode,
|
||||||
if (pFullscreenDisplayMode != nullptr) {
|
gfdm_parameters);
|
||||||
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
|
||||||
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
|
||||||
gfdm_parameters.fullscreen_display_modes[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
#include "d3d9_fake_swapchain.h"
|
#include "d3d9_fake_swapchain.h"
|
||||||
|
#include "d3d9_gfdm.h"
|
||||||
#include "d3d9_swapchain.h"
|
#include "d3d9_swapchain.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -264,6 +265,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
IDirect3DDevice9 *pReal;
|
IDirect3DDevice9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
bool is_d3d9ex = false;
|
||||||
|
|
||||||
|
// set from the creation flags; without it the runtime does no locking of its own, so
|
||||||
|
// nothing but the present thread may touch the device
|
||||||
|
bool device_multithreaded = false;
|
||||||
|
|
||||||
std::atomic_ulong refs = 1;
|
std::atomic_ulong refs = 1;
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||||
@@ -275,6 +280,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
||||||
bool gfdm_logical_group_parameters_valid = false;
|
bool gfdm_logical_group_parameters_valid = false;
|
||||||
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
||||||
|
GfdmSmallHead gfdm_small_head;
|
||||||
|
|
||||||
std::mutex gfdm_recovery_mutex;
|
std::mutex gfdm_recovery_mutex;
|
||||||
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
||||||
|
|||||||
@@ -16,6 +16,54 @@ bool gfdm_two_head_exclusive() {
|
|||||||
&& !GRAPHICS_WINDOWED;
|
&& !GRAPHICS_WINDOWED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::pair<UINT, UINT> gfdm_small_head_size() {
|
||||||
|
if (!gfdm_two_head_exclusive()) {
|
||||||
|
return { GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT };
|
||||||
|
}
|
||||||
|
return games::gitadora::arena_subscreen_host_size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// scaling is only needed when the head is not the panel size the game draws into
|
||||||
|
static bool gfdm_small_head_scaled() {
|
||||||
|
const auto [width, height] = gfdm_small_head_size();
|
||||||
|
return width != GFDM_SMALL_WIDTH || height != GFDM_SMALL_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hand the resolved MAIN/SMALL heads back to the game's own array. The SMALL entry keeps
|
||||||
|
// the portrait size the game asked for: that is what it renders, and the next reset finds
|
||||||
|
// the SMALL head by matching it.
|
||||||
|
void gfdm_publish_two_head_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
const GfdmTwoHeadDeviceState &state)
|
||||||
|
{
|
||||||
|
if (logical_presentation_parameters == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UINT small = state.logical_small_swapchain;
|
||||||
|
const bool publish_modes = logical_fullscreen_display_modes != nullptr
|
||||||
|
&& state.fullscreen_display_modes != nullptr;
|
||||||
|
|
||||||
|
logical_presentation_parameters[0] = state.presentation_parameters[0];
|
||||||
|
logical_presentation_parameters[small] = state.presentation_parameters[1];
|
||||||
|
if (publish_modes) {
|
||||||
|
logical_fullscreen_display_modes[0] = state.fullscreen_display_modes[0];
|
||||||
|
logical_fullscreen_display_modes[small] = state.fullscreen_display_modes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gfdm_small_head_scaled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logical_presentation_parameters[small].BackBufferWidth = GFDM_SMALL_WIDTH;
|
||||||
|
logical_presentation_parameters[small].BackBufferHeight = GFDM_SMALL_HEIGHT;
|
||||||
|
if (publish_modes) {
|
||||||
|
logical_fullscreen_display_modes[small].Width = GFDM_SMALL_WIDTH;
|
||||||
|
logical_fullscreen_display_modes[small].Height = GFDM_SMALL_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
@@ -186,6 +234,26 @@ HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
|||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the game keeps rendering the portrait subscreen; only the head it is scanned out
|
||||||
|
// on changes size, and the scaling happens when the head is presented
|
||||||
|
if (gfdm_small_head_scaled()) {
|
||||||
|
const auto [host_width, host_height] = gfdm_small_head_size();
|
||||||
|
log_info(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: {} SMALL head {}x{} -> {}x{}",
|
||||||
|
operation,
|
||||||
|
secondary.BackBufferWidth,
|
||||||
|
secondary.BackBufferHeight,
|
||||||
|
host_width,
|
||||||
|
host_height);
|
||||||
|
secondary.BackBufferWidth = host_width;
|
||||||
|
secondary.BackBufferHeight = host_height;
|
||||||
|
if (fullscreen_display_modes != nullptr) {
|
||||||
|
fullscreen_display_modes[1].Width = host_width;
|
||||||
|
fullscreen_display_modes[1].Height = host_height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return D3D_OK;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,20 +386,21 @@ HRESULT validate_gfdm_two_head_exclusive(
|
|||||||
|
|
||||||
const auto &main = presentation_parameters[0];
|
const auto &main = presentation_parameters[0];
|
||||||
const auto &small_params = presentation_parameters[1];
|
const auto &small_params = presentation_parameters[1];
|
||||||
|
const auto [expected_small_width, expected_small_height] = gfdm_small_head_size();
|
||||||
if (main.Windowed || small_params.Windowed) {
|
if (main.Windowed || small_params.Windowed) {
|
||||||
log_warning(
|
log_warning(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"two-head exclusive mode requires both group heads to be fullscreen");
|
"two-head exclusive mode requires both group heads to be fullscreen");
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|
if (small_params.BackBufferWidth != expected_small_width
|
||||||
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
|| small_params.BackBufferHeight != expected_small_height)
|
||||||
{
|
{
|
||||||
log_warning(
|
log_warning(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"SMALL head must be {}x{}; got {}x{}",
|
"SMALL head must be {}x{}; got {}x{}",
|
||||||
GFDM_SMALL_WIDTH,
|
expected_small_width,
|
||||||
GFDM_SMALL_HEIGHT,
|
expected_small_height,
|
||||||
small_params.BackBufferWidth,
|
small_params.BackBufferWidth,
|
||||||
small_params.BackBufferHeight);
|
small_params.BackBufferHeight);
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
@@ -547,6 +616,7 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
|||||||
{
|
{
|
||||||
if (presentation_parameters == nullptr) {
|
if (presentation_parameters == nullptr) {
|
||||||
gfdm_logical_group_parameters_valid = false;
|
gfdm_logical_group_parameters_valid = false;
|
||||||
|
gfdm_small_head.resolve(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::copy_n(
|
std::copy_n(
|
||||||
@@ -554,6 +624,8 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
|||||||
gfdm_logical_group_parameters.size(),
|
gfdm_logical_group_parameters.size(),
|
||||||
gfdm_logical_group_parameters.begin());
|
gfdm_logical_group_parameters.begin());
|
||||||
gfdm_logical_group_parameters_valid = true;
|
gfdm_logical_group_parameters_valid = true;
|
||||||
|
gfdm_small_head.resolve(
|
||||||
|
&gfdm_logical_group_parameters[gfdm_logical_small_swapchain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
FakeIDirect3DSwapChain9 *
|
FakeIDirect3DSwapChain9 *
|
||||||
@@ -599,6 +671,110 @@ void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GfdmSmallHead::resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters) {
|
||||||
|
release();
|
||||||
|
|
||||||
|
active = gfdm_small_head_scaled();
|
||||||
|
format = D3DFMT_X8R8G8B8;
|
||||||
|
multisample = D3DMULTISAMPLE_NONE;
|
||||||
|
multisample_quality = 0;
|
||||||
|
if (!active || logical_small_parameters == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logical_small_parameters->BackBufferFormat != D3DFMT_UNKNOWN) {
|
||||||
|
format = logical_small_parameters->BackBufferFormat;
|
||||||
|
}
|
||||||
|
multisample = logical_small_parameters->MultiSampleType;
|
||||||
|
multisample_quality = logical_small_parameters->MultiSampleQuality;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfdmSmallHead::apply_logical_size(UINT *width, UINT *height) const {
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (width != nullptr) {
|
||||||
|
*width = GFDM_SMALL_WIDTH;
|
||||||
|
}
|
||||||
|
if (height != nullptr) {
|
||||||
|
*height = GFDM_SMALL_HEIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT GfdmSmallHead::backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out) {
|
||||||
|
if (device == nullptr || out == nullptr || !active) {
|
||||||
|
return D3DERR_INVALIDCALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (surface == nullptr) {
|
||||||
|
const HRESULT result = device->CreateRenderTarget(
|
||||||
|
GFDM_SMALL_WIDTH,
|
||||||
|
GFDM_SMALL_HEIGHT,
|
||||||
|
format,
|
||||||
|
multisample,
|
||||||
|
multisample_quality,
|
||||||
|
FALSE,
|
||||||
|
&surface,
|
||||||
|
nullptr);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
log_warning(
|
||||||
|
"graphics::d3d9",
|
||||||
|
"two-head exclusive: could not create the portrait SMALL surface, hr={}",
|
||||||
|
FMT_HRESULT(result));
|
||||||
|
surface = nullptr;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
device->ColorFill(surface, nullptr, D3DCOLOR_XRGB(0, 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
surface->AddRef();
|
||||||
|
*out = surface;
|
||||||
|
return D3D_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void GfdmSmallHead::release() {
|
||||||
|
if (surface != nullptr) {
|
||||||
|
surface->Release();
|
||||||
|
surface = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT GfdmSmallHead::compose(IDirect3DDevice9 *device) {
|
||||||
|
IDirect3DSurface9 *proxy = nullptr;
|
||||||
|
HRESULT result = backbuffer(device, &proxy);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
IDirect3DSurface9 *head = nullptr;
|
||||||
|
result = device->GetBackBuffer(
|
||||||
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
|
0,
|
||||||
|
D3DBACKBUFFER_TYPE_MONO,
|
||||||
|
&head);
|
||||||
|
if (FAILED(result)) {
|
||||||
|
proxy->Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
result = head->GetDesc(&desc);
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
const RECT content = games::gitadora::arena_subscreen_content_rect(
|
||||||
|
static_cast<LONG>(desc.Width),
|
||||||
|
static_cast<LONG>(desc.Height));
|
||||||
|
|
||||||
|
// discard swap effect leaves the whole head undefined every frame, so the bars
|
||||||
|
// have to be repainted along with the image
|
||||||
|
device->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0));
|
||||||
|
result = device->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
head->Release();
|
||||||
|
proxy->Release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
|
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
|
||||||
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
gfdm_recovery_armed = false;
|
gfdm_recovery_armed = false;
|
||||||
|
|||||||
@@ -13,6 +13,34 @@ inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGH
|
|||||||
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
|
||||||
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
||||||
|
|
||||||
|
// Owns the portrait surface the game draws into while the SMALL head is scanned out at a
|
||||||
|
// different resolution, and composes it onto the real head. Allocates nothing and reports
|
||||||
|
// nothing until resolve() finds a head that is not the panel size.
|
||||||
|
struct GfdmSmallHead {
|
||||||
|
GfdmSmallHead() = default;
|
||||||
|
~GfdmSmallHead() { release(); }
|
||||||
|
GfdmSmallHead(const GfdmSmallHead &) = delete;
|
||||||
|
GfdmSmallHead &operator=(const GfdmSmallHead &) = delete;
|
||||||
|
|
||||||
|
// settled with the device, so it cannot change under the game mid-session
|
||||||
|
void resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters);
|
||||||
|
bool scaled() const { return active; }
|
||||||
|
|
||||||
|
HRESULT backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out);
|
||||||
|
HRESULT compose(IDirect3DDevice9 *device);
|
||||||
|
void release();
|
||||||
|
|
||||||
|
// the game only ever sees the portrait size it asked for
|
||||||
|
void apply_logical_size(UINT *width, UINT *height) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool active = false;
|
||||||
|
D3DFORMAT format = D3DFMT_X8R8G8B8;
|
||||||
|
D3DMULTISAMPLE_TYPE multisample = D3DMULTISAMPLE_NONE;
|
||||||
|
DWORD multisample_quality = 0;
|
||||||
|
IDirect3DSurface9 *surface = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
struct GfdmTwoHeadDeviceState {
|
struct GfdmTwoHeadDeviceState {
|
||||||
explicit GfdmTwoHeadDeviceState(
|
explicit GfdmTwoHeadDeviceState(
|
||||||
D3DPRESENT_PARAMETERS *presentation_parameters,
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
@@ -41,6 +69,10 @@ struct GfdmTwoHeadDeviceState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool gfdm_two_head_exclusive();
|
bool gfdm_two_head_exclusive();
|
||||||
|
void gfdm_publish_two_head_parameters(
|
||||||
|
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
|
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
|
const GfdmTwoHeadDeviceState &state);
|
||||||
bool is_fake_subscreen_adapter(UINT adapter);
|
bool is_fake_subscreen_adapter(UINT adapter);
|
||||||
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
||||||
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "d3d9_readback.h"
|
#include "d3d9_readback.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -10,6 +13,13 @@ namespace d3d9_readback {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
// the snapshot path stamps frames with this to recognise one left behind by a break in the
|
||||||
|
// request stream
|
||||||
|
uint64_t now_us() {
|
||||||
|
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||||
|
}
|
||||||
|
|
||||||
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||||
IDirect3DSurface9 *surface = nullptr;
|
IDirect3DSurface9 *surface = nullptr;
|
||||||
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
||||||
@@ -160,21 +170,8 @@ ReadbackPool &pool() {
|
|||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
// the back buffer plus the checks every caller has to make before copying out of it
|
||||||
|
SurfacePtr open_backbuffer(IDirect3DSwapChain9 *swap_chain, int screen, D3DSURFACE_DESC &desc) {
|
||||||
void release_device_resources(IDirect3DDevice9 *device) {
|
|
||||||
pool().clear_device(device);
|
|
||||||
}
|
|
||||||
|
|
||||||
BackbufferCopy::~BackbufferCopy() {
|
|
||||||
if (this->pooled && this->surface) {
|
|
||||||
pool().release(this->device, std::move(this->surface));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|
||||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
|
|
||||||
|
|
||||||
IDirect3DSurface9 *buffer = nullptr;
|
IDirect3DSurface9 *buffer = nullptr;
|
||||||
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||||
if (FAILED(hr) || buffer == nullptr) {
|
if (FAILED(hr) || buffer == nullptr) {
|
||||||
@@ -182,17 +179,17 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|||||||
"failed to get back buffer for screen {}, hr={}",
|
"failed to get back buffer for screen {}, hr={}",
|
||||||
screen,
|
screen,
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
return std::nullopt;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
D3DSURFACE_DESC desc {};
|
SurfacePtr surface(buffer);
|
||||||
hr = buffer->GetDesc(&desc);
|
|
||||||
|
hr = surface->GetDesc(&desc);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
"failed to acquire back buffer descriptor, hr={}",
|
"failed to acquire back buffer descriptor, hr={}",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
buffer->Release();
|
return nullptr;
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRenderTargetData rejects multisampled sources. no supported game has been
|
// GetRenderTargetData rejects multisampled sources. no supported game has been
|
||||||
@@ -204,7 +201,399 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|||||||
"back buffer is multisampled ({}), screenshots and capture are unsupported",
|
"back buffer is multisampled ({}), screenshots and capture are unsupported",
|
||||||
static_cast<uint32_t>(desc.MultiSampleType));
|
static_cast<uint32_t>(desc.MultiSampleType));
|
||||||
});
|
});
|
||||||
buffer->Release();
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
SurfacePtr create_snapshot_target(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||||
|
IDirect3DSurface9 *surface = nullptr;
|
||||||
|
|
||||||
|
// matching the back buffer keeps the blit a straight copy and leaves the pixels in the
|
||||||
|
// format the conversion step would have seen without the detour
|
||||||
|
const HRESULT hr = device->CreateRenderTarget(
|
||||||
|
desc.Width, desc.Height, desc.Format,
|
||||||
|
D3DMULTISAMPLE_NONE, 0, FALSE, &surface, nullptr);
|
||||||
|
|
||||||
|
if (FAILED(hr) || surface == nullptr) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to create snapshot target, hr={}",
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SurfacePtr(surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
// two render targets per screen, reused across frames. two because one holds the frame waiting
|
||||||
|
// out its deferral while the other takes the next one; a read only holds its target until the
|
||||||
|
// pixels are in system memory, which is far shorter than the gap between requests.
|
||||||
|
//
|
||||||
|
// unlike the readback surfaces these live in the default pool, so they have to be gone before a
|
||||||
|
// Reset and not merely before the device is released
|
||||||
|
class SnapshotTargets {
|
||||||
|
public:
|
||||||
|
SurfacePtr acquire(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
int screen,
|
||||||
|
const D3DSURFACE_DESC &desc,
|
||||||
|
uint64_t *out_generation) {
|
||||||
|
|
||||||
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
|
||||||
|
if (this->device != device) {
|
||||||
|
this->drop();
|
||||||
|
this->device = device;
|
||||||
|
this->usable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &entry = this->screens[screen];
|
||||||
|
|
||||||
|
Slot *free_slot = nullptr;
|
||||||
|
for (size_t i = 0; i < SLOTS_PER_SCREEN; i++) {
|
||||||
|
auto &candidate = entry.slots[(entry.next + i) % SLOTS_PER_SCREEN];
|
||||||
|
if (!candidate.busy) {
|
||||||
|
free_slot = &candidate;
|
||||||
|
entry.next = (entry.next + i + 1) % SLOTS_PER_SCREEN;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (free_slot == nullptr) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto &slot = *free_slot;
|
||||||
|
|
||||||
|
if (slot.target
|
||||||
|
&& (slot.width != desc.Width
|
||||||
|
|| slot.height != desc.Height
|
||||||
|
|| slot.format != desc.Format)) {
|
||||||
|
slot.target.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slot.target) {
|
||||||
|
slot.target = create_snapshot_target(device, desc);
|
||||||
|
if (!slot.target) {
|
||||||
|
// a back buffer format the device will not give us a render target for. the
|
||||||
|
// inline path can still read it, so stop trying rather than lose the stream
|
||||||
|
this->usable = false;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.width = desc.Width;
|
||||||
|
slot.height = desc.Height;
|
||||||
|
slot.format = desc.Format;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot.busy = true;
|
||||||
|
*out_generation = this->current_generation;
|
||||||
|
|
||||||
|
slot.target->AddRef();
|
||||||
|
return SurfacePtr(slot.target.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// holds the fresh snapshot back and returns the one from the previous request, which by now
|
||||||
|
// has had a full frame for its blit and transfer to land
|
||||||
|
std::optional<Snapshot> rotate(int screen, std::optional<Snapshot> fresh) {
|
||||||
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint64_t now = now_us();
|
||||||
|
std::optional<Snapshot> previous;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
previous = std::move(this->pending[screen]);
|
||||||
|
this->pending[screen] = std::move(fresh);
|
||||||
|
}
|
||||||
|
|
||||||
|
// a frame waits here for the next request rather than for a deadline, so a gap in
|
||||||
|
// requests, a client reconnecting most obviously, would otherwise hand the new session
|
||||||
|
// a frame from before the gap. showing a stale frame is worse than showing none
|
||||||
|
if (previous.has_value() && now - previous->issued_us > MAX_DEFERRAL_US) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a reader whose target was already thrown away must not free the slot, or it would free
|
||||||
|
// the snapshot that replaced it while that one is still being read
|
||||||
|
void finish(int screen, uint64_t generation, IDirect3DSurface9 *target) {
|
||||||
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
if (generation != this->current_generation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->free_slot(screen, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->idle.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool still_current(uint64_t generation) {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
return generation == this->current_generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_usable() {
|
||||||
|
std::lock_guard<std::mutex> lock(this->mutex);
|
||||||
|
return this->usable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset fails outright while the device still owns default pool resources, so dropping our
|
||||||
|
// own references is not enough and any read in flight has to finish first. the wait is
|
||||||
|
// bounded because a failed Reset is something games retry and a stalled present thread is
|
||||||
|
// not something they survive
|
||||||
|
void discard(IDirect3DDevice9 *device) {
|
||||||
|
// destroyed after the lock is released, since dropping a snapshot calls back in here
|
||||||
|
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> stale;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(this->mutex);
|
||||||
|
if (this->device != nullptr && this->device != device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// deferred frames are abandoned rather than waited for; only a read that is already
|
||||||
|
// running has to be allowed to finish
|
||||||
|
for (auto &held : this->pending) {
|
||||||
|
if (held.has_value() && held->surface) {
|
||||||
|
this->free_slot(held->screen, held->surface.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stale = std::move(this->pending);
|
||||||
|
this->pending = {};
|
||||||
|
|
||||||
|
const bool drained = this->idle.wait_for(
|
||||||
|
lock,
|
||||||
|
std::chrono::milliseconds(100),
|
||||||
|
[this] { return !this->any_busy(); });
|
||||||
|
|
||||||
|
if (!drained) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"capture snapshot still in flight, discarding its target anyway");
|
||||||
|
}
|
||||||
|
|
||||||
|
this->drop();
|
||||||
|
this->device = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t SLOTS_PER_SCREEN = 2;
|
||||||
|
|
||||||
|
// generous next to the frame interval this is meant to bridge, so that ordinary jitter
|
||||||
|
// never trips it and only a real break in the request stream does
|
||||||
|
static constexpr uint64_t MAX_DEFERRAL_US = 250'000;
|
||||||
|
|
||||||
|
struct Slot {
|
||||||
|
SurfacePtr target;
|
||||||
|
UINT width = 0;
|
||||||
|
UINT height = 0;
|
||||||
|
D3DFORMAT format = D3DFMT_UNKNOWN;
|
||||||
|
bool busy = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Screen {
|
||||||
|
std::array<Slot, SLOTS_PER_SCREEN> slots;
|
||||||
|
size_t next = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
void free_slot(int screen, IDirect3DSurface9 *target) {
|
||||||
|
for (auto &slot : this->screens[screen].slots) {
|
||||||
|
if (slot.target.get() == target) {
|
||||||
|
slot.busy = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool any_busy() const {
|
||||||
|
for (const auto &entry : this->screens) {
|
||||||
|
for (const auto &slot : entry.slots) {
|
||||||
|
if (slot.busy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drop() {
|
||||||
|
for (auto &entry : this->screens) {
|
||||||
|
for (auto &slot : entry.slots) {
|
||||||
|
slot.target.reset();
|
||||||
|
slot.busy = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.next = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->current_generation++;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::mutex mutex;
|
||||||
|
std::condition_variable idle;
|
||||||
|
std::array<Screen, GRAPHICS_CAPTURE_SCREEN_NO> screens;
|
||||||
|
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> pending;
|
||||||
|
IDirect3DDevice9 *device = nullptr;
|
||||||
|
uint64_t current_generation = 1;
|
||||||
|
bool usable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// never destroyed, for the same reason the readback pool is not
|
||||||
|
SnapshotTargets &targets() {
|
||||||
|
static SnapshotTargets *instance = new SnapshotTargets();
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void release_device_resources(IDirect3DDevice9 *device) {
|
||||||
|
targets().discard(device);
|
||||||
|
pool().clear_device(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
void discard_snapshot_targets(IDirect3DDevice9 *device) {
|
||||||
|
targets().discard(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool snapshots_supported() {
|
||||||
|
return targets().is_usable();
|
||||||
|
}
|
||||||
|
|
||||||
|
BackbufferCopy::~BackbufferCopy() {
|
||||||
|
if (this->pooled && this->surface) {
|
||||||
|
pool().release(this->device, std::move(this->surface));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Snapshot::~Snapshot() {
|
||||||
|
// still holding the target means the read never ran, and the slot would otherwise stay
|
||||||
|
// marked busy and take the screen out of capture permanently
|
||||||
|
if (this->surface) {
|
||||||
|
targets().finish(this->screen, this->generation, this->surface.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::optional<Snapshot> take_snapshot(
|
||||||
|
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
|
||||||
|
|
||||||
|
const uint64_t started_us = now_us();
|
||||||
|
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
auto buffer = open_backbuffer(swap_chain, screen, desc);
|
||||||
|
if (!buffer) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t generation = 0;
|
||||||
|
auto target = targets().acquire(device, screen, desc, &generation);
|
||||||
|
if (!target) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// built before the blit so that a failure below hands the slot back through the destructor
|
||||||
|
Snapshot snapshot;
|
||||||
|
snapshot.screen = screen;
|
||||||
|
snapshot.desc = desc;
|
||||||
|
snapshot.device = device;
|
||||||
|
snapshot.surface = std::move(target);
|
||||||
|
snapshot.generation = generation;
|
||||||
|
snapshot.issued_us = started_us;
|
||||||
|
|
||||||
|
// the point of the whole exercise: this is queued rather than waited on, so the game pays
|
||||||
|
// for issuing the copy and not for it completing. identical size and format, so there is
|
||||||
|
// no filtering to ask for
|
||||||
|
const HRESULT hr = device->StretchRect(
|
||||||
|
buffer.get(), nullptr, snapshot.surface.get(), nullptr, D3DTEXF_NONE);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to snapshot back buffer for screen {}, hr={}",
|
||||||
|
screen,
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<Snapshot> snapshot_backbuffer(
|
||||||
|
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
|
||||||
|
|
||||||
|
return targets().rotate(screen, take_snapshot(device, swap_chain, screen));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot) {
|
||||||
|
if (!snapshot.surface) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a Reset between the blit and now means the target no longer holds the captured frame
|
||||||
|
if (!targets().still_current(snapshot.generation)) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto destination = pool().acquire(snapshot.device, snapshot.desc);
|
||||||
|
if (!destination) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HRESULT hr = snapshot.device->GetRenderTargetData(
|
||||||
|
snapshot.surface.get(), destination.get());
|
||||||
|
|
||||||
|
// the target is reusable as soon as the pixels are in system memory. dropping the
|
||||||
|
// reference before freeing the slot keeps the destructor from freeing it twice
|
||||||
|
const int screen = snapshot.screen;
|
||||||
|
const uint64_t generation = snapshot.generation;
|
||||||
|
IDirect3DSurface9 *target = snapshot.surface.get();
|
||||||
|
snapshot.surface.reset();
|
||||||
|
targets().finish(screen, generation, target);
|
||||||
|
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to read snapshot contents, hr={}",
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
pool().release(snapshot.device, std::move(destination));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
BackbufferCopy copy;
|
||||||
|
copy.screen = screen;
|
||||||
|
copy.desc = snapshot.desc;
|
||||||
|
copy.device = snapshot.device;
|
||||||
|
copy.surface = std::move(destination);
|
||||||
|
copy.pooled = true;
|
||||||
|
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||||
|
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
|
||||||
|
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
auto buffer = open_backbuffer(swap_chain, screen, desc);
|
||||||
|
if (!buffer) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +601,10 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|||||||
? pool().acquire(device, desc)
|
? pool().acquire(device, desc)
|
||||||
: create_readback_surface(device, desc);
|
: create_readback_surface(device, desc);
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
buffer->Release();
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr = device->GetRenderTargetData(buffer, destination.get());
|
const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get());
|
||||||
buffer->Release();
|
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
@@ -39,6 +40,51 @@ namespace d3d9_readback {
|
|||||||
int screen,
|
int screen,
|
||||||
bool pooled);
|
bool pooled);
|
||||||
|
|
||||||
|
// GPU side copy of a back buffer, taken while the contents are still the frame that was
|
||||||
|
// presented, so that reading them into system memory no longer has to happen before it
|
||||||
|
struct Snapshot {
|
||||||
|
int screen {};
|
||||||
|
D3DSURFACE_DESC desc {};
|
||||||
|
IDirect3DDevice9 *device = nullptr;
|
||||||
|
SurfacePtr surface;
|
||||||
|
uint64_t generation {};
|
||||||
|
|
||||||
|
// when the blit was issued, so a frame left behind by a break in the request stream can
|
||||||
|
// be recognised as stale rather than handed over
|
||||||
|
uint64_t issued_us {};
|
||||||
|
|
||||||
|
Snapshot() = default;
|
||||||
|
Snapshot(Snapshot &&) noexcept = default;
|
||||||
|
Snapshot &operator=(Snapshot &&) noexcept = default;
|
||||||
|
Snapshot(const Snapshot &) = delete;
|
||||||
|
Snapshot &operator=(const Snapshot &) = delete;
|
||||||
|
~Snapshot();
|
||||||
|
};
|
||||||
|
|
||||||
|
// for the present thread, between the last EndScene and Present. blits the current frame,
|
||||||
|
// then returns the snapshot taken on the *previous* call: waiting a frame before reading
|
||||||
|
// means the blit and its system memory transfer have already happened, so the read does not
|
||||||
|
// stall on the GPU. costs the stream one frame of latency.
|
||||||
|
//
|
||||||
|
// returns nothing on the first call of a stream, and whenever the frame could not be taken,
|
||||||
|
// which is the caller's cue to skip rather than to wait
|
||||||
|
std::optional<Snapshot> snapshot_backbuffer(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
IDirect3DSwapChain9 *swap_chain,
|
||||||
|
int screen);
|
||||||
|
|
||||||
|
// the expensive half, for a thread that is not the present thread. only legal on a device
|
||||||
|
// created with D3DCREATE_MULTITHREADED
|
||||||
|
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot);
|
||||||
|
|
||||||
|
// false once a device has refused to give up a render target matching its back buffer,
|
||||||
|
// which leaves reading the back buffer directly as the only way to capture it
|
||||||
|
bool snapshots_supported();
|
||||||
|
|
||||||
|
// snapshot targets live in the default pool, so unlike the readback surfaces they have to
|
||||||
|
// be gone before a Reset and not merely before the device is released
|
||||||
|
void discard_snapshot_targets(IDirect3DDevice9 *device);
|
||||||
|
|
||||||
// pooled surfaces hold references on the device; call this before releasing it
|
// pooled surfaces hold references on the device; call this before releasing it
|
||||||
void release_device_resources(IDirect3DDevice9 *device);
|
void release_device_resources(IDirect3DDevice9 *device);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
#include <external/robin_hood.h>
|
#include <external/robin_hood.h>
|
||||||
#include <external/fpng/fpng.h>
|
#include <external/fpng/fpng.h>
|
||||||
|
|
||||||
|
#include "api/capture_pump.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "misc/clipboard.h"
|
#include "misc/clipboard.h"
|
||||||
@@ -169,6 +170,13 @@ ThreadPool &encode_pool() {
|
|||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// where a capture's pixels are converted and handed to the api. never destroyed: the read
|
||||||
|
// pool below can still be working at process exit, and it queues onto this one
|
||||||
|
ThreadPool &capture_save_pool() {
|
||||||
|
static auto *instance = new ThreadPool(2);
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
// normalize the supported D3D formats to packed 24bpp RGB. callers screen the
|
// normalize the supported D3D formats to packed 24bpp RGB. callers screen the
|
||||||
// format through surface_pixel_size first, so the black fill below is a fallback
|
// format through surface_pixel_size first, so the black fill below is a fallback
|
||||||
void surface_to_rgb(
|
void surface_to_rgb(
|
||||||
@@ -437,11 +445,106 @@ static void dispatch_capture_save(PendingCapture capture) {
|
|||||||
if (image_processing_must_be_inline()) {
|
if (image_processing_must_be_inline()) {
|
||||||
capture_process();
|
capture_process();
|
||||||
} else {
|
} else {
|
||||||
static auto pool = ThreadPool(2);
|
capture_save_pool().add(std::move(capture_process));
|
||||||
pool.add(std::move(capture_process));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// destroying the BackbufferCopy returns its surface to the pool, which is a device call, so
|
||||||
|
// it has to happen on whichever thread was cleared to do the read
|
||||||
|
static void read_and_dispatch_capture(int screen, BackbufferCopy copy) {
|
||||||
|
PendingCapture capture;
|
||||||
|
if (!read_capture_surface(copy, capture)) {
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch_capture_save(std::move(capture));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether the readback runs on the present thread or a pool thread trades the game's frame
|
||||||
|
// time against the risk of two threads being inside the device at once.
|
||||||
|
//
|
||||||
|
// The read is a LockRect plus a row by row memcpy of the whole back buffer: roughly 635us at
|
||||||
|
// 720p and 1270us at 1080p. On the present thread that comes straight out of the game's frame
|
||||||
|
// budget, and at 120Hz with a 60fps stream running it measured as a drop to 117fps. Moving it
|
||||||
|
// to a pool thread gave the full 120 back.
|
||||||
|
//
|
||||||
|
// Only streaming is worth that trade. It is the only path that pays the cost on every frame,
|
||||||
|
// and it is the only one the user has opted into by connecting a client. Screenshots and the
|
||||||
|
// one off api captures stay inline: they are rare enough that a single slow frame does not
|
||||||
|
// matter, and the hazard being avoided is reproduced rather than theoretical, since a pool
|
||||||
|
// thread in LockRect while the present thread sat inside GetRenderTargetData deadlocked
|
||||||
|
// DDR X2, whose device has no internal locking. Games already known to dislike threaded image
|
||||||
|
// processing are excluded as well, on the assumption that whatever breaks them applies here.
|
||||||
|
static bool capture_read_off_thread(int screen) {
|
||||||
|
return api::capture_pump::screen_claimed(screen) && !image_processing_must_be_inline();
|
||||||
|
}
|
||||||
|
|
||||||
|
ThreadPool &capture_read_pool() {
|
||||||
|
// one worker, so reads finish in the order they were submitted: a second worker could
|
||||||
|
// overtake a descheduled one and enqueue a stale frame over a newer one. never destroyed,
|
||||||
|
// so a read still running at process exit cannot touch a dead pool
|
||||||
|
static auto *instance = new ThreadPool(1);
|
||||||
|
return *instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Takes the frame on the present thread as a queued GPU blit and hands the readback to a pool
|
||||||
|
// thread, so the game waits for neither. Only viable where the whole read can go off thread,
|
||||||
|
// since the back buffer is overwritten right after Present and a snapshot the present thread
|
||||||
|
// then had to read itself would cost more than reading the back buffer directly.
|
||||||
|
//
|
||||||
|
// Returns false when the frame could not be taken, including the ordinary case of the previous
|
||||||
|
// snapshot of this screen still being read, which paces capture to what the reader sustains.
|
||||||
|
static bool snapshot_capture(
|
||||||
|
IDirect3DDevice9 *device,
|
||||||
|
WrappedIDirect3DDevice9 *wrapped_device,
|
||||||
|
int screen) {
|
||||||
|
|
||||||
|
IDirect3DSwapChain9 *swap_chain = nullptr;
|
||||||
|
const HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
|
||||||
|
if (FAILED(hr) || swap_chain == nullptr) {
|
||||||
|
log_warning("graphics::d3d9",
|
||||||
|
"failed to get swap chain for screen {}, hr={}",
|
||||||
|
screen,
|
||||||
|
FMT_HRESULT(hr));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto snapshot = d3d9_readback::snapshot_backbuffer(device, swap_chain, screen);
|
||||||
|
swap_chain->Release();
|
||||||
|
|
||||||
|
if (!snapshot.has_value()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
capture_read_pool().add([screen, snapshot = std::move(*snapshot)]() mutable {
|
||||||
|
// an escape from here would cross a thread boundary and terminate
|
||||||
|
try {
|
||||||
|
auto copy = d3d9_readback::read_snapshot(std::move(snapshot));
|
||||||
|
if (!copy.has_value()) {
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
read_and_dispatch_capture(screen, std::move(*copy));
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9", "capture read failed: {}", error.what());
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
} catch (...) {
|
||||||
|
log_warning("graphics::d3d9", "capture read failed");
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
// the snapshot went into the lambda before the queue could fail, so it is already
|
||||||
|
// destroyed and its target handed back; the client just misses this frame
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// by this point the pixels are plain memory, so none of this needs the device
|
// by this point the pixels are plain memory, so none of this needs the device
|
||||||
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
|
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
|
||||||
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
||||||
@@ -586,6 +689,18 @@ static void process_image_request(
|
|||||||
const ImageRequest &request) {
|
const ImageRequest &request) {
|
||||||
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
||||||
|
|
||||||
|
if (!screenshot
|
||||||
|
&& wrapped_device->device_multithreaded
|
||||||
|
&& capture_read_off_thread(request.screen)
|
||||||
|
&& d3d9_readback::snapshots_supported()) {
|
||||||
|
|
||||||
|
if (!snapshot_capture(device, wrapped_device, request.screen)) {
|
||||||
|
graphics_capture_skip(request.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<int> screens { request.screen };
|
std::vector<int> screens { request.screen };
|
||||||
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
||||||
screens.clear();
|
screens.clear();
|
||||||
@@ -623,14 +738,34 @@ static void process_image_request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!screenshot) {
|
if (!screenshot) {
|
||||||
PendingCapture capture;
|
auto copy = std::move(copies.front());
|
||||||
if (!read_capture_surface(copies.front(), capture)) {
|
copies.clear();
|
||||||
|
|
||||||
|
if (capture_read_off_thread(request.screen)) {
|
||||||
|
try {
|
||||||
|
capture_read_pool().add(
|
||||||
|
[screen = request.screen, copy = std::move(copy)]() mutable {
|
||||||
|
// an escape from here would cross a thread boundary and terminate
|
||||||
|
try {
|
||||||
|
read_and_dispatch_capture(screen, std::move(copy));
|
||||||
|
} catch (const std::exception &error) {
|
||||||
|
log_warning("graphics::d3d9", "capture read failed: {}", error.what());
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
} catch (...) {
|
||||||
|
log_warning("graphics::d3d9", "capture read failed");
|
||||||
|
graphics_capture_skip(screen);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
// the copy went into the lambda before the queue could fail, so there is
|
||||||
|
// nothing left to read here and the client misses this frame
|
||||||
graphics_capture_skip(request.screen);
|
graphics_capture_skip(request.screen);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
copies.clear();
|
read_and_dispatch_capture(request.screen, std::move(copy));
|
||||||
dispatch_capture_save(std::move(capture));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
} \
|
} \
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
// inert unless this is the SMALL head and it is being scaled onto a differently sized monitor
|
||||||
|
static bool scales_small_head(const WrappedIDirect3DSwapChain9 *chain) {
|
||||||
|
return chain->native_group_head == WrappedIDirect3DSwapChain9::NativeGroupHead::Small
|
||||||
|
&& chain->pDev->gfdm_small_head.scaled();
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
|
||||||
if (ppvObj == nullptr) {
|
if (ppvObj == nullptr) {
|
||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
@@ -89,6 +95,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scales_small_head(this)) {
|
||||||
|
pDev->gfdm_small_head.compose(pDev->pReal);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT result = pReal->Present(
|
HRESULT result = pReal->Present(
|
||||||
pSourceRect,
|
pSourceRect,
|
||||||
pDestRect,
|
pDestRect,
|
||||||
@@ -138,6 +148,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect
|
|||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
|
||||||
IDirect3DSurface9 **ppBackBuffer)
|
IDirect3DSurface9 **ppBackBuffer)
|
||||||
{
|
{
|
||||||
|
if (scales_small_head(this) && iBackBuffer == 0 && Type == D3DBACKBUFFER_TYPE_MONO) {
|
||||||
|
CHECK_RESULT(pDev->gfdm_small_head.backbuffer(pDev->pReal, ppBackBuffer));
|
||||||
|
}
|
||||||
|
|
||||||
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
|
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
||||||
@@ -159,7 +173,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetDevice(IDirect3DDevice9
|
|||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
|
||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
||||||
{
|
{
|
||||||
CHECK_RESULT(pReal->GetPresentParameters(pPresentationParameters));
|
HRESULT result = pReal->GetPresentParameters(pPresentationParameters);
|
||||||
|
if (SUCCEEDED(result) && pPresentationParameters != nullptr && scales_small_head(this)) {
|
||||||
|
pDev->gfdm_small_head.apply_logical_size(
|
||||||
|
&pPresentationParameters->BackBufferWidth,
|
||||||
|
&pPresentationParameters->BackBufferHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
|
|||||||
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
||||||
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
||||||
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
||||||
|
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
|
||||||
|
HWND NDD_MAIN_WINDOW = nullptr;
|
||||||
bool FAKE_SUBSCREEN_ADAPTER = false;
|
bool FAKE_SUBSCREEN_ADAPTER = false;
|
||||||
|
|
||||||
// icon
|
// icon
|
||||||
@@ -634,6 +636,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
|
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
|
||||||
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
|
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
|
||||||
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
|
||||||
|
const bool is_ndd = avs::game::is_model("NDD");
|
||||||
|
bool is_ndd_sub_window = is_ndd && window_name.starts_with("Aska MultiDisplay");
|
||||||
|
bool is_ndd_main_window = is_ndd && window_name == "ASKA";
|
||||||
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||||
? gitadora_canonical_window_name(effective_window_name)
|
? gitadora_canonical_window_name(effective_window_name)
|
||||||
: "";
|
: "";
|
||||||
@@ -777,6 +782,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (is_ndd_sub_window) {
|
||||||
|
NDD_SUBSCREEN_WINDOW = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_ndd_main_window) {
|
||||||
|
NDD_MAIN_WINDOW = result;
|
||||||
|
}
|
||||||
|
|
||||||
disable_touch_gestures(result);
|
disable_touch_gestures(result);
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics",
|
"graphics",
|
||||||
@@ -902,6 +915,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// the sub screen renders into a fixed 800x480 buffer, but the game's saved layout asks for rects
|
||||||
|
// that do not match it, and dxgi stretches the buffer to fill whatever the client area ends up as
|
||||||
|
static void ndd_subscreen_size(HWND hWnd, int &width, int &height) {
|
||||||
|
RECT rect {};
|
||||||
|
SetRect(&rect, 0, 0, 800, 480);
|
||||||
|
AdjustWindowRect(&rect, GetWindowLongA(hWnd, GWL_STYLE), 0);
|
||||||
|
|
||||||
|
width = rect.right - rect.left;
|
||||||
|
height = rect.bottom - rect.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the saved layout drops the sub window wherever it sat on the machine that wrote the file
|
||||||
|
static void ndd_subscreen_position(int &x, int &y) {
|
||||||
|
RECT main {};
|
||||||
|
if (NDD_MAIN_WINDOW != nullptr && GetWindowRect(NDD_MAIN_WINDOW, &main)) {
|
||||||
|
x = main.right;
|
||||||
|
y = main.top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
|
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
|
||||||
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
|
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
|
||||||
fmt::ptr(hWnd),
|
fmt::ptr(hWnd),
|
||||||
@@ -931,6 +964,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
|
|||||||
nHeight = rect.bottom - rect.top;
|
nHeight = rect.bottom - rect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
|
||||||
|
ndd_subscreen_size(hWnd, nWidth, nHeight);
|
||||||
|
ndd_subscreen_position(X, Y);
|
||||||
|
}
|
||||||
|
|
||||||
// iidx windowed TDJ mode
|
// iidx windowed TDJ mode
|
||||||
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
|
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||||
if (GRAPHICS_IIDX_WSUB) {
|
if (GRAPHICS_IIDX_WSUB) {
|
||||||
@@ -1073,6 +1111,15 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
|
|||||||
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
||||||
int X, int Y, int cx, int cy, UINT uFlags) {
|
int X, int Y, int cx, int cy, UINT uFlags) {
|
||||||
|
|
||||||
|
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
|
||||||
|
if (!(uFlags & SWP_NOSIZE)) {
|
||||||
|
ndd_subscreen_size(hWnd, cx, cy);
|
||||||
|
}
|
||||||
|
if (!(uFlags & SWP_NOMOVE)) {
|
||||||
|
ndd_subscreen_position(X, Y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (is_gfdm_two_head_small_window(hWnd) &&
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
((uFlags & SWP_HIDEWINDOW) ||
|
((uFlags & SWP_HIDEWINDOW) ||
|
||||||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
||||||
@@ -1481,6 +1528,27 @@ void graphics_capture_skip(int screen) {
|
|||||||
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool graphics_capture_last_size(int screen, int *width, int *height) {
|
||||||
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// consuming a frame clears the pixels but leaves the size, so this survives the read
|
||||||
|
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
|
||||||
|
const auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
||||||
|
if (!capture.width || !capture.height) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width != nullptr) {
|
||||||
|
*width = capture.width;
|
||||||
|
}
|
||||||
|
if (height != nullptr) {
|
||||||
|
*height = capture.height;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
||||||
int divide, uint64_t *timestamp,
|
int divide, uint64_t *timestamp,
|
||||||
int *width, int *height) {
|
int *width, int *height) {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
|
|||||||
extern HWND SDVX_SUBSCREEN_WINDOW;
|
extern HWND SDVX_SUBSCREEN_WINDOW;
|
||||||
extern HWND POPN_SUBSCREEN_WINDOW;
|
extern HWND POPN_SUBSCREEN_WINDOW;
|
||||||
extern HWND GFDM_SUBSCREEN_WINDOW;
|
extern HWND GFDM_SUBSCREEN_WINDOW;
|
||||||
|
extern HWND NDD_MAIN_WINDOW;
|
||||||
|
|
||||||
extern bool SUBSCREEN_FORCE_REDRAW;
|
extern bool SUBSCREEN_FORCE_REDRAW;
|
||||||
extern bool FAKE_SUBSCREEN_ADAPTER;
|
extern bool FAKE_SUBSCREEN_ADAPTER;
|
||||||
@@ -146,6 +147,9 @@ void graphics_capture_trigger(int screen);
|
|||||||
bool graphics_capture_consume(int *screen);
|
bool graphics_capture_consume(int *screen);
|
||||||
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
|
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
|
||||||
void graphics_capture_skip(int screen);
|
void graphics_capture_skip(int screen);
|
||||||
|
// size of the last frame captured off this screen, before any caller side downscale; false
|
||||||
|
// until one has been captured, so it cannot report a size for a screen the game never drew
|
||||||
|
bool graphics_capture_last_size(int screen, int *width, int *height);
|
||||||
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
|
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
|
||||||
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
|
||||||
int divide = 0,
|
int divide = 0,
|
||||||
|
|||||||
@@ -336,9 +336,11 @@ void hooks::lang::early_init() {
|
|||||||
&GetLocaleInfoA_orig);
|
&GetLocaleInfoA_orig);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for TDJ subscreen search keyboard and T44 narrow-string handling
|
// for TDJ subscreen search keyboard
|
||||||
|
// T44 narrow-string handling
|
||||||
|
// NDD text measuring
|
||||||
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
|
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
|
||||||
avs::game::is_model("T44")) {
|
avs::game::is_model({ "T44", "NDD" })) {
|
||||||
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -358,8 +360,9 @@ void hooks::lang::early_init() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
|
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
|
||||||
const auto hook_wide_char_to_multi_byte =
|
const auto hook_wide_char_to_multi_byte =
|
||||||
games::gitadora::is_arena_model() || avs::game::is_model("T44");
|
games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" });
|
||||||
#else
|
#else
|
||||||
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
|
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
|
||||||
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
|
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
|
||||||
|
|||||||
@@ -696,6 +696,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) {
|
if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) {
|
||||||
games::gitadora::ALLOW_REALTEK_AUDIO = true;
|
games::gitadora::ALLOW_REALTEK_AUDIO = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraSubscreenLandscape].value_bool()) {
|
||||||
|
games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
||||||
attach_nostalgia = true;
|
attach_nostalgia = true;
|
||||||
}
|
}
|
||||||
@@ -2487,7 +2490,6 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
|
|
||||||
// initialize raw input
|
// initialize raw input
|
||||||
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
RI_MGR = std::make_unique<rawinput::RawInputManager>();
|
||||||
hotkeys::enable_raw_input();
|
|
||||||
for (const auto &device : sextet_devices) {
|
for (const auto &device : sextet_devices) {
|
||||||
RI_MGR->sextet_register(device);
|
RI_MGR->sextet_register(device);
|
||||||
}
|
}
|
||||||
@@ -2511,6 +2513,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
dump_analog_bindings();
|
dump_analog_bindings();
|
||||||
|
|
||||||
// mappings are ready; begin screenshot and coin polling during late startup
|
// mappings are ready; begin screenshot and coin polling during late startup
|
||||||
|
hotkeys::enable_raw_input();
|
||||||
hotkeys::enable_input();
|
hotkeys::enable_input();
|
||||||
|
|
||||||
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
|
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
|
||||||
|
|||||||
@@ -1729,7 +1729,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.",
|
"Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.",
|
||||||
.type = OptionType::Integer,
|
.type = OptionType::Integer,
|
||||||
.setting_name = "(0-100)",
|
.setting_name = "(0-100)",
|
||||||
.category = "Companion & API",
|
.category = "API Dev",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// APIScreenMirrorDivide
|
// APIScreenMirrorDivide
|
||||||
@@ -1741,20 +1741,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
"Value must be 1 or greater. Default: 1.",
|
"Value must be 1 or greater. Default: 1.",
|
||||||
.type = OptionType::Integer,
|
.type = OptionType::Integer,
|
||||||
.setting_name = "1",
|
.setting_name = "1",
|
||||||
.category = "Companion & API",
|
.category = "API Dev",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// APIStreamEnable
|
// APIStreamEnable
|
||||||
.title = "API Video Stream Server Enable",
|
.title = "API Video Stream Server Enable (EXPERIMENTAL)",
|
||||||
.name = "apistream",
|
.name = "apistream",
|
||||||
.desc = "Serves the mirrored screen as a video stream, on the API port plus two; "
|
.desc = "Allows companion apps to receive compressed video streams over the API. "
|
||||||
"alternative to API screen capture. Requires -api.\n\n"
|
"Companion app must support this feature to take advantage of it.\n\n"
|
||||||
"http://host:apiport+2/stream.mjpg - MJPEG\n\n"
|
"Video is served unencrypted over the network. "
|
||||||
"http://host:apiport+2/stream.h264 - H.264\n\n"
|
"May cause older games to hang and crash.\n\n"
|
||||||
"Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n"
|
"Developers: see the wiki page for more details.",
|
||||||
"Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n"
|
|
||||||
"VIEW ONLY - touch input still requires -api. "
|
|
||||||
"No password protection or encryption of any kind; video sent in the clear!",
|
|
||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "Companion & API",
|
.category = "Companion & API",
|
||||||
},
|
},
|
||||||
@@ -3430,6 +3427,19 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.type = OptionType::Bool,
|
.type = OptionType::Bool,
|
||||||
.category = "General Overlay",
|
.category = "General Overlay",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraSubscreenLandscape
|
||||||
|
.title = "GitaDora Arena Landscape Subscreen (EXPERIMENTAL)",
|
||||||
|
.name = "gdsublandscape",
|
||||||
|
.desc = "For Arena Model full screen two-window mode: drive the SMALL touch subscreen "
|
||||||
|
"with a landscape monitor instead of a portrait one.\n\n"
|
||||||
|
"The portrait image is centered and black bars fill the space on either side.\n\n"
|
||||||
|
"Launches at 1920x1080; use -forceressub if you need a different resolution.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
.quick_setting_category = "Game",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
|
||||||
|
|||||||
@@ -323,7 +323,8 @@ namespace launcher {
|
|||||||
OBSWebSocketPassword,
|
OBSWebSocketPassword,
|
||||||
OBSWebSocketDebug,
|
OBSWebSocketDebug,
|
||||||
ScreenshotIncludeOverlay,
|
ScreenshotIncludeOverlay,
|
||||||
ScreenshotSubscreens
|
ScreenshotSubscreens,
|
||||||
|
GitaDoraSubscreenLandscape
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
@@ -1402,10 +1402,50 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
|
|
||||||
Contributions
|
Wslay (MIT)
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
cardio - Felix - MIT License
|
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
|
||||||
scard - nolm - MIT License
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
Wslay UTF-8 decoder (MIT)
|
||||||
|
-------------------------------------------
|
||||||
|
Copyright (c) 2008-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
x264 (GPL-2.0-or-later)
|
x264 (GPL-2.0-or-later)
|
||||||
-------------------------------------------
|
-------------------------------------------
|
||||||
@@ -1426,3 +1466,8 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|||||||
You should have received a copy of the GNU General Public License along with
|
You should have received a copy of the GNU General Public License along with
|
||||||
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
|
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
|
||||||
Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
|
||||||
|
Contributions
|
||||||
|
-------------------------------------------
|
||||||
|
cardio - Felix - MIT License
|
||||||
|
scard - nolm - MIT License
|
||||||
|
|||||||
@@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl device_set_coinblocker_open(char number, char open) {
|
static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) {
|
||||||
return 0;
|
// libdevice exposes one open flag per coin slot. Since spice has a shared
|
||||||
|
// software coin stock, consider it blocked only when both slots are closed.
|
||||||
|
eamuse_coin_set_block(!slot1_open && !slot2_open);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void __cdecl device_set_coincounter_merge() {
|
static void __cdecl device_set_coincounter_merge() {
|
||||||
@@ -617,7 +619,9 @@ void spicedevice_attach() {
|
|||||||
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
|
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
|
||||||
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
|
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
|
||||||
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
|
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
|
||||||
DEVICE_INSTANCE, {"device_get_coinstock", "?device_get_coinstock@@YAXPEAG0@Z"}));
|
DEVICE_INSTANCE, {"device_get_coinstock",
|
||||||
|
"?device_get_coinstock@@YAXPEAG0@Z",
|
||||||
|
"?device_get_coinstock@@YAXPAG0@Z"}));
|
||||||
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
|
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
|
||||||
DEVICE_INSTANCE, {"device_get_coinstock_all",
|
DEVICE_INSTANCE, {"device_get_coinstock_all",
|
||||||
"?device_get_coinstock_all@@YAXPEAG0@Z",
|
"?device_get_coinstock_all@@YAXPEAG0@Z",
|
||||||
|
|||||||
+28
-14
@@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
|
|||||||
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
||||||
static std::string EAMUSE_GAME_NAME;
|
static std::string EAMUSE_GAME_NAME;
|
||||||
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
||||||
|
static std::mutex KEYPAD_BINDINGS_LOCK;
|
||||||
|
|
||||||
// auto card
|
// auto card
|
||||||
bool AUTO_INSERT_CARD[2] = {false, false};
|
bool AUTO_INSERT_CARD[2] = {false, false};
|
||||||
@@ -87,23 +88,23 @@ bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get file path
|
|
||||||
std::filesystem::path path;
|
|
||||||
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
|
|
||||||
path = KEYPAD_BINDINGS.card_paths[index];
|
|
||||||
} else {
|
|
||||||
path = index > 0 ? "card1.txt" : "card0.txt";
|
|
||||||
}
|
|
||||||
|
|
||||||
// call the next function
|
// call the next function
|
||||||
return eamuse_get_card(path, card, index);
|
return eamuse_get_card(eamuse_get_card_path(index), card, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path eamuse_get_card_path(size_t index) {
|
||||||
|
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
|
||||||
|
if (index >= std::size(KEYPAD_BINDINGS.card_paths)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
|
||||||
|
return KEYPAD_BINDINGS.card_paths[index];
|
||||||
|
}
|
||||||
|
return index > 0 ? "card1.txt" : "card0.txt";
|
||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
// do a quick copy under lock
|
const auto card_override = eamuse_get_card_override(index);
|
||||||
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
|
|
||||||
const auto card_override = CARD_OVERRIDES[index];
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// Check if card overrides are present
|
// Check if card overrides are present
|
||||||
if (!card_override.empty()) {
|
if (!card_override.empty()) {
|
||||||
@@ -149,6 +150,14 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
|
|||||||
return eamuse_get_card_from_file(path, card, index);
|
return eamuse_get_card_from_file(path, card, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string eamuse_get_card_override(size_t index) {
|
||||||
|
std::lock_guard<std::mutex> lock(CARD_OVERRIDES_LOCK);
|
||||||
|
if (index >= std::size(CARD_OVERRIDES)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return CARD_OVERRIDES[index];
|
||||||
|
}
|
||||||
|
|
||||||
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
|
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
|
|
||||||
// open file
|
// open file
|
||||||
@@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_keypad_state_naive() {
|
bool eamuse_keypad_state_naive() {
|
||||||
|
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
|
||||||
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
|
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,7 +605,9 @@ void eamuse_set_game(std::string game) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_update_keypad_bindings() {
|
void eamuse_update_keypad_bindings() {
|
||||||
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
||||||
|
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
|
||||||
|
KEYPAD_BINDINGS = std::move(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string &eamuse_get_game() {
|
const std::string &eamuse_get_game() {
|
||||||
@@ -718,6 +730,8 @@ void eamuse_autodetect_game() {
|
|||||||
eamuse_set_game("Mahjong Fight Girl");
|
eamuse_set_game("Mahjong Fight Girl");
|
||||||
else if (avs::game::is_model("XIF"))
|
else if (avs::game::is_model("XIF"))
|
||||||
eamuse_set_game("Polaris Chord");
|
eamuse_set_game("Polaris Chord");
|
||||||
|
else if (avs::game::is_model("NDD"))
|
||||||
|
eamuse_set_game("Silent Scope: Bone Eater");
|
||||||
else {
|
else {
|
||||||
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
|
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
|
||||||
eamuse_set_game("unknown");
|
eamuse_set_game("unknown");
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ extern std::string AUTO_PIN_MACRO_TRIGGER[2];
|
|||||||
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
||||||
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
||||||
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index);
|
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index);
|
||||||
|
std::filesystem::path eamuse_get_card_path(size_t index);
|
||||||
|
std::string eamuse_get_card_override(size_t index);
|
||||||
|
|
||||||
void eamuse_card_insert(int unit);
|
void eamuse_card_insert(int unit);
|
||||||
void eamuse_card_insert(int unit, const uint8_t *card);
|
void eamuse_card_insert(int unit, const uint8_t *card);
|
||||||
|
|||||||
@@ -21,12 +21,36 @@ namespace ImGui {
|
|||||||
base.y + overlay::apply_scaling(1.0f));
|
base.y + overlay::apply_scaling(1.0f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tooltips follow the mouse and only avoid a small box around the cursor, so
|
||||||
|
// they can cover the option they describe. Anchor to the hovered item and let
|
||||||
|
// ImGui's popup placer pick a side that clears it and stays on-screen. The
|
||||||
|
// size is taken from the tooltip window, so it covers any content.
|
||||||
|
static void SetNextTooltipPos() {
|
||||||
|
ImGuiContext& g = *GImGui;
|
||||||
|
char name[32];
|
||||||
|
ImFormatString(name, IM_COUNTOF(name), "##Tooltip_%02d", g.TooltipOverrideCount);
|
||||||
|
ImGuiWindow *tooltip = ImGui::FindWindowByName(name);
|
||||||
|
if (tooltip == nullptr || !tooltip->WasActive) {
|
||||||
|
// Reopened tooltips are hidden for one frame while measuring size.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ImRect item(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||||
|
ImGui::SetNextWindowPos(ImGui::FindBestWindowPosForPopupEx(
|
||||||
|
item.Min,
|
||||||
|
ImGui::CalcWindowNextAutoFitSize(tooltip),
|
||||||
|
&tooltip->AutoPosLastDirection,
|
||||||
|
ImGui::GetPopupAllowedExtentRect(tooltip),
|
||||||
|
item,
|
||||||
|
ImGuiPopupPositionPolicy_Tooltip));
|
||||||
|
}
|
||||||
|
|
||||||
void HelpTooltip(const char* desc) {
|
void HelpTooltip(const char* desc) {
|
||||||
ImGui::PushStyleColor(ImGuiCol_Border, bg);
|
ImGui::PushStyleColor(ImGuiCol_Border, bg);
|
||||||
ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg);
|
ImGui::PushStyleColor(ImGuiCol_BorderShadow, bg);
|
||||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
|
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, fg);
|
ImGui::PushStyleColor(ImGuiCol_Text, fg);
|
||||||
|
|
||||||
|
SetNextTooltipPos();
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
||||||
ImGui::TextUnformatted(desc);
|
ImGui::TextUnformatted(desc);
|
||||||
@@ -49,6 +73,7 @@ namespace ImGui {
|
|||||||
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
|
ImGui::PushStyleColor(ImGuiCol_PopupBg, bg);
|
||||||
ImGui::PushStyleColor(ImGuiCol_Text, fg);
|
ImGui::PushStyleColor(ImGuiCol_Text, fg);
|
||||||
|
|
||||||
|
SetNextTooltipPos();
|
||||||
ImGui::BeginTooltip();
|
ImGui::BeginTooltip();
|
||||||
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
ImGui::PushTextWrapPos(ImGui::GetFontSize() * 35.0f);
|
||||||
if (desc && desc[0]) {
|
if (desc && desc[0]) {
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ namespace overlay::notifications {
|
|||||||
// small gutter past the accent bar, then wrapped text
|
// small gutter past the accent bar, then wrapped text
|
||||||
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
|
ImGui::Dummy(ImVec2(apply_scaling(2.0f), 0.f));
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
ImGui::PushTextWrapPos(win_pos.x + win_size.x - apply_scaling(TOAST_PAD_X));
|
// wrap pos is in window-local space, not screen space - adding win_pos.x here
|
||||||
|
// pushed the boundary far past the window's own width, so it never wrapped
|
||||||
|
ImGui::PushTextWrapPos(win_size.x - apply_scaling(TOAST_PAD_X));
|
||||||
ImGui::TextUnformatted(n.text.c_str());
|
ImGui::TextUnformatted(n.text.c_str());
|
||||||
ImGui::PopTextWrapPos();
|
ImGui::PopTextWrapPos();
|
||||||
|
|
||||||
|
|||||||
@@ -1607,6 +1607,7 @@ typedef enum SPICE_SDK_DANCERUSH_BUTTONS {
|
|||||||
DRS_Button_P2_Down,
|
DRS_Button_P2_Down,
|
||||||
DRS_Button_P2_Left,
|
DRS_Button_P2_Left,
|
||||||
DRS_Button_P2_Right,
|
DRS_Button_P2_Right,
|
||||||
|
DRS_Button_DownMotion,
|
||||||
} SPICE_SDK_DANCERUSH_BUTTONS;
|
} SPICE_SDK_DANCERUSH_BUTTONS;
|
||||||
|
|
||||||
typedef enum SPICE_SDK_DANCERUSH_LIGHTS {
|
typedef enum SPICE_SDK_DANCERUSH_LIGHTS {
|
||||||
|
|||||||
Reference in New Issue
Block a user