diff --git a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go index 1c418f4..1e5e972 100644 --- a/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go +++ b/Overlord-Client/cmd/agent/capture/h264_encoder_windows.go @@ -7,9 +7,11 @@ import ( "fmt" "image" "log" + "strings" "sync" "sync/atomic" "syscall" + "time" "unsafe" "golang.org/x/sys/windows" @@ -23,16 +25,25 @@ const ( eNotImpl = 0x80004001 mftEnumFlagSyncMFT = 0x00000001 + mftEnumFlagAsyncMFT = 0x00000002 mftEnumFlagHardware = 0x00000004 mftEnumFlagSortAndFilter = 0x00000040 mftMessageNotifyBeginStreaming = 0x10000000 mftMessageNotifyStartOfStream = 0x10000003 + mftMessageCommandFlush = 0x00000000 + mftMessageNotifyEndOfStream = 0x10000002 + mftMessageNotifyEndStreaming = 0x10000001 mftOutputStreamProvidesSamples = 0x00000100 mfETransformNeedMoreInput = 0xC00D6D72 mfETransformStreamChange = 0xC00D6D61 + mfENoEventsAvailable = 0xC00D3E80 + + meTransformNeedInput = 601 + meTransformHaveOutput = 602 + meTransformDrainComplete = 603 h264ProfileMain = 77 ) @@ -56,24 +67,30 @@ var ( ) var ( - CLSID_CMSH264EncoderMFT = windows.GUID{Data1: 0x6ca50344, Data2: 0x051a, Data3: 0x4ded, Data4: [8]byte{0x97, 0x79, 0xa4, 0x33, 0x05, 0x16, 0x5e, 0x35}} - IID_IMFTransform = windows.GUID{Data1: 0xbf94c121, Data2: 0x5b05, Data3: 0x4e6f, Data4: [8]byte{0x80, 0x00, 0xba, 0x59, 0x89, 0x61, 0x41, 0x4d}} - IID_IMFMediaType = windows.GUID{Data1: 0x44ae0fa8, Data2: 0xea31, Data3: 0x4109, Data4: [8]byte{0x8d, 0x2e, 0x4c, 0xae, 0x49, 0x97, 0xc5, 0x55}} - MFT_CATEGORY_VIDEO_ENCODER = windows.GUID{Data1: 0xf79eac7d, Data2: 0xe545, Data3: 0x4387, Data4: [8]byte{0xbd, 0xee, 0xd6, 0x47, 0xd7, 0xbd, 0xe4, 0x2a}} - MFMediaType_Video = windows.GUID{Data1: 0x73646976, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} - MFVideoFormat_H264 = windows.GUID{Data1: 0x34363248, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} - MFVideoFormat_I420 = windows.GUID{Data1: 0x30323449, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} - MFVideoFormat_NV12 = windows.GUID{Data1: 0x3231564e, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} - MF_MT_MAJOR_TYPE = windows.GUID{Data1: 0x48eba18e, Data2: 0xf8c9, Data3: 0x4687, Data4: [8]byte{0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f}} - MF_MT_SUBTYPE = windows.GUID{Data1: 0xf7e34c9a, Data2: 0x42e8, Data3: 0x4714, Data4: [8]byte{0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5}} - MF_MT_FRAME_SIZE = windows.GUID{Data1: 0x1652c33d, Data2: 0xd6b2, Data3: 0x4012, Data4: [8]byte{0xb8, 0x34, 0x72, 0x03, 0x08, 0x49, 0xa3, 0x7d}} - MF_MT_FRAME_RATE = windows.GUID{Data1: 0xc459a2e8, Data2: 0x3d2c, Data3: 0x4e44, Data4: [8]byte{0xb1, 0x32, 0xfe, 0xe5, 0x15, 0x6c, 0x7b, 0xb0}} - MF_MT_PIXEL_ASPECT_RATIO = windows.GUID{Data1: 0xc6376a1e, Data2: 0x8d0a, Data3: 0x4027, Data4: [8]byte{0xbe, 0x45, 0x6d, 0x9a, 0x0a, 0xd3, 0x9b, 0xb6}} - MF_MT_INTERLACE_MODE = windows.GUID{Data1: 0xe2724bb8, Data2: 0xe676, Data3: 0x4806, Data4: [8]byte{0xb4, 0xb2, 0xa8, 0xd6, 0xef, 0xb4, 0x4c, 0xcd}} - MF_MT_AVG_BITRATE = windows.GUID{Data1: 0x20332624, Data2: 0xfB0d, Data3: 0x4d9e, Data4: [8]byte{0xbd, 0x0d, 0xcb, 0xf6, 0x78, 0x6c, 0x10, 0x2e}} - MF_MT_MPEG2_LEVEL = windows.GUID{Data1: 0x96f66574, Data2: 0x11c5, Data3: 0x4015, Data4: [8]byte{0x86, 0x66, 0xbf, 0xf5, 0x16, 0x43, 0x6d, 0xa7}} - MF_MT_MPEG2_PROFILE = windows.GUID{Data1: 0xad76a80b, Data2: 0x2d5c, Data3: 0x4e0b, Data4: [8]byte{0xb3, 0x75, 0x64, 0xe5, 0x20, 0x13, 0x70, 0x36}} - MF_LOW_LATENCY = windows.GUID{Data1: 0x9c27891a, Data2: 0xed7a, Data3: 0x40e1, Data4: [8]byte{0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee}} + CLSID_CMSH264EncoderMFT = windows.GUID{Data1: 0x6ca50344, Data2: 0x051a, Data3: 0x4ded, Data4: [8]byte{0x97, 0x79, 0xa4, 0x33, 0x05, 0x16, 0x5e, 0x35}} + IID_IMFTransform = windows.GUID{Data1: 0xbf94c121, Data2: 0x5b05, Data3: 0x4e6f, Data4: [8]byte{0x80, 0x00, 0xba, 0x59, 0x89, 0x61, 0x41, 0x4d}} + IID_IMFMediaType = windows.GUID{Data1: 0x44ae0fa8, Data2: 0xea31, Data3: 0x4109, Data4: [8]byte{0x8d, 0x2e, 0x4c, 0xae, 0x49, 0x97, 0xc5, 0x55}} + MFT_CATEGORY_VIDEO_ENCODER = windows.GUID{Data1: 0xf79eac7d, Data2: 0xe545, Data3: 0x4387, Data4: [8]byte{0xbd, 0xee, 0xd6, 0x47, 0xd7, 0xbd, 0xe4, 0x2a}} + MFMediaType_Video = windows.GUID{Data1: 0x73646976, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} + MFVideoFormat_H264 = windows.GUID{Data1: 0x34363248, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} + MFVideoFormat_I420 = windows.GUID{Data1: 0x30323449, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} + MFVideoFormat_NV12 = windows.GUID{Data1: 0x3231564e, Data2: 0x0000, Data3: 0x0010, Data4: [8]byte{0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}} + MF_MT_MAJOR_TYPE = windows.GUID{Data1: 0x48eba18e, Data2: 0xf8c9, Data3: 0x4687, Data4: [8]byte{0xbf, 0x11, 0x0a, 0x74, 0xc9, 0xf9, 0x6a, 0x8f}} + MF_MT_SUBTYPE = windows.GUID{Data1: 0xf7e34c9a, Data2: 0x42e8, Data3: 0x4714, Data4: [8]byte{0xb7, 0x4b, 0xcb, 0x29, 0xd7, 0x2c, 0x35, 0xe5}} + MF_MT_FRAME_SIZE = windows.GUID{Data1: 0x1652c33d, Data2: 0xd6b2, Data3: 0x4012, Data4: [8]byte{0xb8, 0x34, 0x72, 0x03, 0x08, 0x49, 0xa3, 0x7d}} + MF_MT_FRAME_RATE = windows.GUID{Data1: 0xc459a2e8, Data2: 0x3d2c, Data3: 0x4e44, Data4: [8]byte{0xb1, 0x32, 0xfe, 0xe5, 0x15, 0x6c, 0x7b, 0xb0}} + MF_MT_PIXEL_ASPECT_RATIO = windows.GUID{Data1: 0xc6376a1e, Data2: 0x8d0a, Data3: 0x4027, Data4: [8]byte{0xbe, 0x45, 0x6d, 0x9a, 0x0a, 0xd3, 0x9b, 0xb6}} + MF_MT_INTERLACE_MODE = windows.GUID{Data1: 0xe2724bb8, Data2: 0xe676, Data3: 0x4806, Data4: [8]byte{0xb4, 0xb2, 0xa8, 0xd6, 0xef, 0xb4, 0x4c, 0xcd}} + MF_MT_AVG_BITRATE = windows.GUID{Data1: 0x20332624, Data2: 0xfB0d, Data3: 0x4d9e, Data4: [8]byte{0xbd, 0x0d, 0xcb, 0xf6, 0x78, 0x6c, 0x10, 0x2e}} + MF_MT_MPEG2_LEVEL = windows.GUID{Data1: 0x96f66574, Data2: 0x11c5, Data3: 0x4015, Data4: [8]byte{0x86, 0x66, 0xbf, 0xf5, 0x16, 0x43, 0x6d, 0xa7}} + MF_MT_MPEG2_PROFILE = windows.GUID{Data1: 0xad76a80b, Data2: 0x2d5c, Data3: 0x4e0b, Data4: [8]byte{0xb3, 0x75, 0x64, 0xe5, 0x20, 0x13, 0x70, 0x36}} + MF_LOW_LATENCY = windows.GUID{Data1: 0x9c27891a, Data2: 0xed7a, Data3: 0x40e1, Data4: [8]byte{0x88, 0xe8, 0xb2, 0x27, 0x27, 0xa0, 0x24, 0xee}} + MFT_FRIENDLY_NAME_Attribute = windows.GUID{Data1: 0x314ffbae, Data2: 0x5b41, Data3: 0x4c95, Data4: [8]byte{0x9c, 0x19, 0x4e, 0x7d, 0x58, 0x6f, 0xac, 0xe3}} + MFT_ENUM_HARDWARE_URL_Attribute = windows.GUID{Data1: 0x2fb866ac, Data2: 0xb078, Data3: 0x4942, Data4: [8]byte{0xab, 0x6c, 0x00, 0x3d, 0x05, 0xcd, 0xa6, 0x74}} + MF_TRANSFORM_ASYNC = windows.GUID{Data1: 0xf81a699a, Data2: 0x649a, Data3: 0x497d, Data4: [8]byte{0x8c, 0x73, 0x29, 0xf8, 0xfe, 0xd6, 0xad, 0x7a}} + MF_TRANSFORM_ASYNC_UNLOCK = windows.GUID{Data1: 0xe5666d6b, Data2: 0x3422, Data3: 0x4eb6, Data4: [8]byte{0xa4, 0x21, 0xda, 0x7d, 0xb1, 0xf8, 0xe2, 0x07}} + MF_SA_D3D11_AWARE = windows.GUID{Data1: 0x206b4fc8, Data2: 0xfcf9, Data3: 0x4c51, Data4: [8]byte{0xaf, 0xe3, 0x97, 0x64, 0x36, 0x9e, 0x33, 0xa0}} + IID_IMFMediaEventGenerator = windows.GUID{Data1: 0x2cd0bd52, Data2: 0xbcd5, Data3: 0x4b89, Data4: [8]byte{0xb6, 0x2c, 0xea, 0xdc, 0x0c, 0x03, 0x1e, 0x7d}} ) type mfAttributesVtbl struct { @@ -134,6 +151,21 @@ func (a *mfAttributes) SetUINT64(key *windows.GUID, value uint64) uintptr { return callSyscallN(a.lpVtbl.SetUINT64, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(value)) } +func (a *mfAttributes) GetUINT32(key *windows.GUID, value *uint32) uintptr { + return callSyscallN(a.lpVtbl.GetUINT32, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(value))) +} + +func (a *mfAttributes) GetAllocatedString(key *windows.GUID) (string, bool) { + var value *uint16 + var length uint32 + hr := callSyscallN(a.lpVtbl.GetAllocatedString, uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(&value)), uintptr(unsafe.Pointer(&length))) + if failedHR(hr) || value == nil { + return "", false + } + defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(value))) + return windows.UTF16PtrToString(value), true +} + type mfMediaType struct { lpVtbl *mfAttributesVtbl } @@ -215,6 +247,56 @@ func (t *mfTransform) ProcessOutput(flags uint32, count uint32, samples *mftOutp return callSyscallN(t.lpVtbl.ProcessOutput, uintptr(unsafe.Pointer(t)), uintptr(flags), uintptr(count), uintptr(unsafe.Pointer(samples)), uintptr(unsafe.Pointer(status))) } +type mfMediaEventGeneratorVtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + GetEvent uintptr + BeginGetEvent uintptr + EndGetEvent uintptr + QueueEvent uintptr +} + +type mfMediaEventGenerator struct { + lpVtbl *mfMediaEventGeneratorVtbl +} + +func (g *mfMediaEventGenerator) Release() { + if g != nil && g.lpVtbl != nil { + callSyscallN(g.lpVtbl.Release, uintptr(unsafe.Pointer(g))) + } +} + +func (g *mfMediaEventGenerator) GetEvent(flags uint32, event **mfMediaEvent) uintptr { + return callSyscallN(g.lpVtbl.GetEvent, uintptr(unsafe.Pointer(g)), uintptr(flags), uintptr(unsafe.Pointer(event))) +} + +type mfMediaEventVtbl struct { + mfAttributesVtbl + GetType uintptr + GetExtendedType uintptr + GetStatus uintptr + GetValue uintptr +} + +type mfMediaEvent struct { + lpVtbl *mfMediaEventVtbl +} + +func (e *mfMediaEvent) Release() { + if e != nil && e.lpVtbl != nil { + callSyscallN(e.lpVtbl.Release, uintptr(unsafe.Pointer(e))) + } +} + +func (e *mfMediaEvent) GetType(eventType *uint32) uintptr { + return callSyscallN(e.lpVtbl.GetType, uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(eventType))) +} + +func (e *mfMediaEvent) GetStatus(status *uint32) uintptr { + return callSyscallN(e.lpVtbl.GetStatus, uintptr(unsafe.Pointer(e)), uintptr(unsafe.Pointer(status))) +} + type mfMediaBufferVtbl struct { QueryInterface uintptr AddRef uintptr @@ -371,6 +453,10 @@ type mfActivate struct { lpVtbl *mfActivateVtbl } +func (a *mfActivate) attrs() *mfAttributes { + return (*mfAttributes)(unsafe.Pointer(a)) +} + func (a *mfActivate) Release() { if a != nil && a.lpVtbl != nil { callSyscallN(a.lpVtbl.Release, uintptr(unsafe.Pointer(a))) @@ -412,6 +498,10 @@ type mfH264Encoder struct { provider string inputSubtype windows.GUID inputFormat string + asynchronous bool + events *mfMediaEventGenerator + inputReady int + pendingOut []byte } type h264FrameEncoder interface { @@ -502,11 +592,11 @@ func h264AvailabilityDetail() string { if err := ensureMFStartup(); err != nil { return fmt.Sprintf("Windows Media Foundation unavailable: %v", err) } - ok, reason := hardwareH264MFTStatus() + ok, detail := hardwareH264MFTStatus() if ok { - return "Windows Media Foundation H.264 MFT (hardware encoder available)" + return "Windows Media Foundation H.264 MFT (hardware encoder available: " + detail + ")" } - return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + reason + ")" + return "Windows Media Foundation H.264 MFT (Microsoft H.264 encoder fallback; hardware unavailable: " + detail + ")" } func SetH264TargetFPS(fps int) { @@ -530,7 +620,7 @@ func resetH264Encoder() { } func RequestDesktopH264Keyframe() { - requestNativeH264D3D11TextureKeyframe() + requestH264D3D11TextureKeyframe() resetH264Encoder() } @@ -566,11 +656,12 @@ func newMFH264Encoder(width, height, fps int) (*mfH264Encoder, error) { return nil, err } - if transform, err := activateHardwareH264MFT(); err == nil && transform != nil { - log.Printf("capture: media foundation h264 hardware encoder selected") + if transform, detail, err := activateHardwareH264MFTDetailed(); err == nil && transform != nil { + provider := detail.provider() + log.Printf("capture: media foundation h264 hardware encoder selected provider=%q", provider) candidates := hardwareH264Candidates(fps) for idx, candidate := range candidates { - enc := newMFH264EncoderFromTransform(transform, width, height, fps, candidate.fps, true, "hardware", candidate.inputSubtype, candidate.inputFormat) + enc := newMFH264EncoderFromTransform(transform, width, height, fps, candidate.fps, true, provider, candidate.inputSubtype, candidate.inputFormat) if err := enc.configure(); err == nil { logH264EncoderActive(enc) return enc, nil @@ -581,11 +672,13 @@ func newMFH264Encoder(width, height, fps int) (*mfH264Encoder, error) { if idx != len(candidates)-1 { var nextErr error - transform, nextErr = activateHardwareH264MFT() + var nextDetail mfHardwareEncoderDetail + transform, nextDetail, nextErr = activateHardwareH264MFTDetailed() if nextErr != nil { log.Printf("capture: media foundation h264 hardware encoder reactivation failed after fps retry: %v; using Microsoft software encoder", nextErr) break } + provider = nextDetail.provider() } } log.Printf("capture: media foundation h264 hardware encoder rejected all fps candidates for %dx%d requested_fps=%d; using Microsoft software encoder", width, height, fps) @@ -672,6 +765,13 @@ func logH264EncoderActive(enc *mfH264Encoder) { func (e *mfH264Encoder) Close() { if e != nil && e.transform != nil { + _ = e.transform.ProcessMessage(mftMessageNotifyEndOfStream, 0) + _ = e.transform.ProcessMessage(mftMessageCommandFlush, 0) + _ = e.transform.ProcessMessage(mftMessageNotifyEndStreaming, 0) + if e.events != nil { + e.events.Release() + e.events = nil + } e.transform.Release() e.transform = nil } @@ -682,6 +782,9 @@ func (e *mfH264Encoder) Matches(width, height, fps int) bool { } func (e *mfH264Encoder) configure() error { + if err := e.configureAsyncMode(); err != nil { + return err + } e.setLowLatency() outType, err := createVideoType(MFVideoFormat_H264, e.width, e.height, e.fps) @@ -714,6 +817,36 @@ func (e *mfH264Encoder) configure() error { if hr := e.transform.ProcessMessage(mftMessageNotifyStartOfStream, 0); failedHR(hr) && hr != eNotImpl { return fmt.Errorf("mf h264: start stream failed 0x%x", hr) } + if e.asynchronous { + if err := e.waitForAsyncInput(500 * time.Millisecond); err != nil { + return err + } + } + return nil +} + +func (e *mfH264Encoder) configureAsyncMode() error { + var attrs *mfAttributes + hr := e.transform.GetAttributes(&attrs) + if failedHR(hr) || attrs == nil { + return nil + } + defer attrs.Release() + var async uint32 + _ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async) + e.asynchronous = async != 0 + if !e.asynchronous { + return nil + } + if hr := attrs.SetUINT32(&MF_TRANSFORM_ASYNC_UNLOCK, 1); failedHR(hr) { + return fmt.Errorf("mf h264: unlock asynchronous transform failed 0x%x", hr) + } + if e.events == nil { + hr := (*iunknown)(unsafe.Pointer(e.transform)).QueryInterface(&IID_IMFMediaEventGenerator, unsafe.Pointer(&e.events)) + if failedHR(hr) || e.events == nil { + return fmt.Errorf("mf h264: asynchronous transform has no event generator (0x%x)", hr) + } + } return nil } @@ -741,6 +874,9 @@ func (e *mfH264Encoder) Encode(img *image.RGBA) ([]byte, error) { return nil, err } defer sample.Release() + if e.asynchronous { + return e.encodeAsync(sample) + } hr := e.transform.ProcessInput(0, sample, 0) if failedHR(hr) { @@ -755,6 +891,106 @@ func (e *mfH264Encoder) Encode(img *image.RGBA) ([]byte, error) { return out, nil } +func (e *mfH264Encoder) encodeAsync(sample *mfSample) ([]byte, error) { + if err := e.waitForAsyncInput(500 * time.Millisecond); err != nil { + return nil, err + } + e.inputReady-- + hr := e.transform.ProcessInput(0, sample, 0) + if failedHR(hr) { + return nil, fmt.Errorf("mf h264: asynchronous ProcessInput failed 0x%x", hr) + } + e.frame++ + + deadline := time.Now().Add(100 * time.Millisecond) + for len(e.pendingOut) == 0 && time.Now().Before(deadline) { + hadEvent, err := e.pumpAsyncEvent() + if err != nil { + return nil, err + } + if !hadEvent { + if e.inputReady > 0 { + break + } + time.Sleep(time.Millisecond) + } + } + return e.takePendingOutput(), nil +} + +func (e *mfH264Encoder) waitForAsyncInput(timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for { + for { + hadEvent, err := e.pumpAsyncEvent() + if err != nil { + return err + } + if !hadEvent { + break + } + } + if e.inputReady > 0 { + return nil + } + if time.Now().After(deadline) { + return errors.New("mf h264: timed out waiting for asynchronous input request") + } + time.Sleep(time.Millisecond) + } +} + +func (e *mfH264Encoder) pumpAsyncEvent() (bool, error) { + if e.events == nil { + return false, errors.New("mf h264: asynchronous event generator is unavailable") + } + var event *mfMediaEvent + hr := e.events.GetEvent(1, &event) // MF_EVENT_FLAG_NO_WAIT + if hr == mfENoEventsAvailable { + return false, nil + } + if failedHR(hr) { + return false, fmt.Errorf("mf h264: GetEvent failed 0x%x", hr) + } + if event == nil { + return false, errors.New("mf h264: GetEvent returned a nil event") + } + defer event.Release() + var eventType uint32 + if hr := event.GetType(&eventType); failedHR(hr) { + return false, fmt.Errorf("mf h264: event GetType failed 0x%x", hr) + } + var eventStatus uint32 + if hr := event.GetStatus(&eventStatus); failedHR(hr) { + return false, fmt.Errorf("mf h264: event GetStatus failed 0x%x", hr) + } + if failedHR(uintptr(eventStatus)) { + return false, fmt.Errorf("mf h264: asynchronous event %d failed 0x%x", eventType, eventStatus) + } + switch eventType { + case meTransformNeedInput: + e.inputReady++ + case meTransformHaveOutput: + out, err := e.processOutput() + if err != nil { + return false, err + } + e.pendingOut = append(e.pendingOut, out...) + case meTransformDrainComplete: + // No action is required during normal streaming. + } + return true, nil +} + +func (e *mfH264Encoder) takePendingOutput() []byte { + if len(e.pendingOut) == 0 { + return nil + } + out := append([]byte(nil), e.pendingOut...) + e.pendingOut = e.pendingOut[:0] + return out +} + func (e *mfH264Encoder) processOutput() ([]byte, error) { var info mftOutputStreamInfo hr := e.transform.GetOutputStreamInfo(0, &info) @@ -800,6 +1036,9 @@ func (e *mfH264Encoder) processOutput() ([]byte, error) { if output.sample == nil { return nil, nil } + if outSample == nil { + defer output.sample.Release() + } return sampleBytes(output.sample) } @@ -925,9 +1164,10 @@ func sampleBytes(sample *mfSample) ([]byte, error) { } func createH264Transform() (*mfTransform, bool, string, error) { - if transform, err := activateHardwareH264MFT(); err == nil && transform != nil { - log.Printf("capture: media foundation h264 hardware encoder selected") - return transform, true, "hardware", nil + if transform, detail, err := activateHardwareH264MFTDetailed(); err == nil && transform != nil { + provider := detail.provider() + log.Printf("capture: media foundation h264 hardware encoder selected provider=%q", provider) + return transform, true, provider, nil } else { log.Printf("capture: media foundation h264 hardware encoder unavailable: %v; using Microsoft software encoder", err) } @@ -949,52 +1189,86 @@ func createSoftwareH264Transform() (*mfTransform, bool, string, error) { return transform, false, "Microsoft software", nil } +type mfHardwareEncoderDetail struct { + name string + hardwareURL string + asynchronous bool + d3d11Aware bool +} + +func (d mfHardwareEncoderDetail) provider() string { + if strings.TrimSpace(d.name) != "" { + return d.name + } + return "Media Foundation hardware" +} + func activateHardwareH264MFT() (*mfTransform, error) { + transform, _, err := activateHardwareH264MFTDetailed() + return transform, err +} + +func activateHardwareH264MFTDetailed() (*mfTransform, mfHardwareEncoderDetail, error) { input := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_NV12} output := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_H264} var activates **mfActivate var count uint32 hr, _, _ := procMFTEnumEx.Call( uintptr(unsafe.Pointer(&MFT_CATEGORY_VIDEO_ENCODER)), - uintptr(mftEnumFlagSyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter), + uintptr(mftEnumFlagSyncMFT|mftEnumFlagAsyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter), uintptr(unsafe.Pointer(&input)), uintptr(unsafe.Pointer(&output)), uintptr(unsafe.Pointer(&activates)), uintptr(unsafe.Pointer(&count)), ) if failedHR(hr) || count == 0 || activates == nil { - return nil, fmt.Errorf("mf h264: no synchronous hardware encoder (0x%x)", hr) + return nil, mfHardwareEncoderDetail{}, fmt.Errorf("mf h264: no hardware encoder (0x%x)", hr) } defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(activates))) list := unsafe.Slice(activates, int(count)) var found *mfTransform + var foundDetail mfHardwareEncoderDetail for _, activate := range list { if activate == nil { continue } + detail := mfHardwareEncoderDetail{} + detail.name, _ = activate.attrs().GetAllocatedString(&MFT_FRIENDLY_NAME_Attribute) + detail.hardwareURL, _ = activate.attrs().GetAllocatedString(&MFT_ENUM_HARDWARE_URL_Attribute) if found == nil { var transform *mfTransform hr := activate.ActivateObject(&IID_IMFTransform, unsafe.Pointer(&transform)) if !failedHR(hr) && transform != nil { + var attrs *mfAttributes + if !failedHR(transform.GetAttributes(&attrs)) && attrs != nil { + var async, d3d11 uint32 + _ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async) + _ = attrs.GetUINT32(&MF_SA_D3D11_AWARE, &d3d11) + detail.asynchronous = async != 0 + detail.d3d11Aware = d3d11 != 0 + attrs.Release() + } found = transform + foundDetail = detail } } activate.Release() } if found != nil { - return found, nil + log.Printf("capture: media foundation hardware h264 candidate selected name=%q asynchronous=%t d3d11_aware=%t hardware_url=%q", foundDetail.provider(), foundDetail.asynchronous, foundDetail.d3d11Aware, foundDetail.hardwareURL) + return found, foundDetail, nil } - return nil, errors.New("mf h264: hardware encoder activation failed") + return nil, mfHardwareEncoderDetail{}, errors.New("mf h264: hardware encoder activation failed") } func hardwareH264MFTStatus() (bool, string) { - transform, err := activateHardwareH264MFT() + transform, detail, err := activateHardwareH264MFTDetailed() if err != nil { return false, err.Error() } transform.Release() - return true, "" + return true, detail.provider() } func ensureMFStartup() error { diff --git a/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go b/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go index 7989c91..2cedc16 100644 --- a/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go +++ b/Overlord-Client/cmd/agent/capture/h264_smoke_windows.go @@ -6,6 +6,7 @@ import ( "fmt" "image" "time" + "unsafe" ) type H264SmokeResolution struct { @@ -39,6 +40,72 @@ type H264SmokeResult struct { AvgBytes float64 `json:"avg_bytes"` TotalBytes int `json:"total_bytes"` Error string `json:"error,omitempty"` + Asynchronous bool `json:"asynchronous"` +} + +type H264MFTFinding struct { + Index int `json:"index"` + Name string `json:"name"` + HardwareURL string `json:"hardware_url,omitempty"` + Asynchronous bool `json:"asynchronous"` + D3D11Aware bool `json:"d3d11_aware"` + ActivationOK bool `json:"activation_ok"` + Error string `json:"error,omitempty"` +} + +func FindH264HardwareMFTs() ([]H264MFTFinding, error) { + if err := ensureMFStartup(); err != nil { + return nil, err + } + input := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_NV12} + output := mftRegisterTypeInfo{majorType: MFMediaType_Video, subtype: MFVideoFormat_H264} + var activates **mfActivate + var count uint32 + hr, _, _ := procMFTEnumEx.Call( + uintptr(unsafe.Pointer(&MFT_CATEGORY_VIDEO_ENCODER)), + uintptr(mftEnumFlagSyncMFT|mftEnumFlagAsyncMFT|mftEnumFlagHardware|mftEnumFlagSortAndFilter), + uintptr(unsafe.Pointer(&input)), uintptr(unsafe.Pointer(&output)), + uintptr(unsafe.Pointer(&activates)), uintptr(unsafe.Pointer(&count)), + ) + if failedHR(hr) { + return nil, fmt.Errorf("MFTEnumEx failed 0x%x", hr) + } + if activates == nil || count == 0 { + return []H264MFTFinding{}, nil + } + defer procMFCoTaskMemFree.Call(uintptr(unsafe.Pointer(activates))) + list := unsafe.Slice(activates, int(count)) + findings := make([]H264MFTFinding, 0, len(list)) + for index, activate := range list { + finding := H264MFTFinding{Index: index} + if activate == nil { + finding.Error = "nil activation object" + findings = append(findings, finding) + continue + } + finding.Name, _ = activate.attrs().GetAllocatedString(&MFT_FRIENDLY_NAME_Attribute) + finding.HardwareURL, _ = activate.attrs().GetAllocatedString(&MFT_ENUM_HARDWARE_URL_Attribute) + var transform *mfTransform + hr := activate.ActivateObject(&IID_IMFTransform, unsafe.Pointer(&transform)) + if failedHR(hr) || transform == nil { + finding.Error = fmt.Sprintf("ActivateObject failed 0x%x", hr) + } else { + finding.ActivationOK = true + var attrs *mfAttributes + if !failedHR(transform.GetAttributes(&attrs)) && attrs != nil { + var async, d3d11 uint32 + _ = attrs.GetUINT32(&MF_TRANSFORM_ASYNC, &async) + _ = attrs.GetUINT32(&MF_SA_D3D11_AWARE, &d3d11) + finding.Asynchronous = async != 0 + finding.D3D11Aware = d3d11 != 0 + attrs.Release() + } + transform.Release() + } + activate.Release() + findings = append(findings, finding) + } + return findings, nil } func DefaultH264SmokeOptions() H264SmokeOptions { @@ -125,9 +192,11 @@ func runH264SmokeCase(provider string, width, height, requestedFPS int, candidat enc := newMFH264EncoderFromTransform(transform, width, height, requestedFPS, candidate.fps, hardware, providerName, candidate.inputSubtype, candidate.inputFormat) t0 := time.Now() - if err := enc.configure(); err != nil { + configureErr := enc.configure() + result.Asynchronous = enc.asynchronous + if configureErr != nil { result.ConfigureMS = float64(time.Since(t0).Microseconds()) / 1000 - result.Error = err.Error() + result.Error = configureErr.Error() enc.Close() return result } @@ -179,11 +248,11 @@ func runH264SmokeCase(provider string, width, height, requestedFPS int, candidat func createH264SmokeTransform(provider string) (*mfTransform, bool, string, error) { switch provider { case "hardware": - transform, err := activateHardwareH264MFT() + transform, detail, err := activateHardwareH264MFTDetailed() if err != nil { return nil, false, "hardware", err } - return transform, true, "hardware", nil + return transform, true, detail.provider(), nil case "software": return createSoftwareH264Transform() default: diff --git a/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go new file mode 100644 index 0000000..fdbc59e --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows.go @@ -0,0 +1,117 @@ +//go:build windows + +package capture + +import ( + "fmt" + "strings" + "sync" + "unsafe" +) + +type h264D3D11TextureRequest struct { + Device unsafe.Pointer + Texture unsafe.Pointer + InputWidth int + InputHeight int + EncodeWidth int + EncodeHeight int + FPS int + DXGIFormat uint32 + ForceIDR bool +} + +type h264D3D11TextureBackend interface { + Name() string + Encode(h264D3D11TextureRequest) ([]byte, error) + RequestKeyframe() + Reset() +} + +type nvencD3D11TextureBackend struct{} + +func (nvencD3D11TextureBackend) Name() string { return "NVIDIA NVENC" } + +func (nvencD3D11TextureBackend) Encode(req h264D3D11TextureRequest) ([]byte, error) { + return encodeNativeH264D3D11Texture(req.Device, req.Texture, req.InputWidth, req.InputHeight, + req.EncodeWidth, req.EncodeHeight, req.FPS, req.DXGIFormat, req.ForceIDR) +} + +func (nvencD3D11TextureBackend) RequestKeyframe() { + requestNativeH264D3D11TextureKeyframe() +} + +func (nvencD3D11TextureBackend) Reset() { + resetNativeH264D3D11TextureEncoder() +} + +var d3d11H264TextureRegistry = struct { + sync.Mutex + backends []h264D3D11TextureBackend + active h264D3D11TextureBackend +}{ + backends: []h264D3D11TextureBackend{nvencD3D11TextureBackend{}}, +} + +func encodeH264D3D11Texture(req h264D3D11TextureRequest) ([]byte, string, error) { + if req.Device == nil || req.Texture == nil { + return nil, "", fmt.Errorf("nil D3D11 device or texture") + } + d3d11H264TextureRegistry.Lock() + defer d3d11H264TextureRegistry.Unlock() + + failedActiveName := "" + errorsByBackend := make([]string, 0, len(d3d11H264TextureRegistry.backends)) + if active := d3d11H264TextureRegistry.active; active != nil { + out, err := active.Encode(req) + if err == nil { + return out, active.Name(), nil + } + active.Reset() + failedActiveName = active.Name() + errorsByBackend = append(errorsByBackend, active.Name()+": "+err.Error()) + d3d11H264TextureRegistry.active = nil + } + + for _, backend := range d3d11H264TextureRegistry.backends { + if backend == nil || (failedActiveName != "" && backend.Name() == failedActiveName) { + continue + } + out, err := backend.Encode(req) + if err == nil { + d3d11H264TextureRegistry.active = backend + return out, backend.Name(), nil + } + backend.Reset() + errorsByBackend = append(errorsByBackend, backend.Name()+": "+err.Error()) + } + if len(errorsByBackend) == 0 { + return nil, "", fmt.Errorf("no D3D11 H.264 texture encoder backends registered") + } + return nil, "", fmt.Errorf("D3D11 H.264 texture encoders unavailable: %s", strings.Join(errorsByBackend, "; ")) +} + +func requestH264D3D11TextureKeyframe() { + d3d11H264TextureRegistry.Lock() + defer d3d11H264TextureRegistry.Unlock() + if d3d11H264TextureRegistry.active != nil { + d3d11H264TextureRegistry.active.RequestKeyframe() + return + } + for _, backend := range d3d11H264TextureRegistry.backends { + if backend != nil { + backend.RequestKeyframe() + } + } +} + +func resetH264D3D11TextureEncoder() { + d3d11H264TextureRegistry.Lock() + defer d3d11H264TextureRegistry.Unlock() + for _, backend := range d3d11H264TextureRegistry.backends { + if backend != nil { + backend.Reset() + } + } + d3d11H264TextureRegistry.active = nil +} diff --git a/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows_test.go b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows_test.go new file mode 100644 index 0000000..68c93d3 --- /dev/null +++ b/Overlord-Client/cmd/agent/capture/h264_texture_encoder_windows_test.go @@ -0,0 +1,81 @@ +//go:build windows + +package capture + +import ( + "errors" + "testing" + "unsafe" +) + +type fakeD3D11TextureBackend struct { + name string + outputs [][]byte + errors []error + encodeCalls int + resetCalls int + keyframeCalls int +} + +func (b *fakeD3D11TextureBackend) Name() string { return b.name } +func (b *fakeD3D11TextureBackend) Encode(h264D3D11TextureRequest) ([]byte, error) { + index := b.encodeCalls + b.encodeCalls++ + if index < len(b.errors) && b.errors[index] != nil { + return nil, b.errors[index] + } + if index < len(b.outputs) { + return b.outputs[index], nil + } + return []byte{byte(index + 1)}, nil +} +func (b *fakeD3D11TextureBackend) RequestKeyframe() { b.keyframeCalls++ } +func (b *fakeD3D11TextureBackend) Reset() { b.resetCalls++ } + +func TestD3D11TextureBackendSelectionAndFailover(t *testing.T) { + first := &fakeD3D11TextureBackend{name: "first", errors: []error{errors.New("unsupported")}} + second := &fakeD3D11TextureBackend{name: "second", outputs: [][]byte{{1, 2, 3}, {4, 5, 6}}} + + d3d11H264TextureRegistry.Lock() + savedBackends := d3d11H264TextureRegistry.backends + savedActive := d3d11H264TextureRegistry.active + d3d11H264TextureRegistry.backends = []h264D3D11TextureBackend{first, second} + d3d11H264TextureRegistry.active = nil + d3d11H264TextureRegistry.Unlock() + t.Cleanup(func() { + d3d11H264TextureRegistry.Lock() + d3d11H264TextureRegistry.backends = savedBackends + d3d11H264TextureRegistry.active = savedActive + d3d11H264TextureRegistry.Unlock() + }) + + device, texture := 1, 2 + req := h264D3D11TextureRequest{Device: unsafe.Pointer(&device), Texture: unsafe.Pointer(&texture)} + out, provider, err := encodeH264D3D11Texture(req) + if err != nil { + t.Fatalf("first encode: %v", err) + } + if provider != "second" || len(out) != 3 { + t.Fatalf("provider=%q out=%v", provider, out) + } + if first.resetCalls != 1 || second.encodeCalls != 1 { + t.Fatalf("unexpected calls first.reset=%d second.encode=%d", first.resetCalls, second.encodeCalls) + } + + _, provider, err = encodeH264D3D11Texture(req) + if err != nil || provider != "second" { + t.Fatalf("active backend reuse provider=%q err=%v", provider, err) + } + if first.encodeCalls != 1 || second.encodeCalls != 2 { + t.Fatalf("active backend was not reused: first=%d second=%d", first.encodeCalls, second.encodeCalls) + } + + requestH264D3D11TextureKeyframe() + if second.keyframeCalls != 1 || first.keyframeCalls != 0 { + t.Fatalf("keyframe routed incorrectly: first=%d second=%d", first.keyframeCalls, second.keyframeCalls) + } + resetH264D3D11TextureEncoder() + if first.resetCalls != 2 || second.resetCalls != 1 { + t.Fatalf("reset did not reach every backend: first=%d second=%d", first.resetCalls, second.resetCalls) + } +} diff --git a/Overlord-Client/cmd/agent/capture/win_duplication.go b/Overlord-Client/cmd/agent/capture/win_duplication.go index 2395531..aa46aa9 100644 --- a/Overlord-Client/cmd/agent/capture/win_duplication.go +++ b/Overlord-Client/cmd/agent/capture/win_duplication.go @@ -665,7 +665,7 @@ func (s *duplicationState) closeLocked() { s.h264LastTex = nil s.h264LastDesc = d3d11Texture2DDesc{} } - resetNativeH264D3D11TextureEncoder() + resetH264D3D11TextureEncoder() if s.context != nil { s.context.Release() s.context = nil @@ -887,7 +887,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, return nil, 0, 0, 0, 0, false, fmt.Errorf("direct h264 computed invalid scaled output %dx%d", encodeW, encodeH) } directH264ScaleOnce.Do(func() { - log.Printf("capture: direct DXGI->NVENC h264 using NVENC scale=%.2f input=%dx%d output=%dx%d", scale, width, height, encodeW, encodeH) + log.Printf("capture: direct DXGI hardware h264 scaling enabled scale=%.2f input=%dx%d output=%dx%d", scale, width, height, encodeW, encodeH) }) } @@ -899,11 +899,15 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, 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) + out, _, err := encodeH264D3D11Texture(h264D3D11TextureRequest{ + Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex), + InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH, + FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: 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) + log.Printf("capture: direct DXGI hardware 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 } @@ -955,13 +959,13 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, } if srcDesc.Format != dxgiFormatB8G8R8A8UNorm && srcDesc.Format != dxgiFormatR8G8B8A8UNorm { directH264WarnOnce.Do(func() { - log.Printf("capture: direct DXGI->NVENC h264 unavailable: unsupported DXGI texture format %d; using readback path", srcDesc.Format) + log.Printf("capture: direct DXGI hardware h264 unavailable: unsupported DXGI texture format %d; using readback path", srcDesc.Format) }) 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) + log.Printf("capture: direct DXGI hardware 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 } @@ -972,11 +976,15 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, 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) + out, provider, err := encodeH264D3D11Texture(h264D3D11TextureRequest{ + Device: unsafe.Pointer(s.device), Texture: unsafe.Pointer(s.h264LastTex), + InputWidth: width, InputHeight: height, EncodeWidth: encodeW, EncodeHeight: encodeH, + FPS: fps, DXGIFormat: s.h264LastDesc.Format, ForceIDR: 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, s.h264LastDesc.Format, err) + log.Printf("capture: direct DXGI hardware 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 } @@ -984,7 +992,7 @@ func (s *duplicationState) captureH264(display int, forceKeyframe bool) ([]byte, return nil, encodeW, encodeH, captureDur, encodeDur, true, nil } directH264ActiveOnce.Do(func() { - log.Printf("capture: direct DXGI->NVENC h264 path active input=%dx%d output=%dx%d fps=%d dxgi_format=%d", width, height, encodeW, encodeH, fps, srcDesc.Format) + log.Printf("capture: direct DXGI hardware h264 path active provider=%q input=%dx%d output=%dx%d fps=%d dxgi_format=%d", provider, width, height, encodeW, encodeH, fps, srcDesc.Format) }) return out, encodeW, encodeH, captureDur, encodeDur, true, nil } diff --git a/Overlord-Client/cmd/mf-h264-smoke/main_windows.go b/Overlord-Client/cmd/mf-h264-smoke/main_windows.go index 13ba940..0853560 100644 --- a/Overlord-Client/cmd/mf-h264-smoke/main_windows.go +++ b/Overlord-Client/cmd/mf-h264-smoke/main_windows.go @@ -8,12 +8,36 @@ import ( "fmt" "os" "os/exec" + "runtime" "strconv" "strings" + "time" "overlord-client/cmd/agent/capture" + "overlord-client/cmd/agent/sysinfo" ) +type machineInfo struct { + Hostname string `json:"hostname"` + OS string `json:"os"` + Arch string `json:"arch"` + CPU string `json:"cpu"` + GPU string `json:"gpu"` + RAM string `json:"ram"` + GeneratedAt string `json:"generated_at"` +} + +type smokeReport struct { + Machine machineInfo `json:"machine"` + Findings []capture.H264MFTFinding `json:"media_foundation_findings"` + FindingError string `json:"media_foundation_finding_error,omitempty"` + H264 []capture.H264SmokeResult `json:"h264"` + Capture []capture.CaptureSmokeResult `json:"capture,omitempty"` + NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"` + NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"` + Nvidia []string `json:"nvidia_smi,omitempty"` +} + func main() { var frames int var captureFrames int @@ -27,6 +51,7 @@ func main() { var resArg string var fpsArg string var providersArg string + var reportPath string flag.IntVar(&frames, "frames", 30, "frames to feed each encoder case") flag.IntVar(&captureFrames, "capture-frames", 30, "frames to capture for each capture backend") flag.IntVar(&captureDisplay, "display", 0, "display index for capture smoke") @@ -39,7 +64,12 @@ func main() { flag.StringVar(&resArg, "res", "1280x720,1920x1080,2560x1440,3840x2160", "comma-separated resolutions") flag.StringVar(&fpsArg, "fps", "30,60,120", "comma-separated requested FPS values") flag.StringVar(&providersArg, "providers", "hardware,software", "comma-separated providers: hardware,software") + flag.StringVar(&reportPath, "report", "", "also write the complete JSON report to this file") flag.Parse() + hostname, _ := os.Hostname() + 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() opts := capture.DefaultH264SmokeOptions() opts.Frames = frames @@ -66,22 +96,28 @@ func main() { nvidiaSMI = queryNvidiaSMI() } } + report := smokeReport{Machine: machine, Findings: findings, H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI} + if findingErr != nil { + report.FindingError = findingErr.Error() + } + if reportPath != "" { + if err := writeJSONReport(reportPath, report); err != nil { + fmt.Fprintf(os.Stderr, "write report failed: %v\n", err) + os.Exit(1) + } + } if jsonOut { enc := json.NewEncoder(os.Stdout) enc.SetIndent("", " ") - payload := struct { - H264 []capture.H264SmokeResult `json:"h264"` - Capture []capture.CaptureSmokeResult `json:"capture,omitempty"` - NVENC *capture.NVENCSmokeResult `json:"nvenc,omitempty"` - NVENCD3D11 []capture.NVENCD3D11SmokeResult `json:"nvenc_d3d11,omitempty"` - Nvidia []string `json:"nvidia_smi,omitempty"` - }{H264: results, Capture: captureResults, NVENC: nvencResult, NVENCD3D11: nvencD3D11Results, Nvidia: nvidiaSMI} - if err := enc.Encode(payload); err != nil { + if err := enc.Encode(report); err != nil { fmt.Fprintf(os.Stderr, "json encode failed: %v\n", err) os.Exit(1) } return } + printMachine(machine) + printMFTFindings(findings, findingErr) + fmt.Println() printTable(results) if includeCapture { fmt.Println() @@ -102,6 +138,51 @@ func main() { } } +func writeJSONReport(path string, report smokeReport) error { + f, err := os.Create(path) + if err != nil { + return err + } + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + return enc.Encode(report) +} + +func printMachine(info machineInfo) { + fmt.Println("Machine:") + fmt.Printf(" Host: %s\n OS: %s (%s)\n CPU: %s\n GPU: %s\n RAM: %s\n Time: %s\n", info.Hostname, info.OS, info.Arch, info.CPU, info.GPU, info.RAM, info.GeneratedAt) +} + +func printMFTFindings(findings []capture.H264MFTFinding, err error) { + fmt.Println("Media Foundation hardware H.264 findings:") + if err != nil { + fmt.Printf(" ERROR: %v\n", err) + return + } + if len(findings) == 0 { + fmt.Println(" No hardware H.264 MFTs found.") + return + } + for _, finding := range findings { + status := "ok" + if !finding.ActivationOK { + status = "failed" + } + mode := "sync" + if finding.Asynchronous { + mode = "async" + } + fmt.Printf(" [%d] %s | activation=%s | mode=%s | d3d11=%t\n", finding.Index, finding.Name, status, mode, finding.D3D11Aware) + if finding.HardwareURL != "" { + fmt.Printf(" device: %s\n", finding.HardwareURL) + } + if finding.Error != "" { + fmt.Printf(" error: %s\n", finding.Error) + } + } +} + func parseResolutions(raw string) []capture.H264SmokeResolution { parts := parseStrings(raw) out := make([]capture.H264SmokeResolution, 0, len(parts)) @@ -143,8 +224,8 @@ func parseStrings(raw string) []string { } func printTable(results []capture.H264SmokeResult) { - fmt.Printf("%-18s %-5s %9s %7s %7s %8s %9s %8s %7s %9s %9s %s\n", - "provider", "input", "size", "reqfps", "cfgfps", "config", "first_ms", "avg_ms", "frames", "avg_bytes", "total", "error") + fmt.Printf("%-28s %-5s %-5s %9s %7s %7s %8s %9s %8s %7s %9s %9s %s\n", + "provider", "mode", "input", "size", "reqfps", "cfgfps", "config", "first_ms", "avg_ms", "frames", "avg_bytes", "total", "error") for _, r := range results { status := "fail" if r.ConfigureOK && r.EncodeOK { @@ -152,8 +233,12 @@ func printTable(results []capture.H264SmokeResult) { } else if r.ConfigureOK { status = "no-data" } - fmt.Printf("%-18s %-5s %4dx%-4d %7d %7d %8s %9.2f %8.2f %7d %9.0f %9d %s\n", - r.Provider, r.Input, r.Width, r.Height, r.RequestedFPS, r.ConfiguredFPS, status, + mode := "sync" + if r.Asynchronous { + mode = "async" + } + fmt.Printf("%-28s %-5s %-5s %4dx%-4d %7d %7d %8s %9.2f %8.2f %7d %9.0f %9d %s\n", + r.Provider, mode, r.Input, r.Width, r.Height, r.RequestedFPS, r.ConfiguredFPS, status, r.FirstOutputMS, r.AvgEncodeMS, r.FramesTried, r.AvgBytes, r.TotalBytes, r.Error) } }