Negative result, written down so nobody spends the day I spent on it.
After #583 fixed the decode choice, the macOS export profile was two waits and almost nothing else:
enc.send_frame 7.658 s 40.6 % CPU waiting on VideoToolbox
gpu.wait 7.501 s 39.8 % CPU waiting on Metal
Two different engines, one serial loop. The obvious move — and the one that was worth −30 % on the Linux path — is to put the encoder on its own thread so those two waits overlap. It was implemented (bounded queue, backpressure, error propagation, av_frame_free on the consumer side) and measured.
It changes nothing. Three cycles, one ffmpeg floor per cycle, closing drift 1.0003, machine 84–85 % idle, Mac mini M1:
|
median |
MAD |
cost |
| serial |
23 039 ms |
1 ms |
1.308× floor |
| encode thread |
22 995 ms |
2 ms |
1.305× floor |
+0.2 %. Output byte-identical either way.
Why
A probe on the producer's blocking (enc.queue_wait) answers it exactly:
|
serial |
threaded |
enc.send_frame (consumer side) |
7.658 s |
17.258 s |
enc.queue_wait (producer side) |
— |
7.631 s |
gpu.wait |
7.501 s |
7.498 s |
| walk wall clock |
18.847 s |
18.823 s |
The thread moved the wait, it did not remove it. Blocking on a full queue (7.631 s) equals the old blocking inside avcodec_send_frame (7.658 s) to within 0.4 %.
The export is bound by VideoToolbox's encode throughput, not by CPU serialisation. The main thread's own work is 9.64 s across 3600 frames (2.68 ms/frame — it could feed 373 fps); the encoder absorbs about 191. No scheduling changes that.
The ceiling, measured separately
Feeding h264_videotoolbox from system memory on the same machine and content, 3600 frames of 1080p60 at 8 Mbps:
decode only 1 864 ms 1931 fps
encode only 15 763 ms 228 fps <- the ceiling
export walk 18 823 ms 191 fps
So there is still ~19 % of headroom in the walk, but the walk can never go below ~15.8 s on this hardware.
What is NOT established: the 228 fps ceiling was measured feeding frames from system memory, while the export feeds VideoToolbox frames from a hw_frames_ctx pool that Metal has just written. Whether the remaining 3 s is the zero-copy feed path costing encoder throughput, or simply an unsaturated pipeline at queue depth 4, was not separated. That is the next thing worth measuring, and it is the only reason this issue is not simply "closed, at the hardware ceiling".
Recommendation
Do not ship the thread as it stands — it adds concurrency, a queue, a second error path and a Drop that must join, for a measured 0.2 %.
It could pay on hardware where the encoder is not the constraint: an M-series Pro/Max/Ultra has more encode blocks, and there the producer would become the limit. That is a hypothesis, not a measurement, and shipping unmeasured concurrency on the strength of it is exactly the mistake #583 documents.
Negative result, written down so nobody spends the day I spent on it.
After #583 fixed the decode choice, the macOS export profile was two waits and almost nothing else:
Two different engines, one serial loop. The obvious move — and the one that was worth −30 % on the Linux path — is to put the encoder on its own thread so those two waits overlap. It was implemented (bounded queue, backpressure, error propagation,
av_frame_freeon the consumer side) and measured.It changes nothing. Three cycles, one ffmpeg floor per cycle, closing drift 1.0003, machine 84–85 % idle, Mac mini M1:
+0.2 %. Output byte-identical either way.
Why
A probe on the producer's blocking (
enc.queue_wait) answers it exactly:enc.send_frame(consumer side)enc.queue_wait(producer side)gpu.waitThe thread moved the wait, it did not remove it. Blocking on a full queue (7.631 s) equals the old blocking inside
avcodec_send_frame(7.658 s) to within 0.4 %.The export is bound by VideoToolbox's encode throughput, not by CPU serialisation. The main thread's own work is 9.64 s across 3600 frames (2.68 ms/frame — it could feed 373 fps); the encoder absorbs about 191. No scheduling changes that.
The ceiling, measured separately
Feeding
h264_videotoolboxfrom system memory on the same machine and content, 3600 frames of 1080p60 at 8 Mbps:So there is still ~19 % of headroom in the walk, but the walk can never go below ~15.8 s on this hardware.
What is NOT established: the 228 fps ceiling was measured feeding frames from system memory, while the export feeds VideoToolbox frames from a
hw_frames_ctxpool that Metal has just written. Whether the remaining 3 s is the zero-copy feed path costing encoder throughput, or simply an unsaturated pipeline at queue depth 4, was not separated. That is the next thing worth measuring, and it is the only reason this issue is not simply "closed, at the hardware ceiling".Recommendation
Do not ship the thread as it stands — it adds concurrency, a queue, a second error path and a
Dropthat must join, for a measured 0.2 %.It could pay on hardware where the encoder is not the constraint: an M-series Pro/Max/Ultra has more encode blocks, and there the producer would become the limit. That is a hypothesis, not a measurement, and shipping unmeasured concurrency on the strength of it is exactly the mistake #583 documents.