diff --git a/code/bmpman/bmpman.cpp b/code/bmpman/bmpman.cpp index 9b9788adb67..539a9abe21f 100644 --- a/code/bmpman/bmpman.cpp +++ b/code/bmpman/bmpman.cpp @@ -2660,6 +2660,37 @@ void bm_page_in_aabitmap(int handle, int nframes) { } } +/** + * Frees the GPU copy of a bitmap the new mission did not preload. Whole animations only: freeing one frame of a shared + * texture array would leave the others pointing at a dead array. + * @returns true if the GPU copy was freed + */ +static bool bm_free_gpu_copy(bitmap_entry& entry) { + // Vulkan drops every texture at page-in start and its free deletes the slot's backend object + if (gr_screen.mode != GraphicsAPI::OpenGL) + return false; + + if (entry.type == BM_TYPE_USER || entry.type == BM_TYPE_3D) + return false; + + if (bm_is_anim(&entry) && entry.info.ani.first_frame != entry.handle) + return false; + + int num_frames = bm_is_anim(&entry) ? entry.info.ani.num_frames : 1; + + for (int i = 0; i < num_frames; i++) { + if (bm_get_entry(entry.handle + i)->ref_count != 0) + return false; + } + + nprintf(("BmpMan", "Freeing the GPU copy of %s (%d frames)\n", entry.filename, num_frames)); + + for (int i = 0; i < num_frames; i++) + gr_bm_free_data(bm_get_slot(entry.handle + i), true); + + return true; +} + void bm_page_in_start() { Bm_paging = 1; @@ -2691,6 +2722,7 @@ void bm_page_in_stop() { // Load all the ones that are supposed to be loaded for this level. int n = 0; + int n_freed = 0; int bm_preloading = 1; @@ -2733,12 +2765,15 @@ void bm_page_in_stop() { } } else { bm_unload_fast(entry.handle); + if (bm_free_gpu_copy(entry)) + n_freed++; } } } } nprintf(("BmpInfo", "BMPMAN: Loaded %d bitmaps that are marked as used for this level.\n", n)); + nprintf(("BmpInfo", "BMPMAN: Freed the GPU copies of %d bitmaps the level does not preload.\n", n_freed)); #ifndef NDEBUG int total_bitmaps = 0; diff --git a/code/graphics/opengl/gropengl.cpp b/code/graphics/opengl/gropengl.cpp index 73f1e100c88..08021ef2b8c 100644 --- a/code/graphics/opengl/gropengl.cpp +++ b/code/graphics/opengl/gropengl.cpp @@ -472,6 +472,11 @@ void gr_opengl_dump_envmap(const char* filename) auto height = 512; // in future envmap resolution should be dynamic, or at least extracted from envmap_render_target auto sphere_width = 4 * width; auto sphere_height = 2 * height; + float u_scale, v_scale; + uint32_t array_index; + if (!gr_opengl_tcache_set(ENVMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 0)) { + return; + } auto env_tex = bm_get_gr_info(ENVMAP); glBindTexture(env_tex->texture_target, env_tex->texture_id); diff --git a/code/graphics/opengl/gropengldraw.cpp b/code/graphics/opengl/gropengldraw.cpp index 774fb956a59..432dd128384 100644 --- a/code/graphics/opengl/gropengldraw.cpp +++ b/code/graphics/opengl/gropengldraw.cpp @@ -1300,6 +1300,11 @@ void gr_opengl_calculate_irrmap() auto previous_target = gr_screen.rendering_to_texture; int irr_shader = gr_opengl_maybe_create_shader(SDR_TYPE_IRRADIANCE_MAP_GEN, 0); opengl_shader_set_current(irr_shader); + float u_scale, v_scale; + uint32_t array_index; + if (!gr_opengl_tcache_set(ENVMAP, TCACHE_TYPE_CUBEMAP, &u_scale, &v_scale, &array_index, 0)) { + return; + } auto env_tex = bm_get_gr_info(ENVMAP); glBindTexture(env_tex->texture_target, env_tex->texture_id); GL_state.Texture.Enable(0, GL_TEXTURE_CUBE_MAP, env_tex->texture_id); diff --git a/test/src/bmpman/test_bmpman_evict.cpp b/test/src/bmpman/test_bmpman_evict.cpp new file mode 100644 index 00000000000..ff24a924023 --- /dev/null +++ b/test/src/bmpman/test_bmpman_evict.cpp @@ -0,0 +1,239 @@ +#include + +#include +#include +#include +#include + +namespace { + +struct RecordedFree { + int handle; + bool release; +}; + +class BmpmanEvictTest : public test::FSTestFixture { + public: + BmpmanEvictTest() : test::FSTestFixture(INIT_CFILE | INIT_GRAPHICS | INIT_MOD_TABLE) {} + + protected: + SCP_vector frees; + + void SetUp() override { + test::FSTestFixture::SetUp(); + + // the fixture runs as a standalone server, where bm_load substitutes placeholder images + saved_standalone = Is_standalone; + Is_standalone = 0; + + // the free runs only on OpenGL (Vulkan flushes on its own); the stub renderer's hooks stay in place + saved_mode = gr_screen.mode; + gr_screen.mode = GraphicsAPI::OpenGL; + + saved_free = gr_screen.gf_bm_free_data; + // a lock's loader also calls the renderer with release=false to drop stale data; only release frees + // are the frees under test + gr_screen.gf_bm_free_data = [this](bitmap_slot* slot, bool release) { + if (release) + frees.push_back({slot->entry.handle, release}); + }; + } + + void TearDown() override { + gr_screen.gf_bm_free_data = saved_free; + gr_screen.mode = saved_mode; + Is_standalone = saved_standalone; + + test::FSTestFixture::TearDown(); + } + + // counts the recorded frees of the frames [handle, handle + nframes), so other fixture bitmaps cannot skew a test + size_t frees_in(int handle, int nframes = 1) const { + size_t n = 0; + for (const auto& f : frees) { + if (f.handle >= handle && f.handle < handle + nframes) + n++; + } + return n; + } + + static void page_in() { + bm_page_in_start(); + bm_page_in_stop(); + } + + static int load_still() { + int handle = bm_load("attacker"); + EXPECT_GE(handle, 0); + return handle; + } + + static int load_anim(int* nframes) { + int handle = bm_load_animation("CursorWeb", nframes); + EXPECT_GE(handle, 0); + EXPECT_GT(*nframes, 1); + return handle; + } + + private: + int saved_standalone = 0; + GraphicsAPI saved_mode = GraphicsAPI::Stub; + std::function saved_free; +}; + +TEST_F(BmpmanEvictTest, not_preloaded_still_freed_at_page_in) { + int handle = load_still(); + + page_in(); + + ASSERT_EQ(1u, frees_in(handle)); + for (const auto& f : frees) { + if (f.handle == handle) { + EXPECT_TRUE(f.release); + } + } + + // the entry survives the free: the next lock reloads the pixels from the file + ASSERT_NE(nullptr, bm_lock(handle, 16, BMP_TEX_OTHER)); + bm_unlock(handle); +} + +TEST_F(BmpmanEvictTest, freed_again_at_every_page_in) { + int handle = load_still(); + + page_in(); + page_in(); + + EXPECT_EQ(2u, frees_in(handle)); +} + +TEST_F(BmpmanEvictTest, preloaded_still_kept) { + int handle = load_still(); + + // the stub renderer's preload reports failure; the free lives in the other branch either way + bm_page_in_start(); + bm_page_in_texture(handle); + bm_page_in_stop(); + + EXPECT_EQ(0u, frees_in(handle)); +} + +TEST_F(BmpmanEvictTest, lock_then_unlock_does_not_keep) { + int handle = load_still(); + + ASSERT_NE(nullptr, bm_lock(handle, 16, BMP_TEX_OTHER)); + bm_unlock(handle); + page_in(); + + EXPECT_EQ(1u, frees_in(handle)); +} + +TEST_F(BmpmanEvictTest, animation_freed_whole_with_release) { + int nframes = 0; + int handle = load_anim(&nframes); + + page_in(); + + ASSERT_EQ(static_cast(nframes), frees_in(handle, nframes)); + for (int i = 0; i < nframes; i++) { + EXPECT_EQ(1u, frees_in(handle + i)) << "frame " << i; + } + for (const auto& f : frees) { + if (f.handle >= handle && f.handle < handle + nframes) { + EXPECT_TRUE(f.release); + } + } + + for (int i = 0; i < nframes; i++) { + ASSERT_NE(nullptr, bm_lock(handle + i, 16, BMP_TEX_OTHER)) << "frame " << i; + bm_unlock(handle + i); + } +} + +TEST_F(BmpmanEvictTest, locked_frame_keeps_whole_animation) { + int nframes = 0; + int handle = load_anim(&nframes); + + ASSERT_NE(nullptr, bm_lock(handle + 1, 16, BMP_TEX_OTHER)); + page_in(); + + EXPECT_EQ(0u, frees_in(handle, nframes)); + + bm_unlock(handle + 1); + page_in(); + + EXPECT_EQ(static_cast(nframes), frees_in(handle, nframes)); +} + +TEST_F(BmpmanEvictTest, later_frame_preloaded_alone_frees_then_rebuilds) { + int nframes = 0; + int handle = load_anim(&nframes); + + // frames are visited in handle order: the first frame's free runs before the preload of frame 1 rebuilds the array + bm_page_in_start(); + bm_page_in_texture(handle + 1, 1); + bm_page_in_stop(); + + EXPECT_EQ(static_cast(nframes), frees_in(handle, nframes)); +} + +TEST_F(BmpmanEvictTest, first_frame_preloaded_alone_keeps_animation) { + int nframes = 0; + int handle = load_anim(&nframes); + + bm_page_in_start(); + bm_page_in_texture(handle, 1); + bm_page_in_stop(); + + EXPECT_EQ(0u, frees_in(handle, nframes)); +} + +TEST_F(BmpmanEvictTest, user_bitmap_never_freed) { + ubyte pixels[4 * 4 * 4] = {0}; + int handle = bm_create(32, 4, 4, pixels); + ASSERT_GE(handle, 0); + + page_in(); + + EXPECT_EQ(0u, frees_in(handle)); + bm_release(handle); +} + +TEST_F(BmpmanEvictTest, volume_bitmap_never_freed) { + ubyte pixels[4 * 4 * 4 * 4] = {0}; + int handle = bm_create_3d(32, 4, 4, 4, pixels); + ASSERT_GE(handle, 0); + + page_in(); + + EXPECT_EQ(0u, frees_in(handle)); + bm_release(handle); +} + +TEST_F(BmpmanEvictTest, render_target_never_freed) { + ubyte pixels[4 * 4 * 4] = {0}; + int handle = bm_create(32, 4, 4, pixels); + ASSERT_GE(handle, 0); + + // the stub renderer cannot make a render target, so the slot is retyped by hand for the page-in filter + bm_get_entry(handle)->type = BM_TYPE_RENDER_TARGET_STATIC; + page_in(); + bm_get_entry(handle)->type = BM_TYPE_RENDER_TARGET_DYNAMIC; + page_in(); + + EXPECT_EQ(0u, frees_in(handle)); + + bm_get_entry(handle)->type = BM_TYPE_USER; + bm_release(handle); +} + +TEST_F(BmpmanEvictTest, no_eviction_outside_opengl) { + gr_screen.mode = GraphicsAPI::Vulkan; + int handle = load_still(); + + page_in(); + + EXPECT_EQ(0u, frees_in(handle)); +} + +} // namespace diff --git a/test/src/source_groups.cmake b/test/src/source_groups.cmake index 1c268dda86e..c8ed10a3974 100644 --- a/test/src/source_groups.cmake +++ b/test/src/source_groups.cmake @@ -41,6 +41,10 @@ add_file_folder("menuui" menuui/test_intel_parse.cpp ) +add_file_folder("bmpman" + bmpman/test_bmpman_evict.cpp +) + add_file_folder("mod" mod/test_mod_table.cpp )