diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 9963254..c7be737 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -1478,7 +1478,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) { void graphics_d3d9_on_present( HWND hFocusWindow, IDirect3DDevice9 *device, - IDirect3DDevice9 *wrapped_device) { + WrappedIDirect3DDevice9 *wrapped_device) { // image resize / orientation swap. run here (the present path) rather than from `EndScene`, // which may fire several times per frame on multi-pass / render-to-texture games. this is the @@ -1493,7 +1493,7 @@ void graphics_d3d9_on_present( // before the overlay render so the screenshot excludes it if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { - graphics_d3d9_process_screenshot(device, SUB_SWAP_CHAIN); + graphics_d3d9_process_screenshot(device, wrapped_device); } // Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never @@ -1517,7 +1517,7 @@ void graphics_d3d9_on_present( // after the overlay render so the screenshot includes toasts / menus if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { - graphics_d3d9_process_screenshot(device, SUB_SWAP_CHAIN); + graphics_d3d9_process_screenshot(device, wrapped_device); } // for IIDX TDJ / SDVX UFC, handle subscreen @@ -1535,7 +1535,7 @@ void graphics_d3d9_on_present( } // API capture always includes the overlay - graphics_d3d9_process_capture(device, SUB_SWAP_CHAIN); + graphics_d3d9_process_capture(device, wrapped_device); } void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h index 7d4f47d..f08b8e9 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.h @@ -4,6 +4,8 @@ #include "d3d9_gfdm.h" +struct WrappedIDirect3DDevice9; + // {EEE9CCF6-53D6-4326-9AE5-60921B3DB394} static const GUID IID_WrappedIDirect3D9 = { 0xeee9ccf6, 0x53d6, 0x4326, { 0x9a, 0xe5, 0x60, 0x92, 0x1b, 0x3d, 0xb3, 0x94 } @@ -13,7 +15,7 @@ void graphics_d3d9_init(); void graphics_d3d9_on_present( HWND hFocusWindow, IDirect3DDevice9 *device, - IDirect3DDevice9 *wrapped_device); + WrappedIDirect3DDevice9 *wrapped_device); void graphics_d3d9_notify_subscreen_present(); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index a341663..a8b5365 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -23,6 +23,11 @@ #include "shaders/vertex_shader.h" #endif +// maps arena's cached additional swap chains (SMALL, LEFT, RIGHT) to screen numbers. +// MAIN is the implicit swap chain, is not in those slots, and is always screen 0. +// screen 1 is the subscreen for every other game, so SMALL takes that number here too. +static constexpr int GFDM_ARENA_SLOT_SCREENS[] { 1, 2, 3 }; + #define CHECK_RESULT_FMT(x, fmt, ...) \ HRESULT __ret = (x); \ if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \ @@ -336,20 +341,25 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( int index = 0; bool create_swap_chain = false; bool create_fake_swap_chain = false; + bool arena_slot = false; if (avs::game::is_model({"LDJ", "KFC", "M39"})) { create_swap_chain = true; } else if (games::gitadora::is_arena_model() && - (GRAPHICS_PREVENT_SECONDARY_WINDOWS || GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) { + (GRAPHICS_SCREENSHOT_SUBSCREENS || + GRAPHICS_PREVENT_SECONDARY_WINDOWS || + GRAPHICS_GITADORA_HIDE_SIDE_WINDOWS)) { if (pPresentationParameters->BackBufferWidth == 800) { // SMALL (subscreen) create_swap_chain = true; + arena_slot = true; index = 0; } else if (pPresentationParameters->BackBufferWidth == 1080) { // LEFT/RIGHT create_swap_chain = true; + arena_slot = true; index = 1; if (sub_swapchain[index] || fake_sub_swapchain[index]) { index = 2; @@ -361,6 +371,11 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain( } } + // the api lists screens from this registry, so arena heads need their logical numbers in it + if (arena_slot) { + graphics_screens_register(GFDM_ARENA_SLOT_SCREENS[index]); + } + if (create_fake_swap_chain) { if (!fake_sub_swapchain[index]) { log_info( @@ -539,6 +554,64 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() { return n; } +void WrappedIDirect3DDevice9::get_screenshot_screens(std::vector &screens) const { + if (games::gitadora::is_arena_model()) { + screens.push_back(0); + + // every head the game renders into, whether or not it reaches a display + for (int slot = 0; slot < 3; slot++) { + if (sub_swapchain[slot] != nullptr || fake_sub_swapchain[slot] != nullptr) { + screens.push_back(GFDM_ARENA_SLOT_SCREENS[slot]); + } + } + return; + } + + graphics_screens_get(screens); + + // the sub screen is only registered once the game asks for it by index + if (sub_swapchain[0] != nullptr && + avs::game::is_model({"LDJ", "KFC", "M39"}) && + std::find(screens.begin(), screens.end(), 1) == screens.end()) + { + screens.push_back(1); + } +} + +HRESULT WrappedIDirect3DDevice9::get_screenshot_swap_chain( + UINT iSwapChain, + IDirect3DSwapChain9 **ppSwapChain) +{ + if (ppSwapChain == nullptr) { + return D3DERR_INVALIDCALL; + } + + // the game numbers the two-head SMALL head itself; keep screen 1 meaning SMALL + if (games::gitadora::is_arena_model() && is_gfdm_two_head_exclusive() && iSwapChain == 1) { + return GetSwapChain(gfdm_logical_small_swapchain, ppSwapChain); + } + + if (games::gitadora::is_arena_model()) { + for (int slot = 0; slot < 3; slot++) { + if (GFDM_ARENA_SLOT_SCREENS[slot] != (int) iSwapChain) { + continue; + } + if (sub_swapchain[slot] != nullptr) { + sub_swapchain[slot]->AddRef(); + *ppSwapChain = sub_swapchain[slot]; + return D3D_OK; + } + if (fake_sub_swapchain[slot] != nullptr) { + fake_sub_swapchain[slot]->AddRef(); + *ppSwapChain = fake_sub_swapchain[slot]; + return D3D_OK; + } + } + } + + return GetSwapChain(iSwapChain, ppSwapChain); +} + HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset( D3DPRESENT_PARAMETERS *pPresentationParameters) { diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h index d9637cb..f5b517c 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -234,6 +235,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { virtual HRESULT STDMETHODCALLTYPE GetDisplayModeEx(UINT iSwapChain, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) override; #pragma endregion + // logical screens the game draws, and the swap chain each one lives on + void get_screenshot_screens(std::vector &screens) const; + HRESULT get_screenshot_swap_chain(UINT iSwapChain, IDirect3DSwapChain9 **ppSwapChain); + bool is_gfdm_two_head_exclusive() const; bool is_gfdm_logical_small_swapchain(UINT swapchain) const; bool is_gfdm_logical_side_swapchain(UINT swapchain) const; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp index 0a38100..0197e31 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.cpp @@ -1,10 +1,13 @@ #include "d3d9_screenshot.h" #include +#include #include +#include #include #include #include +#include #include @@ -21,6 +24,8 @@ #include "util/logging.h" #include "util/threadpool.h" +#include "d3d9_device.h" + #ifdef __GNUC__ typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t; #else @@ -36,6 +41,9 @@ typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)( static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false; +// genpath picks free filenames by probing the disk, so only one save may run at a time +static std::mutex SCREENSHOT_SAVE_M; + namespace { enum class ImageRequestKind { @@ -57,6 +65,7 @@ struct SurfaceReleaser { using SurfacePtr = std::unique_ptr; struct BackbufferCopy { + int screen {}; D3DSURFACE_DESC desc {}; SurfacePtr surface; }; @@ -142,7 +151,7 @@ static void save_capture( graphics_capture_enqueue(screen, pixels.release(), width, height); } -static void save_screenshot( +static bool save_screenshot( const std::string &file_path, D3DFORMAT format, UINT width, @@ -156,7 +165,7 @@ static void save_screenshot( HRESULT hr = surface->LockRect(&finished_copy, nullptr, 0); if (FAILED(hr)) { log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; + return false; } const size_t pitch = finished_copy.Pitch; @@ -170,7 +179,7 @@ static void save_screenshot( hr = surface->UnlockRect(); if (FAILED(hr)) { log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr)); - return; + return false; } } @@ -210,48 +219,45 @@ static void save_screenshot( } } - if (D3DXSaveSurfaceToFileA_ptr != nullptr) { - - // save to file - log_info("graphics::d3d9", "saving screenshot to {}", file_path); - const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr( - file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr); - - if (FAILED(save_result)) { - log_warning("graphics::d3d9", "Failed to save screenshot"); - overlay::notifications::add( - overlay::notifications::Severity::Error, - "Screenshot failed to save"); - return; - } - - // save to clipboard - clipboard::copy_image(file_path); - - overlay::notifications::add( - overlay::notifications::Severity::Success, - fmt::format("Screenshot saved: {}", fileutils::basename(file_path))); - } else { + if (D3DXSaveSurfaceToFileA_ptr == nullptr) { log_warning("graphics::d3d9", "Direct3D save helper function not available"); + return false; } + + // save to file + log_info("graphics::d3d9", "saving screenshot to {}", file_path); + const HRESULT save_result = D3DXSaveSurfaceToFileA_ptr( + file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr); + + if (FAILED(save_result)) { + log_warning("graphics::d3d9", "Failed to save screenshot"); + return false; + } + + return true; +} + +// screen 0 keeps the plain name so existing tooling and the clipboard copy are unaffected +static std::string screenshot_path_for_screen(const std::string &primary_path, int screen) { + if (screen == 0) { + return primary_path; + } + + const std::filesystem::path path(primary_path); + return (path.parent_path() / + fmt::format("{}_{}{}", path.stem().string(), screen, path.extension().string())) + .string(); } static std::optional acquire_backbuffer_copy( - IDirect3DDevice9 *device, IDirect3DSwapChain9 *sub_swap_chain, int screen) { - - HRESULT hr = S_OK; - - // TODO: verify screen is a valid swapchain + IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) { IDirect3DSurface9 *buffer = nullptr; - if (sub_swap_chain != nullptr && screen & 1) { - hr = sub_swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } else { - hr = device->GetBackBuffer(screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer); - } + HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer); if (FAILED(hr) || buffer == nullptr) { log_warning("graphics::d3d9", - "failed to get back buffer, hr={}", + "failed to get back buffer for screen {}, hr={}", + screen, FMT_HRESULT(hr)); return std::nullopt; } @@ -293,6 +299,7 @@ static std::optional acquire_backbuffer_copy( buffer->Release(); return BackbufferCopy { + .screen = screen, .desc = desc, .surface = SurfacePtr(temp_surface), }; @@ -300,10 +307,12 @@ static std::optional acquire_backbuffer_copy( static void dispatch_surface_save( const ImageRequest &request, - BackbufferCopy copy) { - auto surface_process = [request, copy = std::move(copy)]() { + std::vector copies, + size_t screen_count) { + auto surface_process = [request, screen_count, copies = std::move(copies)]() { switch (request.kind) { - case ImageRequestKind::Capture: + case ImageRequestKind::Capture: { + const auto © = copies.front(); save_capture( request.screen, copy.desc.Format, @@ -311,16 +320,61 @@ static void dispatch_surface_save( copy.desc.Height, copy.surface.get()); break; + } case ImageRequestKind::Screenshot: { - auto file_path = graphics_screenshot_genpath(); - if (!file_path.empty()) { - save_screenshot( - file_path, + std::lock_guard lock(SCREENSHOT_SAVE_M); + + std::vector screens; + screens.reserve(copies.size()); + for (const auto © : copies) { + screens.push_back(copy.screen); + } + + const auto base_path = graphics_screenshot_genpath(screens); + if (base_path.empty()) { + break; + } + + // screens missing from copies already failed to be acquired + size_t failed = screen_count - copies.size(); + std::string primary_path; + std::string notify_path; + for (const auto © : copies) { + const auto path = screenshot_path_for_screen(base_path, copy.screen); + if (!save_screenshot( + path, copy.desc.Format, copy.desc.Width, copy.desc.Height, - copy.surface.get()); + copy.surface.get())) { + failed++; + continue; + } + if (notify_path.empty()) { + notify_path = path; + } + if (copy.screen == 0) { + primary_path = path; + } + } + + // only the primary screen goes to the clipboard, but any saved file is a success + if (!primary_path.empty()) { + clipboard::copy_image(primary_path); + } + if (!notify_path.empty()) { + overlay::notifications::add( + overlay::notifications::Severity::Success, + fmt::format("Screenshot saved: {}", fileutils::basename(notify_path))); + } else { + overlay::notifications::add( + overlay::notifications::Severity::Error, + "Screenshot failed to save"); + } + + if (failed > 0) { + log_warning("graphics::d3d9", "{} screenshot screen(s) missing", failed); } break; } @@ -350,27 +404,53 @@ static void dispatch_surface_save( static void process_image_request( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain, + WrappedIDirect3DDevice9 *wrapped_device, const ImageRequest &request) { - auto copy = acquire_backbuffer_copy( - device, - sub_swap_chain, - request.screen); - if (!copy.has_value()) { - if (request.kind == ImageRequestKind::Capture) { - graphics_capture_skip(request.screen); + const bool screenshot = request.kind == ImageRequestKind::Screenshot; + + std::vector screens { request.screen }; + if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) { + screens.clear(); + wrapped_device->get_screenshot_screens(screens); + } + + std::vector copies; + copies.reserve(screens.size()); + for (const int screen : screens) { + std::optional copy; + + IDirect3DSwapChain9 *swap_chain = nullptr; + 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)); + } else { + copy = acquire_backbuffer_copy(device, swap_chain, screen); + swap_chain->Release(); } + + if (copy.has_value()) { + copies.emplace_back(std::move(*copy)); + } else if (!screenshot) { + graphics_capture_skip(request.screen); + return; + } + } + + if (copies.empty()) { return; } - dispatch_surface_save(request, std::move(*copy)); + dispatch_surface_save(request, std::move(copies), screens.size()); } void graphics_d3d9_process_screenshot( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain) { + WrappedIDirect3DDevice9 *wrapped_device) { if (graphics_screenshot_consume()) { - process_image_request(device, sub_swap_chain, ImageRequest { + process_image_request(device, wrapped_device, ImageRequest { .kind = ImageRequestKind::Screenshot, .screen = 0, }); @@ -379,10 +459,10 @@ void graphics_d3d9_process_screenshot( void graphics_d3d9_process_capture( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain) { + WrappedIDirect3DDevice9 *wrapped_device) { int screen = 0; if (graphics_capture_consume(&screen)) { - process_image_request(device, sub_swap_chain, ImageRequest { + process_image_request(device, wrapped_device, ImageRequest { .kind = ImageRequestKind::Capture, .screen = screen, }); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h index 47775ba..d539c5e 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_screenshot.h @@ -2,10 +2,12 @@ #include +struct WrappedIDirect3DDevice9; + void graphics_d3d9_process_screenshot( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain); + WrappedIDirect3DDevice9 *wrapped_device); void graphics_d3d9_process_capture( IDirect3DDevice9 *device, - IDirect3DSwapChain9 *sub_swap_chain); + WrappedIDirect3DDevice9 *wrapped_device); diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 4fd384a..d382253 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -119,6 +119,7 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0; std::string GRAPHICS_DEVICEID = "PCI\\VEN_1002&DEV_7146"; std::string GRAPHICS_SCREENSHOT_DIR = ".\\screenshots"; bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = false; +bool GRAPHICS_SCREENSHOT_SUBSCREENS = false; static decltype(ChangeDisplaySettingsA) *ChangeDisplaySettingsA_orig = nullptr; static decltype(ChangeDisplaySettingsExA) *ChangeDisplaySettingsExA_orig = nullptr; @@ -1531,7 +1532,7 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, return success; } -std::string graphics_screenshot_genpath() { +std::string graphics_screenshot_genpath(const std::vector &screens) { // verify dir path if (GRAPHICS_SCREENSHOT_DIR.empty()) { @@ -1556,11 +1557,21 @@ std::string graphics_screenshot_genpath() { auto tm_now = *std::gmtime(&t_now); auto prefix = to_string(std::put_time(&tm_now, "%Y%m%d")); - // find next filename + // find next filename; the whole set has to be free so one shot stays numbered together size_t id = 0; while (true) { auto filepath = fmt::format("{}\\{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id); - if (!fileutils::file_exists(filepath)) { + bool available = !fileutils::file_exists(filepath); + for (const auto screen : screens) { + if (!available) { + break; + } + if (screen != 0) { + available = !fileutils::file_exists(fmt::format( + "{}\\{}_{}_{}.png", GRAPHICS_SCREENSHOT_DIR, prefix, id, screen)); + } + } + if (available) { return filepath; } diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index e996ecc..c8823ec 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -112,6 +112,7 @@ extern bool FAKE_SUBSCREEN_ADAPTER; extern std::string GRAPHICS_DEVICEID; extern std::string GRAPHICS_SCREENSHOT_DIR; extern bool GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY; +extern bool GRAPHICS_SCREENSHOT_SUBSCREENS; // Direct3D 9 settings extern std::optional D3D9_ADAPTER; @@ -146,7 +147,8 @@ bool graphics_capture_receive_jpeg(int screen, TooJpeg::WRITE_ONE_BYTE receiver, bool rgb = true, int quality = 80, bool downsample = true, int divide = 0, uint64_t *timestamp = nullptr, int *width = nullptr, int *height = nullptr); -std::string graphics_screenshot_genpath(); +// the returned path is for screen 0; any extra screens only reserve their suffixed names +std::string graphics_screenshot_genpath(const std::vector &screens = {}); // graphics_windowed.cpp void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 5e880f4..9917f67 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -1106,6 +1106,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::ScreenshotIncludeOverlay].value_bool()) { GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY = true; } + if (options[launcher::Options::ScreenshotSubscreens].value_bool()) { + GRAPHICS_SCREENSHOT_SUBSCREENS = true; + } if (options[launcher::Options::DisableColoredOutput].value_bool()) { logger::COLOR = false; } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 6e78cd1..be55c36 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -3405,6 +3405,14 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "General Overlay", }, + { + // ScreenshotSubscreens + .title = "Include Subscreens in Screenshots", + .name = "screenshotsub", + .desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.", + .type = OptionType::Bool, + .category = "General Overlay", + }, }; const std::vector &launcher::get_categories(Options::OptionsCategory category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index da29167..c14a061 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -321,7 +321,8 @@ namespace launcher { OBSWebSocketPort, OBSWebSocketPassword, OBSWebSocketDebug, - ScreenshotIncludeOverlay + ScreenshotIncludeOverlay, + ScreenshotSubscreens }; enum class OptionsCategory {