Skip to content

Evict unused non-model textures from the GPU between missions - #7878

Open
dustinspace217 wants to merge 2 commits into
scp-fs2open:masterfrom
dustinspace217:graphics/gpu-texture-eviction
Open

dustinspace217 wants to merge 2 commits into
scp-fs2open:masterfrom
dustinspace217:graphics/gpu-texture-eviction

Conversation

@dustinspace217

@dustinspace217 dustinspace217 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What?

Textures a mission does not preload are now freed from the GPU at mission load, instead of staying until you quit. OpenGL only. Adds the first unit tests for bmpman's mission-load logic.

Why?

In OpenGL, VRAM use only ever grows during a campaign. Every explosion, impact and particle animation any mission has shown stays loaded, because the mission-load cleanup frees the system-RAM copy but not the GPU one. Mods with big effect sets hit the wall after a few missions: loads slow down, then the game dies. Fixes #7873.

How?

At the end of mission load the engine already walks every bitmap and drops the RAM copy of anything the new mission does not preload. The GPU copy now goes with it. Whole animations are freed together, since their frames can share one texture array; locked bitmaps and user or volume bitmaps are skipped; Vulkan is untouched because it already flushes at mission load. A texture the mission later draws without having preloaded it is read and uploaded again on first use, the same path as its first use ever.

The first version of this PR kept anything drawn last mission for one more mission and added a table key for that. qazwsxal asked why not do what Vulkan does. I measured the re-upload and it is not visible: the worst frame in a fight on a revisited mission was 12.6 ms against 12.4 ms on the first visit (RTX 5080, FS2 with MediaVPs 5.0.2), so this version does what Vulkan does.

One consumer read a texture id straight out of its slot instead of going through the texture cache: the irradiance pass, when a mission names a file cubemap with $Environment Map:. It now binds through the cache like the deferred pass does, which also fixes the first mission that uses a file cubemap, where the pass used to bind texture 0.

Known limit, shared with Vulkan: a streaming EFF animation that a mod script keeps across a mission load shows its first frame until it next advances. A follow-up PR will give EFF streams a user bitmap of their own, the way ANI and APNG streams already work, so the current frame survives any flush.

Testing?

Linux, Release and Debug, headless, one process chained through missions, MediaVPs 5.0.2. FS2 four missions and A,B,C,A on master and this branch; a combat A,B,A with two wingmen destroyed each mission; Star Fox Event Horizon four missions and A,B,C,A; screenshots of four missions plus the main hall after each, two runs per build; Vulkan on an RTX 5080; a scratch mission with $Environment Map: cubemap played, left, and replayed with the irradiance pass logging the texture it binds. 13 unit tests, suites green in Release and Debug.

Anything Else?

Written with Claude Code; the measurements, review and testing ran on my machine.

Resident GPU texture bytes as the engine counts them after each mission load (OpenGL rows are software-rendered; VRAM is per process from nvidia-smi on the Vulkan rows). Loads are wall time from start to gameplay, single runs.

Scenario Renderer Build Resident after each mission Loads, missions 2+ Eviction
FS2 SM1-01, SM1-02, SM2-07, SM2-08 OpenGL master 2.21, 2.53, 2.47, 2.42 GB 3.06, 1.60, 0.48 s never
same OpenGL patch 2.21, 2.52, 2.38, 2.32 GB 3.11, 1.44, 0.56 s every load
A,B,C,A (SM1-01, SM1-02, SM2-07, SM1-01) OpenGL patch 2.21, 2.52, 2.38, 2.21 GB 2.79, 1.13, 1.38 s second A equals the first
Combat A,B,A (SM1-01, SM2-07, SM1-01) OpenGL master / patch 2.21, 2.47, 2.21 vs 2.21, 2.38, 2.21 GB within 0.1 s revisit re-uploads 131 vs 123 textures (6 MB more)
Event Horizon LW_01, LW_02, SC_01, SC_02 OpenGL master 7.46, 7.99, killed at mission 3 4.2 s, then OOM never
same OpenGL patch 7.39, 7.90, 7.38, 7.89 GB 16.8 (cold disk cache), 2.9, 5.4 s LW set freed at SC_01
Event Horizon A,B,C,A OpenGL patch 7.39, 7.41, 7.89, 7.39 GB 2.7, 5.0, 2.8 s reload to the first visit's size
Screenshots, 4 missions + main hall after each OpenGL master vs patch main hall pixel-identical; missions differ 0.5 to 76% of pixels, the same as master against itself (0.4 to 71%; SM2-07 and SM2-08 are nebula missions)
FS2 four missions, two runs each Vulkan, RTX master / patch VRAM 2.76-3.02, 3.07-3.58, 3.52, 3.33-3.59 vs 2.76, 3.58, 3.52, 3.33 GiB within 0.1 s unchanged code path; the nvidia-smi sample scatters 0.5 GiB between stock runs
$Environment Map: cubemap mission, SM1-02, same mission OpenGL master / patch irradiance pass binds texture 0 then 521 on master (first visit broken); 518 then 321 on the patch

