From 365c6052454f6a370a1a951398d327abf08810eb Mon Sep 17 00:00:00 2001 From: the-e Date: Thu, 9 Jul 2026 15:28:14 +0200 Subject: [PATCH 1/4] Add intermediate framebuffer for gamma correction to prevent flickering --- code/graphics/opengl/gropengl.cpp | 18 ++++++++++- code/graphics/opengl/gropengldraw.cpp | 43 ++++++++++++++++++++++++++- code/graphics/opengl/gropengldraw.h | 3 ++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index 06a5452f3b2..c16d996e33f 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -121,7 +121,13 @@ void gr_opengl_flip() if (Cmdline_window_res) { GL_state.PopFramebufferState(); - glViewport(0, 0, Cmdline_window_res->first, Cmdline_window_res->second); + // Gamma-correct Back_texture into an offscreen scratch texture. This is an FBO -> FBO + // draw, the same class of operation as every other post-process pass. Presenting straight + // into the window-system default framebuffer from a shader draw that samples a + // just-rendered texture caused flicker on some drivers, so the hand-off to the screen + // always goes through glBlitFramebuffer instead. + GL_state.BindFrameBuffer(GammaBlit_framebuffer); + glViewport(0, 0, gr_screen.max_w, gr_screen.max_h); opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_GAMMA_BLIT, 0)); @@ -137,6 +143,16 @@ void gr_opengl_flip() }); opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f); + + GL_state.BindFrameBuffer(0, GL_DRAW_FRAMEBUFFER); + GL_state.BindFrameBuffer(GammaBlit_framebuffer, GL_READ_FRAMEBUFFER); + + glReadBuffer(GL_COLOR_ATTACHMENT0); + glDrawBuffer(GL_BACK); + glBlitFramebuffer(0, 0, gr_screen.max_w, gr_screen.max_h, 0, 0, Cmdline_window_res->first, Cmdline_window_res->second, GL_COLOR_BUFFER_BIT, GL_LINEAR); + glDrawBuffer(GL_NONE); + + GL_state.BindFrameBuffer(0, GL_READ_FRAMEBUFFER); } if (Cmdline_gl_finish) diff --git a/code/graphics/opengl/gropengldraw.cpp b/code/graphics/opengl/gropengldraw.cpp index 4e5a039e979..98363525609 100644 --- a/code/graphics/opengl/gropengldraw.cpp +++ b/code/graphics/opengl/gropengldraw.cpp @@ -55,6 +55,9 @@ GLuint Back_framebuffer; GLuint Back_texture; GLuint Back_depth_texture; +GLuint GammaBlit_framebuffer; +GLuint GammaBlit_texture; + GLuint Distortion_framebuffer = 0; GLuint Distortion_texture[2]; int Distortion_switch = 0; @@ -578,6 +581,34 @@ void opengl_setup_scene_textures() glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_TEXTURE_2D, Back_depth_texture, 0); gr_zbuffer_set(GR_ZBUFF_FULL); glClear(GL_DEPTH_BUFFER_BIT); + + // Intermediate target for the gamma-correction pass. The gamma pass samples Back_texture and writes + // here (an offscreen FBO->FBO draw, same as every other post-process pass); the result is then + // presented to the window via glBlitFramebuffer. Some drivers show flicker when a shader draw that + // samples a just-rendered texture targets the window-system default framebuffer directly, so the + // final presentation step always goes through a blit instead. + glGenFramebuffers(1, &GammaBlit_framebuffer); + GL_state.BindFrameBuffer(GammaBlit_framebuffer); + opengl_set_object_label(GL_FRAMEBUFFER, GammaBlit_framebuffer, "Gamma blit framebuffer"); + + glGenTextures(1, &GammaBlit_texture); + + GL_state.Texture.SetActiveUnit(0); + GL_state.Texture.SetTarget(GL_TEXTURE_2D); + GL_state.Texture.Enable(GammaBlit_texture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, gr_screen.max_w, gr_screen.max_h, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, GammaBlit_texture, 0); + opengl_set_object_label(GL_TEXTURE, GammaBlit_texture, "Gamma blit texture"); + + GL_state.BindFrameBuffer(Back_framebuffer); } //Setup thruster distortion framebuffer @@ -695,7 +726,17 @@ void opengl_scene_texture_shutdown() glDeleteFramebuffers(1, &Back_framebuffer); Back_framebuffer = 0; } - + + if (GammaBlit_texture) { + glDeleteTextures(1, &GammaBlit_texture); + GammaBlit_texture = 0; + } + + if (GammaBlit_framebuffer) { + glDeleteFramebuffers(1, &GammaBlit_framebuffer); + GammaBlit_framebuffer = 0; + } + glDeleteTextures(2, Distortion_texture); Distortion_texture[0] = 0; Distortion_texture[1] = 0; diff --git a/code/graphics/opengl/gropengldraw.h b/code/graphics/opengl/gropengldraw.h index f85da1b1ca9..3cfd3183c80 100644 --- a/code/graphics/opengl/gropengldraw.h +++ b/code/graphics/opengl/gropengldraw.h @@ -39,6 +39,9 @@ extern GLuint Cockpit_depth_texture; extern GLuint Back_framebuffer; extern GLuint Back_texture; +extern GLuint GammaBlit_framebuffer; +extern GLuint GammaBlit_texture; + void gr_opengl_update_distortion(); void gr_opengl_sphere(material *material_def, float rad); From d4d0310752d3a2b65b10cb2e2c5e29171fb89b25 Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 14 Jul 2026 08:14:36 +0200 Subject: [PATCH 2/4] Fix frozen SCPUI briefing screen by resetting color mask before gamma pass A masked draw pass inside the briefing map render leaves the GL color mask fully disabled, and the Rocket UI material path never restores it. Before cc63bfa7 this didn't matter: the frame was presented with a pure glBlitFramebuffer, which ignores the color mask. The gamma-by-shader change turned presentation into a regular draw, which respects the mask, so every flip wrote nothing and the screen froze on stale swapchain content while the engine kept rendering correctly underneath. Hovering a briefing icon appended draws whose materials re-enabled color writes, which is why the screen only updated while hovering. Neutralize color mask and stencil state before the gamma draw, matching the blend and depth-buffer neutralization already done there. Fixes #7590 Co-Authored-By: Claude Fable 5 --- code/graphics/opengl/gropengl.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index c16d996e33f..f584ff4abdc 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -136,6 +136,11 @@ void gr_opengl_flip() GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE); GL_state.SetZbufferType(ZBUFFER_TYPE_NONE); + // The last material of the frame may have left color writes disabled (e.g. a masked + // model pass in the briefing map render). The old blit-only present ignored the color + // mask, but this is a regular draw and needs color writes and no stencil to take effect. + GL_state.ColorMask(true, true, true, true); + GL_state.StencilTest(GL_FALSE); opengl_set_generic_uniform_data( [](graphics::generic_data::gamma_blit_data* data) { From f32b1c01b90137bf11d8de4d09304e2d7cca3aea Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 14 Jul 2026 08:21:06 +0200 Subject: [PATCH 3/4] Skip the gamma-correction pass when gamma is 1.0 The gamma shader is an identity transform at gamma 1.0, so present Back_framebuffer directly via glBlitFramebuffer in that case (the same present path used before the gamma shader was introduced), saving a fullscreen draw per frame. The shader pass through GammaBlit_framebuffer only runs when the effective gamma actually differs from 1.0. Co-Authored-By: Claude Fable 5 --- code/graphics/opengl/gropengl.cpp | 65 ++++++++++++++++++------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index f584ff4abdc..1c0f10aed93 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -121,36 +121,45 @@ void gr_opengl_flip() if (Cmdline_window_res) { GL_state.PopFramebufferState(); - // Gamma-correct Back_texture into an offscreen scratch texture. This is an FBO -> FBO - // draw, the same class of operation as every other post-process pass. Presenting straight - // into the window-system default framebuffer from a shader draw that samples a - // just-rendered texture caused flicker on some drivers, so the hand-off to the screen - // always goes through glBlitFramebuffer instead. - GL_state.BindFrameBuffer(GammaBlit_framebuffer); - glViewport(0, 0, gr_screen.max_w, gr_screen.max_h); - - opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_GAMMA_BLIT, 0)); - - GL_state.Texture.Enable(0, GL_TEXTURE_2D, Back_texture); - Current_shader->program->Uniforms.setTextureUniform("tex", 0); - - GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE); - GL_state.SetZbufferType(ZBUFFER_TYPE_NONE); - // The last material of the frame may have left color writes disabled (e.g. a masked - // model pass in the briefing map render). The old blit-only present ignored the color - // mask, but this is a regular draw and needs color writes and no stencil to take effect. - GL_state.ColorMask(true, true, true, true); - GL_state.StencilTest(GL_FALSE); - - opengl_set_generic_uniform_data( - [](graphics::generic_data::gamma_blit_data* data) { - data->gamma = Cmdline_no_set_gamma ? 1.f : Gr_gamma; - }); - - opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f); + const float gamma = Cmdline_no_set_gamma ? 1.0f : Gr_gamma; + GLuint present_source = Back_framebuffer; + + // At gamma 1.0 the correction shader is an identity transform, so skip the extra + // fullscreen pass and present Back_framebuffer directly. + if (gamma != 1.0f) { + // Gamma-correct Back_texture into an offscreen scratch texture. This is an FBO -> FBO + // draw, the same class of operation as every other post-process pass. Presenting straight + // into the window-system default framebuffer from a shader draw that samples a + // just-rendered texture caused flicker on some drivers, so the hand-off to the screen + // always goes through glBlitFramebuffer instead. + GL_state.BindFrameBuffer(GammaBlit_framebuffer); + glViewport(0, 0, gr_screen.max_w, gr_screen.max_h); + + opengl_shader_set_current(gr_opengl_maybe_create_shader(SDR_TYPE_GAMMA_BLIT, 0)); + + GL_state.Texture.Enable(0, GL_TEXTURE_2D, Back_texture); + Current_shader->program->Uniforms.setTextureUniform("tex", 0); + + GL_state.SetAlphaBlendMode(gr_alpha_blend::ALPHA_BLEND_NONE); + GL_state.SetZbufferType(ZBUFFER_TYPE_NONE); + // The last material of the frame may have left color writes disabled (e.g. a masked + // model pass in the briefing map render). The old blit-only present ignored the color + // mask, but this is a regular draw and needs color writes and no stencil to take effect. + GL_state.ColorMask(true, true, true, true); + GL_state.StencilTest(GL_FALSE); + + opengl_set_generic_uniform_data( + [gamma](graphics::generic_data::gamma_blit_data* data) { + data->gamma = gamma; + }); + + opengl_draw_full_screen_textured(0.0f, 0.0f, 1.0f, 1.0f); + + present_source = GammaBlit_framebuffer; + } GL_state.BindFrameBuffer(0, GL_DRAW_FRAMEBUFFER); - GL_state.BindFrameBuffer(GammaBlit_framebuffer, GL_READ_FRAMEBUFFER); + GL_state.BindFrameBuffer(present_source, GL_READ_FRAMEBUFFER); glReadBuffer(GL_COLOR_ATTACHMENT0); glDrawBuffer(GL_BACK); From a018053afae85f85cc22db387bd36359e7938304 Mon Sep 17 00:00:00 2001 From: the-e Date: Tue, 14 Jul 2026 08:54:11 +0200 Subject: [PATCH 4/4] Reuse Scene_ldr_texture as the gamma-correction pass target Scene_ldr_texture is dead scratch by flip time: all of its consumers (tonemap, FXAA/SMAA, the final post-process pass) write before reading within gr_opengl_post_process_end(), which completes before the flip, and the gamma pass fully overwrites it before blitting it to the window. Attaching it to GammaBlit_framebuffer instead of allocating a dedicated texture saves a screen-sized RGBA8 allocation (~8 MB at 1080p, ~33 MB at 4K). The dedicated allocation remains as a fallback for the case where the scene textures were clamped to GL_max_renderbuffer_size and don't cover the screen. Co-Authored-By: Claude Fable 5 --- code/graphics/opengl/gropengldraw.cpp | 42 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/code/graphics/opengl/gropengldraw.cpp b/code/graphics/opengl/gropengldraw.cpp index 98363525609..63ac62ffe6b 100644 --- a/code/graphics/opengl/gropengldraw.cpp +++ b/code/graphics/opengl/gropengldraw.cpp @@ -591,22 +591,32 @@ void opengl_setup_scene_textures() GL_state.BindFrameBuffer(GammaBlit_framebuffer); opengl_set_object_label(GL_FRAMEBUFFER, GammaBlit_framebuffer, "Gamma blit framebuffer"); - glGenTextures(1, &GammaBlit_texture); - - GL_state.Texture.SetActiveUnit(0); - GL_state.Texture.SetTarget(GL_TEXTURE_2D); - GL_state.Texture.Enable(GammaBlit_texture); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); - - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, gr_screen.max_w, gr_screen.max_h, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); - - glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, GammaBlit_texture, 0); - opengl_set_object_label(GL_TEXTURE, GammaBlit_texture, "Gamma blit texture"); + if (Scene_ldr_texture != 0 && Scene_texture_width == gr_screen.max_w && Scene_texture_height == gr_screen.max_h) { + // Scene_ldr_texture is dead scratch by flip time: its consumers (tonemap, AA, the final + // post pass) all write before reading within gr_opengl_post_process_end(), which completes + // before the flip. Reuse it as the gamma pass target instead of allocating another + // screen-sized texture. GammaBlit_texture stays 0 so shutdown won't delete it. + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, Scene_ldr_texture, 0); + } else { + // Scene textures are clamped to GL_max_renderbuffer_size and may not cover the screen; + // fall back to a dedicated texture in that case. + glGenTextures(1, &GammaBlit_texture); + + GL_state.Texture.SetActiveUnit(0); + GL_state.Texture.SetTarget(GL_TEXTURE_2D); + GL_state.Texture.Enable(GammaBlit_texture); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, gr_screen.max_w, gr_screen.max_h, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, nullptr); + + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, GammaBlit_texture, 0); + opengl_set_object_label(GL_TEXTURE, GammaBlit_texture, "Gamma blit texture"); + } GL_state.BindFrameBuffer(Back_framebuffer); }