diff --git a/src/spice2x/api/modules/capture.cpp b/src/spice2x/api/modules/capture.cpp index 5bef6a3..7d393f1 100644 --- a/src/spice2x/api/modules/capture.cpp +++ b/src/spice2x/api/modules/capture.cpp @@ -1,5 +1,7 @@ #include "capture.h" #include +#include +#include #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 CAPTURE_BUFFER; + struct CachedFrame { + std::vector jpeg; + uint64_t timestamp = 0; + int width = 0; + int height = 0; + }; + + static std::mutex FRAME_CACHE_M; + static std::unordered_map FRAME_CACHE; + + static void add_jpeg_response( + int screen, + uint64_t timestamp, + int width, + int height, + const std::vector &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 lock(FRAME_CACHE_M); + FRAME_CACHE[screen] = {jpeg, timestamp, width, height}; + } + + static bool try_cached_response(int screen, Response &res) { + std::lock_guard 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); } } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index c4a077c..0db8671 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -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; } diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index b2f01b7..a478bd4 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -5,6 +5,7 @@ #include "graphics.h" +#include #include #include #include @@ -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 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 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 lock(GRAPHICS_CAPTURE_SCREENS_M); + std::lock_guard 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(GRAPHICS_CAPTURE_SCREEN_NO)) { + return; + } + + { + std::lock_guard 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(GRAPHICS_CAPTURE_SCREEN_NO)) { + return false; + } + + // wait for capture event (with timeout) std::unique_lock 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;