Repository navigation
perf(ffmpeg): reference engine frames instead of copying them - #1827
patrickhampson wants to merge 3 commits into
Conversation
make_av_video_frame copied every frame the ffmpeg consumer received into a new AVFrame. Wrap the engine's image in read-only AVBuffers that hold a reference to the const_frame instead. A filter that writes in place gets its own copy through av_frame_make_writable, and an image that does not meet FFmpeg's alignment is still copied as before.
dedicatedbroadcastsolutions
left a comment
There was a problem hiding this comment.
Reviewed the zero-copy change against the frame ownership and GPU readback allocation paths at this commit. One source-verified padding concern is noted inline. This is a documented buffer-contract concern, not a reproduced crash; I have not run the standalone harness or verified Windows behavior.
The ffmpeg consumer now passes the mixer's frames to FFmpeg without copying them, and some FFmpeg filters and swscale read up to 16 bytes past the end of a frame. Readback buffers and the empty frame were allocated at exactly the frame size, so a 1080p BGRA frame ended on a page boundary. Allocate both with 64 bytes of padding. The array size is unchanged, so consumers that copy image_data().size() bytes are unaffected. The Vulkan host buffer pool now returns buffers under the requested size. buf->size() is the allocation size, which can be rounded up, so a padded readback would otherwise go back to a pool that is never drawn from.
|
Added padding the backing allocations, as you suggested:
Testing Before (first commit):
After (your suggestion):
|
dedicatedbroadcastsolutions
left a comment
There was a problem hiding this comment.
@/tmp/review_body.txt
|
I had to delete the inline review as it would not allow me to resolve it. Note: This is an AI review so please treat accordingly. Here is some feedback I was able to get from AI that should be considered. The whole test harness thing needs defined and there is a thread on the forum about that. "The implementation of the invariant is fragile, and I undersold that in my first pass: It's an invisible, distributed contract. The safety of the zero-copy branch in av_util.cpp depends on five separate allocation sites in four unrelated files (image_mixer.cpp ×2, device.cpp, device.cpp/transfer.cpp) all remembering to add exactly the same magic 64. The only thing enforcing this is a one-line comment at each site. Nothing checks it — not a static_assert, not a runtime assertion, not a type. If someone adds a third accelerator backend, changes the GL/Vulkan readback path, or "cleans up" one of those + 64s without reading the comment, the zero-copy path in av_util.cpp silently becomes an out-of-bounds read again — with no compile error and no obvious crash until a specific filter/alignment/allocator-reuse combination triggers it. These sound like mainly maintainability improvements that are probably wise to think ahead about. |
The zero-copy path in make_av_video_frame relies on every readback buffer and the empty frame being allocated with padding past the end of the image. Each site used a bare 64, so nothing tied them together. Define core::frame_padding next to const_frame and use it at each site.
Moved padding to a constant that should hopefully make it less distributed.
I agree that the function can’t check the guarantee itself, since it only sees size(). Fixing that properly means adding a capacity to caspar::array in common/array.h and filling it in for every storage type it wraps. That change reaches well beyond this PR, while every frame a consumer receives today comes from the mixer’s readback or its empty frame, and both are padded. I’d rather not widen this PR for it. |
make_av_video_framecopied every frame the FFmpeg consumer received into a newly allocatedAVFrame(it carried aTODO (perf) Avoid extra memcpy). On an interlaced channel the consumer receives two full frames per tick, so a consumer withfps=25on a 1080i50 channel copied 50 frames a second and the filter then discarded half of them.This change wraps the frame's image data in read-only
AVBuffers that hold a reference to theconst_frame, so the frame stays alive until FFmpeg releases it. Because the buffers are read-only, a filter that writes in place gets its own copy throughav_frame_make_writable, and the engine's frame is never modified. Image data that does not meet FFmpeg's alignment (32 bytes, or 64 for 16-bit) is still copied as before.The FFmpeg consumer is the only caller of
make_av_video_frame.Measurements
Three FFmpeg consumers on 1080i50 channels, each
-filter:v fps=25,format=bgra -codec:v h264_nvenc, with nothing playing. CPU of the server process fromtop(100% = one core).Before,
perfshowed about one core in libc, almost all of itmemcpy. After, libc is about 0.36 cores, which matches NVENC copying the 25 frames per second thatfps=keeps into its own input buffers.perfshows no time in the copy fallback.Testing
v2.5.1-stablewith this commit cherry-picked (it applies cleanly).const_frames and ran them throughmake_av_video_frame, five filter chains (null,fps=25,drawboxwhich writes in place,fps=25,format=yuv420p,scale) and an FFV1 encoder. It compared the zero-copy path against the copy fallback, built against FFmpeg 6.1:Summary by CodeRabbit
Performance
Reliability