Windows hardware encoding: encoder selection and the input path

Chapter 2: Windows·about 1241 words

The encoder layer: one candidate chain, hardware just comes first

Encoders and decoders use the same mechanism: one registry, each platform hangs its implementations in, and each entry carries a hardware flag. Selection starts with a preferred name, the rest are appended in registration order, and only when the first frame cannot be opened does it move to the next candidate.

That mechanism was covered in the decoding article; the encoding side is built the same way, so I will not repeat it.

With the hardware-encode switch on, the preferred names are the two built in-house for this platform — ff_h264_dx11 and ff_hevc_dx11. But there is no implementation under either name yet: the D3D11 path is not wired to encoding in these two classes, and the table cannot resolve the preferred name.

When the preferred name cannot be resolved, the candidate chain falls to the rest of the registry. That is the chain's built-in fallback: if the preferred one cannot be opened, walk down in order.

Who actually gets opened in this version

How the candidate chain is ordered, and the fork when opening an encoder

Besides the two built in-house, the registry also holds the encoders FFmpeg registers in bulk. After the name is resolved at open, the Windows side has one more fork: if the input is NV12, it picks by whether a hardware device context can be created — trying QSV, AMF and NVENC in turn, using the encoder name of whichever vendor can create a device; only when none of the three can does it fall back to the name injected by the build channel.

In the Windows package the injected name is the two MF ones, i.e. the system's Media Foundation transforms. So turning hardware encoding on in this version opens either one of the three vendor hardware encoders or the MF wrapper — both are inside the FFmpeg branch.

This fork does not consult a hardware capability table; it simply tries to create the device, and if it can be created, hardware counts as available. The case where a device can be created but the encoder cannot be used is handled by moving down the candidates.

You can follow this step in the log: the preferred name is looked up in the table first, and a miss is a warning; then it takes by registration order, and the name that actually opens appears in the log. So there is no need to guess “who is running in this version” — the first line of the log tells you.

What gets configured at open, and each encoder's own vocabulary

Whichever branch the encoder comes from, the common settings at open are the same:

Pixel format follows the description from above; the color group has three things that must be supplied together — color range, color space, and primaries plus transfer characteristics. The consequence of omitting them is concrete: this information is unspecified in the stream the encoder writes, the player interprets it as limited range by default, and high-saturation pictures get clipped and the colors shift.

The time base is milliseconds, matching how timestamps are taken along the whole chain; the keyframe interval is converted to frames as a multiple, i.e. the interval times the frame rate; B-frames are off; the bitrate is computed as resolution times frame rate times a factor; thread count and thread type are supplied too, with both frame-level and slice-level enabled.

Below that it depends on the specific encoder; they tune with different words: the x264 family uses a quality factor plus the fastest preset and zeroes the average bitrate; QSV uses a faster preset and keeps the bitrate; NVENC uses the fastest preset plus a low-latency tuning option; AMF prioritizes speed. The two MF ones do not get a preset at all — their preset is a numeric enum, and passing a string makes open fail outright.

This dispatch recognizes by the markers in the name. So when adding an encoder, the parameter-setting section needs a matching branch, or it will use the previous vendor's words.

Input form: direct texture feed is design only, memory upload is what runs

After the encoder is chosen there is still the question of input form. The direct-texture path is not finished yet; what runs now is memory upload.

The input is CPU planar frames: of the three overloads, what finally reaches the encoder is the Y, U and V plane pointers in memory, and the encoder uploads the data itself. In other words, a frame coming out of rendering or decoding must first be read back into memory before it goes to the encoder — the encoder itself is hardware, but this path has no zero-copy.

Two input paths — direct texture feed and memory upload; the latter is what runs

The direct-texture path corresponds to the in-house implementation: it takes GPU frames, first converts the RGBA texture into an NV12 texture with a compute shader, then puts that texture into the data structure handed to the encoder; the device is created by borrowing FFmpeg's D3D11 hardware-decode device context, the same device-borrowing as the decoding implementation. This path is not finished yet.

Why this path is not easy: the D3D11 hardware-decode device context belongs to the decoding side; for encoding to use the same device, it has to go through the Media Foundation device manager, or the encoder has to expose an input surface that accepts textures. Having the device created does not mean the encoder will take textures — that step is the real joint of this path.

The infrastructure shared with the decoding side

To hand frames straight to rendering, the decoding side has already built a set of infrastructure: borrow FFmpeg to create a D3D11 device, hand it to the rendering side to reuse the same one, then hand out the RGBA shared texture converted from the decoder output together with a shared fence.

The direct-texture encode path needs the same things: the context and buffer in the frame object were left for exactly this — the context's get-texture interface gives the texture, and on a hardware-decoded frame the buffer field holds the platform texture pointer. The shell implementation on the encoding side takes its texture from here. So this infrastructure is connected between decoding and rendering, and is not yet connected on the encoding side.

How the same table looks on other platforms

Put the three platforms side by side and the table is used the same way; the difference is who actually filled in an implementation.

On Linux the equivalent branch is connected: it implements the hardware-acceleration interface, registers into the table, and is the preferred name at selection, walking the same candidate chain. The two Vulkan names are not done either; they are placeholders for the “encode inside Vulkan” route.

So this is not a missing piece of mechanism but an unimplemented branch: the table, the candidate chain, the switch before the first frame and the failure handling are all in place; what is missing is the last step inside those two classes — handing the frame to the encoder as a texture. It is the same kind of situation as the two in-house decoder implementations set aside in the hardware-decode article: the mechanism is the same one, the implementation is unfinished.

On Windows, “hardware encoding” in this project currently means the encoder is hardware while the input is still memory; the direct-texture path is unfinished for lack of time.

The avox engine is open source on GitHub: github.com/xxxzhou/avox; the encoder registry and selection, and this shared D3D11 device, are all in there.