From 61a217c4623ffab692b51964b6abd97b7ab8e917 Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 18 Jul 2026 01:27:27 -0400 Subject: [PATCH 1/2] Exempt user bitmaps from texture detail culling At texture detail below maximum, opengl_create_texture() culls texture dimensions on upload, but user bitmaps are streaming surfaces (streamed animations, video) whose frames are updated through gr_update_texture() with the bitmap's own dimensions. Once culled, the glTexSubImage3D update exceeds the texture's actual size, so OpenGL rejects it with GL_INVALID_VALUE and the animation silently freezes on its first, downsampled frame. Culling them also saves nothing, since full-size frame data is streamed every frame regardless. Also assert in gr_opengl_update_texture() that the update dimensions match the texture, so any future mismatch of this kind is caught in debug builds instead of failing silently. Co-Authored-By: Claude Fable 5 --- code/graphics/opengl/gropengltexture.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/code/graphics/opengl/gropengltexture.cpp b/code/graphics/opengl/gropengltexture.cpp index b31c54945e8..9278a6abee5 100644 --- a/code/graphics/opengl/gropengltexture.cpp +++ b/code/graphics/opengl/gropengltexture.cpp @@ -978,7 +978,11 @@ int opengl_create_texture(int bitmap_handle, int bitmap_type, tcache_slot_opengl auto base_level = 0; auto resize = false; - if ( (Detail.hardware_textures < 4) && (bitmap_type != TCACHE_TYPE_AABITMAP) && (bitmap_type != TCACHE_TYPE_INTERFACE) + // User bitmaps are excluded from culling because they are streaming surfaces (e.g. animations and + // video) that are updated through gr_update_texture() with the bitmap's own dimensions each frame, + // which requires the texture to match the bitmap's size. + if ( (Detail.hardware_textures < 4) && (bm_get_type(bitmap_handle) != BM_TYPE_USER) + && (bitmap_type != TCACHE_TYPE_AABITMAP) && (bitmap_type != TCACHE_TYPE_INTERFACE) && (bitmap_type != TCACHE_TYPE_CUBEMAP) && (bitmap_type != TCACHE_TYPE_3DTEX) && ((bitmap_type != TCACHE_TYPE_COMPRESSED) || ((bitmap_type == TCACHE_TYPE_COMPRESSED) && (max_levels > 1))) ) { @@ -1565,6 +1569,13 @@ void gr_opengl_update_texture(int bitmap_handle, int bpp, const ubyte* data, int auto t = bm_get_gr_info(bitmap_handle); if(!t->texture_id) return; + + // This overwrites the texture with data at the bitmap's own size, so the texture must not have been + // culled when it was created (user bitmaps are exempt from texture detail culling for this reason). + Assertion((t->w == width) && (t->h == height), + "gr_opengl_update_texture() called for bitmap %s with %dx%d data, but its texture is %dx%d.", + bm_get_filename(bitmap_handle), width, height, t->w, t->h); + int byte_mult = (bpp >> 3); int true_byte_mult = (t->bpp >> 3); ubyte* texmem = NULL; From 35312d379c9304cc3768edd0539eaca885aac46e Mon Sep 17 00:00:00 2001 From: Goober5000 Date: Sat, 18 Jul 2026 01:28:09 -0400 Subject: [PATCH 2/2] Check texture bind results in the volumetric nebula pass gr_opengl_tcache_set() leaves the previous binding active when it fails, so the glTexParameteri calls that follow would modify the filter state of an unrelated texture. Only set the parameter when the volume texture actually bound. Co-Authored-By: Claude Fable 5 --- code/graphics/opengl/gropengldeferred.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/code/graphics/opengl/gropengldeferred.cpp b/code/graphics/opengl/gropengldeferred.cpp index d7115500804..4d43ebdddc6 100644 --- a/code/graphics/opengl/gropengldeferred.cpp +++ b/code/graphics/opengl/gropengldeferred.cpp @@ -605,12 +605,16 @@ void gr_opengl_deferred_lighting_finish() float u_scale, v_scale; uint32_t array_index; gr_set_texture_addressing(TMAP_ADDRESS_CLAMP); - gr_opengl_tcache_set(neb.getVolumeBitmapHandle(), TCACHE_TYPE_3DTEX, &u_scale, &v_scale, &array_index, 3); - glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + // only set the filter parameter if the bind succeeded, or else it is applied to whatever + // texture was previously bound to this unit + if (gr_opengl_tcache_set(neb.getVolumeBitmapHandle(), TCACHE_TYPE_3DTEX, &u_scale, &v_scale, &array_index, 3)) { + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } if (neb.getNoiseActive()) { gr_set_texture_addressing(TMAP_ADDRESS_WRAP); - gr_opengl_tcache_set(neb.getNoiseVolumeBitmapHandle(), TCACHE_TYPE_3DTEX, &u_scale, &v_scale, &array_index, 4); - glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + if (gr_opengl_tcache_set(neb.getNoiseVolumeBitmapHandle(), TCACHE_TYPE_3DTEX, &u_scale, &v_scale, &array_index, 4)) { + glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + } } }