Compare commits

...

2 Commits

Author SHA1 Message Date
bicarus f5888609a8 graphics: option to include subscreens in screenshot (#866)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Adds `-screenshotsub`. Off by default. When on, screenshots include the
subscreens. All subscreens are captured, even if they are hidden from
view. Works for all four screens of gitadora arena model as well.

This should also fix an issue with API / companion app not capturing
gitadora subscreen correctly.

Note: only done for DX9... DX11 will need another PR to make this work.

Unrelated to this PR, there seems to be a bug with gitadora not
accepting mouse or api touch input.

## Testing
2026-08-14 04:05:50 -07:00
bicarus c6cd72c528 graphics: exclude overlay from screenshots (#865)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change

Adds the `-screenshotoverlay` option.

By default, file screenshots exclude the Spice overlay. Enabling this
option captures screenshots after the overlay is rendered.

Also updates DX11 backend to allow screenshots even when overlay is
disabled.

## Testing
2026-08-13 23:21:46 -07:00
12 changed files with 396 additions and 113 deletions
@@ -105,8 +105,62 @@ Present1_t Present1_orig = nullptr;
bool g_swapchain_hooked = false;
bool g_swapchain1_hooked = false;
// sub-screens / IME helpers are usually child or zero-sized windows.
// visibility isn't checked - the game may present before showing the window.
bool looks_like_game_window(HWND hwnd) {
RECT client {};
return GetAncestor(hwnd, GA_ROOT) == hwnd
&& GetClientRect(hwnd, &client)
&& client.right > client.left
&& client.bottom > client.top;
}
// only the main game window; ignore sub-screens / IME helpers.
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
// checks are ordered cheapest first, since this runs on every present
void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain || overlay::OVERLAY) {
if (!swapchain) {
return;
}
// overlay is disabled by user
if (!overlay::ENABLED) {
return;
}
// overlay is already enabled and attached
if (overlay::OVERLAY) {
return;
}
// ignore sub windows
if (!is_main_game_swapchain(swapchain)) {
return;
}
@@ -115,12 +169,6 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
return;
}
// only attach to the main game window; ignore sub-screens / IME helpers.
HWND main = d3d11_hooks::main_hwnd();
if (main && desc.OutputWindow != main) {
return;
}
// theme the native title bar; first present is the only reliable point for
// windows whose swapchain bypasses our factory hooks (e.g. UnityPlayer.dll)
set_window_dark_titlebar(desc.OutputWindow);
@@ -146,29 +194,42 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
device->Release();
}
void pump_overlay(IDXGISwapChain *swapchain) {
if (!overlay::OVERLAY || !overlay::OVERLAY->uses_swapchain(swapchain)) {
// screenshots have to keep working with the overlay disabled, so they are not gated on it
void pump_frame(IDXGISwapChain *swapchain) {
const bool has_overlay =
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
return;
}
graphics_poll_screenshot_hotkey();
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
// imgui would draw past the RTV and the mouse mapping would be off.
DXGI_SWAP_CHAIN_DESC desc {};
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
ImGui_ImplSpice_SetDisplaySizeOverride(
(float) desc.BufferDesc.Width,
(float) desc.BufferDesc.Height);
// before the overlay render so the screenshot excludes it
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
d3d11_hooks::try_screenshot(swapchain);
}
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
if (has_overlay) {
// after overlay render so toasts/menus end up in the saved image.
d3d11_hooks::try_screenshot(swapchain);
// size imgui to the backbuffer (not window client). dxgi may upscale
// a small backbuffer into a larger client rect; without this override
// imgui would draw past the RTV and the mouse mapping would be off.
DXGI_SWAP_CHAIN_DESC desc {};
if (SUCCEEDED(swapchain->GetDesc(&desc))) {
ImGui_ImplSpice_SetDisplaySizeOverride(
(float) desc.BufferDesc.Width,
(float) desc.BufferDesc.Height);
}
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
}
// after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
d3d11_hooks::try_screenshot(swapchain);
}
}
// ----------------------------------------------------------------------
@@ -177,8 +238,11 @@ void pump_overlay(IDXGISwapChain *swapchain) {
HRESULT STDMETHODCALLTYPE Present_hook(
IDXGISwapChain *swapchain, UINT SyncInterval, UINT Flags)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
// a test present doesn't display anything; don't pick a window or take a screenshot off it
if (!(Flags & DXGI_PRESENT_TEST)) {
try_create_overlay(swapchain);
pump_frame(swapchain);
}
return Present_orig(swapchain, SyncInterval, Flags);
}
@@ -186,8 +250,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
IDXGISwapChain1 *swapchain, UINT SyncInterval, UINT Flags,
const DXGI_PRESENT_PARAMETERS *pParams)
{
try_create_overlay(swapchain);
pump_overlay(swapchain);
if (!(Flags & DXGI_PRESENT_TEST)) {
try_create_overlay(swapchain);
pump_frame(swapchain);
}
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
}
@@ -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
@@ -1489,6 +1489,13 @@ void graphics_d3d9_on_present(
SurfaceHook(device);
}
graphics_poll_screenshot_hotkey();
// before the overlay render so the screenshot excludes it
if (!GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
}
// Do overlay init as many d3d9 hooks create a dummy instance to get vtable offsets and never
// call `Present`. This avoids race conditions on `IDirect3D9::CreateDevice` like with
// `dx9osd.dll` for pfreepanic.
@@ -1508,6 +1515,11 @@ void graphics_d3d9_on_present(
device->EndScene();
}
// after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
}
// for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
@@ -1522,8 +1534,8 @@ void graphics_d3d9_on_present(
wintouchemu::update();
}
graphics_poll_screenshot_hotkey();
graphics_d3d9_process_screenshot_and_capture(device, SUB_SWAP_CHAIN);
// API capture always includes the overlay
graphics_d3d9_process_capture(device, wrapped_device);
}
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
@@ -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();
@@ -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<int> &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)
{
@@ -3,6 +3,7 @@
#include <array>
#include <atomic>
#include <mutex>
#include <vector>
#include <initguid.h>
#include <d3d9.h>
@@ -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<int> &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;
@@ -1,10 +1,13 @@
#include "d3d9_screenshot.h"
#include <cstdint>
#include <filesystem>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <external/robin_hood.h>
@@ -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<IDirect3DSurface9, SurfaceReleaser>;
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<BackbufferCopy> 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<BackbufferCopy> acquire_backbuffer_copy(
buffer->Release();
return BackbufferCopy {
.screen = screen,
.desc = desc,
.surface = SurfacePtr(temp_surface),
};
@@ -300,10 +307,12 @@ static std::optional<BackbufferCopy> acquire_backbuffer_copy(
static void dispatch_surface_save(
const ImageRequest &request,
BackbufferCopy copy) {
auto surface_process = [request, copy = std::move(copy)]() {
std::vector<BackbufferCopy> 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 &copy = 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<std::mutex> lock(SCREENSHOT_SAVE_M);
std::vector<int> screens;
screens.reserve(copies.size());
for (const auto &copy : 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 &copy : 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;
}
@@ -348,43 +402,69 @@ static void dispatch_surface_save(
}
}
static std::optional<ImageRequest> consume_image_request() {
static void process_image_request(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device,
const ImageRequest &request) {
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
std::vector<int> screens { request.screen };
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
screens.clear();
wrapped_device->get_screenshot_screens(screens);
}
std::vector<BackbufferCopy> copies;
copies.reserve(screens.size());
for (const int screen : screens) {
std::optional<BackbufferCopy> 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(copies), screens.size());
}
void graphics_d3d9_process_screenshot(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device) {
if (graphics_screenshot_consume()) {
return ImageRequest {
process_image_request(device, wrapped_device, ImageRequest {
.kind = ImageRequestKind::Screenshot,
.screen = 0,
};
});
}
int capture_screen = 0;
if (graphics_capture_consume(&capture_screen)) {
return ImageRequest {
.kind = ImageRequestKind::Capture,
.screen = capture_screen,
};
}
return std::nullopt;
}
void graphics_d3d9_process_screenshot_and_capture(
void graphics_d3d9_process_capture(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain) {
const auto request = consume_image_request();
if (!request.has_value()) {
return;
WrappedIDirect3DDevice9 *wrapped_device) {
int screen = 0;
if (graphics_capture_consume(&screen)) {
process_image_request(device, wrapped_device, ImageRequest {
.kind = ImageRequestKind::Capture,
.screen = screen,
});
}
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);
}
return;
}
dispatch_surface_save(*request, std::move(*copy));
}
@@ -2,6 +2,12 @@
#include <d3d9.h>
void graphics_d3d9_process_screenshot_and_capture(
struct WrappedIDirect3DDevice9;
void graphics_d3d9_process_screenshot(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *sub_swap_chain);
WrappedIDirect3DDevice9 *wrapped_device);
void graphics_d3d9_process_capture(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device);
+15 -3
View File
@@ -118,6 +118,8 @@ uint32_t GRAPHICS_FS_ORIGINAL_HEIGHT = 0;
// settings
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;
@@ -1530,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<int> &screens) {
// verify dir path
if (GRAPHICS_SCREENSHOT_DIR.empty()) {
@@ -1555,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;
}
+4 -1
View File
@@ -111,6 +111,8 @@ extern bool FAKE_SUBSCREEN_ADAPTER;
// settings
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<UINT> D3D9_ADAPTER;
@@ -145,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<int> &screens = {});
// graphics_windowed.cpp
void graphics_windowed_wndproc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
+6
View File
@@ -1103,6 +1103,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::ScreenshotFolder].is_active()) {
GRAPHICS_SCREENSHOT_DIR = options[launcher::Options::ScreenshotFolder].value_text();
}
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;
}
+16
View File
@@ -3397,6 +3397,22 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.category = "OBS Control",
},
{
// ScreenshotIncludeOverlay
.title = "Include Overlay in Screenshots",
.name = "screenshotoverlay",
.desc = "Includes Spice overlay in screenshots.",
.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<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+3 -1
View File
@@ -320,7 +320,9 @@ namespace launcher {
OBSWebSocketHost,
OBSWebSocketPort,
OBSWebSocketPassword,
OBSWebSocketDebug
OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens
};
enum class OptionsCategory {