mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
Fix capture.get_jpg blocking during game load for Companion Mirror (#750)
Add a timeout and skip signalling to the D3D9 capture path so the API thread no longer waits indefinitely when Present stops during loading. Return the last successful JPEG frame as a fallback. ## Link to GitHub Issue or related Pull Request, if one exists #746 ## Description of change * Fix `capture.get_jpg` blocking indefinitely when D3D9 screen capture cannot complete during game loading (e.g. no Present calls). * Add a 2-second timeout to `graphics_capture_receive_jpeg()` and cancel pending capture requests on timeout. * Signal capture skip from D3D9 failure paths via `graphics_capture_skip()`. * Cache the last successful JPEG per screen and return it as a fallback when a new capture fails. Tested with **stock iOS Spice Companion** only. I have not tested Android or other Companion clients. The iOS client implementation differs from the others; this fix is server-side only. No config file, CLI option, or API schema changes. ## Testing * Built locally with llvm-mingw cross-compiler (WSL). * Docker build (`src/spice2x/build_docker.sh`): Pending * Tested with stock iOS Spice Companion on iPad: Mirror remains connected during game loading instead of returning to KeyPad.
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
#include "capture.h"
|
#include "capture.h"
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <mutex>
|
||||||
|
#include <unordered_map>
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
@@ -14,6 +16,56 @@ namespace api::modules {
|
|||||||
|
|
||||||
static thread_local std::vector<uint8_t> CAPTURE_BUFFER;
|
static thread_local std::vector<uint8_t> CAPTURE_BUFFER;
|
||||||
|
|
||||||
|
struct CachedFrame {
|
||||||
|
std::vector<uint8_t> jpeg;
|
||||||
|
uint64_t timestamp = 0;
|
||||||
|
int width = 0;
|
||||||
|
int height = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::mutex FRAME_CACHE_M;
|
||||||
|
static std::unordered_map<int, CachedFrame> FRAME_CACHE;
|
||||||
|
|
||||||
|
static void add_jpeg_response(
|
||||||
|
int screen,
|
||||||
|
uint64_t timestamp,
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
const std::vector<uint8_t> &jpeg,
|
||||||
|
Response &res) {
|
||||||
|
|
||||||
|
auto encoded = crypt::base64_encode(jpeg.data(), jpeg.size());
|
||||||
|
|
||||||
|
Value data;
|
||||||
|
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
|
||||||
|
res.add_data(timestamp);
|
||||||
|
res.add_data(width);
|
||||||
|
res.add_data(height);
|
||||||
|
res.add_data(data);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
|
||||||
|
FRAME_CACHE[screen] = {jpeg, timestamp, width, height};
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool try_cached_response(int screen, Response &res) {
|
||||||
|
std::lock_guard<std::mutex> lock(FRAME_CACHE_M);
|
||||||
|
const auto pos = FRAME_CACHE.find(screen);
|
||||||
|
if (pos == FRAME_CACHE.end() || pos->second.jpeg.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &cached = pos->second;
|
||||||
|
auto encoded = crypt::base64_encode(cached.jpeg.data(), cached.jpeg.size());
|
||||||
|
|
||||||
|
Value data;
|
||||||
|
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
|
||||||
|
res.add_data(cached.timestamp);
|
||||||
|
res.add_data(cached.width);
|
||||||
|
res.add_data(cached.height);
|
||||||
|
res.add_data(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -41,6 +93,7 @@ namespace api::modules {
|
|||||||
* reduce: uint for dividing image size
|
* reduce: uint for dividing image size
|
||||||
*/
|
*/
|
||||||
void Capture::get_jpg(Request &req, Response &res) {
|
void Capture::get_jpg(Request &req, Response &res) {
|
||||||
|
CAPTURE_BUFFER.clear();
|
||||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
CAPTURE_BUFFER.reserve(1024 * 128);
|
||||||
|
|
||||||
// settings
|
// settings
|
||||||
@@ -71,24 +124,15 @@ namespace api::modules {
|
|||||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
||||||
CAPTURE_BUFFER.push_back(byte);
|
CAPTURE_BUFFER.push_back(byte);
|
||||||
}, true, quality, true, divide, ×tamp, &width, &height);
|
}, true, quality, true, divide, ×tamp, &width, &height);
|
||||||
if (!success) {
|
|
||||||
|
if (success) {
|
||||||
|
add_jpeg_response(screen, timestamp, width, height, CAPTURE_BUFFER, res);
|
||||||
|
CAPTURE_BUFFER.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// encode to base64
|
// fall back to the last successful frame while the game is busy loading
|
||||||
auto encoded = crypt::base64_encode(
|
|
||||||
CAPTURE_BUFFER.data(),
|
|
||||||
CAPTURE_BUFFER.size());
|
|
||||||
|
|
||||||
// clear buffer
|
|
||||||
CAPTURE_BUFFER.clear();
|
CAPTURE_BUFFER.clear();
|
||||||
|
try_cached_response(screen, res);
|
||||||
// add data to response
|
|
||||||
Value data;
|
|
||||||
data.SetString(encoded.c_str(), encoded.length(), res.doc()->GetAllocator());
|
|
||||||
res.add_data(timestamp);
|
|
||||||
res.add_data(width);
|
|
||||||
res.add_data(height);
|
|
||||||
res.add_data(data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1535,6 +1535,9 @@ void graphics_d3d9_on_present(
|
|||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
"failed to get back buffer, hr={}",
|
"failed to get back buffer, hr={}",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
|
if (capture) {
|
||||||
|
graphics_capture_skip(capture_screen);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1545,6 +1548,9 @@ void graphics_d3d9_on_present(
|
|||||||
"failed to acquire back buffer descriptor, hr={}",
|
"failed to acquire back buffer descriptor, hr={}",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
buffer->Release();
|
buffer->Release();
|
||||||
|
if (capture) {
|
||||||
|
graphics_capture_skip(capture_screen);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1558,6 +1564,9 @@ void graphics_d3d9_on_present(
|
|||||||
"failed to acquire temporary surface, hr={}",
|
"failed to acquire temporary surface, hr={}",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
buffer->Release();
|
buffer->Release();
|
||||||
|
if (capture) {
|
||||||
|
graphics_capture_skip(capture_screen);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1568,6 +1577,9 @@ void graphics_d3d9_on_present(
|
|||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
temp_surface->Release();
|
temp_surface->Release();
|
||||||
buffer->Release();
|
buffer->Release();
|
||||||
|
if (capture) {
|
||||||
|
graphics_capture_skip(capture_screen);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "graphics.h"
|
#include "graphics.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -61,6 +62,19 @@ static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
|
|||||||
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
static CaptureData GRAPHICS_CAPTURE_BUFFER[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
static std::mutex GRAPHICS_CAPTURE_BUFFER_M[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
static std::condition_variable GRAPHICS_CAPTURE_CV[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
|
static bool GRAPHICS_CAPTURE_SKIP_SIGNAL[GRAPHICS_CAPTURE_SCREEN_NO] {};
|
||||||
|
static constexpr std::chrono::milliseconds GRAPHICS_CAPTURE_RECEIVE_TIMEOUT {2000};
|
||||||
|
|
||||||
|
static void graphics_capture_cancel_pending(int screen) {
|
||||||
|
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
|
||||||
|
|
||||||
|
for (auto it = GRAPHICS_CAPTURE_SCREENS.rbegin(); it != GRAPHICS_CAPTURE_SCREENS.rend(); ++it) {
|
||||||
|
if (*it == screen) {
|
||||||
|
GRAPHICS_CAPTURE_SCREENS.erase(std::next(it).base());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static std::optional<graphics_orientation> target_orientation_on_boot;
|
static std::optional<graphics_orientation> target_orientation_on_boot;
|
||||||
static UINT target_refresh_rate_on_boot = 0;
|
static UINT target_refresh_rate_on_boot = 0;
|
||||||
@@ -1209,18 +1223,20 @@ void graphics_capture_trigger(int screen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool graphics_capture_consume(int *screen) {
|
bool graphics_capture_consume(int *screen) {
|
||||||
auto flag = !GRAPHICS_CAPTURE_SCREENS.empty();
|
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
|
||||||
if (flag) {
|
|
||||||
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
|
|
||||||
|
|
||||||
*screen = GRAPHICS_CAPTURE_SCREENS.back();
|
if (GRAPHICS_CAPTURE_SCREENS.empty()) {
|
||||||
GRAPHICS_CAPTURE_SCREENS.pop_back();
|
return false;
|
||||||
}
|
}
|
||||||
return flag;
|
|
||||||
|
*screen = GRAPHICS_CAPTURE_SCREENS.back();
|
||||||
|
GRAPHICS_CAPTURE_SCREENS.pop_back();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
|
GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
|
||||||
|
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = false;
|
||||||
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
||||||
capture.data.reset(data);
|
capture.data.reset(data);
|
||||||
capture.width = width;
|
capture.width = width;
|
||||||
@@ -1231,6 +1247,14 @@ void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t he
|
|||||||
}
|
}
|
||||||
|
|
||||||
void graphics_capture_skip(int screen) {
|
void graphics_capture_skip(int screen) {
|
||||||
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
|
||||||
|
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = true;
|
||||||
|
}
|
||||||
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
GRAPHICS_CAPTURE_CV[screen].notify_one();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1238,11 +1262,32 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver,
|
|||||||
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
|
bool rgb, int quality, bool downsample, int divide, uint64_t *timestamp,
|
||||||
int *width, int *height) {
|
int *width, int *height) {
|
||||||
|
|
||||||
// wait for capture event
|
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait for capture event (with timeout)
|
||||||
std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
|
std::unique_lock<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
|
||||||
GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] {
|
const bool ready = GRAPHICS_CAPTURE_CV[screen].wait_for(
|
||||||
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr;
|
lock,
|
||||||
});
|
GRAPHICS_CAPTURE_RECEIVE_TIMEOUT,
|
||||||
|
[screen] {
|
||||||
|
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr
|
||||||
|
|| GRAPHICS_CAPTURE_SKIP_SIGNAL[screen];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
lock.unlock();
|
||||||
|
graphics_capture_cancel_pending(screen);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GRAPHICS_CAPTURE_SKIP_SIGNAL[screen]) {
|
||||||
|
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = false;
|
||||||
|
lock.unlock();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
||||||
auto capture_data = capture.data;
|
auto capture_data = capture.data;
|
||||||
auto capture_width = capture.width;
|
auto capture_width = capture.width;
|
||||||
|
|||||||
Reference in New Issue
Block a user