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:
jessemarthin
2026-06-12 11:14:47 +09:00
committed by GitHub
parent 18d6d9783a
commit 3037214932
3 changed files with 126 additions and 25 deletions
+59 -15
View File
@@ -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, &timestamp, &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;
}
+52 -7
View File
@@ -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);
if (GRAPHICS_CAPTURE_SCREENS.empty()) {
return false;
}
*screen = GRAPHICS_CAPTURE_SCREENS.back();
GRAPHICS_CAPTURE_SCREENS.pop_back();
}
return flag;
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;