AMF + NVENC control and support

This commit is contained in:
KingKDot
2026-07-11 14:55:26 -05:00
parent 62cd920936
commit 424ed348e3
31 changed files with 761 additions and 35 deletions
@@ -0,0 +1,7 @@
//go:build windows && (!cgo || no_amf)
package capture
func newAMFD3D11TextureBackend() h264D3D11TextureBackend { return nil }
func probeAMFD3D11() (bool, string) { return false, "AMD AMF requires a Windows CGO build" }
@@ -0,0 +1,86 @@
//go:build windows && cgo && !no_amf
package capture
import (
"fmt"
"overlord-client/cmd/agent/capture/amfbridge"
"sync/atomic"
"unsafe"
)
type amfD3D11TextureBackend struct {
encoder *amfbridge.Encoder
device unsafe.Pointer
inputWidth int
inputHeight int
encodeWidth int
encodeHeight int
fps int
dxgiFormat uint32
output []byte
forceIDR atomic.Bool
}
func newAMFD3D11TextureBackend() h264D3D11TextureBackend {
return &amfD3D11TextureBackend{output: make([]byte, 1024*1024)}
}
func (b *amfD3D11TextureBackend) Name() string { return "AMD AMF" }
func (b *amfD3D11TextureBackend) matches(req h264D3D11TextureRequest) bool {
return b.encoder != nil && b.device == req.Device && b.inputWidth == req.InputWidth &&
b.inputHeight == req.InputHeight && b.encodeWidth == req.EncodeWidth &&
b.encodeHeight == req.EncodeHeight && b.fps == req.FPS && b.dxgiFormat == req.DXGIFormat
}
func (b *amfD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) {
if !b.matches(req) {
b.Reset()
encoder, err := amfbridge.Create(req.Device, req.InputWidth, req.InputHeight, req.EncodeWidth,
req.EncodeHeight, req.FPS, req.DXGIFormat, targetH264Bitrate(req.EncodeWidth, req.EncodeHeight, req.FPS))
if err != nil {
return nil, err
}
b.encoder = encoder
b.device, b.inputWidth, b.inputHeight = req.Device, req.InputWidth, req.InputHeight
b.encodeWidth, b.encodeHeight, b.fps, b.dxgiFormat = req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat
}
forceIDR := req.ForceIDR || b.forceIDR.Swap(false)
for attempts := 0; attempts < 2; attempts++ {
result, outputSize, err := b.encoder.Encode(req.Texture, forceIDR, b.output)
if err != nil {
return nil, err
}
if result == 2 {
if outputSize <= 0 || outputSize > 64*1024*1024 {
return nil, fmt.Errorf("AMF returned invalid output size %d", int(outputSize))
}
b.output = make([]byte, outputSize)
forceIDR = false
continue
}
if outputSize <= 0 {
return nil, nil
}
out := make([]byte, outputSize)
copy(out, b.output[:outputSize])
return out, nil
}
return nil, fmt.Errorf("AMF output buffer resize retry failed")
}
func (b *amfD3D11TextureBackend) RequestKeyframe() { b.forceIDR.Store(true) }
func (b *amfD3D11TextureBackend) Reset() {
if b.encoder != nil {
b.encoder.Close()
}
b.encoder = nil
b.device = nil
b.forceIDR.Store(false)
}
func probeAMFD3D11() (bool, string) {
return amfbridge.Probe()
}
@@ -0,0 +1,218 @@
//go:build windows && cgo
#include "bridge_windows.h"
#include <windows.h>
#include <algorithm>
#include <chrono>
#include <cstring>
#include <string>
#include <thread>
#include <vector>
#include "../../../../third_party/amf/include/core/Factory.h"
#include "../../../../third_party/amf/include/core/Buffer.h"
#include "../../../../third_party/amf/include/components/VideoConverter.h"
#include "../../../../third_party/amf/include/components/VideoEncoderVCE.h"
namespace {
struct AMFEncoder {
HMODULE runtime = nullptr;
amf::AMFFactory* factory = nullptr;
amf::AMFContext* context = nullptr;
amf::AMFComponent* converter = nullptr;
amf::AMFComponent* encoder = nullptr;
std::vector<uint8_t> pending;
};
void set_error(char* dst, int capacity, const std::string& value) {
if (!dst || capacity <= 0) return;
const size_t count = std::min(value.size(), static_cast<size_t>(capacity - 1));
std::memcpy(dst, value.data(), count);
dst[count] = 0;
}
std::string amf_error(const char* stage, AMF_RESULT result) {
return std::string(stage) + " failed (AMF_RESULT=" + std::to_string(static_cast<int>(result)) + ")";
}
void destroy(AMFEncoder* state) {
if (!state) return;
if (state->encoder) {
state->encoder->Terminate();
state->encoder->Release();
}
if (state->converter) {
state->converter->Terminate();
state->converter->Release();
}
if (state->context) {
state->context->Terminate();
state->context->Release();
}
if (state->runtime) FreeLibrary(state->runtime);
delete state;
}
HMODULE load_runtime() {
return LoadLibraryW(sizeof(void*) == 8 ? L"amfrt64.dll" : L"amfrt32.dll");
}
AMF_RESULT query_with_wait(amf::AMFComponent* component, amf::AMFData** data, int timeout_ms) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms);
AMF_RESULT result = AMF_REPEAT;
do {
result = component->QueryOutput(data);
if (result != AMF_REPEAT && result != AMF_NEED_MORE_INPUT) return result;
if (*data) return AMF_OK;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
} while (std::chrono::steady_clock::now() < deadline);
return result;
}
AMF_RESULT collect_encoder_output(AMFEncoder* state, int timeout_ms) {
amf::AMFData* data = nullptr;
AMF_RESULT result = query_with_wait(state->encoder, &data, timeout_ms);
if (!data) {
return (result == AMF_REPEAT || result == AMF_NEED_MORE_INPUT) ? AMF_OK : result;
}
amf::AMFBufferPtr buffer(data);
data->Release();
if (!buffer) return AMF_INVALID_DATA_TYPE;
const auto* bytes = static_cast<const uint8_t*>(buffer->GetNative());
const size_t size = static_cast<size_t>(buffer->GetSize());
if (bytes && size) state->pending.insert(state->pending.end(), bytes, bytes + size);
return AMF_OK;
}
} // namespace
extern "C" int overlord_amf_probe(char* error_text, int error_capacity) {
HMODULE runtime = load_runtime();
if (!runtime) {
set_error(error_text, error_capacity, "AMF runtime DLL is not installed");
return 0;
}
auto init = reinterpret_cast<AMFInit_Fn>(GetProcAddress(runtime, AMF_INIT_FUNCTION_NAME));
amf::AMFFactory* factory = nullptr;
AMF_RESULT result = init ? init(AMF_FULL_VERSION, &factory) : AMF_FAIL;
FreeLibrary(runtime);
if (result != AMF_OK || !factory) {
set_error(error_text, error_capacity, amf_error("AMFInit", result));
return 0;
}
return 1;
}
extern "C" int overlord_amf_create(void* d3d11_device, int input_width, int input_height,
int encode_width, int encode_height, int fps,
uint32_t dxgi_format, int bitrate,
overlord_amf_encoder* output,
char* error_text, int error_capacity) {
if (!output || !d3d11_device) {
set_error(error_text, error_capacity, "invalid AMF create arguments");
return 0;
}
*output = nullptr;
auto* state = new AMFEncoder();
state->runtime = load_runtime();
if (!state->runtime) {
set_error(error_text, error_capacity, "AMF runtime DLL is not installed");
destroy(state);
return 0;
}
auto init = reinterpret_cast<AMFInit_Fn>(GetProcAddress(state->runtime, AMF_INIT_FUNCTION_NAME));
AMF_RESULT result = init ? init(AMF_FULL_VERSION, &state->factory) : AMF_FAIL;
if (result != AMF_OK || !state->factory) goto fail_init;
result = state->factory->CreateContext(&state->context);
if (result != AMF_OK || !state->context) goto fail_context;
result = state->context->InitDX11(d3d11_device, amf::AMF_DX11_0);
if (result != AMF_OK) goto fail_dx11;
result = state->factory->CreateComponent(state->context, AMFVideoConverter, &state->converter);
if (result != AMF_OK || !state->converter) goto fail_converter;
state->converter->SetProperty(AMF_VIDEO_CONVERTER_OUTPUT_FORMAT, static_cast<amf_int64>(amf::AMF_SURFACE_NV12));
state->converter->SetProperty(AMF_VIDEO_CONVERTER_MEMORY_TYPE, static_cast<amf_int64>(amf::AMF_MEMORY_DX11));
state->converter->SetProperty(AMF_VIDEO_CONVERTER_OUTPUT_SIZE, AMFConstructSize(encode_width, encode_height));
{
const amf::AMF_SURFACE_FORMAT input_format = dxgi_format == 28 ? amf::AMF_SURFACE_RGBA : amf::AMF_SURFACE_BGRA;
result = state->converter->Init(input_format, input_width, input_height);
}
if (result != AMF_OK) goto fail_converter_init;
result = state->factory->CreateComponent(state->context, AMFVideoEncoderVCE_AVC, &state->encoder);
if (result != AMF_OK || !state->encoder) goto fail_encoder;
state->encoder->SetProperty(AMF_VIDEO_ENCODER_USAGE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_PROFILE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_PROFILE_MAIN));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMESIZE, AMFConstructSize(encode_width, encode_height));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_FRAMERATE, AMFConstructRate(fps, 1));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_TARGET_BITRATE, static_cast<amf_int64>(bitrate));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_B_PIC_PATTERN, static_cast<amf_int64>(0));
state->encoder->SetProperty(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, true);
state->encoder->SetProperty(AMF_VIDEO_ENCODER_HEADER_INSERTION_SPACING, static_cast<amf_int64>(60));
result = state->encoder->Init(amf::AMF_SURFACE_NV12, encode_width, encode_height);
if (result != AMF_OK) goto fail_encoder_init;
*output = state;
return 1;
fail_encoder_init: set_error(error_text, error_capacity, amf_error("H.264 encoder Init", result)); destroy(state); return 0;
fail_encoder: set_error(error_text, error_capacity, amf_error("CreateComponent encoder", result)); destroy(state); return 0;
fail_converter_init: set_error(error_text, error_capacity, amf_error("video converter Init", result)); destroy(state); return 0;
fail_converter: set_error(error_text, error_capacity, amf_error("CreateComponent converter", result)); destroy(state); return 0;
fail_dx11: set_error(error_text, error_capacity, amf_error("InitDX11", result)); destroy(state); return 0;
fail_context: set_error(error_text, error_capacity, amf_error("CreateContext", result)); destroy(state); return 0;
fail_init: set_error(error_text, error_capacity, amf_error("AMFInit", result)); destroy(state); return 0;
}
extern "C" int overlord_amf_encode(overlord_amf_encoder opaque, void* d3d11_texture,
int force_idr, uint8_t* output, int output_capacity,
int* output_size, char* error_text, int error_capacity) {
auto* state = static_cast<AMFEncoder*>(opaque);
if (!state || !d3d11_texture || !output_size) return 0;
*output_size = 0;
if (!state->pending.empty()) {
*output_size = static_cast<int>(state->pending.size());
if (output_capacity < *output_size || !output) return 2;
std::memcpy(output, state->pending.data(), state->pending.size());
state->pending.clear();
return 1;
}
amf::AMFSurface* source = nullptr;
AMF_RESULT result = state->context->CreateSurfaceFromDX11Native(d3d11_texture, &source, nullptr);
if (result != AMF_OK || !source) goto fail_source;
result = state->converter->SubmitInput(source);
source->Release();
if (result != AMF_OK) goto fail_converter_submit;
{
amf::AMFData* converted_data = nullptr;
result = query_with_wait(state->converter, &converted_data, 100);
if (result != AMF_OK || !converted_data) goto fail_converter_output;
amf::AMFSurfacePtr converted(converted_data);
converted_data->Release();
if (!converted) { result = AMF_INVALID_DATA_TYPE; goto fail_converter_output; }
if (force_idr) {
converted->SetProperty(AMF_VIDEO_ENCODER_FORCE_PICTURE_TYPE, static_cast<amf_int64>(AMF_VIDEO_ENCODER_PICTURE_TYPE_IDR));
converted->SetProperty(AMF_VIDEO_ENCODER_INSERT_SPS, true);
converted->SetProperty(AMF_VIDEO_ENCODER_INSERT_PPS, true);
}
result = state->encoder->SubmitInput(converted);
if (result != AMF_OK) goto fail_encoder_submit;
}
result = collect_encoder_output(state, 20);
if (result != AMF_OK) goto fail_encoder_output;
if (!state->pending.empty()) {
*output_size = static_cast<int>(state->pending.size());
if (output_capacity < *output_size || !output) return 2;
std::memcpy(output, state->pending.data(), state->pending.size());
state->pending.clear();
}
return 1;
fail_encoder_output: set_error(error_text, error_capacity, amf_error("encoder QueryOutput", result)); return 0;
fail_encoder_submit: set_error(error_text, error_capacity, amf_error("encoder SubmitInput", result)); return 0;
fail_converter_output: set_error(error_text, error_capacity, amf_error("converter QueryOutput", result)); return 0;
fail_converter_submit: set_error(error_text, error_capacity, amf_error("converter SubmitInput", result)); return 0;
fail_source: set_error(error_text, error_capacity, amf_error("CreateSurfaceFromDX11Native", result)); return 0;
}
extern "C" void overlord_amf_destroy(overlord_amf_encoder encoder) {
destroy(static_cast<AMFEncoder*>(encoder));
}
@@ -0,0 +1,84 @@
//go:build windows && cgo
package amfbridge
/*
#cgo CXXFLAGS: -std=c++17
#cgo LDFLAGS: -lole32
#include <stdlib.h>
#include "bridge_windows.h"
*/
import "C"
import (
"unsafe"
)
type Encoder struct{ handle C.overlord_amf_encoder }
func Probe() (bool, string) {
errText := make([]byte, 512)
ok := C.overlord_amf_probe((*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
if ok == 0 {
return false, message(errText, "AMF unavailable")
}
return true, "AMF runtime available"
}
func Create(device unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, bitrate int) (*Encoder, error) {
var handle C.overlord_amf_encoder
errText := make([]byte, 512)
ok := C.overlord_amf_create(device, C.int(inputWidth), C.int(inputHeight), C.int(encodeWidth), C.int(encodeHeight),
C.int(fps), C.uint32_t(dxgiFormat), C.int(bitrate), &handle,
(*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
if ok == 0 || handle == nil {
return nil, &Error{Text: message(errText, "AMF encoder creation failed")}
}
return &Encoder{handle: handle}, nil
}
func (e *Encoder) Encode(texture unsafe.Pointer, forceIDR bool, output []byte) (result int, size int, err error) {
if e == nil || e.handle == nil {
return 0, 0, &Error{Text: "AMF encoder is not initialized"}
}
errText := make([]byte, 512)
var outputSize C.int
var outputPtr *C.uint8_t
if len(output) > 0 {
outputPtr = (*C.uint8_t)(unsafe.Pointer(&output[0]))
}
force := C.int(0)
if forceIDR {
force = 1
}
r := C.overlord_amf_encode(e.handle, texture, force, outputPtr, C.int(len(output)), &outputSize,
(*C.char)(unsafe.Pointer(&errText[0])), C.int(len(errText)))
if r == 0 {
return 0, int(outputSize), &Error{Text: message(errText, "AMF encode failed")}
}
return int(r), int(outputSize), nil
}
func (e *Encoder) Close() {
if e != nil && e.handle != nil {
C.overlord_amf_destroy(e.handle)
e.handle = nil
}
}
type Error struct{ Text string }
func (e *Error) Error() string { return e.Text }
func message(value []byte, fallback string) string {
for i, b := range value {
if b == 0 {
value = value[:i]
break
}
}
if len(value) == 0 {
return fallback
}
return string(value)
}
@@ -0,0 +1,23 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef void* overlord_amf_encoder;
int overlord_amf_probe(char* error_text, int error_capacity);
int overlord_amf_create(void* d3d11_device, int input_width, int input_height,
int encode_width, int encode_height, int fps,
uint32_t dxgi_format, int bitrate,
overlord_amf_encoder* encoder,
char* error_text, int error_capacity);
int overlord_amf_encode(overlord_amf_encoder encoder, void* d3d11_texture,
int force_idr, uint8_t* output, int output_capacity,
int* output_size, char* error_text, int error_capacity);
void overlord_amf_destroy(overlord_amf_encoder encoder);
#ifdef __cplusplus
}
#endif
@@ -53,6 +53,16 @@ type H264MFTFinding struct {
Error string `json:"error,omitempty"`
}
type AMFSmokeResult struct {
Available bool `json:"available"`
Detail string `json:"detail"`
}
func RunAMFSmoke() AMFSmokeResult {
ok, detail := probeAMFD3D11()
return AMFSmokeResult{Available: ok, Detail: detail}
}
func FindH264HardwareMFTs() ([]H264MFTFinding, error) {
if err := ensureMFStartup(); err != nil {
return nil, err
@@ -53,6 +53,12 @@ var d3d11H264TextureRegistry = struct {
backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}},
}
func init() {
if backend := newAMFD3D11TextureBackend(); backend != nil {
d3d11H264TextureRegistry.backends = append(d3d11H264TextureRegistry.backends, backend)
}
}
func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) {
if req.Device == nil || req.Texture == nil {
return nil, "", fmt.Errorf("nil D3D11 device or texture")
@@ -1,4 +1,4 @@
//go:build windows && !cgo
//go:build windows && (!cgo || no_nvenc)
package capture
@@ -1,4 +1,4 @@
//go:build !windows || !cgo
//go:build !windows || !cgo || no_nvenc
package capture
@@ -1,4 +1,4 @@
//go:build windows && cgo
//go:build windows && cgo && !no_nvenc
package capture
@@ -1,4 +1,4 @@
//go:build !windows
//go:build !windows || no_nvenc
package capture
@@ -1,4 +1,4 @@
//go:build windows
//go:build windows && !no_nvenc
package capture