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 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-22 12:42:34 -07:00
parent c2cf5ffd03
commit 12fed869fe
5 changed files with 273 additions and 7 deletions
+24
View File
@@ -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
+2 -1
View File
@@ -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.
+199
View File
@@ -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 <windows.h>
#include <initguid.h> /* this TU defines the D3D11/DXGI IIDs it uses */
#include <d3d11.h>
#include <dxgi1_3.h>
#include "psx_sdl.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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
+43 -3
View File
@@ -105,6 +105,7 @@
#include "present_thread.h"
#include "psx_sdl.h"
#include "gl_dxgi_present.h"
#if defined(PSX_SDL3)
#include <SDL3/SDL_opengl.h>
#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 <SDL_syswm.h>
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]);
+5 -3
View File
@@ -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);