fast3d: reliable pre-swap frame capture + fb_id 0 = window

- gfx_opengl_start_draw_to_framebuffer: fb_id 0 now binds GL framebuffer 0
  (the window) instead of framebuffers[0].fbo, a generated attachment-less
  FBO. Harmless on Windows (verified: -level_09 renders, mean RGB ~80);
  strict GL (Mesa) treats the incomplete FBO as an error and drops draws.
- GE_PCDUMP / F12 screenshots move to a pre-swap hook (gfx_pre_swap_hook,
  called from gfx_sdl_swap_buffers_begin before SDL_GL_SwapWindow). Reading
  the back buffer AFTER the swap is undefined on buffer-exchange drivers
  (Mesa/WSLg) -> black capture. Also: read GL_BACK explicitly, glFinish
  first, GL_PACK_ALIGNMENT=1 (639*3 is not 4-aligned -> row shear), and log
  any glReadPixels error.

Windows -level_09 verified: renders, no crash, capture non-black.
This commit is contained in:
James Dansereau committed 2026-09-02 23:54:35 -04:00
1 parent bd7a6cde8a
commit 4d406462bb
3 files changed
+42 -12

No files matched your search

+13 -1
View File
@@ -1083,7 +1083,15 @@ extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const
size_t n = (size_t)width * height * 3;
uint8_t* pixels = (uint8_t*)malloc(n);
if (!pixels) return false;
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glFinish();
glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
{
GLenum e = glGetError();
if (e) sysLogPrintf(LOG_WARNING, "dump_bound_fbo: glReadPixels err 0x%x (%ux%u)", (unsigned)e, width, height);
}
FILE* f = fopen(path, "wb");
if (!f) { free(pixels); return false; }
fprintf(f, "P6\n%u %u\n255\n", width, height);
@@ -1199,7 +1207,11 @@ bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) {
if (noise_scale != 0.0f) {
current_noise_scale = 1.0f / noise_scale;
}
glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo);
/* fb_id 0 is the on-screen target: bind the window's default
* framebuffer (GL name 0), not framebuffers[0].fbo (a generated but
* attachment-less FBO -> incomplete -> nothing renders on a strict
* GL implementation like Mesa/llvmpipe). */
glBindFramebuffer(GL_FRAMEBUFFER, fb_id == 0 ? 0 : fb.fbo);
current_framebuffer = fb_id;
return true;
} else {
+9
View File
@@ -427,10 +427,19 @@ static inline void sync_framerate_with_timer(void) {
previous_time = t;
}
/* Optional hook run with the freshly-composited frame still in the back
* buffer, immediately before the swap. video.c uses it for GE_PCDUMP /
* screenshots: reading the back buffer AFTER SDL_GL_SwapWindow is undefined
* on buffer-exchange drivers (Mesa, WSLg) and yields a black capture. */
extern "C" void (*gfx_pre_swap_hook)(void) = NULL;
static void gfx_sdl_swap_buffers_begin(void) {
if (target_fps) {
sync_framerate_with_timer();
}
if (gfx_pre_swap_hook) {
gfx_pre_swap_hook();
}
SDL_GL_SwapWindow(wnd);
}
+20 -11
View File
@@ -100,6 +100,10 @@ static u32 frames = 0;
/* Set by the host event pump (F12), consumed on the render thread in
* videoEndFrame where a GL context is current. */
static volatile int screenshotReq = 0;
/* Pre-swap capture hook (defined below, registered in videoInit). */
static void videoPreSwapCapture(void);
extern void (*gfx_pre_swap_hook)(void);
static double fpsWindowStart = 0.0;
static int fpsNumFrames = 0;
static float vidAvgFPS = 0.f;
@@ -172,6 +176,8 @@ int videoInit(void)
* still needs a current context on this thread. */
gfx_sdl_release_context();
gfx_pre_swap_hook = videoPreSwapCapture;
initDone = 1;
sysLogPrintf(LOG_INFO, "video: %dx%d window (native %dx%d)",
(int)gfx_current_dimensions.width, (int)gfx_current_dimensions.height,
@@ -289,18 +295,13 @@ void videoSubmitCommands(Gfx *cmds)
gfx_run(cmds);
}
void videoEndFrame(void)
/* Runs from gfx_sdl_swap_buffers_begin with the composited frame still in the
* back buffer, just before SDL_GL_SwapWindow. Reading the back buffer after
* the swap is undefined on buffer-exchange drivers (Mesa/WSLg) -> black. */
static void videoPreSwapCapture(void)
{
if (!initDone) {
return;
}
gfx_end_frame();
/* TEMP D70 (env-gated, debug-only, env-gated): GE_PCDUMP="first-last"
* or "first-last:step" dumps the presented frame as ./ppm/frame_NNNNNN.ppm
* (one frame behind real time — read happens after SwapWindow). Used to
* pixel-verify the intro's 3D content. Also honours [Debug] FrameDump in
* ge007.ini (env var wins). */
/* GE_PCDUMP="first-last" / "first-last:step" -> ./ppm/frame_NNNNNN.ppm.
* Also honours [Debug] FrameDump in ge007.ini (env var wins). */
const char *pcdump = configGetFrameDump();
if (pcdump) {
static int lo = -1, hi = 0, step = 1;
@@ -335,6 +336,14 @@ void videoEndFrame(void)
sysLogPrintf(LOG_WARNING, "video: screenshot failed");
}
}
}
void videoEndFrame(void)
{
if (!initDone) {
return;
}
gfx_end_frame();
++frames;
++fpsNumFrames;