#include "d3d9_backend.h" #include #include #include #include #include #include #ifdef __GNUC__ #include #endif #include "avs/game.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 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 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(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(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++) { const auto *params = &pPresentationParameters[i]; 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; } 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; } // 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(pReal)->GetAdapterModeCountEx(Adapter, pFilter); } HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModesEx( UINT Adapter, const D3DDISPLAYMODEFILTER *pFilter, UINT Mode, D3DDISPLAYMODEEX *pMode) { assert(is_d3d9ex); CHECK_RESULT(static_cast(pReal)->EnumAdapterModesEx(Adapter, pFilter, Mode, pMode)); } HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterDisplayModeEx( UINT Adapter, D3DDISPLAYMODEEX *pMode, D3DDISPLAYROTATION *pRotation) { assert(is_d3d9ex); CHECK_RESULT(static_cast(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++) { const auto *params = &pPresentationParameters[i]; 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++) { const auto *fullscreen_display_mode = &pFullscreenDisplayMode[i]; 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; 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; } // call original HRESULT result = static_cast(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(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(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(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"); // 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( 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 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); } } }