From 0209b80a22d6ddc743ef9cf75eb49cf9a9096c02 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 29 May 2026 09:33:31 -0700 Subject: [PATCH] graphics: address perf issues with DX9 image resize logic (#712) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Move resize logic from `EndScene` to `Present`/`PresentEx` (resolves Live2D frame drops in some extreme settings) Plug small memory leak Remove unnecessary surface Lock/Unlock ## Testing Tested IIDX, SDVX, DDR. --- .../graphics/backends/d3d9/d3d9_backend.cpp | 9 ++++ .../graphics/backends/d3d9/d3d9_device.cpp | 46 ++++++++++--------- .../graphics/backends/d3d9/d3d9_device.h | 4 ++ src/spice2x/overlay/windows/screen_resize.cpp | 17 ++----- 4 files changed, 41 insertions(+), 35 deletions(-) diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 4037ba2..c4a077c 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -1458,6 +1458,15 @@ void graphics_d3d9_on_present( IDirect3DDevice9 *device, IDirect3DDevice9 *wrapped_device) { + // image resize / orientation swap. run here (the present path) rather than from `EndScene`, + // which may fire several times per frame on multi-pass / render-to-texture games. this is the + // single point guaranteed to be after the game's last `EndScene` and before the real `Present`, + // so the back buffer is fully drawn and the expensive StretchRect work happens exactly once per + // frame. it must run before the overlay is rendered so the overlay isn't scaled with the image. + if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) { + SurfaceHook(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. diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index 38a2c37..e24ae2e 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -760,18 +760,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::BeginScene() { static IDirect3DSurface9 *topSurface = nullptr; static IDirect3DSurface9 *backbuffer = nullptr; -static LPDIRECT3DSWAPCHAIN9 mSwapChain = nullptr; -static IDirect3DTexture9* tex; +static IDirect3DTexture9 *tex = nullptr; static UINT topSurface_width = 0; static UINT topSurface_height = 0; static float topSurface_aspect_ratio = 1.f; +static UINT backbuffer_width = 0; +static UINT backbuffer_height = 0; void SurfaceHook(IDirect3DDevice9 *pReal) { - D3DPRESENT_PARAMETERS param {}; - - pReal->GetSwapChain(0, &mSwapChain); - mSwapChain->GetPresentParameters(¶m); // phase 0 - create a surface that has the same aspect ratio as the original back buffer // but larger in size. this is needed to ensure that when the user zooms out, we have @@ -779,8 +776,20 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { // // (only done once) if (!topSurface) { - topSurface_width = param.BackBufferWidth * 3; - topSurface_height = param.BackBufferHeight * 3; + // GetSwapChain/GetBackBuffer AddRef their out parameters; the swap chain is only + // needed here (during one-time init), so release it after use to avoid a per-frame + // ref leak. The backbuffer ref is intentionally retained for the lifetime of the + // cached resources. + LPDIRECT3DSWAPCHAIN9 swapChain = nullptr; + D3DPRESENT_PARAMETERS param {}; + + pReal->GetSwapChain(0, &swapChain); + swapChain->GetPresentParameters(¶m); + + backbuffer_width = param.BackBufferWidth; + backbuffer_height = param.BackBufferHeight; + topSurface_width = backbuffer_width * 3; + topSurface_height = backbuffer_height * 3; topSurface_aspect_ratio = (float)topSurface_width / (float)topSurface_height; if (pReal->CreateTexture(topSurface_width, topSurface_height, 1, D3DUSAGE_RENDERTARGET, D3DFMT_A8R8G8B8, @@ -793,16 +802,18 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { param.BackBufferWidth, param.BackBufferHeight, param.BackBufferCount); tex->GetSurfaceLevel(0, &topSurface); - if (mSwapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) { + if (swapChain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &backbuffer) != D3D_OK) { log_warning("graphics::d3d9", "SurfaceHook - GetBackBuffer failed"); } + + swapChain->Release(); } // pre-calculate dimensions used for phase 1 - const int rectLeft = param.BackBufferWidth; - const int rectTop = param.BackBufferHeight; - const int w = param.BackBufferWidth; - const int h = param.BackBufferHeight; + const int rectLeft = backbuffer_width; + const int rectTop = backbuffer_height; + const int w = backbuffer_width; + const int h = backbuffer_height; // this code used to clear the surface using ColorFill on every call, but // this turned out to be very expensive, leading to major frame drops in @@ -812,9 +823,7 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { cfg::SCREENRESIZE->need_surface_clean = false; } - D3DLOCKED_RECT rect; HRESULT hr; - topSurface->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT); // phase 1 - copy the original back buffer onto the new surface. // we draw it 1:1 in the center of the surface. @@ -877,7 +886,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { originRect2.left, originRect2.top, originRect2.right, originRect2.bottom); } } - topSurface->UnlockRect(); // phase 2 - viewport calculation - do the actual zoom / offset math // figure out what region of the surface to copy back to the back buffer. @@ -952,7 +960,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { // targetRect.left, targetRect.top, targetRect.right, targetRect.bottom); // phase 3 - draw the surface to back buffer - backbuffer->LockRect(&rect, NULL, D3DLOCK_DONOTWAIT); bool use_linear_filter = true; if (cfg::SCREENRESIZE->enable_screen_resize) { use_linear_filter = cfg::SCREENRESIZE->enable_linear_filter; @@ -961,7 +968,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { topSurface, &targetRect, backbuffer, nullptr, use_linear_filter ? D3DTEXF_LINEAR : D3DTEXF_NONE); - backbuffer->UnlockRect(); if (hr != D3D_OK) { log_warning( "graphics::d3d9", @@ -973,10 +979,6 @@ void SurfaceHook(IDirect3DDevice9 *pReal) { HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::EndScene() { WRAP_DEBUG; - if (cfg::SCREENRESIZE->enable_screen_resize || GRAPHICS_FS_ORIENTATION_SWAP) { - SurfaceHook(pReal); - } - static std::once_flag printed; std::call_once(printed, []() { log_misc("graphics::d3d9", "WrappedIDirect3DDevice9::EndScene"); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h index be82009..964d8b6 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.h @@ -35,6 +35,10 @@ static const GUID IID_WrappedIDirect3DDevice9 = { 0x6dec0d40, 0x1339, 0x4bda, { 0xa5, 0xf2, 0x22, 0x31, 0xd4, 0x1, 0xf, 0xd1 } }; +// applies the image resize / orientation swap by copying the back buffer through an +// oversized intermediate surface and back. expects the real (unwrapped) device. +void SurfaceHook(IDirect3DDevice9 *pReal); + struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex { explicit WrappedIDirect3DDevice9(HWND hFocusWindow, IDirect3DDevice9 *orig) : hFocusWindow(hFocusWindow), pReal(orig), is_d3d9ex(false) { diff --git a/src/spice2x/overlay/windows/screen_resize.cpp b/src/spice2x/overlay/windows/screen_resize.cpp index 6bda121..e9a299a 100644 --- a/src/spice2x/overlay/windows/screen_resize.cpp +++ b/src/spice2x/overlay/windows/screen_resize.cpp @@ -103,19 +103,10 @@ namespace overlay::windows { void ScreenResize::build_fullscreen_config() { - if (avs::game::is_model("KFC")) { - ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f), - "Warning: Enabling Image Resize uses more GPU\n" - "resources and may significantly lower framerate\n" - "for songs with Live2D! Results may vary, use at\n" - "your own risk."); - } else { - ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f), - "Warning: Enabling Image Resize uses more GPU\n" - "resources and may significantly lower framerate\n" - "in some situations! Results may vary, use at\n" - "your own risk."); - } + ImGui::TextColored(ImVec4(1, 0.5f, 0.5f, 1.f), + "Warning: Enabling Image Resize uses more GPU\n" + "resources and may lower framerate on some\n" + "lower-end PCs."); // enable checkbox ImGui::Checkbox("Enable", &cfg::SCREENRESIZE->enable_screen_resize);