Repository navigation
Give EFF streaming animations their own user bitmap - #7886
Open
dustinspace217 wants to merge 1 commit into
Open
dustinspace217 wants to merge 1 commit into
dustinspace217 wants to merge 1 commit into
Conversation
The EFF stream path drew from the bitmap of its first frame file and wrote later frames straight into that bitmap's GPU texture, so anything that drops the GPU copy (the Vulkan backend's flush at mission load, a texture detail change, the OpenGL eviction proposed in scp-fs2open#7878) left the animation showing frame zero until it next advanced, and a paused stream stayed on the wrong frame. ANI and APNG streams already draw from a user bitmap over a buffer they own, which the renderer rebuilds from the current pixels and never culls by texture detail. EFF streams now do the same: the frame files are read into the stream's buffer and uploaded from there. The copy converts what the loader's depth needs: a 24 bpp PNG frame gains an opaque alpha for a 32 bpp stream, and color folds to luminance for an 8 bpp HUD-color stream, as the OpenGL update path did before. Frames the loader hands back compressed, at another size, or at a depth with no conversion are skipped with a log line. A stream restarted on the same generic_anim (talking heads replay without unloading) keeps its buffer and bitmap when they still fit, and a frame zero that fails to lock fails the stream cleanly.
This was referenced Oct 17, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What?
EFF streaming animations now draw from a user bitmap over a buffer the stream owns, the way ANI and APNG streams already do, instead of from the bitmap of their first frame file. Adds the first unit tests for the streaming code.
Why?
A streamed EFF kept its current frame only in the GPU texture: the frame files were read one at a time and written straight into the texture of the frame-zero bitmap. Anything that drops that texture makes the next draw rebuild it from the frame-zero file. In Vulkan that happens at every mission load, so a streaming animation a mod script keeps across missions shows its first frame afterwards, and if it is paused it stays there. I hit this while reviewing #7878, which will do the same texture free in OpenGL. It also covers the 24-bit frame case #7882 addresses with a one-line change, since no raw frame upload remains. Reproduced on master with Vulkan: an animation paused on frame 4 before a mission load drew frame 0 after it, while its frame index still read 4.
How?
When an EFF stream starts, the stream allocates a buffer the size of one frame, copies the first frame into it, releases the frame file, and creates its bitmap over that buffer with
bm_create. Each time the animation advances, the new frame is copied into the buffer and uploaded from there, so a rebuilt texture always shows the current frame. The buffer is freed when the stream is unloaded. User bitmaps are also exempt from texture detail culling, which the old file bitmap was not, so a stream at a reduced detail setting no longer uploads full-size frames into a smaller texture.The copy converts what the loader's depth needs: the PNG reader returns a frame at its file depth whatever was asked for, so a 24-bit frame gains an opaque alpha for a 32-bit stream, and color folds to luminance for an 8-bit HUD-color stream, the same math the OpenGL update path used before. Frames the loader hands back compressed (DXT on hardware that supports it), at another size, or at a depth with no conversion are skipped with a debug log line; before this change a compressed frame was uploaded as if it were a full uncompressed one. The size check only bounds this code's own copy: a later frame that is bigger than the first still overflows inside the loader before the stream sees it, because bm_reload keeps the slot's first-file size. That is #7883, and the bmpman guard in #7885 handles it; the two changes are meant to land together. A stream restarted on the same animation (talking heads replay without unloading) keeps its buffer and bitmap when they still fit, and a first frame that fails to load fails the stream cleanly, leaving the animation in a state unload accepts. Frame zero and the next-frame slot are both loaded as private bitmap slots: the plain load finds slots by name, and a stream renames its next-frame slot to the frame it holds, so two streams of the same file used to share a slot and lose it when one of them unloaded.
One behavior change worth knowing: an EFF whose frames are DXT compressed on hardware with S3TC now streams blank, with a line in the log. Before, the first frame showed and the animation froze on it, because the uncompressed update onto a compressed texture failed. I found no shipped mod that streams a DDS EFF (every streamed EFF in my library is PNG), so I left decoding for a follow-up if content turns up.
Testing?
Twenty-four unit tests, green in Release and Debug on Linux. They cover the user bitmap and its buffer through the first frame, an advance and unload; compressed, decoded, 24-bit, HUD-color, smaller-than-the-first and corrupt frames; restarts that reuse or replace the pair and keep the next-frame load count flat; a failed stream that still unloads and releases nothing it did not load; two streams of the same file keeping their own slots through each other's start and unload; a one-frame EFF; and a failed lookup that keeps the ownership record.
Headless harness cell on an RTX 5080 with MediaVPs 5.0.2: a Lua
On Framehook draws an eight-frame PNG EFF, paused on frame 4 at mission time 6, through SM1-01 into SM1-02. Vulkan master: frame 0 after the load. Vulkan with this change: frame 4. OpenGL: unchanged on both. A second harness session ran its #7883 repro mods against this build on retail data: 24-bit DDS frames now draw on Vulkan, a grayscale HUD-color EFF on Vulkan draws as an image instead of a block, and a first frame bigger than the rest draws frame 0 cleanly; the bigger-later-frame case still aborts inside the loader, as described above.The test mod is eight 128 by 128 PNG frames of solid color with the frame number drawn on them, a three-line
streamdemo.eff($Type: PNG,$Frames: 8,$FPS: 2), and this table:streamdemo-sct.tbm
Anything Else?
One frame of RAM per live EFF stream and one memcpy per frame advance, the same cost the ANI and APNG streams already pay. Written with Claude Code; the review and testing ran on my machine.