mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20: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 <functional>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
#include "external/rapidjson/document.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "util/crypt.h"
|
||||
@@ -14,6 +16,56 @@ namespace api::modules {
|
||||
|
||||
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") {
|
||||
functions["get_screens"] = std::bind(&Capture::get_screens, 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
|
||||
*/
|
||||
void Capture::get_jpg(Request &req, Response &res) {
|
||||
CAPTURE_BUFFER.clear();
|
||||
CAPTURE_BUFFER.reserve(1024 * 128);
|
||||
|
||||
// settings
|
||||
@@ -71,24 +124,15 @@ namespace api::modules {
|
||||
bool success = graphics_capture_receive_jpeg(screen, [] (uint8_t byte) {
|
||||
CAPTURE_BUFFER.push_back(byte);
|
||||
}, 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;
|
||||
}
|
||||
|
||||
// encode to base64
|
||||
auto encoded = crypt::base64_encode(
|
||||
CAPTURE_BUFFER.data(),
|
||||
CAPTURE_BUFFER.size());
|
||||
|
||||
// clear buffer
|
||||
// fall back to the last successful frame while the game is busy loading
|
||||
CAPTURE_BUFFER.clear();
|
||||
|
||||
// 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);
|
||||
try_cached_response(screen, res);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1535,6 +1535,9 @@ void graphics_d3d9_on_present(
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to get back buffer, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1545,6 +1548,9 @@ void graphics_d3d9_on_present(
|
||||
"failed to acquire back buffer descriptor, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1558,6 +1564,9 @@ void graphics_d3d9_on_present(
|
||||
"failed to acquire temporary surface, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1568,6 +1577,9 @@ void graphics_d3d9_on_present(
|
||||
FMT_HRESULT(hr));
|
||||
temp_surface->Release();
|
||||
buffer->Release();
|
||||
if (capture) {
|
||||
graphics_capture_skip(capture_screen);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "graphics.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
@@ -61,6 +62,19 @@ static std::mutex GRAPHICS_CAPTURE_SCREENS_M {};
|
||||
static CaptureData GRAPHICS_CAPTURE_BUFFER[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 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 UINT target_refresh_rate_on_boot = 0;
|
||||
@@ -1209,18 +1223,20 @@ void graphics_capture_trigger(int screen) {
|
||||
}
|
||||
|
||||
bool graphics_capture_consume(int *screen) {
|
||||
auto flag = !GRAPHICS_CAPTURE_SCREENS.empty();
|
||||
if (flag) {
|
||||
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
|
||||
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_SCREENS_M);
|
||||
|
||||
*screen = GRAPHICS_CAPTURE_SCREENS.back();
|
||||
GRAPHICS_CAPTURE_SCREENS.pop_back();
|
||||
if (GRAPHICS_CAPTURE_SCREENS.empty()) {
|
||||
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) {
|
||||
GRAPHICS_CAPTURE_BUFFER_M[screen].lock();
|
||||
GRAPHICS_CAPTURE_SKIP_SIGNAL[screen] = false;
|
||||
auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
|
||||
capture.data.reset(data);
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
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]);
|
||||
GRAPHICS_CAPTURE_CV[screen].wait(lock, [screen] {
|
||||
return GRAPHICS_CAPTURE_BUFFER[screen].data != nullptr;
|
||||
});
|
||||
const bool ready = GRAPHICS_CAPTURE_CV[screen].wait_for(
|
||||
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_data = capture.data;
|
||||
auto capture_width = capture.width;
|
||||
|
||||
Reference in New Issue
Block a user