#include "d3d9_gfdm.h" #include #include #include #include #include "games/gitadora/gitadora.h" #include "hooks/graphics/graphics.h" #include "util/logging.h" #include "d3d9_device.h" // windows.h defines many stupid things #undef small bool gfdm_two_head_exclusive() { return games::gitadora::is_arena_model() && games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE && !GRAPHICS_WINDOWED; } static std::pair gfdm_small_head_size() { if (!gfdm_two_head_exclusive()) { return { GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT }; } return games::gitadora::arena_subscreen_host_size(); } // scaling is only needed when the head is not the panel size the game draws into static bool gfdm_small_head_scaled() { const auto [width, height] = gfdm_small_head_size(); return width != GFDM_SMALL_WIDTH || height != GFDM_SMALL_HEIGHT; } // Hand the resolved MAIN/SMALL heads back to the game's own array. The SMALL entry keeps // the portrait size the game asked for: that is what it renders, and the next reset finds // the SMALL head by matching it. void gfdm_publish_two_head_parameters( D3DPRESENT_PARAMETERS *logical_presentation_parameters, D3DDISPLAYMODEEX *logical_fullscreen_display_modes, const GfdmTwoHeadDeviceState &state) { if (logical_presentation_parameters == nullptr) { return; } const UINT small = state.logical_small_swapchain; const bool publish_modes = logical_fullscreen_display_modes != nullptr && state.fullscreen_display_modes != nullptr; logical_presentation_parameters[0] = state.presentation_parameters[0]; logical_presentation_parameters[small] = state.presentation_parameters[1]; if (publish_modes) { logical_fullscreen_display_modes[0] = state.fullscreen_display_modes[0]; logical_fullscreen_display_modes[small] = state.fullscreen_display_modes[1]; } if (!gfdm_small_head_scaled()) { return; } logical_presentation_parameters[small].BackBufferWidth = GFDM_SMALL_WIDTH; logical_presentation_parameters[small].BackBufferHeight = GFDM_SMALL_HEIGHT; if (publish_modes) { logical_fullscreen_display_modes[small].Width = GFDM_SMALL_WIDTH; logical_fullscreen_display_modes[small].Height = GFDM_SMALL_HEIGHT; } } HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters( const D3DPRESENT_PARAMETERS *logical_presentation_parameters, const D3DDISPLAYMODEEX *logical_fullscreen_display_modes, D3DPRESENT_PARAMETERS *native_presentation_parameters, D3DDISPLAYMODEEX *native_fullscreen_display_modes, UINT *logical_small_swapchain, const char *operation) { if (!gfdm_two_head_exclusive() || logical_presentation_parameters == nullptr || native_presentation_parameters == nullptr || logical_small_swapchain == nullptr) { return D3DERR_INVALIDCALL; } UINT small_slot = GFDM_LOGICAL_HEAD_COUNT; for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) { const auto ¶ms = logical_presentation_parameters[i]; if (params.BackBufferWidth != GFDM_SMALL_WIDTH || params.BackBufferHeight != GFDM_SMALL_HEIGHT) { continue; } if (small_slot != GFDM_LOGICAL_HEAD_COUNT) { log_warning( "graphics::d3d9", "two-head exclusive: {} found multiple SMALL slots: {} and {}", operation, small_slot, i); return D3DERR_INVALIDCALL; } small_slot = i; } if (small_slot == GFDM_LOGICAL_HEAD_COUNT) { log_warning( "graphics::d3d9", "two-head exclusive: {} could not find an 800x1280 logical SMALL descriptor", operation); return D3DERR_INVALIDCALL; } if (logical_fullscreen_display_modes != nullptr && (logical_fullscreen_display_modes[small_slot].Width != GFDM_SMALL_WIDTH || logical_fullscreen_display_modes[small_slot].Height != GFDM_SMALL_HEIGHT)) { log_warning( "graphics::d3d9", "two-head exclusive: {} SMALL slot {} mode is {}x{}, expected {}x{}", operation, small_slot, logical_fullscreen_display_modes[small_slot].Width, logical_fullscreen_display_modes[small_slot].Height, GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT); return D3DERR_INVALIDCALL; } native_presentation_parameters[0] = logical_presentation_parameters[0]; native_presentation_parameters[1] = logical_presentation_parameters[small_slot]; if (logical_fullscreen_display_modes != nullptr && native_fullscreen_display_modes != nullptr) { native_fullscreen_display_modes[0] = logical_fullscreen_display_modes[0]; native_fullscreen_display_modes[1] = logical_fullscreen_display_modes[small_slot]; } log_info( "graphics::d3d9", "two-head exclusive: {} selected logical SMALL slot {} -> native group head 1{}", operation, small_slot, logical_presentation_parameters[small_slot].hDeviceWindow == GFDM_SUBSCREEN_WINDOW ? " (named SMALL host)" : ""); *logical_small_swapchain = small_slot; return D3D_OK; } bool is_fake_subscreen_adapter(UINT adapter) { // MAIN and SMALL remain native D3D heads; LEFT/RIGHT are virtual. if (gfdm_two_head_exclusive()) { return adapter >= 2 && adapter < GFDM_LOGICAL_HEAD_COUNT; } return FAKE_SUBSCREEN_ADAPTER && adapter > 0; } void get_fake_subscreen_display_mode( UINT adapter, D3DDISPLAYMODE *mode) { (void) adapter; if (gfdm_two_head_exclusive()) { mode->Width = GFDM_SIDE_WIDTH; mode->Height = GFDM_SIDE_HEIGHT; } else { mode->Width = 1280; mode->Height = 800; } mode->RefreshRate = 60; mode->Format = D3DFMT_X8R8G8B8; } void get_fake_subscreen_display_mode_ex( UINT adapter, D3DDISPLAYMODEEX *mode) { D3DDISPLAYMODE legacy_mode {}; get_fake_subscreen_display_mode(adapter, &legacy_mode); mode->Size = sizeof(*mode); mode->Width = legacy_mode.Width; mode->Height = legacy_mode.Height; mode->RefreshRate = legacy_mode.RefreshRate; mode->Format = legacy_mode.Format; mode->ScanLineOrdering = D3DSCANLINEORDERING_PROGRESSIVE; } HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters( D3DPRESENT_PARAMETERS *presentation_parameters, D3DDISPLAYMODEEX *fullscreen_display_modes, const char *operation) { if (!gfdm_two_head_exclusive()) { return D3D_OK; } if (presentation_parameters == nullptr) { return D3DERR_INVALIDCALL; } auto &secondary = presentation_parameters[1]; if (secondary.BackBufferWidth != GFDM_SMALL_WIDTH || secondary.BackBufferHeight != GFDM_SMALL_HEIGHT) { log_warning( "graphics::d3d9", "two-head exclusive: {} expected physical SMALL {}x{} at group slot 1, got {}x{}", operation, GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT, secondary.BackBufferWidth, secondary.BackBufferHeight); return D3DERR_INVALIDCALL; } if (GFDM_SUBSCREEN_WINDOW == nullptr || !IsWindow(GFDM_SUBSCREEN_WINDOW)) { log_warning( "graphics::d3d9", "two-head exclusive: {} cannot use the named SMALL window as physical group head 1", operation); return D3DERR_INVALIDCALL; } secondary.hDeviceWindow = GFDM_SUBSCREEN_WINDOW; if (fullscreen_display_modes != nullptr && (fullscreen_display_modes[1].Width != GFDM_SMALL_WIDTH || fullscreen_display_modes[1].Height != GFDM_SMALL_HEIGHT)) { log_warning( "graphics::d3d9", "two-head exclusive: {} SMALL head mode is {}x{}, expected 800x1280", operation, fullscreen_display_modes[1].Width, fullscreen_display_modes[1].Height); return D3DERR_INVALIDCALL; } // the game keeps rendering the portrait subscreen; only the head it is scanned out // on changes size, and the scaling happens when the head is presented if (gfdm_small_head_scaled()) { const auto [host_width, host_height] = gfdm_small_head_size(); log_info( "graphics::d3d9", "two-head exclusive: {} SMALL head {}x{} -> {}x{}", operation, secondary.BackBufferWidth, secondary.BackBufferHeight, host_width, host_height); secondary.BackBufferWidth = host_width; secondary.BackBufferHeight = host_height; if (fullscreen_display_modes != nullptr) { fullscreen_display_modes[1].Width = host_width; fullscreen_display_modes[1].Height = host_height; } } return D3D_OK; } void graphics_d3d9_gfdm_align_two_head_refresh_to_desktop( IDirect3D9Ex *d3d, UINT master_adapter, D3DPRESENT_PARAMETERS *presentation_parameters, D3DDISPLAYMODEEX *fullscreen_display_modes, const char *operation) { if (d3d == nullptr || presentation_parameters == nullptr || fullscreen_display_modes == nullptr) { return; } for (UINT head = 0; head < 2; head++) { const bool explicitly_forced = (head == 0 && GRAPHICS_FORCE_REFRESH > 0) || (head == 1 && GRAPHICS_FORCE_REFRESH_SUB.has_value()); if (explicitly_forced) { continue; } D3DDISPLAYMODEEX desktop_mode {}; desktop_mode.Size = sizeof(desktop_mode); D3DDISPLAYROTATION rotation = D3DDISPLAYROTATION_IDENTITY; const HRESULT result = d3d->GetAdapterDisplayModeEx( master_adapter + head, &desktop_mode, &rotation); if (FAILED(result)) { log_warning( "graphics::d3d9", "two-head exclusive: {} could not query native head {} desktop refresh, hr={}", operation, head, FMT_HRESULT(result)); continue; } const auto ¶meters = presentation_parameters[head]; if (desktop_mode.Width != parameters.BackBufferWidth || desktop_mode.Height != parameters.BackBufferHeight || desktop_mode.Format != parameters.BackBufferFormat || desktop_mode.RefreshRate == 0) { log_warning( "graphics::d3d9", "two-head exclusive: {} head {} mode differs from the desktop", operation, head); continue; } if (parameters.FullScreen_RefreshRateInHz != desktop_mode.RefreshRate || fullscreen_display_modes[head].RefreshRate != desktop_mode.RefreshRate) { log_info( "graphics::d3d9", "two-head exclusive: {} head {} refresh {} / {} -> {} (rotation {})", operation, head, parameters.FullScreen_RefreshRateInHz, fullscreen_display_modes[head].RefreshRate, desktop_mode.RefreshRate, static_cast(rotation)); presentation_parameters[head].FullScreen_RefreshRateInHz = desktop_mode.RefreshRate; fullscreen_display_modes[head].RefreshRate = desktop_mode.RefreshRate; } } } HRESULT validate_gfdm_two_head_exclusive( IDirect3D9 *d3d, UINT adapter, D3DDEVTYPE device_type, DWORD behavior_flags, const D3DPRESENT_PARAMETERS *presentation_parameters) { if (!(behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) { log_warning( "graphics::d3d9", "two-head mode requires D3DCREATE_ADAPTERGROUP_DEVICE"); return D3DERR_NOTAVAILABLE; } if (adapter != 0) { log_warning( "graphics::d3d9", "two-head exclusive mode currently requires master adapter 0, game requested {}", adapter); return D3DERR_NOTAVAILABLE; } if (presentation_parameters == nullptr) { return D3DERR_INVALIDCALL; } const UINT native_adapter_count = d3d->GetAdapterCount(); if (native_adapter_count != 2) { log_warning( "graphics::d3d9", "two-head mode requires two native D3D9 adapters; found {}", native_adapter_count); return D3DERR_NOTAVAILABLE; } D3DCAPS9 caps {}; const HRESULT caps_result = d3d->GetDeviceCaps(adapter, device_type, &caps); if (FAILED(caps_result)) { log_warning( "graphics::d3d9", "two-head exclusive mode could not query native adapter caps, hr={}", FMT_HRESULT(caps_result)); return caps_result; } if (caps.MasterAdapterOrdinal != adapter || caps.AdapterOrdinalInGroup != 0 || caps.NumberOfAdaptersInGroup != 2) { log_warning( "graphics::d3d9", "invalid adapter group: master={}, index={}, size={}", caps.MasterAdapterOrdinal, caps.AdapterOrdinalInGroup, caps.NumberOfAdaptersInGroup); return D3DERR_NOTAVAILABLE; } const auto &main = presentation_parameters[0]; const auto &small_params = presentation_parameters[1]; const auto [expected_small_width, expected_small_height] = gfdm_small_head_size(); if (main.Windowed || small_params.Windowed) { log_warning( "graphics::d3d9", "two-head exclusive mode requires both group heads to be fullscreen"); return D3DERR_INVALIDCALL; } if (small_params.BackBufferWidth != expected_small_width || small_params.BackBufferHeight != expected_small_height) { log_warning( "graphics::d3d9", "SMALL head must be {}x{}; got {}x{}", expected_small_width, expected_small_height, small_params.BackBufferWidth, small_params.BackBufferHeight); return D3DERR_INVALIDCALL; } if (main.EnableAutoDepthStencil != small_params.EnableAutoDepthStencil) { log_warning( "graphics::d3d9", "two-head mode requires matching automatic depth-stencil settings"); return D3DERR_INVALIDCALL; } if (main.EnableAutoDepthStencil && (main.AutoDepthStencilFormat != small_params.AutoDepthStencilFormat || main.BackBufferWidth != small_params.BackBufferWidth || main.BackBufferHeight != small_params.BackBufferHeight || main.BackBufferFormat != small_params.BackBufferFormat)) { log_warning( "graphics::d3d9", "two-head mode cannot use unequal sizes with automatic depth-stencil"); return D3DERR_INVALIDCALL; } return D3D_OK; } static bool gfdm_find_alternate_small_mode( IDirect3D9Ex *d3d, UINT adapter, const D3DDISPLAYMODEEX &desired, D3DDISPLAYMODEEX *alternate) { if (d3d == nullptr || alternate == nullptr) { return false; } D3DDISPLAYMODEFILTER filter {}; filter.Size = sizeof(filter); filter.Format = desired.Format; filter.ScanLineOrdering = D3DSCANLINEORDERING_UNKNOWN; const UINT count = d3d->GetAdapterModeCountEx(adapter, &filter); const bool portrait = desired.Width < desired.Height; for (UINT index = 0; index < count; index++) { D3DDISPLAYMODEEX mode {}; mode.Size = sizeof(mode); if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode)) || mode.Width != desired.Width || mode.Height != desired.Height || mode.Format != desired.Format || (mode.RefreshRate == desired.RefreshRate && mode.ScanLineOrdering == desired.ScanLineOrdering)) { continue; } *alternate = mode; return true; } for (int preserve_orientation = 1; preserve_orientation >= 0; preserve_orientation--) { for (int preserve_refresh = 1; preserve_refresh >= 0; preserve_refresh--) { for (UINT index = 0; index < count; index++) { D3DDISPLAYMODEEX mode {}; mode.Size = sizeof(mode); if (FAILED(d3d->EnumAdapterModesEx(adapter, &filter, index, &mode)) || (mode.Width == desired.Width && mode.Height == desired.Height) || mode.Format != desired.Format) { continue; } if (preserve_orientation && (mode.Width < mode.Height) != portrait) { continue; } if (preserve_refresh && mode.RefreshRate != desired.RefreshRate) { continue; } *alternate = mode; return true; } } } return false; } static bool gfdm_wait_for_small_mode( HWND window, const D3DDISPLAYMODEEX &expected) { const HMONITOR monitor = window != nullptr ? MonitorFromWindow(window, MONITOR_DEFAULTTONULL) : nullptr; MONITORINFOEXA monitor_info {}; monitor_info.cbSize = sizeof(monitor_info); if (monitor == nullptr || !GetMonitorInfoA(monitor, &monitor_info)) { return false; } for (UINT poll = 0; poll < 50; poll++) { DEVMODEA current {}; current.dmSize = sizeof(current); if (EnumDisplaySettingsExA( monitor_info.szDevice, ENUM_CURRENT_SETTINGS, ¤t, 0) && current.dmPelsWidth == expected.Width && current.dmPelsHeight == expected.Height && (expected.RefreshRate == 0 || current.dmDisplayFrequency == expected.RefreshRate)) { return true; } Sleep(10); } return false; } HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode( IDirect3D9Ex *d3d, IDirect3DDevice9Ex *device, UINT master_adapter, const D3DPRESENT_PARAMETERS *desired_parameters, const D3DDISPLAYMODEEX *desired_modes) { if (d3d == nullptr || device == nullptr || desired_parameters == nullptr || desired_modes == nullptr) { return D3DERR_INVALIDCALL; } D3DDISPLAYMODEEX alternate_small {}; if (!gfdm_find_alternate_small_mode( d3d, master_adapter + 1, desired_modes[1], &alternate_small)) { log_warning("graphics::d3d9", "no alternate SMALL mode is available for recovery"); return D3DERR_NOTAVAILABLE; } D3DPRESENT_PARAMETERS temporary_parameters[2] { desired_parameters[0], desired_parameters[1]}; D3DDISPLAYMODEEX temporary_modes[2] { desired_modes[0], desired_modes[1]}; temporary_parameters[1].BackBufferWidth = alternate_small.Width; temporary_parameters[1].BackBufferHeight = alternate_small.Height; temporary_parameters[1].BackBufferFormat = alternate_small.Format; temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate; temporary_modes[1] = alternate_small; HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes); const bool temporary_settled = temporary_result == D3D_OK && gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, alternate_small); D3DPRESENT_PARAMETERS restore_parameters[2] { desired_parameters[0], desired_parameters[1]}; D3DDISPLAYMODEEX restore_modes[2] { desired_modes[0], desired_modes[1]}; HRESULT restore_result = device->ResetEx(restore_parameters, restore_modes); const bool restore_settled = restore_result == D3D_OK && gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]); if (temporary_result != D3D_OK) { return temporary_result; } if (restore_result != D3D_OK) { return restore_result; } if (!temporary_settled || !restore_settled) { return D3DERR_NOTAVAILABLE; } return device->CheckDeviceState(desired_parameters[0].hDeviceWindow) == D3D_OK ? D3D_OK : D3DERR_NOTAVAILABLE; } bool WrappedIDirect3DDevice9::is_gfdm_two_head_exclusive() const { return ::gfdm_two_head_exclusive(); } bool WrappedIDirect3DDevice9::is_gfdm_logical_small_swapchain( UINT swapchain) const { return swapchain == gfdm_logical_small_swapchain; } bool WrappedIDirect3DDevice9::is_gfdm_logical_side_swapchain( UINT swapchain) const { return swapchain > 0 && swapchain < GFDM_LOGICAL_HEAD_COUNT && swapchain != gfdm_logical_small_swapchain; } size_t WrappedIDirect3DDevice9::gfdm_hidden_side_swapchain_slot( UINT swapchain) const { assert(is_gfdm_logical_side_swapchain(swapchain)); return swapchain < gfdm_logical_small_swapchain ? swapchain : swapchain - 1; } void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters( const D3DPRESENT_PARAMETERS *presentation_parameters) { if (presentation_parameters == nullptr) { gfdm_logical_group_parameters_valid = false; gfdm_small_head.resolve(nullptr); return; } std::copy_n( presentation_parameters, gfdm_logical_group_parameters.size(), gfdm_logical_group_parameters.begin()); gfdm_logical_group_parameters_valid = true; gfdm_small_head.resolve( &gfdm_logical_group_parameters[gfdm_logical_small_swapchain]); } FakeIDirect3DSwapChain9 * WrappedIDirect3DDevice9::ensure_gfdm_hidden_side_swapchain(UINT iSwapChain) { assert(is_gfdm_logical_side_swapchain(iSwapChain)); const size_t index = gfdm_hidden_side_swapchain_slot(iSwapChain); if (fake_sub_swapchain[index] == nullptr) { D3DPRESENT_PARAMETERS params {}; if (gfdm_logical_group_parameters_valid) { params = gfdm_logical_group_parameters[iSwapChain]; } else { params.BackBufferWidth = 1080; params.BackBufferHeight = 1920; params.BackBufferFormat = D3DFMT_X8R8G8B8; params.BackBufferCount = 1; params.MultiSampleType = D3DMULTISAMPLE_NONE; params.SwapEffect = D3DSWAPEFFECT_DISCARD; params.Windowed = FALSE; params.FullScreen_RefreshRateInHz = 60; params.EnableAutoDepthStencil = FALSE; params.PresentationInterval = D3DPRESENT_INTERVAL_ONE; } fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9( this, ¶ms, is_d3d9ex, true); } return fake_sub_swapchain[index]; } void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() { for (UINT i = 1; i < GFDM_LOGICAL_HEAD_COUNT; i++) { if (!is_gfdm_logical_side_swapchain(i)) { continue; } const size_t index = gfdm_hidden_side_swapchain_slot(i); if (fake_sub_swapchain[index] != nullptr) { fake_sub_swapchain[index]->Release(); fake_sub_swapchain[index] = nullptr; } } } void GfdmSmallHead::resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters) { release(); active = gfdm_small_head_scaled(); format = D3DFMT_X8R8G8B8; multisample = D3DMULTISAMPLE_NONE; multisample_quality = 0; if (!active || logical_small_parameters == nullptr) { return; } if (logical_small_parameters->BackBufferFormat != D3DFMT_UNKNOWN) { format = logical_small_parameters->BackBufferFormat; } multisample = logical_small_parameters->MultiSampleType; multisample_quality = logical_small_parameters->MultiSampleQuality; } void GfdmSmallHead::apply_logical_size(UINT *width, UINT *height) const { if (!active) { return; } if (width != nullptr) { *width = GFDM_SMALL_WIDTH; } if (height != nullptr) { *height = GFDM_SMALL_HEIGHT; } } HRESULT GfdmSmallHead::backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out) { if (device == nullptr || out == nullptr || !active) { return D3DERR_INVALIDCALL; } if (surface == nullptr) { const HRESULT result = device->CreateRenderTarget( GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT, format, multisample, multisample_quality, FALSE, &surface, nullptr); if (FAILED(result)) { log_warning( "graphics::d3d9", "two-head exclusive: could not create the portrait SMALL surface, hr={}", FMT_HRESULT(result)); surface = nullptr; return result; } device->ColorFill(surface, nullptr, D3DCOLOR_XRGB(0, 0, 0)); } surface->AddRef(); *out = surface; return D3D_OK; } void GfdmSmallHead::release() { if (surface != nullptr) { surface->Release(); surface = nullptr; } } // largest centered rect inside bounds that keeps the source's aspect ratio static RECT gfdm_fit_centered(const RECT &bounds, LONG source_width, LONG source_height) { const LONG bounds_width = bounds.right - bounds.left; const LONG bounds_height = bounds.bottom - bounds.top; if (bounds_width <= 0 || bounds_height <= 0 || source_width <= 0 || source_height <= 0) { return RECT {}; } LONG width = MulDiv(bounds_height, source_width, source_height); LONG height = bounds_height; if (width > bounds_width) { width = bounds_width; height = MulDiv(bounds_width, source_height, source_width); } const LONG left = bounds.left + (bounds_width - width) / 2; const LONG top = bounds.top + (bounds_height - height) / 2; return RECT { left, top, left + width, top + height }; } static void gfdm_compose_side_heads( WrappedIDirect3DDevice9 *device, IDirect3DSurface9 *head, LONG host_width, LONG host_height, const RECT &content) { if (!games::gitadora::arena_subscreen_shows_sides()) { return; } const RECT bars[2] = { { 0, 0, content.left, host_height }, { content.right, 0, host_width, host_height }, }; for (UINT swapchain = 1; swapchain < GFDM_LOGICAL_HEAD_COUNT; swapchain++) { if (!device->is_gfdm_logical_side_swapchain(swapchain)) { continue; } // nothing has been drawn yet if the game never asked for this head's back buffer const auto *chain = device->fake_sub_swapchain[ device->gfdm_hidden_side_swapchain_slot(swapchain)]; if (chain == nullptr || chain->render_targets.empty()) { continue; } // the head's own window is the only thing that says which side it is, so an // unnamed one is left out rather than guessed at and possibly mirrored const char *name = graphics_gitadora_window_name( device->gfdm_logical_group_parameters[swapchain].hDeviceWindow); const bool named_left = name != nullptr && strcmp(name, "LEFT") == 0; const bool named_right = name != nullptr && strcmp(name, "RIGHT") == 0; if (!named_left && !named_right) { static std::once_flag warned; std::call_once(warned, [] { log_warning( "graphics::d3d9", "two-head exclusive: a side head has no LEFT or RIGHT window, " "leaving its bar black"); }); continue; } const RECT &bar = bars[named_left ? 0 : 1]; if (bar.right <= bar.left) { continue; } IDirect3DSurface9 *source = chain->render_targets[0]; D3DSURFACE_DESC source_desc {}; if (FAILED(source->GetDesc(&source_desc))) { continue; } const RECT target = gfdm_fit_centered( bar, static_cast(source_desc.Width), static_cast(source_desc.Height)); const HRESULT result = device->pReal->StretchRect( source, nullptr, head, &target, D3DTEXF_LINEAR); if (FAILED(result)) { static std::once_flag warned; std::call_once(warned, [result] { log_warning( "graphics::d3d9", "two-head exclusive: could not draw a side head into the bars, hr={}", FMT_HRESULT(result)); }); } } } HRESULT GfdmSmallHead::compose(WrappedIDirect3DDevice9 *device) { if (device == nullptr || device->pReal == nullptr) { return D3DERR_INVALIDCALL; } IDirect3DDevice9 *real = device->pReal; IDirect3DSurface9 *proxy = nullptr; HRESULT result = backbuffer(real, &proxy); if (FAILED(result)) { return result; } IDirect3DSurface9 *head = nullptr; result = real->GetBackBuffer( GFDM_NATIVE_SMALL_SWAPCHAIN, 0, D3DBACKBUFFER_TYPE_MONO, &head); if (FAILED(result)) { proxy->Release(); return result; } D3DSURFACE_DESC desc {}; result = head->GetDesc(&desc); if (SUCCEEDED(result)) { const auto host_width = static_cast(desc.Width); const auto host_height = static_cast(desc.Height); const RECT content = games::gitadora::arena_subscreen_content_rect( host_width, host_height); // discard swap effect leaves the whole head undefined every frame, so the bars // have to be repainted along with the image real->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0)); result = real->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR); gfdm_compose_side_heads(device, head, host_width, host_height, content); } head->Release(); proxy->Release(); return result; } void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() { std::lock_guard lock(gfdm_recovery_mutex); gfdm_recovery_armed = false; gfdm_recovery_consumed.store(true, std::memory_order_release); } void WrappedIDirect3DDevice9::gfdm_arm_present_mode_recovery( const D3DPRESENT_PARAMETERS *native_presentation_parameters, const D3DDISPLAYMODEEX *native_fullscreen_display_modes) { if (native_presentation_parameters == nullptr || native_fullscreen_display_modes == nullptr) { return; } std::lock_guard lock(gfdm_recovery_mutex); gfdm_recovery_parameters[0] = native_presentation_parameters[0]; gfdm_recovery_parameters[1] = native_presentation_parameters[1]; gfdm_recovery_modes[0] = native_fullscreen_display_modes[0]; gfdm_recovery_modes[1] = native_fullscreen_display_modes[1]; if (gfdm_recovery_modes[0].Size == 0) { gfdm_recovery_modes[0].Size = sizeof(D3DDISPLAYMODEEX); } if (gfdm_recovery_modes[1].Size == 0) { gfdm_recovery_modes[1].Size = sizeof(D3DDISPLAYMODEEX); } gfdm_recovery_armed = !gfdm_recovery_parameters[0].Windowed && !gfdm_recovery_parameters[1].Windowed && IsWindow(gfdm_recovery_parameters[1].hDeviceWindow); gfdm_recovery_consumed.store( !gfdm_recovery_armed, std::memory_order_release); } WrappedIDirect3DDevice9::GfdmPresentModeRecoveryResult WrappedIDirect3DDevice9::gfdm_recover_present_mode_change( HRESULT *failure_result) { if (failure_result != nullptr) { *failure_result = D3D_OK; } if (!is_gfdm_two_head_exclusive() || !is_d3d9ex || gfdm_recovery_consumed.exchange( true, std::memory_order_acq_rel)) { return GfdmPresentModeRecoveryResult::NotAttempted; } if (GetCurrentThreadId() != gfdm_device_creation_thread_id) { if (failure_result != nullptr) { *failure_result = D3DERR_INVALIDCALL; } return GfdmPresentModeRecoveryResult::Failed; } std::array parameters {}; std::array modes {}; { std::lock_guard lock(gfdm_recovery_mutex); if (!gfdm_recovery_armed) { return GfdmPresentModeRecoveryResult::NotAttempted; } parameters = gfdm_recovery_parameters; modes = gfdm_recovery_modes; } IDirect3D9 *raw_d3d = nullptr; HRESULT result = pReal->GetDirect3D(&raw_d3d); IDirect3D9Ex *raw_d3d_ex = nullptr; if (SUCCEEDED(result)) { result = raw_d3d != nullptr ? raw_d3d->QueryInterface(IID_PPV_ARGS(&raw_d3d_ex)) : E_FAIL; } D3DDEVICE_CREATION_PARAMETERS creation {}; if (SUCCEEDED(result)) { result = pReal->GetCreationParameters(&creation); } if (FAILED(result) || raw_d3d_ex == nullptr) { if (raw_d3d_ex != nullptr) { raw_d3d_ex->Release(); } if (raw_d3d != nullptr) { raw_d3d->Release(); } if (failure_result != nullptr) { *failure_result = result; } return GfdmPresentModeRecoveryResult::Failed; } result = graphics_d3d9_gfdm_recover_two_head_present_mode( raw_d3d_ex, static_cast(pReal), creation.AdapterOrdinal, parameters.data(), modes.data()); raw_d3d_ex->Release(); raw_d3d->Release(); if (result == D3DERR_NOTAVAILABLE) { return GfdmPresentModeRecoveryResult::NotAttempted; } if (FAILED(result)) { if (failure_result != nullptr) { *failure_result = result; } return GfdmPresentModeRecoveryResult::Failed; } return GfdmPresentModeRecoveryResult::ReadyToRetry; }