bm_page_in_stop() pages out a bitmap the new mission does not preload with
bm_unload_fast(), which frees only the system-RAM copy.  The OpenGL texture
object stayed resident until exit: gr_opengl_bm_free_data() frees it only on
release, and the per-mission opengl_tcache_flush() has been commented out since
2007.  Only model textures ever left, through bm_release().  A mod with large
per-setting effect sets accumulates every explosion, impact and particle
animation it has ever shown, exhausts VRAM after a few missions, and the driver
spills into system RAM.

Track per bitmap whether it was drawn or locked since the last page-in, and how
many consecutive page-ins it has gone without being preloaded, drawn or locked.
At the tabled count (default 1) the GPU copy is freed, whole animations at a
time so a shared texture array is never left half alive.  Textures the next
mission preloads keep their GPU copy, so shared content is not re-uploaded;
anything drawn last mission survives one more; mission restarts keep
everything.  The marks are cleared in a pass after the page-in loop, because
preloading a later frame binds it and would re-mark a first frame the loop had
already visited.  Eviction runs only on OpenGL: Vulkan already drops every
texture at page-in start, and its free deletes the slot's backend object.

game_settings.tbl: $Evict Unused GPU Textures After Missions: N (OpenGL only;
0 or less restores the old keep-until-exit behavior; values above 255 clamp).
Debug builds log each evicted animation and the per-mission count.

The new test suite drives bm_page_in_start/stop with the stub renderer and a
recording free hook: the table knob, N=0/1/2 semantics, the used mark, preload,
locks held across a page-in, whole-animation frees, user and volume bitmaps,
non-array animations, the renderer gate, and the stale-mark case.

Fixes scp-fs2open#7873
@qazwsxal

Copy link
Copy Markdown
Contributor

If the issue with copying the current vulkan approach is the mid-mission re-upload, why can't that be fixed? Is it even an issue? Is it even particularly slow to load from RAM to gpu memory on mission load? It certainly seems a lot simpler and intuitive than adding a new table value to adjust how long a texture is kept for, and having that applied to one backend only.

@dustinspace217

Copy link
Copy Markdown
Contributor Author

Fair question. I went back and measured the hitch. It was all of 12ms from a couple explosions. Converting to the simpler approach: full eviction of non-preloaded non-model textures.

Replaces the per-bitmap use mark, the unused-mission counter and the
$Evict Unused GPU Textures After Missions: table key with the design
qazwsxal asked about on scp-fs2open#7878: at bm_page_in_stop every bitmap the new
mission does not preload loses its GPU copy along with its RAM copy, the
way the Vulkan backend already flushes at page-in. Textures a mission
draws lazily are re-uploaded on first use; measured on an RTX 5080 that
upload stays inside normal frame time (12.6 vs 12.4 ms worst frame in
the fight window of a revisited mission).

Whole animations are freed or kept together, locked bitmaps and user or
volume bitmaps are skipped, and Vulkan is untouched.

The irradiance pass and the envmap dump read the environment cubemap's
texture id straight from its slot; they now bind it through the texture
cache first, which recreates a freed file cubemap (and uploads a fresh
one, which also fixes the first mission that uses a file cubemap, where
the pass used to bind texture 0).
@dustinspace217

Copy link
Copy Markdown
Contributor Author

The streaming-animation case from the review (an EFF stream that resets to its first frame after a texture free) is handled in #7886, which gives EFF streams their own buffer and user bitmap like the ANI and APNG streams. This PR stays as the eviction alone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenGL never evicts non-model textures between missions

2 participants