Skip to content

Give EFF streaming animations their own user bitmap - #7886

Open
dustinspace217 wants to merge 1 commit into
scp-fs2open:masterfrom
dustinspace217:graphics/eff-stream-user-bitmap
Open

dustinspace217 wants to merge 1 commit into
scp-fs2open:masterfrom
dustinspace217:graphics/eff-stream-user-bitmap

Conversation

@dustinspace217

Copy link
Copy Markdown
Contributor

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 Frame hook 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
#Conditional Hooks
$Application: FS2_Open
$On Frame: [
if streamdemo_anim == nil then
	streamdemo_anim = gr.loadStreamingAnim("streamdemo", true)
end
if streamdemo_anim ~= nil and streamdemo_anim:isValid() then
	streamdemo_anim:process(40, 40, 296, 296)
end
]
#End

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.

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.
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.

1 participant