Files
spice2x.github.io/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp
T
bicarus-dev 104a9cbffd graphics: rewrite image scaler (#278)
## Link to GitHub Issue, if one exists
Fixes #276 

## Description of change

Note: this change is not compatible with previous screen resize config;
i.e., if you had the zoom setting dialed into a certain view, after this
change it won't be the same area, so you'll have to set up a scene
again. I think this is a worthwhile cost since the old logic is just
broken in weird ways, and preserving old settings is really tedious math
(for a feature that not many people use). Besides, this change will only
use the new Scenes values from the JSON.

Rewrite the DX9 image scaler logic.

* Previously, the rendering surface was fixed at `4096*4096 px`. Now,
this is relative to the backbuffer dimensions, which depends on the game
& respects user provided `-forceres`.
* Remove "center" option, make it the default.

Above two changes fix the aspect ratio issue (scaling in portrait games
not respecting screen ratio) and things breaking at higher res (1080p
when zoomed out far enough, or when forced to run at 4k resolution for
example)

* Use `scenes` leaf of screen resize JSON config for Scene 1 as well,
completely removing ourselves from being compatible with older spice2x
versions for these values.
* Add additional bounds check before calling `StretchRect` to avoid
users getting stuck with a bad layout, which can kill performance.

## Compiling
🦾 

## Testing
Tested 32 bit (popn / ddr), 64 bit (ldj), portrait and landscape modes
(kfc)
2025-03-28 17:23:01 -07:00

1457 lines
51 KiB
C++

#include "d3d9_backend.h"
#include <cassert>
#include <memory>
#include <thread>
#include <vector>
#include <external/robin_hood.h>
#include <d3d9.h>
#ifdef __GNUC__
#include <d3dx9tex.h>
#endif
#include "avs/game.h"
#include "cfg/screen_resize.h"
#include "games/iidx/iidx.h"
#include "games/sdvx/sdvx.h"
#include "games/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "launcher/options.h"
#include "launcher/shutdown.h"
#include "misc/clipboard.h"
#include "misc/eamuse.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "util/detour.h"
#include "util/flags_helper.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/utils.h"
#include "util/memutils.h"
#include "util/threadpool.h"
#include "d3d9_device.h"
#ifdef min
#undef min
#endif
#define CHECK_RESULT(x) \
do { \
HRESULT __ret = (x); \
if (GRAPHICS_LOG_HRESULT && FAILED(__ret)) [[unlikely]] { \
log_warning("graphics::d3d9", "{} failed, hr={}", __FUNCTION__, FMT_HRESULT(__ret)); \
} \
return __ret; \
} while (0)
#ifdef __GNUC__
typedef decltype(D3DXSaveSurfaceToFileA) *D3DXSaveSurfaceToFileA_t;
#else
#define D3DXIFF_PNG ((DWORD) 3)
typedef HRESULT (WINAPI *D3DXSaveSurfaceToFileA_t)(
LPCSTR pDestFile,
DWORD DestFormat,
LPDIRECT3DSURFACE9 pSrcSurface,
CONST PALETTEENTRY *pSrcPalette,
CONST RECT *pSrcRect);
#endif
/*
* 9 on 12
*/
#define MAX_D3D9ON12_QUEUES 2
typedef struct _D3D9ON12_ARGS {
BOOL Enable9On12;
IUnknown *pD3D12Device;
IUnknown *ppD3D12Queues[MAX_D3D9ON12_QUEUES];
UINT NumQueues;
UINT NodeMask;
} D3D9ON12_ARGS;
typedef HRESULT (WINAPI *Direct3DCreate9On12Ex_t)(
UINT SDKVersion, D3D9ON12_ARGS *pOverrideList,
UINT NumOverrideEntries, IDirect3D9Ex** ppOutputInterface);
typedef IDirect3D9* (WINAPI *Direct3DCreate9On12_t)(
UINT SDKVersion, D3D9ON12_ARGS *pOverrideList, UINT NumOverrideEntries);
// state
static void *D3D9_DIRECT3D_CREATE9_ADR = nullptr;
static char D3D9_DIRECT3D_CREATE9_CONTENTS[16];
static bool ATTEMPTED_D3DX9_LOAD_LIBRARY = false;
// settings
std::optional<UINT> D3D9_ADAPTER = std::nullopt;
DWORD D3D9_BEHAVIOR_DISABLE = 0;
static decltype(Direct3DCreate9) *Direct3DCreate9_orig = nullptr;
static decltype(Direct3DCreate9Ex) *Direct3DCreate9Ex_orig = nullptr;
static Direct3DCreate9On12_t Direct3DCreate9On12_orig = nullptr;
static Direct3DCreate9On12Ex_t Direct3DCreate9On12Ex_orig = nullptr;
static bool ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = false;
static IDirect3DSwapChain9 *SUB_SWAP_CHAIN = nullptr;
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params);
static std::string behavior2s(DWORD behavior_flags) {
FLAGS_START(behavior_flags);
FLAG(behavior_flags, D3DCREATE_FPU_PRESERVE);
FLAG(behavior_flags, D3DCREATE_MULTITHREADED);
FLAG(behavior_flags, D3DCREATE_PUREDEVICE);
FLAG(behavior_flags, D3DCREATE_SOFTWARE_VERTEXPROCESSING);
FLAG(behavior_flags, D3DCREATE_HARDWARE_VERTEXPROCESSING);
FLAG(behavior_flags, D3DCREATE_MIXED_VERTEXPROCESSING);
FLAG(behavior_flags, D3DCREATE_DISABLE_DRIVER_MANAGEMENT);
FLAG(behavior_flags, D3DCREATE_ADAPTERGROUP_DEVICE);
FLAG(behavior_flags, D3DCREATE_DISABLE_DRIVER_MANAGEMENT_EX);
FLAG(behavior_flags, D3DCREATE_NOWINDOWCHANGES);
FLAG(behavior_flags, D3DCREATE_DISABLE_PSGP_THREADING);
FLAG(behavior_flags, D3DCREATE_ENABLE_PRESENTSTATS);
FLAG(behavior_flags, D3DCREATE_DISABLE_PRINTSCREEN);
FLAG(behavior_flags, D3DCREATE_SCREENSAVER);
FLAGS_END(behavior_flags);
}
static std::string format2s(D3DFORMAT format) {
switch (format) {
ENUM_VARIANT(D3DFMT_UNKNOWN);
ENUM_VARIANT(D3DFMT_R8G8B8);
ENUM_VARIANT(D3DFMT_A8R8G8B8);
ENUM_VARIANT(D3DFMT_X8R8G8B8);
ENUM_VARIANT(D3DFMT_R5G6B5);
ENUM_VARIANT(D3DFMT_X1R5G5B5);
ENUM_VARIANT(D3DFMT_A1R5G5B5);
ENUM_VARIANT(D3DFMT_A4R4G4B4);
ENUM_VARIANT(D3DFMT_R3G3B2);
ENUM_VARIANT(D3DFMT_A8);
ENUM_VARIANT(D3DFMT_A8R3G3B2);
ENUM_VARIANT(D3DFMT_X4R4G4B4);
ENUM_VARIANT(D3DFMT_A2B10G10R10);
ENUM_VARIANT(D3DFMT_A8B8G8R8);
ENUM_VARIANT(D3DFMT_X8B8G8R8);
ENUM_VARIANT(D3DFMT_G16R16);
ENUM_VARIANT(D3DFMT_A2R10G10B10);
ENUM_VARIANT(D3DFMT_A16B16G16R16);
ENUM_VARIANT(D3DFMT_A8P8);
ENUM_VARIANT(D3DFMT_P8);
ENUM_VARIANT(D3DFMT_L8);
ENUM_VARIANT(D3DFMT_A8L8);
ENUM_VARIANT(D3DFMT_A4L4);
ENUM_VARIANT(D3DFMT_V8U8);
ENUM_VARIANT(D3DFMT_L6V5U5);
ENUM_VARIANT(D3DFMT_X8L8V8U8);
ENUM_VARIANT(D3DFMT_Q8W8V8U8);
ENUM_VARIANT(D3DFMT_V16U16);
ENUM_VARIANT(D3DFMT_A2W10V10U10);
ENUM_VARIANT(D3DFMT_UYVY);
ENUM_VARIANT(D3DFMT_YUY2);
ENUM_VARIANT(D3DFMT_DXT1);
ENUM_VARIANT(D3DFMT_DXT2);
ENUM_VARIANT(D3DFMT_DXT3);
ENUM_VARIANT(D3DFMT_DXT4);
ENUM_VARIANT(D3DFMT_DXT5);
ENUM_VARIANT(D3DFMT_MULTI2_ARGB8);
ENUM_VARIANT(D3DFMT_G8R8_G8B8);
ENUM_VARIANT(D3DFMT_R8G8_B8G8);
ENUM_VARIANT(D3DFMT_D16_LOCKABLE);
ENUM_VARIANT(D3DFMT_D32);
ENUM_VARIANT(D3DFMT_D15S1);
ENUM_VARIANT(D3DFMT_D24S8);
ENUM_VARIANT(D3DFMT_D24X8);
ENUM_VARIANT(D3DFMT_D24X4S4);
ENUM_VARIANT(D3DFMT_D16);
ENUM_VARIANT(D3DFMT_L16);
ENUM_VARIANT(D3DFMT_D32F_LOCKABLE);
ENUM_VARIANT(D3DFMT_D24FS8);
ENUM_VARIANT(D3DFMT_D32_LOCKABLE);
ENUM_VARIANT(D3DFMT_S8_LOCKABLE);
ENUM_VARIANT(D3DFMT_VERTEXDATA);
ENUM_VARIANT(D3DFMT_INDEX16);
ENUM_VARIANT(D3DFMT_INDEX32);
ENUM_VARIANT(D3DFMT_Q16W16V16U16);
ENUM_VARIANT(D3DFMT_R16F);
ENUM_VARIANT(D3DFMT_G16R16F);
ENUM_VARIANT(D3DFMT_A16B16G16R16F);
ENUM_VARIANT(D3DFMT_R32F);
ENUM_VARIANT(D3DFMT_G32R32F);
ENUM_VARIANT(D3DFMT_A32B32G32R32F);
ENUM_VARIANT(D3DFMT_CxV8U8);
ENUM_VARIANT(D3DFMT_A1);
ENUM_VARIANT(D3DFMT_A2B10G10R10_XR_BIAS);
ENUM_VARIANT(D3DFMT_BINARYBUFFER);
default:
return fmt::to_string(format);
}
}
static std::string presentation_interval2s(UINT presentation_interval) {
FLAGS_START(presentation_interval);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_DEFAULT);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_ONE);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_TWO);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_THREE);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_FOUR);
FLAG(presentation_interval, D3DPRESENT_INTERVAL_IMMEDIATE);
FLAGS_END(presentation_interval);
}
static void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params);
static bool is_dx9_on_12_enabled() {
switch (GRAPHICS_9_ON_12_STATE) {
case DX9ON12_FORCE_OFF:
log_info("graphics::d3d9", "DirectX 9on12: forced OFF by user (-dx9on12)");
return false;
case DX9ON12_FORCE_ON:
log_info("graphics::d3d9", "DirectX 9on12: forced ON by user (-9on12 or -dx9on12)");
return true;
case DX9ON12_AUTO:
default:
if (GRAPHICS_9_ON_12_REQUESTED_BY_GAME) {
log_info(
"graphics::d3d9",
"DirectX 9on12: enabled automatically for current game (tip: use -dx9on12 to force on or off)");
return true;
} else {
log_info(
"graphics::d3d9",
"DirectX 9on12: disabled by default, using DX9 (tip: use -dx9on12 to force on or off)");
return false;
}
}
}
static IDirect3D9 *WINAPI Direct3DCreate9_hook(UINT SDKVersion) {
log_misc("graphics::d3d9", "Direct3DCreate9 hook hit");
// remove hook
if (D3D9_DIRECT3D_CREATE9_ADR) {
detour::inline_restore(D3D9_DIRECT3D_CREATE9_ADR, D3D9_DIRECT3D_CREATE9_CONTENTS);
if (Direct3DCreate9_orig == nullptr) {
Direct3DCreate9_orig = reinterpret_cast<decltype(Direct3DCreate9) *>(D3D9_DIRECT3D_CREATE9_ADR);
}
}
// create interface
IDirect3D9 *value;
if (is_dx9_on_12_enabled()) {
if (!Direct3DCreate9On12_orig) {
log_fatal("graphics::d3d9", "unable to find Direct3DCreate9On12");
} else {
D3D9ON12_ARGS args = {};
args.Enable9On12 = TRUE;
value = Direct3DCreate9On12_orig(SDKVersion, &args, 1);
}
} else {
value = Direct3DCreate9_orig(SDKVersion);
}
if (value == nullptr) {
log_warning("graphics::d3d9", "failed to create Direct3D interface for {}", SDKVersion);
return value;
}
value = new WrappedIDirect3D9(value);
// add hook
if (D3D9_DIRECT3D_CREATE9_ADR) {
detour::inline_noprotect((void *) Direct3DCreate9_hook, D3D9_DIRECT3D_CREATE9_ADR);
}
// return modified interface
return value;
}
static HRESULT WINAPI Direct3DCreate9Ex_hook(UINT SDKVersion, IDirect3D9Ex **d3d9ex) {
log_misc("graphics::d3d9", "Direct3DCreate9Ex hook hit");
// call original
HRESULT result;
if (is_dx9_on_12_enabled()) {
if (!Direct3DCreate9On12Ex_orig) {
log_fatal("graphics::d3d9", "unable to find Direct3DCreate9On12Ex");
} else {
D3D9ON12_ARGS args = {};
args.Enable9On12 = TRUE;
result = Direct3DCreate9On12Ex_orig(SDKVersion, &args, 1, d3d9ex);
}
} else {
result = Direct3DCreate9Ex_orig(SDKVersion, d3d9ex);
}
if (FAILED(result) || *d3d9ex == nullptr) {
log_warning("graphics::d3d9", "failed to create Direct3D interface for {}, hr={}",
SDKVersion,
FMT_HRESULT(result));
return result;
}
*d3d9ex = new WrappedIDirect3D9(*d3d9ex);
// return original result
return result;
}
void graphics_d3d9_init() {
log_info("graphics::d3d9", "initializing");
// DX9 inline hooks
HMODULE d3d9 = libutils::try_module("d3d9.dll");
if (!d3d9) {
log_info("graphics::d3d9", "skipping inline hooks");
} else {
// 9 on 12
Direct3DCreate9On12_orig = (Direct3DCreate9On12_t) libutils::try_proc(d3d9, "Direct3DCreate9On12");
Direct3DCreate9On12Ex_orig = (Direct3DCreate9On12Ex_t) libutils::try_proc(d3d9, "Direct3DCreate9On12Ex");
// inline hooks
D3D9_DIRECT3D_CREATE9_ADR = (void *) libutils::get_proc(d3d9, "Direct3DCreate9");
detour::inline_preserve(
reinterpret_cast<void *>(Direct3DCreate9_hook),
D3D9_DIRECT3D_CREATE9_ADR,
D3D9_DIRECT3D_CREATE9_CONTENTS);
}
// DX9 IAT hooks
Direct3DCreate9_orig = detour::iat_try("Direct3DCreate9", Direct3DCreate9_hook);
Direct3DCreate9Ex_orig = detour::iat_try("Direct3DCreate9Ex", Direct3DCreate9Ex_hook);
}
/*
* IUnknown
*/
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::QueryInterface(
REFIID riid,
void **ppvObj)
{
if (ppvObj == nullptr) {
return E_POINTER;
}
if (riid == IID_WrappedIDirect3D9 ||
riid == IID_IDirect3D9 ||
riid == IID_IDirect3D9Ex)
{
// update to IDirect3DDevice9Ex interface
if (!is_d3d9ex && riid == IID_IDirect3D9Ex) {
IDirect3D9Ex *ex = nullptr;
HRESULT ret = pReal->QueryInterface(IID_PPV_ARGS(&ex));
if (FAILED(ret) || ex == nullptr) {
if (ret != E_NOINTERFACE) {
log_warning("graphics::d3d9",
"failed to upgrade to IDirect3DDevice9Ex, hr={}",
FMT_HRESULT(ret));
}
return E_NOINTERFACE;
}
pReal->Release();
pReal = ex;
is_d3d9ex = true;
}
this->AddRef();
*ppvObj = this;
return S_OK;
}
return pReal->QueryInterface(riid, ppvObj);
}
ULONG STDMETHODCALLTYPE WrappedIDirect3D9::AddRef() {
return pReal->AddRef();
}
ULONG STDMETHODCALLTYPE WrappedIDirect3D9::Release() {
ULONG refs = this->pReal != nullptr ? this->pReal->Release() : 0;
if (refs == 0) {
delete this;
}
return refs;
}
/*
* IDirect3D9
*/
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pInitializeFunction) {
CHECK_RESULT(pReal->RegisterSoftwareDevice(pInitializeFunction));
}
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
return pReal->GetAdapterCount();
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
UINT Adapter,
DWORD Flags,
D3DADAPTER_IDENTIFIER9 *pIdentifier)
{
CHECK_RESULT(pReal->GetAdapterIdentifier(Adapter, Flags, pIdentifier));
}
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
return pReal->GetAdapterModeCount(Adapter, Format);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Adapter,
D3DFORMAT Format,
UINT Mode,
D3DDISPLAYMODE *pMode)
{
HRESULT ret = pReal->EnumAdapterModes(Adapter, Format, Mode, pMode);
if (SUCCEEDED(ret) && pMode) {
/*
log_misc("graphics::d3d9", "IDirect3D9::EnumAdapterMode({}, {}, {}) => {}x{} @ {} Hz ({})",
Adapter,
format2s(Format),
Mode,
pMode->Width,
pMode->Height,
pMode->RefreshRate,
format2s(pMode->Format));
*/
bool modified = false;
auto width = pMode->Width;
auto height = pMode->Height;
auto refresh = pMode->RefreshRate;
if (avs::game::is_model("LDJ")) {
if (Mode == 0 && games::iidx::TDJ_MODE) {
if (games::iidx::is_tdj_fhd()) {
log_misc("graphics::d3d9", "overriding mode 0 to 1920x1080 @ 120 Hz (for TDJ FHD)");
pMode->Width = 1920;
pMode->Height = 1080;
pMode->RefreshRate = 120;
modified = true;
} else {
log_misc("graphics::d3d9", "overriding mode 0 to 1280x720 @ 120 Hz (for TDJ HD)");
pMode->Width = 1280;
pMode->Height = 720;
pMode->RefreshRate = 120;
modified = true;
}
}
// For whatever reason, TDJ FHD mode prefers to pick lower (~60Hz) resolutions instead
// of 1080p@120Hz. Remove them here and try to force 120+ Hz.
if (!modified && games::iidx::is_tdj_fhd() && refresh < 110) {
if ((width == 1920 && height == 1080) ||
(width == 1280 && height == 720)){
log_misc(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (for TDJ FHD)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
modified = true;
}
}
// zero out display mode for bad entries
// - skip non-native resolutions
// - skip 75 and 90 Hz entries because LDJ game timing is messed up with it
// (TDJ should be fine as it assumes a fixed 60 Hz timing)
if (!modified && (width == 1360 || width == 1366 || refresh == 75 || refresh == 90)) {
log_misc(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (for LDJ/TDJ)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
modified = true;
}
if (!modified && !games::iidx::TDJ_MODE && games::iidx::FORCE_720P && (height > 720)) {
log_misc(
"graphics::d3d9", "removing mode {}, {}x{} @ {}Hz (-iidxforce720p)",
Mode, width, height, refresh);
memset(pMode, 0, sizeof(*pMode));
modified = true;
}
}
if (!modified &&
(width > height) &&
(GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CW ||
GRAPHICS_ADJUST_ORIENTATION == ORIENTATION_CCW)) {
log_misc(
"graphics::d3d9", "swapping width and height for mode {}, {}x{} @ {}Hz (-autoorientation)",
Mode, width, height, refresh);
pMode->Height = width;
pMode->Width = height;
modified = true;
}
}
CHECK_RESULT(ret);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayMode(UINT Adapter, D3DDISPLAYMODE *pMode) {
CHECK_RESULT(pReal->GetAdapterDisplayMode(Adapter, pMode));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceType(
UINT iAdapter,
D3DDEVTYPE DevType,
D3DFORMAT DisplayFormat,
D3DFORMAT BackBufferFormat,
BOOL bWindowed)
{
CHECK_RESULT(pReal->CheckDeviceType(iAdapter, DevType, DisplayFormat, BackBufferFormat, bWindowed));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
UINT Adapter,
D3DDEVTYPE DeviceType,
D3DFORMAT AdapterFormat,
DWORD Usage,
D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat)
{
CHECK_RESULT(pReal->CheckDeviceFormat(Adapter, DeviceType, AdapterFormat, Usage, RType, CheckFormat));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceMultiSampleType(
UINT Adapter,
D3DDEVTYPE DeviceType,
D3DFORMAT SurfaceFormat,
BOOL Windowed,
D3DMULTISAMPLE_TYPE MultiSampleType,
DWORD *pQualityLevels)
{
CHECK_RESULT(pReal->CheckDeviceMultiSampleType(
Adapter,
DeviceType,
SurfaceFormat,
Windowed,
MultiSampleType,
pQualityLevels));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDepthStencilMatch(
UINT Adapter,
D3DDEVTYPE DeviceType,
D3DFORMAT AdapterFormat,
D3DFORMAT RenderTargetFormat,
D3DFORMAT DepthStencilFormat)
{
CHECK_RESULT(pReal->CheckDepthStencilMatch(
Adapter,
DeviceType,
AdapterFormat,
RenderTargetFormat,
DepthStencilFormat));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormatConversion(
UINT Adapter,
D3DDEVTYPE DeviceType,
D3DFORMAT SourceFormat,
D3DFORMAT TargetFormat)
{
CHECK_RESULT(pReal->CheckDeviceFormatConversion(Adapter, DeviceType, SourceFormat, TargetFormat));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetDeviceCaps(UINT Adapter, D3DDEVTYPE DeviceType, D3DCAPS9 *pCaps) {
log_misc("graphics::d3d9", "IDirect3D9::GetDeviceCaps hook hit");
if (!pCaps) {
log_warning("graphics::d3d9", "NULL pointer passed in for required parameter");
return D3DERR_INVALIDCALL;
}
// SDVX uses `NumberOfAdaptersInGroup` to allocate a vector and the Microsoft documentation states:
// "The value will be 0 for a subordinate adapter of a multihead card. Each card can have at most one
// master, but may have many subordinates." Therefore, this must point to the master adapter.
if (Adapter == 0 && D3D9_ADAPTER.has_value()) {
Adapter = D3D9_ADAPTER.value();
// Get the master adapter ordinal
HRESULT result = this->pReal->GetDeviceCaps(Adapter, DeviceType, pCaps);
if (FAILED(result)) {
log_warning("graphics::d3d9", "GetDeviceCaps failed, hr={}", FMT_HRESULT(result));
return result;
}
// Now get the device caps for the master adapter
Adapter = pCaps->MasterAdapterOrdinal;
}
HRESULT ret = this->pReal->GetDeviceCaps(Adapter, DeviceType, pCaps);
if (FAILED(ret)) {
return ret;
}
if (avs::game::is_model("LDJ")) {
if (!GRAPHICS_WINDOWED) {
// use 2 so that the subscreen overlay can be drawn in full screen with single monitor
pCaps->NumberOfAdaptersInGroup = 2;
}
// in windowed mode, LDJ will always launch two windows, no special handling needed here
} else if (avs::game::is_model("KFC")) {
if (GRAPHICS_WINDOWED & GRAPHICS_PREVENT_SECONDARY_WINDOW) {
// user wants windowed mode but does not want subscreen at all
pCaps->NumberOfAdaptersInGroup = 1;
} else {
// in both full screen and windowed mode, use 2 so that the game draws the subscreen
// (if this is 1, the game won't even draw the second window, causing subscreen overlay to not work)
pCaps->NumberOfAdaptersInGroup = 2;
}
} else if (avs::game::is_model({"NBT", "PAN"})) {
// beatstream, nostalgia
if (GRAPHICS_WINDOWED) {
pCaps->NumberOfAdaptersInGroup = std::min(pCaps->NumberOfAdaptersInGroup, 1u);
} else if (GRAPHICS_FORCE_SINGLE_ADAPTER) {
pCaps->NumberOfAdaptersInGroup = 1;
}
}
return ret;
}
HMONITOR STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterMonitor(UINT Adapter) {
return pReal->GetAdapterMonitor(Adapter);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
UINT Adapter,
D3DDEVTYPE DeviceType,
HWND hFocusWindow,
DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS *pPresentationParameters,
IDirect3DDevice9 **ppReturnedDeviceInterface)
{
log_misc("graphics::d3d9", "IDirect3D9::CreateDevice hook hit ({}, {}, {}, {}, {}, {})",
Adapter,
DeviceType,
fmt::ptr(hFocusWindow),
behavior2s(BehaviorFlags),
fmt::ptr(pPresentationParameters),
fmt::ptr(ppReturnedDeviceInterface));
// check parameters
if (!pPresentationParameters || !ppReturnedDeviceInterface) {
log_warning("graphics::d3d9", "NULL pointer passed in for required parameter");
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1;
// behavior flags
if (D3D9_BEHAVIOR_DISABLE) {
BehaviorFlags &= ~D3D9_BEHAVIOR_DISABLE;
}
// when rendering via software, disable pure device
if (BehaviorFlags & D3DCREATE_SOFTWARE_VERTEXPROCESSING) {
BehaviorFlags &= ~D3DCREATE_PUREDEVICE;
}
// override adapter used
if (D3D9_ADAPTER.has_value()) {
Adapter = D3D9_ADAPTER.value();
}
// get number of adapters for info dump
if (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE) {
D3DCAPS9 device_caps {};
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
num_adapters = device_caps.NumberOfAdaptersInGroup;
}
}
// dump presentation parameters
for (size_t i = 0; i < num_adapters; i++) {
auto *params = &pPresentationParameters[i];
if (!GRAPHICS_WINDOWED && i == 0) {
GRAPHICS_FS_ORIGINAL_WIDTH = params->BackBufferWidth;
GRAPHICS_FS_ORIGINAL_HEIGHT = params->BackBufferHeight;
log_misc("graphics::d3d9", "original resolution: {}x{}", GRAPHICS_FS_ORIGINAL_WIDTH, GRAPHICS_FS_ORIGINAL_HEIGHT);
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
log_misc(
"graphics::d3d9",
"use custom resolution {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second);
params->BackBufferWidth = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
params->BackBufferHeight = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
log_misc(
"graphics::d3d9",
"swap full orientation {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
}
}
log_info("graphics::d3d9",
"D3D9 presentation parameters for adapter {}: BackBufferWidth: {}, BackBufferHeight: {}, "
"Format: {}, BackBufferCount: {}, MultiSampleType: {}, MultiSampleQuality: {}, "
"SwapEffect: {}, Windowed: {}, EnableAutoDepthStencil: {}, AutoDepthStencilFormat: {}, "
"Flags: {}, FullScreen_RefreshRateInHz: {}, PresentationInterval: {}",
i,
params->BackBufferWidth,
params->BackBufferHeight,
format2s(params->BackBufferFormat),
params->BackBufferCount,
params->MultiSampleType,
params->MultiSampleQuality,
params->SwapEffect,
params->Windowed,
params->EnableAutoDepthStencil,
format2s(params->AutoDepthStencilFormat),
params->Flags,
params->FullScreen_RefreshRateInHz,
presentation_interval2s(params->PresentationInterval));
}
// set windowed
if (GRAPHICS_WINDOWED) {
pPresentationParameters->Windowed = true;
pPresentationParameters->FullScreen_RefreshRateInHz = 0;
update_backbuffer_dimensions(pPresentationParameters);
} else if (GRAPHICS_FORCE_REFRESH > 0) {
log_info("graphics::d3d9", "force refresh rate: {} => {} Hz (-graphics-force-refresh option)",
pPresentationParameters->FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH);
pPresentationParameters->FullScreen_RefreshRateInHz = GRAPHICS_FORCE_REFRESH;
} else if (pPresentationParameters->FullScreen_RefreshRateInHz == 0) {
log_warning(
"graphics::d3d9",
"This game sets FullScreen_RefreshRateInHz to 0, which means it will boot with whatever "
"refresh rate you have set in the desktop. If the game is launching at the wrong Hz, "
"either use -graphics-force-refresh option or change the desktop resolution beforehand.");
}
// force single adapter
if (GRAPHICS_FORCE_SINGLE_ADAPTER) {
log_info("graphics::d3d9", "disabling adapter group device with force single adapter mode");
D3D9_BEHAVIOR_DISABLE |= D3DCREATE_ADAPTERGROUP_DEVICE;
BehaviorFlags &= ~D3DCREATE_ADAPTERGROUP_DEVICE;
}
if (GRAPHICS_FORCE_VSYNC_BUFFER.has_value()) {
log_info("graphics::d3d9", "force BackBufferCount: {} => {}",
pPresentationParameters->BackBufferCount,
GRAPHICS_FORCE_VSYNC_BUFFER.value());
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
}
// call original
HRESULT ret = this->pReal->CreateDevice(
Adapter,
DeviceType,
hFocusWindow,
BehaviorFlags,
pPresentationParameters,
ppReturnedDeviceInterface);
// check for error
if (ret != D3D_OK) {
// log error
log_info("graphics::d3d9", "IDirect3D9::CreateDevice failed, hr={}", FMT_HRESULT(ret));
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
}
// return result
return ret;
}
/*
* IDirect3D9Ex
*/
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCountEx(UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter) {
assert(is_d3d9ex);
return static_cast<IDirect3D9Ex *>(pReal)->GetAdapterModeCountEx(Adapter, pFilter);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx(
UINT Adapter,
const D3DDISPLAYMODEFILTER *pFilter,
UINT Mode,
D3DDISPLAYMODEEX *pMode)
{
assert(is_d3d9ex);
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx(
UINT Adapter,
D3DDISPLAYMODEEX *pMode,
D3DDISPLAYROTATION *pRotation)
{
assert(is_d3d9ex);
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterDisplayModeEx(Adapter, pMode, pRotation));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
UINT Adapter,
D3DDEVTYPE DeviceType,
HWND hFocusWindow,
DWORD BehaviorFlags,
D3DPRESENT_PARAMETERS *pPresentationParameters,
D3DDISPLAYMODEEX *pFullscreenDisplayMode,
IDirect3DDevice9Ex **ppReturnedDeviceInterface)
{
assert(is_d3d9ex);
log_misc("graphics::d3d9", "IDirect3D9Ex::CreateDeviceEx hook hit ({}, {}, {}, {}, {}, {})",
Adapter,
DeviceType,
fmt::ptr(hFocusWindow),
behavior2s(BehaviorFlags),
fmt::ptr(pPresentationParameters),
fmt::ptr(ppReturnedDeviceInterface));
// check parameters
if (!pPresentationParameters || !ppReturnedDeviceInterface) {
log_warning("graphics::d3d9", "NULL pointer passed in required parameter");
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1;
// behavior flags
if (D3D9_BEHAVIOR_DISABLE) {
BehaviorFlags &= ~D3D9_BEHAVIOR_DISABLE;
}
// when rendering via software, disable pure device
if (BehaviorFlags & D3DCREATE_SOFTWARE_VERTEXPROCESSING) {
BehaviorFlags &= ~D3DCREATE_PUREDEVICE;
}
// override adapter used
if (D3D9_ADAPTER.has_value()) {
Adapter = D3D9_ADAPTER.value();
}
// get number of adapters for info dump
if (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE) {
D3DCAPS9 device_caps {};
if (SUCCEEDED(this->pReal->GetDeviceCaps(Adapter, DeviceType, &device_caps))) {
num_adapters = device_caps.NumberOfAdaptersInGroup;
}
}
for (size_t i = 0; i < num_adapters; i++) {
auto *params = &pPresentationParameters[i];
if (!GRAPHICS_WINDOWED && i == 0) {
GRAPHICS_FS_ORIGINAL_WIDTH = params->BackBufferWidth;
GRAPHICS_FS_ORIGINAL_HEIGHT = params->BackBufferHeight;
log_misc("graphics::d3d9", "original resolution: {}x{}", GRAPHICS_FS_ORIGINAL_WIDTH, GRAPHICS_FS_ORIGINAL_HEIGHT);
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
log_misc(
"graphics::d3d9",
"use custom resolution {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().first,
GRAPHICS_FS_CUSTOM_RESOLUTION.value().second);
params->BackBufferWidth = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
params->BackBufferHeight = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
log_misc(
"graphics::d3d9",
"swap full orientation {}x{} => {}x{}",
params->BackBufferWidth, params->BackBufferHeight,
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
}
}
log_info("graphics::d3d9",
"D3D9Ex presentation parameters for adapter {}: BackBufferWidth: {}, BackBufferHeight: {}, "
"Format: {}, BackBufferCount: {}, MultiSampleType: {}, MultiSampleQuality: {}, "
"SwapEffect: {}, Windowed: {}, EnableAutoDepthStencil: {}, AutoDepthStencilFormat: {}, "
"Flags: {}, FullScreen_RefreshRateInHz: {}, PresentationInterval: {}",
i,
params->BackBufferWidth,
params->BackBufferHeight,
format2s(params->BackBufferFormat),
params->BackBufferCount,
params->MultiSampleType,
params->MultiSampleQuality,
params->SwapEffect,
params->Windowed,
params->EnableAutoDepthStencil,
format2s(params->AutoDepthStencilFormat),
params->Flags,
params->FullScreen_RefreshRateInHz,
presentation_interval2s(params->PresentationInterval));
}
if (pFullscreenDisplayMode) {
for (size_t i = 0; i < num_adapters; i++) {
auto *fullscreen_display_mode = &pFullscreenDisplayMode[i];
if (!GRAPHICS_WINDOWED && i == 0) {
if (GRAPHICS_FS_CUSTOM_RESOLUTION.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION.value().second;
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
}
}
log_info("graphics::d3d9",
"D3D9Ex fullscreen display mode for adapter {}: Width: {}, Height: {}, RefreshRate: {}, "
"Format: {}, ScanLineOrdering: {}",
i,
fullscreen_display_mode->Width,
fullscreen_display_mode->Height,
fullscreen_display_mode->RefreshRate,
format2s(fullscreen_display_mode->Format),
fullscreen_display_mode->ScanLineOrdering);
}
}
// set windowed
//
// note from MSDN: `pFullscreenDisplayMode` must be NULL for windowed mode.
if (GRAPHICS_WINDOWED) {
if (avs::game::is_model({"LDJ", "KFC"}) && (BehaviorFlags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
log_misc("graphics::d3d9", "disabling adapter group device in windowed mode");
D3D9_BEHAVIOR_DISABLE |= D3DCREATE_ADAPTERGROUP_DEVICE;
BehaviorFlags &= ~D3DCREATE_ADAPTERGROUP_DEVICE;
}
pPresentationParameters->Windowed = true;
pPresentationParameters->FullScreen_RefreshRateInHz = 0;
update_backbuffer_dimensions(pPresentationParameters);
pFullscreenDisplayMode = nullptr;
} else if (GRAPHICS_FORCE_REFRESH > 0) {
log_info("graphics::d3d9", "force refresh rate: {} => {} Hz",
pPresentationParameters->FullScreen_RefreshRateInHz,
GRAPHICS_FORCE_REFRESH);
pPresentationParameters->FullScreen_RefreshRateInHz = GRAPHICS_FORCE_REFRESH;
if (pFullscreenDisplayMode) {
pFullscreenDisplayMode->RefreshRate = GRAPHICS_FORCE_REFRESH;
}
}
// force single adapter
if (GRAPHICS_FORCE_SINGLE_ADAPTER) {
log_info("graphics::d3d9", "disabling adapter group device with force single adapter mode");
D3D9_BEHAVIOR_DISABLE |= D3DCREATE_ADAPTERGROUP_DEVICE;
BehaviorFlags &= ~D3DCREATE_ADAPTERGROUP_DEVICE;
}
if (GRAPHICS_FORCE_VSYNC_BUFFER.has_value()) {
log_info("graphics::d3d9", "force BackBufferCount: {} => {}",
pPresentationParameters->BackBufferCount,
GRAPHICS_FORCE_VSYNC_BUFFER.value());
pPresentationParameters->BackBufferCount = GRAPHICS_FORCE_VSYNC_BUFFER.value();
}
// call original
HRESULT result = static_cast<IDirect3D9Ex *>(this->pReal)->CreateDeviceEx(
Adapter,
DeviceType,
hFocusWindow,
BehaviorFlags,
pPresentationParameters,
pFullscreenDisplayMode,
ppReturnedDeviceInterface);
// check for error
if (result != D3D_OK) {
// log error
log_warning("graphics::d3d9", "CreateDeviceEx failed, hr={}", FMT_HRESULT(result));
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
// initialize sub screen if IIDX/SDVX requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC"}) && (orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
}
}
return result;
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterLUID(UINT Adapter, LUID *pLUID) {
assert(is_d3d9ex);
CHECK_RESULT(static_cast<IDirect3D9Ex *>(pReal)->GetAdapterLUID(Adapter, pLUID));
}
// Create swap chain for TDJ sub screen if needed
//
// The sub screen swap chain should be created if:
// - Running windowed with `NumberOfAdaptersInGroup >= 2` (game expects implicit swap chain to exist)
// - Running fullscreen with `NumberOfAdaptersInGroup < 2` (overridden `GetDeviceCaps` structure)
static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRESENT_PARAMETERS *present_params) {
D3DCAPS9 caps {};
HRESULT hr = device->GetDeviceCaps(&caps);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to get device caps, hr={}", FMT_HRESULT(hr));
return;
}
// check if sub window swap chain is needed
/*
if (GRAPHICS_WINDOWED && caps.NumberOfAdaptersInGroup < 2) {
log_info("graphics::d3d9", "skipping swap chain creation, running windowed with less than two monitors");
return;
}
if (!GRAPHICS_WINDOWED && caps.NumberOfAdaptersInGroup >= 2) {
log_info("graphics::d3d9", "skipping swap chain creation, running fullscreen with two or more monitors");
return;
}
*/
if (GRAPHICS_WINDOWED) {
log_info("graphics::d3d9", "creating additional swap chain");
present_params->Windowed = true;
present_params->FullScreen_RefreshRateInHz = 0;
// calling `WrappedIDirect3DDevice9::CreateAdditionalSwapChain` triggers special handling for
// LDJ calling `GetSwapChain`
hr = device->CreateAdditionalSwapChain(present_params, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to create additional swap chain, hr={}", FMT_HRESULT(hr));
}
} else {
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
} else {
return;
}
hr = device->CreateAdditionalSwapChain(present_params, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to get additional swap chain, hr={}", FMT_HRESULT(hr));
}
}
}
IDirect3DSurface9 *graphics_d3d9_ldj_get_sub_screen() {
if (SUB_SWAP_CHAIN == nullptr) {
return nullptr;
}
IDirect3DSurface9 *surface = nullptr;
HRESULT hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &surface);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to get back buffer of LDJ sub screen, hr={}", FMT_HRESULT(hr));
return nullptr;
}
return surface;
}
static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE = true;
HRESULT hr = wrapped_device->GetSwapChain(1, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning(
"graphics::d3d9",
"failed to acquire sub screeen swap chain! hr={}",
FMT_HRESULT(hr));
return;
}
}
if (SUB_SWAP_CHAIN != nullptr) {
wintouchemu::update();
if (GRAPHICS_WINDOWED || SUBSCREEN_FORCE_REDRAW) {
SUB_SWAP_CHAIN->Present(nullptr, nullptr, nullptr, nullptr, 0);
}
}
}
static void save_capture(
int screen,
D3DFORMAT format,
UINT width,
UINT height,
IDirect3DSurface9 *surface) {
HRESULT hr;
// lock surface to be able to access the data
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// copy pixel data
size_t pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
auto pixels = new uint8_t[width * height * 3];
for (size_t row = 0; row < height; row++) {
size_t offset_pixels = 0;
size_t offset_row = row * width * 3;
switch (format) {
case D3DFMT_R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 3) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8R8G8B8:
case D3DFMT_A8R8G8B8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[2];
pixel[1] = cell[1];
pixel[2] = cell[0];
offset_pixels += 3;
}
break;
}
case D3DFMT_X8B8G8R8:
case D3DFMT_A8B8G8R8: {
for (size_t offset = 0; offset < pitch; offset += 4) {
auto cell = data + row * pitch + offset;
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = cell[0];
pixel[1] = cell[1];
pixel[2] = cell[2];
offset_pixels += 3;
}
break;
}
default: {
for (size_t offset = 0; offset < width; offset++) {
auto pixel = &pixels[offset_row + offset_pixels];
pixel[0] = 0;
pixel[1] = 0;
pixel[2] = 0;
offset_pixels += 3;
}
}
}
}
// unlock surface
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
graphics_capture_skip(screen);
return;
}
// enqueue
graphics_capture_enqueue(screen, pixels, width, height);
}
static void save_screenshot(const std::string &file_path, UINT height, IDirect3DSurface9 *surface) {
HRESULT hr;
D3DLOCKED_RECT finished_copy {};
hr = surface->LockRect(&finished_copy, nullptr, 0);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to lock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// set alpha channel to 255
{
auto pitch = finished_copy.Pitch;
auto data = reinterpret_cast<uint8_t *>(finished_copy.pBits);
for (size_t i = 0; i < height; i++) {
for (int j = 3; j < pitch; j += 4) {
data[i * pitch + j] = 255;
}
}
}
hr = surface->UnlockRect();
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to unlock screenshot surface, hr={}", FMT_HRESULT(hr));
return;
}
// lazy load function
static D3DXSaveSurfaceToFileA_t D3DXSaveSurfaceToFileA_ptr = nullptr;
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>("D3DXSaveSurfaceToFileA");
// check if function was not found, likely because d3dx9 is not loaded
if (!ATTEMPTED_D3DX9_LOAD_LIBRARY && D3DXSaveSurfaceToFileA_ptr == nullptr) {
ATTEMPTED_D3DX9_LOAD_LIBRARY = true;
for (size_t i = 43; i >= 24; i--) {
auto lib_name = fmt::format("d3dx9_{}.dll", i);
auto d3dx9 = libutils::try_library(lib_name);
// Check if library was not found
if (d3dx9 == nullptr) {
continue;
}
D3DXSaveSurfaceToFileA_ptr = libutils::try_proc<D3DXSaveSurfaceToFileA_t>(
d3dx9, "D3DXSaveSurfaceToFileA");
// Check if function was not found
if (D3DXSaveSurfaceToFileA_ptr == nullptr) {
FreeLibrary(d3dx9);
d3dx9 = nullptr;
continue;
}
log_info("graphics::d3d9", "found surface save function in '{}'", lib_name);
break;
}
}
}
if (D3DXSaveSurfaceToFileA_ptr != nullptr) {
// save to file
log_info("graphics::d3d9", "saving screenshot to {}", file_path);
auto hr = D3DXSaveSurfaceToFileA_ptr(file_path.c_str(), D3DXIFF_PNG, surface, nullptr, nullptr);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "Failed to save screenshot");
return;
}
// save to clipboard
clipboard::copy_image(file_path);
} else {
log_warning("graphics::d3d9", "Direct3D save helper function not available");
}
}
void graphics_d3d9_on_present(
HWND hFocusWindow,
IDirect3DDevice9 *device,
IDirect3DDevice9 *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.
if (!overlay::OVERLAY) {
IDirect3D9 *d3d = nullptr;
if (SUCCEEDED(device->GetDirect3D(&d3d)) && d3d != nullptr) {
overlay::create_d3d9(hFocusWindow, d3d, device);
d3d->Release();
}
} else if (overlay::OVERLAY->uses_device(device) && SUCCEEDED(device->BeginScene())) {
// render overlay
overlay::OVERLAY->update();
overlay::OVERLAY->new_frame();
overlay::OVERLAY->render();
device->EndScene();
}
// for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm =
avs::game::is_model("KFC") &&
(avs::game::SPEC[0] == 'G' || avs::game::SPEC[0] == 'H');
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
if (is_vm || is_tdj) {
graphics_d3d9_ldj_on_present(wrapped_device);
}
// check screenshot key
static bool trigger_last = false;
auto buttons = games::get_buttons_overlay(eamuse_get_game());
if (buttons && (!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, buttons->at(games::OverlayButtons::Screenshot)))
{
if (!trigger_last) {
graphics_screenshot_trigger();
}
trigger_last = true;
} else {
trigger_last = false;
}
// process pending screenshot
bool screenshot = false;
bool capture = false;
int capture_screen = 0;
if ((screenshot = graphics_screenshot_consume())
|| ((capture = graphics_capture_consume(&capture_screen)))) {
HRESULT hr = S_OK;
// TODO: verify capture_screen is a valid swapchain
// get back buffer
IDirect3DSurface9 *buffer = nullptr;
if (SUB_SWAP_CHAIN != nullptr && capture_screen & 1) {
hr = SUB_SWAP_CHAIN->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
} else {
hr = device->GetBackBuffer(capture_screen, 0, D3DBACKBUFFER_TYPE_MONO, &buffer);
}
if (FAILED(hr) || buffer == nullptr) {
log_warning("graphics::d3d9",
"failed to get back buffer, hr={}",
FMT_HRESULT(hr));
return;
}
D3DSURFACE_DESC desc {};
hr = buffer->GetDesc(&desc);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr));
buffer->Release();
return;
}
// TODO: cache render targets
IDirect3DSurface9 *temp_surface = nullptr;
hr = device->CreateRenderTarget(
desc.Width, desc.Height, desc.Format, desc.MultiSampleType,
desc.MultiSampleQuality, TRUE, &temp_surface, nullptr);
if (FAILED(hr) || temp_surface == nullptr) {
log_warning("graphics::d3d9",
"failed to acquire temporary surface, hr={}",
FMT_HRESULT(hr));
buffer->Release();
return;
}
hr = device->StretchRect(buffer, nullptr, temp_surface, nullptr, D3DTEXF_NONE);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to copy back buffer contents, hr={}",
FMT_HRESULT(hr));
temp_surface->Release();
buffer->Release();
return;
}
// release original back buffer reference
buffer->Release();
// function for storing the surface
auto surface_process = [=]() {
// capture
if (capture) {
save_capture(capture_screen, desc.Format, desc.Width, desc.Height, temp_surface);
}
// screenshot
if (screenshot) {
// check where we can save it
auto file_path = graphics_screenshot_genpath();
if (!file_path.empty()) {
// write to file
save_screenshot(file_path, desc.Height, temp_surface);
}
}
// release surface
temp_surface->Release();
};
// list of games that crash when running the screenshot processor on another thread
static const robin_hood::unordered_set<std::string> THREAD_BAN {
"JMA",
#ifndef SPICE64
"KFC",
#endif
"KMA",
"KLP",
"LMA",
};
// run the save operation on another thread for supported games
if (THREAD_BAN.contains(avs::game::MODEL)) {
surface_process();
} else {
static auto pool = ThreadPool(2);
pool.add(surface_process);
}
}
}
void update_backbuffer_dimensions(D3DPRESENT_PARAMETERS *params) {
if (!GRAPHICS_WINDOW_BACKBUFFER_SCALE) {
return;
}
// only do this for primary adapter
static bool first_adapter_hooked = false;
if (first_adapter_hooked) {
return;
}
if (GRAPHICS_WINDOW_SIZE.has_value()) {
first_adapter_hooked = true;
params->BackBufferWidth = GRAPHICS_WINDOW_SIZE.value().first;
params->BackBufferHeight = GRAPHICS_WINDOW_SIZE.value().second;
log_info(
"graphics::d3d9",
"override BackBufferWidth / BackBufferHeight with {}x{} (from -windowresize)",
params->BackBufferWidth,
params->BackBufferHeight);
return;
}
if (cfg::SCREENRESIZE->client_width != 0 && cfg::SCREENRESIZE->client_height != 0) {
first_adapter_hooked = true;
params->BackBufferWidth = cfg::SCREENRESIZE->client_width;
params->BackBufferHeight = cfg::SCREENRESIZE->client_height;
log_info(
"graphics::d3d9",
"override BackBufferWidth / BackBufferHeight with {}x{} (from screen resize config file)",
params->BackBufferWidth,
params->BackBufferHeight);
return;
}
}