FFmpeg ARM64 Windows: Qualcomm H.264 hardware encode, native ARM64 vs x64 emulation
In this article
On August 10, 2026, I measured FFmpeg ARM64 Windows H.264 hardware encoding on a Surface Pro 11. A 60-second 1080p input took a median of 7.262 seconds with the native ARM64 executable and 9.855 seconds with the x64 executable. The x64 elapsed time was 1.357x as long.
That speed number is not the whole result. Adding -hw_encoding 1 to h264_mf still failed before encoding a frame when I kept yuv420p as the output pixel format. Both executables worked after I changed it to nv12, and the verbose log then identified QCOM Hardware Encoder - H264. At approximately the same 45MB output size, however, the hardware result had lower PSNR and SSIM than libx264.
My earlier three-job FFmpeg x64 versus ARM64 comparison used a 20-second synthetic input and only three measured runs. That June 30, 2026 test also did not use perfectly matched builds. I did not want to rewrite that article around a new headline. This test instead uses two artifacts from the same FFmpeg commit, a 60-second input, six measured rounds, an explicit hardware requirement, and validation of every output.
I downloaded two builds from the same BtbN snapshot
The binaries came from the GPL archives published by BtbN/FFmpeg-Builds. The script downloads these two URLs:
Both reported the complete version ffmpeg version N-125998-g2a20737f66-20260809. The x64 archive was 170324423 bytes with SHA-256 7d9756960483ae7e9688f8f14f8779a4e4319f91b4c975aff56c50612b6eee2d. The ARM64 archive was 116165718 bytes with SHA-256 2502849debdae3bac3abc415ae55d5e9aeb531daaf3a529857c8cc1f8baca0b3.
The latest URLs will point to different snapshots in the future. The script refuses to compare two downloads if their version tokens differ, but reproducing this exact run requires retaining archives with the hashes above.
I did not infer architecture from a filename. The script reads the PE header of each ffmpeg.exe: the x64 build has Machine 0x8664, while the ARM64 build has 0xAA64. I used the same check in my earlier census of 20 Windows ARM64 command-line tools, where installation paths turned out to be poor architecture labels.
The two executables share an FFmpeg commit, but they are not compiler-identical. The x64 build used GCC 15.2.0 and the ARM64 build used Clang 23.1.0-rc2. libx264 also has architecture-specific assembly. I therefore cannot label the entire difference as a pure emulation tax.
Machine, input, and aggregation
The machine was a Microsoft Surface Pro, 11th Edition with a Snapdragon X Elite X1E80100, 12 cores, and 33893933056 bytes of memory. It ran Windows 11 Pro 10.0.26200 ARM64, and the benchmark process was Python 3.12.10 ARM64. The machine was connected to AC power. My environment probe returned null for the active power-plan name, so I cannot claim that setting was captured.
The common input was a deterministic testsrc2 clip generated by the ARM64 build. It is synthetic rather than camera footage: 1920x1080, 30fps, 60 seconds, no audio, encoded once with libx264 medium at CRF 12.
ffmpeg.exe -f lavfi -i "testsrc2=size=1920x1080:rate=30" -t 60 `
-c:v libx264 -preset medium -crf 12 -pix_fmt yuv420p `
-movflags +faststart input-1080p-60s.mp4
The resulting input contained 1800 frames, occupied 108813804 bytes, and averaged 14508507bit/s. Its SHA-256 was 0920d1089146b3b5cf5041bd55789800ecc819cb445c9669d4c016129331b8d6. I used ffprobe -count_frames to decode and count all 1800 frames before benchmarking.
For each case, the script discards one warm-up and measures six rounds. It balances x64-first and ARM64-first ordering. Wall time comes from Python's perf_counter; process CPU time comes from Windows GetProcessTimes. The report gives minimum, median, and maximum values. CPU seconds are the sum of user and kernel time across threads, not a CPU utilization percentage.
This command reproduces the download, PE inspection, input generation, expected failure, timed encoding, ffprobe validation, PSNR, and SSIM:
python scripts\ffmpeg_arm64_windows_h264.py `
--duration 60 --rounds 6 `
--json .bench\ffmpeg-arm64-windows-h264.json
h264_mf alone did not prove hardware use
My first exploratory run used only -c:v h264_mf. Its log selected a generic H264 Encoder MFT. The name did not establish that Qualcomm hardware was doing the encode, so I added -hw_encoding 1.
With that option, both the x64 and ARM64 logs reported MFT name QCOM Hardware Encoder - H264, hardware URL QC H264 Encode HW-MFT, and CLSID {7790ee16-08e3-426d-aada-f96774308ea1}. That is direct evidence that FFmpeg selected the Qualcomm hardware Media Foundation Transform rather than merely exposing the h264_mf encoder name.
I did not capture ETW counters for GPU Engine or Video Engine utilization. The evidence establishes selection of the MFT that Qualcomm registered as hardware; it does not measure physical video-engine activity.
I hit a reproducible failure next. Forced hardware encoding with yuv420p returned format negotiation failed (1/0) and produced a zero-byte file:
ffmpeg.exe -i input-1080p-60s.mp4 -an `
-c:v h264_mf -hw_encoding 1 -b:v 6M -maxrate 6M -bufsize 12M `
-g 60 -pix_fmt yuv420p output.mp4
Changing the requested output format to nv12 made it work:
ffmpeg.exe -i input-1080p-60s.mp4 -an `
-c:v h264_mf -hw_encoding 1 -b:v 6M -maxrate 6M -bufsize 12M `
-g 60 -pix_fmt nv12 output.mp4
The failure occurred with both PE architectures. On this machine, seeing h264_mf in the encoder list does not mean the hardware transform accepts every pixel format that the FFmpeg wrapper advertises.
Six measured rounds
| Path | PE | wall seconds min / median / max | median CPU seconds | median output size |
|---|---|---|---|---|
| libx264 | x64 0x8664 |
27.364 / 30.863 / 44.146 | 162.695 | 45297754.5 bytes |
| libx264 | ARM64 0xAA64 |
11.326 / 15.036 / 23.177 | 89.906 | 45304151 bytes |
| QCOM H.264 Hardware MFT | x64 0x8664 |
9.600 / 9.855 / 11.227 | 25.859 | 45004511 bytes |
| QCOM H.264 Hardware MFT | ARM64 0xAA64 |
6.335 / 7.262 / 9.323 | 17.805 | 45004511 bytes |
The six libx264 wall times were 30.917 / 27.364 / 31.807 / 30.336 / 30.808 / 44.146 seconds for x64 and 12.863 / 14.013 / 16.863 / 11.326 / 16.059 / 23.177 seconds for ARM64. The x64 median was 2.053x as long. Both architectures also had their slowest result in round six. I did not isolate whether that came from temperature or unrelated background load, so the spread remains part of the result.
The six hardware times were 9.938 / 9.740 / 10.142 / 9.772 / 9.600 / 11.227 seconds for x64 and 6.335 / 7.288 / 9.323 / 7.186 / 7.236 / 8.556 seconds for ARM64. Dividing the 9.855-second x64 median by the 7.262-second ARM64 median gives 1.357. Median process CPU time was also lower for ARM64: 17.805 seconds rather than 25.859 seconds.
The narrow conclusion is that the ARM64 executable was faster for these two BtbN builds and this end-to-end command. I did not time only the hardware encoder core. The measurement includes H.264 input decoding, pixel-format conversion, frame transfer, muxing, and process overhead. I did not force a hardware decoder.
I validated frames, hashes, size, PSNR, and SSIM
Every result in the timing table was H.264 at 1920x1080, exactly 60.000 seconds long, and decodable as 1800 frames. Failed or truncated outputs do not enter the speed comparison.
All 12 hardware outputs were exactly 45004511 bytes. They also shared SHA-256 bb861b146439ac576195e4c76910cfdfe7b53d721759d6f3e3431ac23b1ba4f8 across both architectures and all six rounds. In other words, x64 and ARM64 fed the same Qualcomm transform and produced identical bytes. The representative output measured 42.2793dB average PSNR and 0.99321 SSIM All against the common input.
libx264 behaved differently. I chose the run closest to median wall time as the representative. Its x64 SHA-256 was ee822ce6f6276b8ead40fc8d6b07ed94101a8c57d669aae3277b9fc8c75ac230, while ARM64 produced f5daf049014f2b608a51c86ea997f06e4fb71d1164cd1af7ce129abcf33c2af1. Every one of the six hashes also differed within each architecture, so libx264 output was not bit-stable in this measurement. I did not isolate the cause. The sizes, durations, and 1800 decoded frames still matched closely, and representative quality was close: 44.19154dB / 0.99505 for x64 and 44.19955dB / 0.99506 for ARM64. PSNR and SSIM are from one representative output each, not a six-run distribution.
At a similar 45MB size, libx264 reached roughly 44.19dB while the hardware transform reached 42.2793dB. Native hardware encoding was about 2.07x faster than native libx264 in median wall time, but it did not preserve the same quality score. I do not treat the cross-encoder speed ranking as a like-for-like quality comparison.
What I would use
For a time-sensitive H.264 conversion on this Windows ARM64 machine, I would use the BtbN ARM64 build with h264_mf -hw_encoding 1 -pix_fmt nv12. It completed this 60-second input in a median of 7.262 seconds, and its output was bit-identical to the x64 path.
For a quality-sensitive conversion, I would keep libx264. The ARM64 build reduced its median to 15.036 seconds and the representative PSNR remained 44.19955dB. I have not tested camera footage, 10-bit input, HDR, 4K, HEVC, long-duration thermal saturation, battery draw, or the Qualcomm media-driver version. These results do not establish the same speed or quality for those cases.
The surprising part was that these two executables still differed by 1.357x on this end-to-end command after selecting the hardware MFT. The practical trap was more important: keeping yuv420p yielded no frames at all. Installing FFmpeg ARM64 on Windows ARM64 helped under these conditions, but explicitly selecting the hardware MFT and giving it nv12 was what made the Qualcomm path real and reproducible.