Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions code/bmpman/bmpman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions code/graphics/opengl/gropengl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<tcache_slot_opengl>(ENVMAP);
glBindTexture(env_tex->texture_target, env_tex->texture_id);

Expand Down
5 changes: 5 additions & 0 deletions code/graphics/opengl/gropengldraw.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<tcache_slot_opengl>(ENVMAP);
glBindTexture(env_tex->texture_target, env_tex->texture_id);
GL_state.Texture.Enable(0, GL_TEXTURE_CUBE_MAP, env_tex->texture_id);
Expand Down
239 changes: 239 additions & 0 deletions test/src/bmpman/test_bmpman_evict.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
#include <util/FSTestFixture.h>

#include <bmpman/bmpman.h>
#include <bmpman/bm_internal.h>
#include <globalincs/systemvars.h>
#include <graphics/2d.h>

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<RecordedFree> 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<void(bitmap_slot*, bool)> 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<size_t>(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<size_t>(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<size_t>(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
4 changes: 4 additions & 0 deletions test/src/source_groups.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Loading