From 12fed869fe8d94d3395e7c18af950eb8022d16f4 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Thu, 22 Oct 2026 12:42:34 -0700 Subject: [PATCH] Dynres: hold through a savestate load; opt-in DXGI flip presentation (experimental) - A savestate load is not a heavy scene: hold the dynamic-resolution level for the whole 2 s request and do not arm fast descent, which dropped 18x -> 11x within a second of every load and climbed back slowly. - gl_dxgi_present.c: present the present thread's frames through a DXGI FLIP_DISCARD swapchain via WGL_NV_DX_interop2 (Present(1,0), one frame per refresh). Opt-in (PSX_GL_DXGI_PRESENT=1): sharing the HWND with the GL window surface froze the screen in the first test; needs a child window before it can be default. Co-Authored-By: Claude Opus 5.5 --- runtime/include/gl_dxgi_present.h | 24 ++++ runtime/runtime.cmake | 3 +- runtime/src/gl_dxgi_present.c | 199 ++++++++++++++++++++++++++++++ runtime/src/gpu_gl_renderer.c | 46 ++++++- runtime/src/main.cpp | 8 +- 5 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 runtime/include/gl_dxgi_present.h create mode 100644 runtime/src/gl_dxgi_present.c diff --git a/runtime/include/gl_dxgi_present.h b/runtime/include/gl_dxgi_present.h new file mode 100644 index 00000000..d086f5a7 --- /dev/null +++ b/runtime/include/gl_dxgi_present.h @@ -0,0 +1,24 @@ +/* gl_dxgi_present.h - present GL frames through a DXGI flip-model swapchain + * (Windows; gl_dxgi_present.cpp). All calls run on the present thread with + * its GL context current. */ +#ifndef PSX_GL_DXGI_PRESENT_H +#define PSX_GL_DXGI_PRESENT_H +#ifdef __cplusplus +extern "C" { +#endif + +/* Set up for the window (an HWND); 1 when active. */ +int dxp_init(void *hwnd, int w, int h); +int dxp_active(void); +/* Wait for the swapchain's frame latency, resize to w x h if needed, lock the + * back buffer: the GL framebuffer to draw this frame into, 0 on failure. Its + * origin is the top-left (D3D): draw Y-flipped. */ +unsigned dxp_begin(int w, int h); +/* Unlock and present with vsync (one frame per refresh). 1 on success. */ +int dxp_present(void); +void dxp_shutdown(void); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index 9a9ac8ab..d00a2a94 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -389,6 +389,7 @@ set(PSXRECOMP_RUNTIME_SOURCES ${PSXRECOMP_ROOT}/runtime/src/present_thread.c ${PSXRECOMP_ROOT}/runtime/src/frame_gen.c ${PSXRECOMP_ROOT}/runtime/src/gpu_gl_renderer.c + ${PSXRECOMP_ROOT}/runtime/src/gl_dxgi_present.c ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_lle.c ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_hle.c ${PSXRECOMP_ROOT}/runtime/src/gl_batch_policy_select.c @@ -2320,7 +2321,7 @@ function(psxrecomp_add_runtime_target target) # by opengl32; Phase 2b will load modern GL via SDL_GL_GetProcAddress. # iphlpapi: GetAdaptersAddresses in the launcher's netplay # local-address discovery (MSVC does not link it implicitly). - target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32 shell32) + target_link_libraries(${target} PRIVATE ws2_32 iphlpapi dbghelp comdlg32 opengl32 shell32 d3d11 dxgi) # Newer mingw-w64 maps clock_gettime → clock_gettime64 in libwinpthread. # Link it even when netplay code prefers Win32 clocks, so any residual # POSIX time refs (third-party / debug tools) resolve under -static. diff --git a/runtime/src/gl_dxgi_present.c b/runtime/src/gl_dxgi_present.c new file mode 100644 index 00000000..9dadbddc --- /dev/null +++ b/runtime/src/gl_dxgi_present.c @@ -0,0 +1,199 @@ +/* gl_dxgi_present.c - present the GL renderer's frames through a DXGI + * flip-model swapchain (Windows). + * + * Windowed and borderless GL swaps go through DWM composition. With swap + * interval 0 (NVIDIA's vsync'd SwapBuffers stalls the render queue, #723) the + * present thread's wall-clock pacing drifts against the refresh and DWM merges + * two presents into one refresh: a 60 fps stream showed 35-60 distinct frames + * on screen. A FLIP_DISCARD swapchain presented with Present(1, 0) puts every + * frame in its own refresh, and only the present thread waits for it. + * + * GL draws into the swapchain's back buffer through WGL_NV_DX_interop2: the + * present thread blits its finished slot into a renderbuffer registered on + * the D3D11 back buffer, unlocks it and presents. Anything unavailable (the + * extension, the device, the swapchain) leaves the SDL_GL_SwapWindow path in + * place. Experimental: PSX_GL_DXGI_PRESENT=1 turns it on. */ +#ifdef _WIN32 +#define COBJMACROS +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#include /* this TU defines the D3D11/DXGI IIDs it uses */ +#include +#include +#include "psx_sdl.h" +#include +#include +#include + +#include "gl_dxgi_present.h" + +typedef unsigned int GLuint_; +typedef unsigned int GLenum_; +#define GL_RENDERBUFFER_ 0x8D41 +#define GL_FRAMEBUFFER_ 0x8D40 +#define GL_COLOR_ATTACHMENT0_ 0x8CE0 +#define WGL_ACCESS_WRITE_DISCARD_NV_ 0x0002 + +typedef HANDLE (WINAPI *PFN_wglDXOpenDeviceNV)(void *); +typedef BOOL (WINAPI *PFN_wglDXCloseDeviceNV)(HANDLE); +typedef HANDLE (WINAPI *PFN_wglDXRegisterObjectNV)(HANDLE, void *, GLuint_, GLenum_, GLenum_); +typedef BOOL (WINAPI *PFN_wglDXUnregisterObjectNV)(HANDLE, HANDLE); +typedef BOOL (WINAPI *PFN_wglDXLockObjectsNV)(HANDLE, int, HANDLE *); +typedef const char *(WINAPI *PFN_wglGetExtensionsStringARB)(HDC); +typedef void (APIENTRY *PFN_glGenX)(int, GLuint_ *); +typedef void (APIENTRY *PFN_glDelX)(int, const GLuint_ *); +typedef void (APIENTRY *PFN_glBindX)(GLenum_, GLuint_); +typedef void (APIENTRY *PFN_glFbRb)(GLenum_, GLenum_, GLenum_, GLuint_); + +typedef struct Dxp { + int active; + HWND hwnd; + ID3D11Device *dev; + ID3D11DeviceContext *ctx; + IDXGISwapChain2 *sc; + HANDLE wait, hdev, hbuf; + ID3D11Texture2D *bb; + GLuint_ rb, fbo; + UINT w, h; + PFN_wglDXOpenDeviceNV open; + PFN_wglDXCloseDeviceNV close; + PFN_wglDXRegisterObjectNV reg; + PFN_wglDXUnregisterObjectNV unreg; + PFN_wglDXLockObjectsNV lock, unlock; + PFN_glGenX gen_rb, gen_fb; + PFN_glDelX del_rb, del_fb; + PFN_glBindX bind_rb, bind_fb; + PFN_glFbRb fb_rb; +} Dxp; +static Dxp D; + +static void fail(const char *why) { + fprintf(stdout, "psxrecomp: DXGI present unavailable (%s); swapping through SDL\n", why); +} + +static void unregister_buffer() { + if (D.hbuf) { D.unreg(D.hdev, D.hbuf); D.hbuf = NULL; } + if (D.fbo) { D.del_fb(1, &D.fbo); D.fbo = 0; } + if (D.rb) { D.del_rb(1, &D.rb); D.rb = 0; } + if (D.bb) { ID3D11Texture2D_Release(D.bb); D.bb = NULL; } +} + +static int register_buffer() { + if (FAILED(IDXGISwapChain2_GetBuffer(D.sc, 0, &IID_ID3D11Texture2D, (void **)&D.bb))) return 0; + D.gen_rb(1, &D.rb); + D.hbuf = D.reg(D.hdev, D.bb, D.rb, GL_RENDERBUFFER_, WGL_ACCESS_WRITE_DISCARD_NV_); + if (!D.hbuf) return 0; + D.gen_fb(1, &D.fbo); + D.bind_fb(GL_FRAMEBUFFER_, D.fbo); + D.fb_rb(GL_FRAMEBUFFER_, GL_COLOR_ATTACHMENT0_, GL_RENDERBUFFER_, D.rb); + D.bind_fb(GL_FRAMEBUFFER_, 0); + return 1; +} + +int dxp_init(void *hwnd, int w, int h) { + /* Opt-in while experimental: the GL window surface and the swapchain on + * the same HWND left the screen frozen in the first test. */ + const char *env = getenv("PSX_GL_DXGI_PRESENT"); + if (!env || env[0] != '1') return 0; + if (D.active) return 1; + PFN_wglGetExtensionsStringARB ext = (PFN_wglGetExtensionsStringARB)(void *)wglGetProcAddress("wglGetExtensionsStringARB"); + const char *exts = ext ? ext(wglGetCurrentDC()) : NULL; + if (!exts || !strstr(exts, "WGL_NV_DX_interop2")) { fail("no WGL_NV_DX_interop2"); return 0; } + D.open = (PFN_wglDXOpenDeviceNV)(void *)wglGetProcAddress("wglDXOpenDeviceNV"); + D.close = (PFN_wglDXCloseDeviceNV)(void *)wglGetProcAddress("wglDXCloseDeviceNV"); + D.reg = (PFN_wglDXRegisterObjectNV)(void *)wglGetProcAddress("wglDXRegisterObjectNV"); + D.unreg = (PFN_wglDXUnregisterObjectNV)(void *)wglGetProcAddress("wglDXUnregisterObjectNV"); + D.lock = (PFN_wglDXLockObjectsNV)(void *)wglGetProcAddress("wglDXLockObjectsNV"); + D.unlock = (PFN_wglDXLockObjectsNV)(void *)wglGetProcAddress("wglDXUnlockObjectsNV"); + D.gen_rb = (PFN_glGenX)SDL_GL_GetProcAddress("glGenRenderbuffers"); + D.gen_fb = (PFN_glGenX)SDL_GL_GetProcAddress("glGenFramebuffers"); + D.del_rb = (PFN_glDelX)SDL_GL_GetProcAddress("glDeleteRenderbuffers"); + D.del_fb = (PFN_glDelX)SDL_GL_GetProcAddress("glDeleteFramebuffers"); + D.bind_rb = (PFN_glBindX)SDL_GL_GetProcAddress("glBindRenderbuffer"); + D.bind_fb = (PFN_glBindX)SDL_GL_GetProcAddress("glBindFramebuffer"); + D.fb_rb = (PFN_glFbRb)SDL_GL_GetProcAddress("glFramebufferRenderbuffer"); + if (!D.open || !D.close || !D.reg || !D.unreg || !D.lock || !D.unlock || !D.gen_rb || + !D.gen_fb || !D.del_rb || !D.del_fb || !D.bind_rb || !D.bind_fb || !D.fb_rb) { + fail("interop entry points"); return 0; + } + /* The GL context's adapter: the high-performance GPU (what SDL's GL + * context runs on with a single discrete GPU). */ + IDXGIFactory2 *factory = NULL; + if (FAILED(CreateDXGIFactory1(&IID_IDXGIFactory2, (void **)&factory))) { fail("factory"); return 0; } + IDXGIAdapter1 *adapter = NULL; + if (FAILED(IDXGIFactory2_EnumAdapters1(factory, 0, &adapter))) adapter = NULL; + const D3D_FEATURE_LEVEL levels[1] = {D3D_FEATURE_LEVEL_11_0}; + HRESULT hr = D3D11CreateDevice((IDXGIAdapter *)adapter, adapter ? D3D_DRIVER_TYPE_UNKNOWN : D3D_DRIVER_TYPE_HARDWARE, + NULL, D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, 1, + D3D11_SDK_VERSION, &D.dev, NULL, &D.ctx); + if (adapter) IDXGIAdapter1_Release(adapter); + if (FAILED(hr)) { IDXGIFactory2_Release(factory); fail("D3D11 device"); return 0; } + DXGI_SWAP_CHAIN_DESC1 d; memset(&d, 0, sizeof d); + d.Width = (UINT)(w > 0 ? w : 1); d.Height = (UINT)(h > 0 ? h : 1); + d.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + d.SampleDesc.Count = 1; + d.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + d.BufferCount = 2; + d.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + d.AlphaMode = DXGI_ALPHA_MODE_IGNORE; + d.Flags = DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT; + IDXGISwapChain1 *sc1 = NULL; + hr = IDXGIFactory2_CreateSwapChainForHwnd(factory, (IUnknown *)D.dev, (HWND)hwnd, &d, NULL, NULL, &sc1); + if (SUCCEEDED(hr)) IDXGIFactory2_MakeWindowAssociation(factory, (HWND)hwnd, DXGI_MWA_NO_ALT_ENTER); + IDXGIFactory2_Release(factory); + if (FAILED(hr)) { char b[64]; snprintf(b, sizeof b, "swapchain 0x%08lx", hr); fail(b); dxp_shutdown(); return 0; } + hr = IDXGISwapChain1_QueryInterface(sc1, &IID_IDXGISwapChain2, (void **)&D.sc); + IDXGISwapChain1_Release(sc1); + if (FAILED(hr)) { fail("IDXGISwapChain2"); dxp_shutdown(); return 0; } + IDXGISwapChain2_SetMaximumFrameLatency(D.sc, 1); + D.wait = IDXGISwapChain2_GetFrameLatencyWaitableObject(D.sc); + D.hdev = D.open(D.dev); + if (!D.hdev) { fail("wglDXOpenDeviceNV"); dxp_shutdown(); return 0; } + if (!register_buffer()) { fail("wglDXRegisterObjectNV"); dxp_shutdown(); return 0; } + D.hwnd = (HWND)hwnd; D.w = d.Width; D.h = d.Height; D.active = 1; + fprintf(stdout, "psxrecomp: DXGI flip presentation on (%ux%u, vblank-aligned)\n", D.w, D.h); + return 1; +} + +int dxp_active(void) { return D.active; } + +unsigned dxp_begin(int w, int h) { + if (!D.active || w <= 0 || h <= 0) return 0; + if (D.wait) WaitForSingleObjectEx(D.wait, 100, TRUE); + if ((UINT)w != D.w || (UINT)h != D.h) { + unregister_buffer(); + ID3D11DeviceContext_ClearState(D.ctx); + ID3D11DeviceContext_Flush(D.ctx); + if (FAILED(IDXGISwapChain2_ResizeBuffers(D.sc, 0, (UINT)w, (UINT)h, DXGI_FORMAT_UNKNOWN, + DXGI_SWAP_CHAIN_FLAG_FRAME_LATENCY_WAITABLE_OBJECT)) || + !register_buffer()) { + fail("resize"); dxp_shutdown(); return 0; + } + D.w = (UINT)w; D.h = (UINT)h; + } + if (!D.lock(D.hdev, 1, &D.hbuf)) return 0; + return D.fbo; +} + +int dxp_present(void) { + if (!D.active) return 0; + D.unlock(D.hdev, 1, &D.hbuf); + const HRESULT hr = IDXGISwapChain2_Present(D.sc, 1, 0); + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) { + fail("device removed"); dxp_shutdown(); return 0; + } + return SUCCEEDED(hr); +} + +void dxp_shutdown(void) { + if (D.hdev) unregister_buffer(); + if (D.hdev) { D.close(D.hdev); D.hdev = NULL; } + if (D.wait) { CloseHandle(D.wait); D.wait = NULL; } + if (D.sc) { IDXGISwapChain2_Release(D.sc); D.sc = NULL; } + if (D.ctx) { ID3D11DeviceContext_Release(D.ctx); D.ctx = NULL; } + if (D.dev) { ID3D11Device_Release(D.dev); D.dev = NULL; } + D.active = 0; +} +#endif diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 708e3edb..41c4ef96 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -105,6 +105,7 @@ #include "present_thread.h" #include "psx_sdl.h" +#include "gl_dxgi_present.h" #if defined(PSX_SDL3) #include #else @@ -10195,6 +10196,9 @@ static int pt_cb_ctx(void *user, int current) { s_ptp_fbo[i] = 0; s_ptp_gen[i] = 0; } +#ifdef _WIN32 + dxp_shutdown(); +#endif glFlush(); SDL_GL_MakeCurrent(s_win, NULL); return 1; @@ -10211,6 +10215,29 @@ static void gl_window_swap(void) { #endif } +#ifdef _WIN32 +#include "gl_dxgi_present.h" +#if defined(PSX_SDL3) +static void *pt_hwnd(void) { + return SDL_GetPointerProperty(SDL_GetWindowProperties(s_win), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL); +} +#else +#include +static void *pt_hwnd(void) { + SDL_SysWMinfo info; + SDL_VERSION(&info.version); + return SDL_GetWindowWMInfo(s_win, &info) ? (void *)info.info.win.window : NULL; +} +#endif +/* The DXGI flip-model swapchain (gl_dxgi_present.c), set up on the present + * thread at its first frame: 1 when active. */ +static int pt_dxgi(int ww, int wh) { + static int tried; + if (!tried) { tried = 1; void *h = pt_hwnd(); if (h) (void)dxp_init(h, ww, wh); } + return dxp_active(); +} +#endif + static void *pt_cb_present(void *user, int slot, void *ready) { (void)user; if (s_ptp_interval_gen != s_pt_interval_gen) { @@ -10263,23 +10290,36 @@ static void *pt_cb_present(void *user, int slot, void *ready) { const int sw = s_pts_w[slot], sh = s_pts_h[slot]; int ww = 0, wh = 0; gl_drawable_size( &ww, &wh); + GLuint dx_fbo = 0; +#ifdef _WIN32 + if (pt_dxgi(ww, wh)) dx_fbo = dxp_begin(ww, wh); +#endif + /* The DXGI back buffer's origin is the top: blit Y-flipped into it. */ + const int y0 = dx_fbo ? wh : 0, y1 = dx_fbo ? 0 : wh; p_glBindFramebuffer_raw(PSXGL_READ_FRAMEBUFFER, s_ptp_fbo[slot]); - p_glBindFramebuffer_raw(PSXGL_DRAW_FRAMEBUFFER, 0); + p_glBindFramebuffer_raw(PSXGL_DRAW_FRAMEBUFFER, dx_fbo); glDisable(GL_SCISSOR_TEST); if (ww == sw && wh == sh) { - p_glBlitFramebuffer(0, 0, sw, sh, 0, 0, sw, sh, GL_COLOR_BUFFER_BIT, GL_NEAREST); + p_glBlitFramebuffer(0, 0, sw, sh, 0, y0, sw, y1, GL_COLOR_BUFFER_BIT, GL_NEAREST); } else { /* The window changed after this frame was composed: fit it. */ glViewport(0, 0, ww, wh); glClearColor(0.f, 0.f, 0.f, 1.f); glClear(GL_COLOR_BUFFER_BIT); - p_glBlitFramebuffer(0, 0, sw, sh, 0, 0, ww, wh, GL_COLOR_BUFFER_BIT, GL_LINEAR); + p_glBlitFramebuffer(0, 0, sw, sh, 0, y0, ww, y1, GL_COLOR_BUFFER_BIT, GL_LINEAR); } #ifdef GL_PRESENT_THREAD_TEST_HOOK GL_PRESENT_THREAD_TEST_HOOK(slot); #endif void *done = p_glFenceSync(PT_SYNC_GPU_COMMANDS_COMPLETE, 0); uint64_t swap_start=SDL_GetPerformanceCounter(); +#ifdef _WIN32 + if (dx_fbo) { + p_glBindFramebuffer_raw(PSXGL_DRAW_FRAMEBUFFER, 0); + glFlush(); + psx_video_telemetry_present(dxp_present()); + } else +#endif gl_window_swap(); atomic_fetch_add(&s_pt_window_ticks,SDL_GetPerformanceCounter()-swap_start); if (s_recorded_passes || s_interp_enabled) present_sample(s_pts_identity[slot],s_pts_deadline[slot]); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index f8c3f243..5aae7e2c 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -10915,7 +10915,10 @@ static void dynres_tick(void) { * load re-anchors pacing, the game's first seconds). */ bool soft_tail = !held && tail > 0.0 && why && std::strcmp(why, "game entry") == 0; if (g_dynres.hold_request_s > 0.0) { - if (g_dynres.hold_request_s > tail) { tail = g_dynres.hold_request_s; soft_tail = !held; } + /* A load is not a heavy scene: its first frames re-stage VRAM, the + * HD cache and pacing. Hold the level for the whole request (hard), + * so that transient does not drop the resolution for seconds. */ + if (g_dynres.hold_request_s > tail) { tail = g_dynres.hold_request_s; soft_tail = false; } why = "savestate load"; g_dynres.hold_request_s = 0.0; } @@ -10956,8 +10959,7 @@ static void dynres_tick(void) { /* A new scene: let the first sustained overrun jump several levels * (fast descent, dynamic_resolution.h). Render-thread start and a * new resolution re-initialize, which arms too. */ - if (why && (std::strcmp(why, "savestate load") == 0 || - std::strcmp(why, "game entry") == 0 || + if (why && (std::strcmp(why, "game entry") == 0 || std::strcmp(why, "window resize") == 0)) dynrt_arm_descent(&g_dynres.rt); dynres_tick_rt(now_s, wall, period, held, tail, why);