// Copyright Epic Games, Inc. All Rights Reserved. #include "OutputChannel.h" #include "GPUTextureTransfer.h" #include "RenderingThread.h" #include "SSE2MemCpy.h" #include DECLARE_FLOAT_COUNTER_STAT(TEXT("AJA Audio Delay (s)"), STAT_AjaMediaCapture_Audio_Delay, STATGROUP_Media); #include "Misc/ScopeLock.h" static TAutoConsoleVariable CVarAjaPreRollMethod( TEXT("Aja.PrerollMethod"), 0, TEXT("Preroll method 0: Legacy method where we preroll 3 empty frames. Preroll method 1: We preroll engine frames as they come in."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarAjaPingPongVersion( TEXT("Aja.PingPongVersion"), 0, TEXT("Version 0: Legacy, Version 1: Experimental with reduced waiting."), ECVF_RenderThreadSafe); static TAutoConsoleVariable CVarAjaShowDebugMarkers( TEXT("Aja.EnableDebugMarkers"), false, TEXT("Enable to let Aja Output Channel trace more information when doing a media capture, such as bookmarks for Vertical Interrupts etc."), ECVF_RenderThreadSafe); namespace AJA { namespace Private { /* OutputChannel implementation *****************************************************************************/ bool OutputChannel::Initialize(const AJADeviceOptions& InDeviceOption, const AJAInputOutputChannelOptions& InOptions) { TPromise CompletionPromise; Uninitialize(MoveTemp(CompletionPromise)); ChannelThread.reset(new OutputChannelThread(InDeviceOption, InOptions)); bool bResult = ChannelThread->CanInitialize(); if (bResult) { Device = DeviceCache::GetDevice(InDeviceOption); std::shared_ptr SharedCommand(new IOChannelInitialize_DeviceCommand(Device, ChannelThread)); InitializeCommand = SharedCommand; Device->AddCommand(std::static_pointer_cast(SharedCommand)); } return bResult; } void OutputChannel::Uninitialize(TPromise CompletionPromise) { if (ChannelThread) { if (Device) { // We have to make sure the format is cleared upon uninitialization in case an initialization call happens soon after. Device->ClearFormat(); } std::shared_ptr SharedCommand = InitializeCommand.lock(); if (SharedCommand && Device) { Device->RemoveCommand(std::static_pointer_cast(SharedCommand)); } ChannelThread->Uninitialize(); // delete is done in IOChannelUninitialize_DeviceCommand completed if (Device) { std::shared_ptr UninitializeCommand; UninitializeCommand.reset(new IOChannelUninitialize_DeviceCommand(Device, ChannelThread, MoveTemp(CompletionPromise))); Device->AddCommand(std::static_pointer_cast(UninitializeCommand)); } else { // No need to wait in this case. CompletionPromise.SetValue(); } } else { CompletionPromise.SetValue(); } InitializeCommand.reset(); ChannelThread.reset(); Device.reset(); } bool OutputChannel::SetAncillaryFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InAncillaryBuffer, uint32_t InAncillaryBufferSize) { if (ChannelThread) { return ChannelThread->SetAncillaryFrameData(InFrameData, InAncillaryBuffer, InAncillaryBufferSize); } return false; } bool OutputChannel::SetAudioFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InAudioBuffer, uint32_t InAudioBufferSize) { if (ChannelThread) { return ChannelThread->SetAudioFrameData(InFrameData, InAudioBuffer, InAudioBufferSize); } return false; } bool OutputChannel::SetVideoFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InVideoBuffer, uint32_t InVideoBufferSize) { if (ChannelThread) { return ChannelThread->SetVideoFrameData(InFrameData, InVideoBuffer, InVideoBufferSize); } return false; } bool OutputChannel::DMAWriteAudio(const uint8_t* InAudioBufer, int32_t InAudioBufferSize) { if (ChannelThread) { return ChannelThread->DMAWriteAudio(InAudioBufer, InAudioBufferSize); } return false; } bool OutputChannel::SetVideoFrameData(const AJAOutputFrameBufferData& InFrameData, FRHITexture* RHITexture) { if (ChannelThread) { return ChannelThread->SetVideoFrameData(InFrameData, RHITexture); } return false; } bool OutputChannel::ShouldCaptureThisFrame(::FTimecode Timeocode, uint32 SourceFrameNumber) { if (ChannelThread) { return ChannelThread->ShouldCaptureThisFrame(Timeocode, SourceFrameNumber); } return false; } bool OutputChannel::GetOutputDimension(uint32_t& OutWidth, uint32_t& OutHeight) const { NTV2FormatDescriptor FormatDescriptor = ChannelThread->FormatDescriptor; bool bResult = FormatDescriptor.IsValid(); if (bResult) { OutWidth = ChannelThread->FormatDescriptor.GetRasterWidth(); OutHeight = ChannelThread->FormatDescriptor.GetRasterHeight(); } return bResult; } int32_t OutputChannel::GetNumAudioSamplesPerFrame(const AJAOutputFrameBufferData& InFrameData) const { if (ChannelThread && Device && Device->GetCard()) { NTV2FrameRate FrameRate = NTV2_FRAMERATE_INVALID; AJA_CHECK(Device->GetCard()->GetFrameRate(FrameRate, ChannelThread->Channel)); // @todo: Check if 1080p60, 1080p5994 or 1080p50 and pass SMTPE 372M flag return ::GetAudioSamplesPerFrame(FrameRate, NTV2_AUDIO_48K, InFrameData.FrameIdentifier); } return 0; } /* Frame implementation *****************************************************************************/ OutputChannelThread::Frame::Frame() : AncBuffer(nullptr) , AncF2Buffer(nullptr) , AudioBuffer(nullptr) , VideoBuffer(nullptr) { Clear(); } OutputChannelThread::Frame::~Frame() { if (AncBuffer) { AJAMemory::FreeAligned(AncBuffer); } if (AncF2Buffer) { AJAMemory::FreeAligned(AncF2Buffer); } if (AudioBuffer) { AJAMemory::FreeAligned(AudioBuffer); } if (VideoBuffer) { AJAMemory::FreeAligned(VideoBuffer); } } void OutputChannelThread::Frame::Clear() { CopiedAncBufferSize = 0; CopiedAncF2BufferSize = 0; CopiedAudioBufferSize = 0; CopiedVideoBufferSize = 0; FrameIdentifier = AJAOutputFrameBufferData::InvalidFrameIdentifier; FrameIdentifierF2 = AJAOutputFrameBufferData::InvalidFrameIdentifier; bAncLineFilled = false; bAncF2LineFilled = false; bAudioLineFilled = false; bVideoLineFilled = false; bVideoF2LineFilled = false; } /* OutputChannelThread implementation *****************************************************************************/ OutputChannelThread::OutputChannelThread(const AJADeviceOptions& InDevice, const AJAInputOutputChannelOptions& InOptions) : Super(InDevice, InOptions) , PingPongDropCount(0) , LostFrameCounter(0) , bIsSSE2Available(IsSSE2Available()) , bIsFieldAEven(true) , bIsFirstFetch(true) , bInterlacedTest_ExpectFirstLineToBeWhite(false) , InterlacedTest_FrameCounter(0) { if (FGPUTextureTransferModule::Get().IsEnabled() && FGPUTextureTransferModule::Get().IsInitialized()) { TextureTransfer = FGPUTextureTransferModule::Get().GetTextureTransfer(); } if (InOptions.OutputNumberOfBuffers > 0) { AllFrames.reserve(InOptions.OutputNumberOfBuffers); for (uint32_t Index = 0; Index < InOptions.OutputNumberOfBuffers; ++Index) { Frame* NewFrame = new Frame(); AllFrames.push_back(NewFrame); FrameReadyToWrite.push_back(NewFrame); } } FrameAvailableEvent = FPlatformProcess::GetSynchEventFromPool(); bOutputWithReduceWaiting = CVarAjaPingPongVersion.GetValueOnAnyThread() == 1; bTraceDebugMarkers = CVarAjaShowDebugMarkers.GetValueOnAnyThread(); } OutputChannelThread::~OutputChannelThread() { FPlatformProcess::ReturnSynchEventToPool(FrameAvailableEvent); } bool OutputChannelThread::CanInitialize() const { return Super::CanInitialize() && AllFrames.size() >= 1; } void OutputChannelThread::Uninitialize() { ChannelThreadBase::Uninitialize(); if (InterruptEventHandle.IsValid()) { Device->ClearOnInterruptEvent(Channel, InterruptEventHandle); } if (TextureTransfer && GetOptions().bUseGPUDMA) { std::vector BuffersToClear; BuffersToClear.reserve(AllFrames.size()); std::transform(AllFrames.cbegin(), AllFrames.cend(), std::back_inserter(BuffersToClear), [](Frame* InFrame) { return InFrame->VideoBuffer; }); // Make sure to unregister those buffers on the render thread to avoid a crash in the GPUDirect SDK. ENQUEUE_RENDER_COMMAND(ClearGPUDirectBuffers)( [BuffersToClear = std::move(BuffersToClear), TextureTransferPtr = TextureTransfer](FRHICommandListImmediate& RHICmdList) { if (TextureTransferPtr) { for (uint8* BufferAddress : BuffersToClear) { TextureTransferPtr->UnregisterBuffer(BufferAddress); } } }); } } void OutputChannelThread::DeviceThread_Destroy(DeviceConnection::CommandList& InCommandList) { for (Frame* IttFrame : AllFrames) { delete IttFrame; } AllFrames.clear(); Super::DeviceThread_Destroy(InCommandList); } // For Progressive and PSF. Find the frame that have the same identifier. (the image may have been given but the anc and audio is incoming) // For Interlaced, there is 2 options. Use Timecode to identify which is the odd and even field. OutputChannelThread::Frame* OutputChannelThread::FetchAvailableWritingFrame(const AJAOutputFrameBufferData& InFrameData) { const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; Frame* AvailableWritingFrame = nullptr; { AJAAutoLock AutoLock(&FrameLock); bool bInitFrameIdentifier = false; { // Find a matching frame identifier for (auto Begin = FrameReadyToWrite.begin(); Begin != FrameReadyToWrite.end(); ++Begin) { Frame* FrameItt = *Begin; if (bIsProgressive && FrameItt->FrameIdentifier == InFrameData.FrameIdentifier) { AvailableWritingFrame = FrameItt; break; } if (!bIsProgressive && FrameItt->FrameIdentifierF2 == InFrameData.FrameIdentifier) { AvailableWritingFrame = FrameItt; break; } } } if (AvailableWritingFrame == nullptr && !bIsProgressive) { // Find a frame where the even frame is filled but not the odd frame and have the same timecode (only in interlaced) for (auto Begin = FrameReadyToWrite.begin(); Begin != FrameReadyToWrite.end(); ++Begin) { Frame* FrameItt = *Begin; if (FrameItt->FrameIdentifier != AJAOutputFrameBufferData::InvalidFrameIdentifier && FrameItt->FrameIdentifierF2 == AJAOutputFrameBufferData::InvalidFrameIdentifier) { if (FrameItt->FrameIdentifier + 1 == InFrameData.FrameIdentifier) { if (GetOptions().bOutputInterlacedFieldsTimecodeNeedToMatch) { if (FrameItt->Timecode == InFrameData.Timecode) { AvailableWritingFrame = FrameItt; bInitFrameIdentifier = true; break; } } else { AvailableWritingFrame = FrameItt; bInitFrameIdentifier = true; break; } } } } } // Find a new empty frames if (AvailableWritingFrame == nullptr && !bIsProgressive && !GetOptions().bOutputInterlacedFieldsTimecodeNeedToMatch) { if (!GetOptions().bOutputInterlaceOnEvenFrames) { if (!InFrameData.bEvenFrame) { // If the incoming frame is interlaced and we didn't find the correspondent even field // do not create a new field. Drop it. ++LostFrameCounter; return nullptr; } } } if (AvailableWritingFrame == nullptr) { // Find an empty frame for (auto Begin = FrameReadyToWrite.begin(); Begin != FrameReadyToWrite.end(); ++Begin) { Frame* FrameItt = *Begin; if (FrameItt->FrameIdentifier == AJAOutputFrameBufferData::InvalidFrameIdentifier) { AJA_CHECK(FrameItt->FrameIdentifierF2 == AJAOutputFrameBufferData::InvalidFrameIdentifier); AvailableWritingFrame = FrameItt; bInitFrameIdentifier = true; break; } } } if (AvailableWritingFrame == nullptr) { // Take the oldest frame not yet written if (FrameReadyToWrite.size() > 0) { AvailableWritingFrame = FrameReadyToWrite[0]; uint32_t OldestNumber = AvailableWritingFrame->FrameIdentifier; auto Begin = FrameReadyToWrite.begin(); ++Begin; for (; Begin != FrameReadyToWrite.end(); ++Begin) { Frame* FrameItt = *Begin; if (FrameItt->FrameIdentifier > OldestNumber) { AvailableWritingFrame = FrameItt; OldestNumber = FrameItt->FrameIdentifier; } } // This is not thread-safe with but FetchAvailableWritingFrame & IsFrameReadyToBeRead are both in inside a lock. //So even if we are currently writing in a buffer, it will be cleared and will not be available to be added to the read list. AvailableWritingFrame->Clear(); bInitFrameIdentifier = true; ++LostFrameCounter; } } //if (AvailableWritingFrame == nullptr) //{ // // Take the newest frame that is ready // if (FrameReadyToRead.size() > 0) // { // AvailableWritingFrame = FrameReadyToRead[0]; // uint32_t NewestNumber = AvailableWritingFrame->FrameIdentifier; // auto NewestItt = std::begin(FrameReadyToRead); // auto Begin = NewestItt; // ++Begin; // auto End = std::end(FrameReadyToRead); // for (; Begin != End; ++Begin) // { // Frame* FrameItt = *Begin; // if (FrameItt->FrameIdentifier < NewestNumber) // { // AvailableWritingFrame = FrameItt; // NewestNumber = FrameItt->FrameIdentifier; // NewestItt = Begin; // } // } // // This is not thread-safe (see above) // AvailableWritingFrame->Clear(); // bInitFrameIdentifier = true; // FrameReadyToWrite.push_back(AvailableWritingFrame); // FrameReadyToRead.erase(NewestItt); // ++LostFrameCounter; // } //} if (AvailableWritingFrame && bInitFrameIdentifier) { if (AvailableWritingFrame->FrameIdentifier == AJAOutputFrameBufferData::InvalidFrameIdentifier) { AvailableWritingFrame->FrameIdentifier = InFrameData.FrameIdentifier; AvailableWritingFrame->Timecode = InFrameData.Timecode; if (bIsProgressive) { AvailableWritingFrame->FrameIdentifierF2 = InFrameData.FrameIdentifier; } } else { AvailableWritingFrame->FrameIdentifierF2 = InFrameData.FrameIdentifier; AvailableWritingFrame->Timecode2 = InFrameData.Timecode; } } } return AvailableWritingFrame; } OutputChannelThread::Frame* OutputChannelThread::Thread_FetchAvailableReadingFrame() { Frame* AvailableReadingFrame = nullptr; AJAAutoLock AutoLock(&FrameLock); if (!FrameReadyToRead.empty()) { // Take the oldest frame AvailableReadingFrame = FrameReadyToRead[0]; uint32_t OldestNumber = AvailableReadingFrame->FrameIdentifier; auto OldestItt = std::begin(FrameReadyToRead); auto Begin = OldestItt; ++Begin; auto End = std::end(FrameReadyToRead); for (; Begin != End; ++Begin) { Frame* FrameItt = *Begin; if (FrameItt->FrameIdentifier < OldestNumber) { AvailableReadingFrame = FrameItt; OldestNumber = FrameItt->FrameIdentifier; OldestItt = Begin; } } FrameReadyToRead.erase(OldestItt); } return AvailableReadingFrame; } bool OutputChannelThread::IsFrameReadyToBeRead(Frame* InFrame) { if (InFrame->FrameIdentifier == AJAOutputFrameBufferData::InvalidFrameIdentifier) { InFrame->Clear(); return false; } if (UseAncillary() && (!InFrame->bAncLineFilled || InFrame->CopiedAncBufferSize == 0)) { return false; } if (UseAncillaryField2() && (!InFrame->bAncF2LineFilled || InFrame->CopiedAncF2BufferSize == 0)) { return false; } // When writing audio directly, no need to check if the audio line is filled since we don't use it. if (UseAudio() && !InFrame->bAudioLineFilled && !GetOptions().bDirectlyWriteAudio) { return false; } if (UseVideo() && (!InFrame->bVideoF2LineFilled || !InFrame->bVideoLineFilled || InFrame->CopiedVideoBufferSize == 0)) { return false; } return true; } void OutputChannelThread::PushWhenFrameReady(Frame* InFrame) { AJAAutoLock AutoLock(&FrameLock); if (IsFrameReadyToBeRead(InFrame)) { auto FoundItt = std::find(std::begin(FrameReadyToWrite), std::end(FrameReadyToWrite), InFrame); if (FoundItt != std::end(FrameReadyToWrite)) { FrameReadyToWrite.erase(FoundItt); FrameReadyToRead.push_back(InFrame); FrameAvailableEvent->Trigger(); } } } bool OutputChannelThread::SetAncillaryFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InAncillaryBuffer, uint32_t InAncillaryBufferSize) { const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; if (!UseAncillary()) { return false; } if (InAncillaryBuffer == nullptr || InAncillaryBufferSize > AncBufferSize || (!bIsProgressive && InAncillaryBufferSize > AncF2BufferSize)) { UE_LOG(LogAjaCore, Error, TEXT("SetFrameData: Can't set the ancillary. The buffer is invalid or the buffer size is not the same as the AJA for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } Frame* AvailableWritingFrame = FetchAvailableWritingFrame(InFrameData); if (!AvailableWritingFrame) { return false; } #if AJA_TEST_MEMORY_BUFFER AJA_CHECK(AvailableWritingFrame->AncBuffer[AncBufferSize] == AJA_TEST_MEMORY_END_TAG); AJA_CHECK(AncF2BufferSize == 0 || AvailableWritingFrame->AncF2Buffer[AncF2BufferSize] == AJA_TEST_MEMORY_END_TAG); #endif if (bIsProgressive) { memcpy(AvailableWritingFrame->AncBuffer, InAncillaryBuffer, InAncillaryBufferSize); AvailableWritingFrame->bAncLineFilled = true; AvailableWritingFrame->bAncF2LineFilled = true; AvailableWritingFrame->CopiedAncBufferSize = InAncillaryBufferSize; } else { bool bField1 = AvailableWritingFrame->FrameIdentifier == InFrameData.FrameIdentifier; if (bField1) { memcpy(AvailableWritingFrame->AncBuffer, InAncillaryBuffer, InAncillaryBufferSize); AvailableWritingFrame->bAncLineFilled = true; AvailableWritingFrame->CopiedAncBufferSize = InAncillaryBufferSize; } else { memcpy(AvailableWritingFrame->AncF2Buffer, InAncillaryBuffer, InAncillaryBufferSize); AvailableWritingFrame->bAncF2LineFilled = true; AvailableWritingFrame->CopiedAncF2BufferSize = InAncillaryBufferSize; } } #if AJA_TEST_MEMORY_BUFFER AJA_CHECK(AvailableWritingFrame->AncBuffer[AncBufferSize] == AJA_TEST_MEMORY_END_TAG); AJA_CHECK(AncF2BufferSize == 0 || AvailableWritingFrame->AncF2Buffer[AncF2BufferSize] == AJA_TEST_MEMORY_END_TAG); #endif PushWhenFrameReady(AvailableWritingFrame); return true; } bool OutputChannelThread::SetAudioFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InAudioBuffer, uint32_t InAudioBufferSize) { if (!UseAudio()) { return false; } if (InAudioBufferSize > AudioBufferSize) { UE_LOG(LogAjaCore, Error, TEXT("SetAudioFrameData: Can't set the audio. The buffer size is not the same as the AJA Audio Format for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } Frame* AvailableWritingFrame = FetchAvailableWritingFrame(InFrameData); if (!AvailableWritingFrame) { return false; } #if AJA_TEST_MEMORY_BUFFER AJA_CHECK(AvailableWritingFrame->Audio[AudioBufferSize] == AJA_TEST_MEMORY_END_TAG); #endif if (InAudioBuffer && InAudioBufferSize != 0 && ensure(AvailableWritingFrame->AudioBuffer)) { memcpy(AvailableWritingFrame->AudioBuffer, InAudioBuffer, InAudioBufferSize); } AvailableWritingFrame->bAudioLineFilled = true; AvailableWritingFrame->CopiedAudioBufferSize = InAudioBufferSize; PushWhenFrameReady(AvailableWritingFrame); return true; } bool OutputChannelThread::SetVideoFrameData(const AJAOutputFrameBufferData& InFrameData, uint8_t* InVideoBuffer, uint32_t InVideoBufferSize) { const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; if (!UseVideo()) { return false; } if (InVideoBuffer == nullptr || InVideoBufferSize > VideoBufferSize) { UE_LOG(LogAjaCore, Error, TEXT("SetVideoFrameData: Can't set the video. The buffer is invalid or the buffer size is not the same as the AJA Video Format for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } Frame* AvailableWritingFrame = FetchAvailableWritingFrame(InFrameData); if (!AvailableWritingFrame) { if (bTraceDebugMarkers) { TRACE_BOOKMARK(TEXT("Aja: Dropped frame %d"), InFrameData.FrameIdentifier); } return false; } ULWord Height = FormatDescriptor.GetRasterHeight(); ULWord Stride = FormatDescriptor.GetBytesPerRow(); AJA_CHECK(InVideoBufferSize <= Stride*Height); #if AJA_TEST_MEMORY_BUFFER AJA_CHECK(AvailableWritingFrame->VideoBuffer[VideoBufferSize] == AJA_TEST_MEMORY_END_TAG); #endif if (bIsProgressive) { // Make sure SSE2 is supported and everything is correctly aligned otherwise fall back on regular memcpy if (bIsSSE2Available && IsCorrectlyAlignedForSSE2MemCpy(AvailableWritingFrame->VideoBuffer, InVideoBuffer, InVideoBufferSize)) { SSE2MemCpy(AvailableWritingFrame->VideoBuffer, InVideoBuffer, InVideoBufferSize); } else { memcpy(AvailableWritingFrame->VideoBuffer, InVideoBuffer, InVideoBufferSize); } AvailableWritingFrame->bVideoLineFilled = true; AvailableWritingFrame->bVideoF2LineFilled = true; } else { // only write the even or odd frame bool bField1 = AvailableWritingFrame->FrameIdentifier == InFrameData.FrameIdentifier; for (ULWord IndexY = bField1 ? 0 : 1; IndexY < Height; IndexY += 2) { memcpy(AvailableWritingFrame->VideoBuffer + (Stride*IndexY), InVideoBuffer + (Stride*IndexY), Stride); } if (bField1) { AvailableWritingFrame->bVideoLineFilled = true; } else { AvailableWritingFrame->bVideoF2LineFilled = true; } } #if AJA_TEST_MEMORY_BUFFER AJA_CHECK(AvailableWritingFrame->VideoBuffer[VideoBufferSize] == AJA_TEST_MEMORY_END_TAG); #endif AvailableWritingFrame->CopiedVideoBufferSize = InVideoBufferSize; PushWhenFrameReady(AvailableWritingFrame); if (bTraceDebugMarkers) { TRACE_BOOKMARK(TEXT("Aja: Pushed [%d] (Frame %d)"), InFrameData.FrameIdentifier, InFrameData.Timecode.Frames); } return true; } bool OutputChannelThread::SetVideoFrameData(const AJAOutputFrameBufferData& InFrameData, FRHITexture* RHITexture) { const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; if (!bIsProgressive) { UE_LOG(LogAjaCore, Error, TEXT("SetVideoFrameData: Can't set the video, GPUTextureTransfer is not supported with interlaced video for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } if (!UseVideo()) { return false; } if (!RHITexture) { UE_LOG(LogAjaCore, Error, TEXT("SetVideoFrameData: Can't set the video. The RHI texture is invalid for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } if (!TextureTransfer || !GetOptions().bUseGPUDMA) { UE_LOG(LogAjaCore, Error, TEXT("SetVideoFrameData: Can't set the video. GPU DMA was not setup correctly for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); return false; } Frame* AvailableWritingFrame = FetchAvailableWritingFrame(InFrameData); if (!AvailableWritingFrame) { return false; } // Todo jroy: Verify if Width needs to be fixed, by using the GPUTextureTransfer register struct directly, same for pixel format. const ULWord Stride = FormatDescriptor.GetBytesPerRow(); ULWord Width = Stride / 4; if (GetOptions().PixelFormat == EPixelFormat::PF_10BIT_YCBCR) { // YUV210 has a different stride. Width /= 4; } const ULWord Height = FormatDescriptor.GetRasterHeight(); if (!bDMABuffersRegistered) { for (Frame* IttFrame : AllFrames) { UE::GPUTextureTransfer::FRegisterDMABufferArgs Args; Args.Height = Height; Args.Width = Width; Args.Stride = Stride; Args.Buffer = IttFrame->VideoBuffer; if (GetOptions().PixelFormat == EPixelFormat::PF_8BIT_YCBCR || GetOptions().PixelFormat == EPixelFormat::PF_8BIT_ARGB || GetOptions().PixelFormat == EPixelFormat::PF_10BIT_RGB) // 10 Bit RGB should be considered as 8 bit as far as GPUDirect is concerned. { Args.PixelFormat = UE::GPUTextureTransfer::EPixelFormat::PF_8Bit; } else { Args.PixelFormat = UE::GPUTextureTransfer::EPixelFormat::PF_10Bit; } TextureTransfer->RegisterBuffer(Args); } bDMABuffersRegistered = true; } AvailableWritingFrame->bVideoLineFilled = true; AvailableWritingFrame->bVideoF2LineFilled = true; const UE::GPUTextureTransfer::ETransferDirection Direction = UE::GPUTextureTransfer::ETransferDirection::GPU_TO_CPU; TextureTransfer->TransferTexture(AvailableWritingFrame->VideoBuffer, (FRHITexture*)RHITexture, Direction); AvailableWritingFrame->CopiedVideoBufferSize = VideoBufferSize; PushWhenFrameReady(AvailableWritingFrame); return true; } bool OutputChannelThread::ShouldCaptureThisFrame(::FTimecode Timecode, uint32 SourceFrameNumber) { const bool bInterlace = !::IsProgressivePicture(VideoFormat) && !Options.bOutputInterlaceAsProgressive; if (bInterlace && Timecode.Frames % 2 != 0 && GetOptions().bOutputInterlaceOnEvenFrames) { // Odd frame should always have a matching frame. // Find a frame where the even frame is filled but not the odd frame. for (Frame* Frame : FrameReadyToWrite) { if (Frame->FrameIdentifier != AJAOutputFrameBufferData::InvalidFrameIdentifier && Frame->FrameIdentifierF2 == AJAOutputFrameBufferData::InvalidFrameIdentifier) { if (Frame->FrameIdentifier + 1 == SourceFrameNumber) { return true; } } } if (!FrameReadyToWrite.size()) { UE_LOG(LogAjaCore, Verbose, TEXT("Skipping output because no writable frame is available.")); } else { for (Frame* Frame : FrameReadyToWrite) { UE_LOG(LogAjaCore, VeryVerbose, TEXT("\t F1: %d, F2: %d"), Frame->FrameIdentifier, Frame->FrameIdentifierF2); } } // Dont output an interlace frame starting with an odd frame. return false; } return true; } bool OutputChannelThread::DeviceThread_ConfigureAnc(DeviceConnection::CommandList& InCommandList) { bool bResult = Super::DeviceThread_ConfigureAnc(InCommandList); if (AncBufferSize > 0 || AncF2BufferSize > 0) { AJAAutoLock AutoLock(&FrameLock); for (Frame* IttFrame : AllFrames) { AJA_CHECK(IttFrame->AncBuffer == nullptr); AJA_CHECK(IttFrame->AncF2Buffer == nullptr); if (AncBufferSize > 0) { AJA_CHECK(UseAncillary()); #if AJA_TEST_MEMORY_BUFFER IttFrame->AncBuffer = (uint8_t*)AJAMemory::AllocateAligned(AncBufferSize + 1, AJA_PAGE_SIZE); IttFrame->AncBuffer[AncBufferSize] = AJA_TEST_MEMORY_END_TAG; #else IttFrame->AncBuffer = (uint8_t*)AJAMemory::AllocateAligned(AncBufferSize, AJA_PAGE_SIZE); #endif } if (AncF2BufferSize > 0) { AJA_CHECK(UseAncillaryField2()); #if AJA_TEST_MEMORY_BUFFER IttFrame->AncF2Buffer = (uint8_t*)AJAMemory::AllocateAligned(AncF2BufferSize + 1, AJA_PAGE_SIZE); IttFrame->AncF2Buffer[AncF2BufferSize] = AJA_TEST_MEMORY_END_TAG; #else IttFrame->AncF2Buffer = (uint8_t*)AJAMemory::AllocateAligned(AncF2BufferSize, AJA_PAGE_SIZE); #endif } } } return bResult; } bool OutputChannelThread::DeviceThread_ConfigureAudio(DeviceConnection::CommandList& InCommandList) { bool bResult = Super::DeviceThread_ConfigureAudio(InCommandList); if(bResult && UseAudio()) { NTV2FrameRate CardFrameRate = NTV2_FRAMERATE_INVALID; AJA_CHECK(Device->GetCard()->GetFrameRate(CardFrameRate, Channel)); NumSamplesPerFrame = ::GetAudioSamplesPerFrame(CardFrameRate, NTV2_AUDIO_48K); if (ensureMsgf(AudioBufferSize > 0, TEXT("Audio could not be initialized."))) { if (!GetOptions().bDirectlyWriteAudio) { AJAAutoLock AutoLock(&FrameLock); for (Frame* IttFrame : AllFrames) { AJA_CHECK(IttFrame->AudioBuffer == nullptr); #if AJA_TEST_MEMORY_BUFFER IttFrame->AudioBuffer = (uint8_t*)AJAMemory::AllocateAligned(AudioBufferSize + 1, AJA_PAGE_SIZE); IttFrame->AudioBuffer[AudioBufferSize] = AJA_TEST_MEMORY_END_TAG; #else IttFrame->AudioBuffer = (uint8_t*)AJAMemory::AllocateAligned(AudioBufferSize, AJA_PAGE_SIZE); #endif memset(IttFrame->AudioBuffer, 0, AudioBufferSize); } } if (GetOptions().bUseAutoCirculating) { bResult &= GetDevice().StartAudioOutput(AudioSystem); if (bResult == false) { UE_LOG(LogAjaCore, Error, TEXT("ConfigureAudio: Could not start audio output for audio system %d for channel %d on device %S."), uint32_t(AudioSystem), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); } } } } return bResult; } bool OutputChannelThread::DeviceThread_ConfigureVideo(DeviceConnection::CommandList& InCommandList) { bool bResult = Super::DeviceThread_ConfigureVideo(InCommandList); FrameRate = Helpers::ConvertToUnrealFramerate(DesiredVideoFormat); if (VideoBufferSize > 0) { AJAAutoLock AutoLock(&FrameLock); for (Frame* IttFrame : AllFrames) { AJA_CHECK(IttFrame->VideoBuffer == nullptr); #if AJA_TEST_MEMORY_BUFFER IttFrame->VideoBuffer = (uint8_t*)AJAMemory::AllocateAligned(VideoBufferSize + 1, AJA_PAGE_SIZE); IttFrame->VideoBuffer[VideoBufferSize] = AJA_TEST_MEMORY_END_TAG; #else IttFrame->VideoBuffer = (uint8_t*)AJAMemory::AllocateAligned(VideoBufferSize, AJA_PAGE_SIZE); #endif } } return bResult; } void OutputChannelThread::Thread_PushAvailableReadingFrame(Frame* InCurrentReadingFrame) { // reset the buffer size for the next write AJAAutoLock AutoLock(&FrameLock); InCurrentReadingFrame->Clear(); FrameReadyToWrite.push_back(InCurrentReadingFrame); } bool OutputChannelThread::DeviceThread_ConfigureAutoCirculate(DeviceConnection::CommandList& InCommandList) { const int32_t NumberOfLinkChannel = Helpers::GetNumberOfLinkChannel(GetOptions().TransportType); for (int32_t ChannelIndex = 0; ChannelIndex < NumberOfLinkChannel; ++ChannelIndex) { const NTV2Channel ChannelItt = NTV2Channel(int32_t(Channel) + ChannelIndex); AJA_CHECK(GetDevice().AutoCirculateStop(ChannelItt)); } { // NB. We are already lock ULWord OptionFlags = (UseTimecode() ? AUTOCIRCULATE_WITH_RP188 : 0); OptionFlags |= (UseAncillary() || UseAncillaryField2() ? AUTOCIRCULATE_WITH_ANC : 0); UByte FrameCount = 0; UByte FirstFrame = BaseFrameIndex; UByte EndFrame = FirstFrame + DeviceConnection::NumberOfFrameForAutoCirculate - 1; AJA_CHECK(GetDevice().AutoCirculateInitForOutput(Channel, FrameCount, AudioSystem, OptionFlags, 1, FirstFrame, EndFrame)); } AJA_CHECK(GetDevice().AutoCirculateStart(Channel)); AJA_CHECK(Device->WaitForInputOrOutputInterrupt(Channel)); return true; } void OutputChannelThread::Thread_AutoCirculateLoop() { const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; bool bRunning = true; bool bSleep = true; AUTOCIRCULATE_TRANSFER Transfer; while (bRunning && !bStopRequested) { bool bIsValidWaitForOutput = true; if (bSleep) { // I would expect to sync here, but it seems that causes an extra frame // of latency, so sleep, until the next frame is ready instead. ::Sleep(0); } else { if (bIsProgressive) { bRunning = Device->WaitForInputOrOutputInterrupt(Channel); } else { // In Interlaced, wait for Field0 to continue but warn the user when field is 1 (in case he wants to be v-sync) NTV2FieldID FieldId = NTV2_FIELD_INVALID; bRunning = Device->WaitForInputOrOutputInputField(Channel, 1, FieldId); bIsValidWaitForOutput = (bRunning && FieldId == NTV2_FIELD0); } } if (!bRunning) { UE_LOG(LogAjaCore, Error, TEXT("AutoCirculate: Can't wait for the output field for channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); if (GetOptions().bStopOnTimeout) { break; } else { bRunning = true; } } AUTOCIRCULATE_STATUS ChannelStatus; bRunning = GetDevice().AutoCirculateGetStatus(Channel, ChannelStatus); if (!bRunning) { UE_LOG(LogAjaCore, Error, TEXT("AutoCirculate: Can't get the status for output channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); break; } if (bStopRequested) { // After the WaitForInputOrOutputInterrupt, it's possible that bStopRequested has been modified. bRunning = false; break; } if (!bIsValidWaitForOutput) { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { GetOptions().CallbackInterface->OnOutputFrameStarted(); } continue; } bSleep = true; if (ChannelStatus.CanAcceptMoreOutputFrames()) { bSleep = false; { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { GetOptions().CallbackInterface->OnOutputFrameStarted(); } } Frame* AvailableReadingFrame = Thread_FetchAvailableReadingFrame(); if (AvailableReadingFrame) { Thread_TestInterlacedOutput(AvailableReadingFrame); AJAOutputFrameData FrameData; FrameData.FramesDropped = ChannelStatus.acFramesDropped; FrameData.FramesLost = LostFrameCounter; bSleep = false; if ((UseAncillary() && AvailableReadingFrame->CopiedAncBufferSize) || (UseAncillaryField2() && AvailableReadingFrame->CopiedAncF2BufferSize)) { ULWord* Ancillary = UseAncillary() ? reinterpret_cast(AvailableReadingFrame->AncBuffer) : nullptr; ULWord AncillarySize = UseAncillary() ? AvailableReadingFrame->CopiedAncBufferSize : 0; ULWord* AncillaryF2 = UseAncillaryField2() ? reinterpret_cast(AvailableReadingFrame->AncF2Buffer) : nullptr; ULWord AncillaryF2Size = UseAncillaryField2() ? AvailableReadingFrame->CopiedAncF2BufferSize : 0; Transfer.SetAncBuffers(Ancillary, AncBufferSize, AncillaryF2, AncF2BufferSize); } if (UseAudio() && AvailableReadingFrame->CopiedAudioBufferSize) { Transfer.SetAudioBuffer(reinterpret_cast(AvailableReadingFrame->AudioBuffer), AvailableReadingFrame->CopiedAudioBufferSize); } if (UseVideo() && AvailableReadingFrame->CopiedVideoBufferSize && TextureTransfer && GetOptions().bUseGPUDMA) { const UE::GPUTextureTransfer::ETransferDirection Direction = UE::GPUTextureTransfer::ETransferDirection::GPU_TO_CPU; TextureTransfer->BeginSync(AvailableReadingFrame->VideoBuffer, Direction); } if (UseVideo() && AvailableReadingFrame->CopiedVideoBufferSize) { if (bIsProgressive) { BurnTimecode(AvailableReadingFrame->Timecode, AvailableReadingFrame->VideoBuffer); } else { BurnTimecode(AvailableReadingFrame->Timecode, AvailableReadingFrame->Timecode2, AvailableReadingFrame->VideoBuffer); } Transfer.SetVideoBuffer(reinterpret_cast(AvailableReadingFrame->VideoBuffer), AvailableReadingFrame->CopiedVideoBufferSize); } if (UseTimecode()) { FrameData.Timecode = AvailableReadingFrame->Timecode; const FTimecode ConvertedTimecode = Helpers::AdjustTimecodeFromUE(VideoFormat, AvailableReadingFrame->Timecode); NTV2_RP188 Timecode = Helpers::ConvertTimecodeToRP188(ConvertedTimecode); Transfer.SetOutputTimeCode(Timecode, NTV2ChannelToTimecodeIndex(Channel)); //Set timecode on LTC and VITC. Do not use SetAllOutputtimecode. It trashes timecodes of other pins } AJA_CHECK(GetDevice().AutoCirculateTransfer(Channel, Transfer)); if (UseVideo() && AvailableReadingFrame->CopiedVideoBufferSize && TextureTransfer && GetOptions().bUseGPUDMA) { TextureTransfer->EndSync(AvailableReadingFrame->VideoBuffer); } Thread_PushAvailableReadingFrame(AvailableReadingFrame); { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { bRunning = GetOptions().CallbackInterface->OnOutputFrameCopied(FrameData); } } if (!bRunning) { break; } } } } AJA_CHECK(GetDevice().AutoCirculateStop(Channel)); // Write the requested test pattern into host buffer... { NTV2TestPatternGen testPatternGen; NTV2Buffer testPatternBuffer; testPatternGen.DrawTestPattern(NTV2TestPatternSelect::NTV2_TestPatt_ColorBars100, FormatDescriptor, testPatternBuffer); for (UByte FrameIndex = BaseFrameIndex; FrameIndex < BaseFrameIndex + DeviceConnection::NumberOfFrameForAutoCirculate; ++FrameIndex) { GetDevice().DMAWriteFrame(FrameIndex, reinterpret_cast (testPatternBuffer.GetHostPointer()), uint32_t(testPatternBuffer.GetByteCount())); } } if (!bStopRequested) { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { GetOptions().CallbackInterface->OnCompletion(bRunning); } } } bool OutputChannelThread::DeviceThread_ConfigurePingPong(DeviceConnection::CommandList& InCommandList) { Frame* CurrentFrame = nullptr; { AJAAutoLock AutoLock(&FrameLock); if (AllFrames.size() == 0) { return false; } CurrentFrame = AllFrames.front(); } ThreadLock_PingPongOutputLoop_Memset0(CurrentFrame); Device->WaitForInputOrOutputInterrupt(Channel); if (bTraceDebugMarkers) { InterruptEventHandle = Device->SetOnInterruptEvent(Channel, DeviceConnection::FOnVerticalInterrupt::FDelegate::CreateRaw(this, &OutputChannelThread::OnVerticalInterrupt)); } if (CVarAjaPreRollMethod.GetValueOnAnyThread() == 0) { // Clear the current Buffer and initialize the ping-pong const int32_t NumOutputFrameIndex = 2; for (int32_t Index = 0; Index < NumOutputFrameIndex; ++Index) { AJA_CHECK(GetDevice().SetOutputFrame(Channel, BaseFrameIndex + Index)); //@TODO fix ancillary for pingpong. AJA told us they would help if (UseAncillary()) { NTV2_POINTER AncBufferPointer(CurrentFrame->AncBuffer, CurrentFrame->CopiedAncBufferSize); GetDevice().DMAWriteAnc(Index, AncBufferPointer); } if (UseAncillaryField2()) { NTV2_POINTER AncF2BufferPointer(CurrentFrame->AncF2Buffer, CurrentFrame->CopiedAncF2BufferSize); GetDevice().DMAWriteAnc(Index, AncF2BufferPointer); } if (UseAudio()) { GetDevice().DMAWriteAudio(AudioSystem, reinterpret_cast(CurrentFrame->AudioBuffer), 0, CurrentFrame->CopiedAudioBufferSize); CurrentAudioWriteOffset += CurrentFrame->CopiedAudioBufferSize; } if (UseVideo()) { AJA_CHECK(GetDevice().DMAWriteFrame(BaseFrameIndex + Index, reinterpret_cast(CurrentFrame->VideoBuffer), CurrentFrame->CopiedVideoBufferSize, Channel)); if (TextureTransfer && GetOptions().bUseGPUDMA) { TextureTransfer->EndSync(CurrentFrame->VideoBuffer); } } Device->WaitForInputOrOutputInterrupt(Channel); } ULWord VBICount = 0; AJA_CHECK(GetDevice().GetOutputVerticalInterruptCount(VBICount, Channel)); // SetOutputFrame will tell the card that a frame should be transferred starting on the next VBI, so it will be transferring starting at CurrentVBI + 1, and be done transferring at CurrentVBI + 2 BufferFreeIndex[0] = VBICount + 2; BufferFreeIndex[1] = BufferFreeIndex[0] + 1; // Before the main loop starts, ping-pong the buffers so the hardware will use // different buffers than the ones it was using while idling... uint32_t CurrentOutFrame = 0; CurrentOutFrame ^= 1; AJA_CHECK(GetDevice().SetOutputFrame(Channel, BaseFrameIndex + CurrentOutFrame)); } return true; } void OutputChannelThread::ThreadLock_PingPongOutputLoop_Memset0(Frame* InCurrentReadingFrame) { if (InCurrentReadingFrame->AncBuffer) { AJA_CHECK(UseAncillary()); memset(InCurrentReadingFrame->AncBuffer, 0, AncBufferSize); } if (InCurrentReadingFrame->AncF2Buffer) { AJA_CHECK(UseAncillaryField2()); memset(InCurrentReadingFrame->AncF2Buffer, 0, AncF2BufferSize); } if (InCurrentReadingFrame->AudioBuffer) { AJA_CHECK(UseAudio()); memset(InCurrentReadingFrame->AudioBuffer, 0, AudioBufferSize); } if (InCurrentReadingFrame->VideoBuffer) { AJA_CHECK(UseVideo()); memset(InCurrentReadingFrame->VideoBuffer, 0, VideoBufferSize); } } // As reference: this code was inspired by NTV2LLBurn::ProcessFrames(void) void OutputChannelThread::Thread_PingPongLoop() { bool bHaveOutputOnce = false; const bool bIsProgressive = ::IsProgressivePicture(VideoFormat) || Options.bOutputInterlaceAsProgressive; uint32_t CurrentOutFrame = 0; // If prerolling normally, then we're targetting the free buffer. CurrentOutFrame ^= 1; if (TextureTransfer) { TextureTransfer->ThreadPrep(); } uint32_t LastFrameDropCount = 0; auto LogDropFrames = [&LastFrameDropCount, this]() { UE_LOG(LogAjaCore, Warning, TEXT("PingPong: Dropped %d frames while outputting on channel %d on device %S.\n"), LastFrameDropCount, uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); }; bool bRunning = true; while (bRunning && !bStopRequested) { if (bOutputWithReduceWaiting) { const double MaxWaitTime = FrameRate.AsInterval(); { TRACE_CPUPROFILER_EVENT_SCOPE(AjaOutputChannelThread::WaitingForAvailableFrame); if (!FrameAvailableEvent->Wait(MaxWaitTime * 1000)) { LastFrameDropCount++; continue; } } } // Wait until the input has completed capturing a frame... bool bIsValidWaitForOutput = true; if (bIsProgressive) { if (bOutputWithReduceWaiting) { ULWord VBICount = 0; AJA_CHECK(GetDevice().GetOutputVerticalInterruptCount(VBICount, Channel)); // If we're still using the card buffer, wait until next VBI, otherwise go ahead. const uint32 OffScreenBuffer = CurrentOutFrame ^ 1; if (VBICount + 2 == BufferFreeIndex[CurrentOutFrame]) { TRACE_CPUPROFILER_EVENT_SCOPE(AjaOutputChannelThread::DroppedFrame); // We have already scheduled an output in the same VBI period, this usually occurs after a hitch, so we should drop one of our frames if (Frame* AvailableReadingFrame = Thread_FetchAvailableReadingFrame()) { Thread_PushAvailableReadingFrame(AvailableReadingFrame); } continue; } else if (VBICount < BufferFreeIndex[OffScreenBuffer]) { // Off screen buffer is not available, so wait until a VBI occurs so we know it's free. TRACE_CPUPROFILER_EVENT_SCOPE(AjaOutputChannelThread::WaitForInputOrOutputInterrupt); bRunning = Device->WaitForInputOrOutputInterrupt(Channel); } } else { bRunning = Device->WaitForInputOrOutputInterrupt(Channel); } } else { // In Interlaced, wait for Field0 to continue but warn the user when field is 1 (in case he wants to be v-sync) NTV2FieldID FieldId = NTV2_FIELD_INVALID; bRunning = Device->WaitForInputOrOutputInputField(Channel, 1, FieldId); bIsValidWaitForOutput = (bRunning && FieldId == NTV2_FIELD0); } FScopeLock DeviceLock(&DeviceCriticalSection); if (!bRunning) { UE_LOG(LogAjaCore, Error, TEXT("PingPong: Can't wait for the output field for channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); if (GetOptions().bStopOnTimeout) { break; } else { bRunning = true; } } if (bStopRequested) { // After the WaitForInputOrOutputInterrupt, it's possible that bStopRequested has been modified. bRunning = false; break; } { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { GetOptions().CallbackInterface->OnOutputFrameStarted(); } } if (!bIsValidWaitForOutput) { continue; } Frame* AvailableReadingFrame = Thread_FetchAvailableReadingFrame(); if (AvailableReadingFrame) { if (LastFrameDropCount != 0) { LogDropFrames(); LastFrameDropCount = 0; } Thread_TestInterlacedOutput(AvailableReadingFrame); // Flip sense of the buffers again to refer to the buffers that the hardware isn't using (i.e. the off-screen buffers)... CurrentOutFrame ^= 1; // Check for dropped frames by ensuring the hardware has not started to process // the buffers that were just filled.... uint32_t ReadBackIn; AJA_CHECK(GetDevice().GetOutputFrame(Channel, ReadBackIn)); if (ReadBackIn == BaseFrameIndex + CurrentOutFrame && bHaveOutputOnce) { ++PingPongDropCount; } AJAOutputFrameData FrameData; FrameData.FramesDropped = PingPongDropCount; FrameData.FramesLost = LostFrameCounter; if (AvailableReadingFrame->CopiedAncBufferSize > 0) { NTV2_POINTER AncBufferPointer(AvailableReadingFrame->AncBuffer, AvailableReadingFrame->CopiedAncBufferSize); bRunning = bRunning && GetDevice().DMAWriteAnc(CurrentOutFrame, AncBufferPointer); } if (AvailableReadingFrame->CopiedAncF2BufferSize > 0) { NTV2_POINTER AncF2BufferPointer(AvailableReadingFrame->AncF2Buffer, AvailableReadingFrame->CopiedAncF2BufferSize); bRunning = bRunning && GetDevice().DMAWriteAnc(CurrentOutFrame, AncF2BufferPointer); } ULWord VBICount = 0; AJA_CHECK(GetDevice().GetOutputVerticalInterruptCount(VBICount, Channel)); if (AvailableReadingFrame->CopiedVideoBufferSize > 0) { if (bIsProgressive) { BurnTimecode(AvailableReadingFrame->Timecode, AvailableReadingFrame->VideoBuffer); } else { BurnTimecode(AvailableReadingFrame->Timecode, AvailableReadingFrame->Timecode2, AvailableReadingFrame->VideoBuffer); } if (TextureTransfer && GetOptions().bUseGPUDMA) { const UE::GPUTextureTransfer::ETransferDirection Direction = UE::GPUTextureTransfer::ETransferDirection::GPU_TO_CPU; TextureTransfer->BeginSync(AvailableReadingFrame->VideoBuffer, Direction); } { TRACE_CPUPROFILER_EVENT_SCOPE(AjaOutputChannelThread::DMAWriteFrame); bRunning = bRunning && GetDevice().DMAWriteFrame(BaseFrameIndex + CurrentOutFrame, reinterpret_cast(AvailableReadingFrame->VideoBuffer), AvailableReadingFrame->CopiedVideoBufferSize, Channel); } if (TextureTransfer && GetOptions().bUseGPUDMA) { TextureTransfer->EndSync(AvailableReadingFrame->VideoBuffer); } } if (UseAudio() && AvailableReadingFrame->CopiedAudioBufferSize && !GetOptions().bDirectlyWriteAudio) { bRunning &= Thread_HandleAudio(TEXT("PingPong"), AvailableReadingFrame); } if (!bRunning) { UE_LOG(LogAjaCore, Error, TEXT("PingPong: Can't do the DMA frame transfer for channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); break; } // Determine the for the DMA timecode value if (UseTimecode()) { FrameData.Timecode = AvailableReadingFrame->Timecode; // As reference: this code was inspirered by NTV2LLBurn::InputSignalHasTimecode { const FTimecode ConvertedTimecode = Helpers::AdjustTimecodeFromUE(VideoFormat, AvailableReadingFrame->Timecode); NTV2_RP188 Timecode = Helpers::ConvertTimecodeToRP188(ConvertedTimecode); const int32_t NumberOfLinkChannel = Helpers::GetNumberOfLinkChannel(GetOptions().TransportType); { for (int32_t ChannelIndex = 0; ChannelIndex < NumberOfLinkChannel; ++ChannelIndex) { const NTV2Channel ChannelItt = NTV2Channel(int32_t(Channel) + ChannelIndex); AJA_CHECK(GetDevice().SetRP188Data(ChannelItt, Timecode)); } } } } // Tell the hardware which buffers to start using at the beginning of the next frame... if (bTraceDebugMarkers) { TRACE_BOOKMARK(TEXT("Aja: SetOutputFrame [%d] (Buffer %d)"), AvailableReadingFrame->FrameIdentifier, CurrentOutFrame); } AJA_CHECK(GetDevice().SetOutputFrame(Channel, BaseFrameIndex + CurrentOutFrame)); if (!bRunning) { UE_LOG(LogAjaCore, Error, TEXT("PingPong: Can't do the DMA frame transfer for channel %d on device %S.\n"), uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); Thread_PushAvailableReadingFrame(AvailableReadingFrame); break; } // SetOutputFrame will tell the card that a frame should be transferred starting on the next VBI, so it will be transferring starting at CurrentVBI + 1, and be done transferring at CurrentVBI + 2 uint32 FrameAvailableVBICount = VBICount + 2; BufferFreeIndex[CurrentOutFrame] = FrameAvailableVBICount; BufferFrameIdMapping[CurrentOutFrame] = AvailableReadingFrame->FrameIdentifier; Thread_PushAvailableReadingFrame(AvailableReadingFrame); bHaveOutputOnce = true; { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { bRunning = GetOptions().CallbackInterface->OnOutputFrameCopied(FrameData); } } } else if (GetOptions().bDisplayWarningIfDropFrames) { TRACE_CPUPROFILER_EVENT_SCOPE(OutputChannel::NoFramesAvailable); if (UseAudio() && !GetOptions().bDirectlyWriteAudio) { Thread_HandleLostFrameAudio(); } static constexpr uint32 NumMaxFrameBeforeWarning = 50; LastFrameDropCount++; if (LastFrameDropCount > NumMaxFrameBeforeWarning) { LogDropFrames(); LastFrameDropCount = 0; } } } // Write the requested test pattern into host buffer... { NTV2TestPatternGen testPatternGen; NTV2Buffer testPatternBuffer; testPatternGen.DrawTestPattern(NTV2TestPatternSelect::NTV2_TestPatt_ColorBars100, FormatDescriptor, testPatternBuffer); CurrentOutFrame ^= 1; GetDevice().DMAWriteFrame(BaseFrameIndex + CurrentOutFrame, reinterpret_cast (testPatternBuffer.GetHostPointer()), uint32_t(testPatternBuffer.GetByteCount())); CurrentOutFrame ^= 1; GetDevice().DMAWriteFrame(BaseFrameIndex + CurrentOutFrame, reinterpret_cast (testPatternBuffer.GetHostPointer()), uint32_t(testPatternBuffer.GetByteCount())); } if (!bStopRequested) { AJAAutoLock AutoLock(&Lock); if (GetOptions().CallbackInterface) { GetOptions().CallbackInterface->OnCompletion(bRunning); } } if (TextureTransfer) { TextureTransfer->ThreadCleanup(); } } void OutputChannelThread::Thread_TestInterlacedOutput(Frame* InFrame) { if (!GetOptions().bTEST_OutputInterlaced) { return; } struct FUYV422Format { uint8_t V; uint8_t Y1; uint8_t U; uint8_t Y0; }; auto IsLineWhite = [](const FUYV422Format* pLine) -> bool { return pLine->Y0 >= 0x0 && pLine->Y0 < 0x20 && pLine->Y1 >= 0x0 && pLine->Y1 < 0x20; }; const FUYV422Format* pFirstLine = reinterpret_cast(InFrame->VideoBuffer); const FUYV422Format* pSecondLine = pFirstLine + FormatDescriptor.GetBytesPerRow()/sizeof(FUYV422Format); if (InterlacedTest_FrameCounter == 0) { bInterlacedTest_ExpectFirstLineToBeWhite = IsLineWhite(pFirstLine); } bool bIsFirstLineWhite = IsLineWhite(pFirstLine); bool pIsSecondLineWhite = IsLineWhite(pSecondLine); if (bIsFirstLineWhite == pIsSecondLineWhite) { UE_LOG(LogAjaCore, Error, TEXT("INTERLACED TEST - The 2 lines are the same color. %d\n"), InterlacedTest_FrameCounter); } if (bIsFirstLineWhite != bInterlacedTest_ExpectFirstLineToBeWhite) { bInterlacedTest_ExpectFirstLineToBeWhite = !bInterlacedTest_ExpectFirstLineToBeWhite; UE_LOG(LogAjaCore, Error, TEXT("INTERLACED TEST - The lines has swap color. %d\n"), InterlacedTest_FrameCounter); } ++InterlacedTest_FrameCounter; } bool OutputChannelThread::Thread_GetAudioOffset(int32& OutAudioOffset) { bool bSuccess = true; ULWord AudioWrapOffset = 0; ULWord PlayHeadPosition = 0; bSuccess &= GetDevice().GetAudioWrapAddress(AudioWrapOffset); bSuccess &= GetDevice().ReadAudioLastOut(PlayHeadPosition); // Calculate audio offset if (CurrentAudioWriteOffset > PlayHeadPosition) { OutAudioOffset = CurrentAudioWriteOffset - PlayHeadPosition; } else { OutAudioOffset = (AudioWrapOffset - PlayHeadPosition) + CurrentAudioWriteOffset; } return bSuccess; } bool OutputChannelThread::Thread_GetAudioOffsetInSeconds(double& OutAudioOffset) { int32 OffsetInBytes = 0; if (Thread_GetAudioOffset(OffsetInBytes)) { OutAudioOffset = (OffsetInBytes / sizeof(int32)) / 48000.f / Options.NumberOfAudioChannel; return true; } return false; } bool OutputChannelThread::Thread_GetAudioOffsetInSamples(int32& OutAudioOffset) { int32 OffsetInBytes = 0; if (Thread_GetAudioOffset(OffsetInBytes)) { OutAudioOffset = FMath::RoundToInt32((float)OffsetInBytes / Options.NumberOfAudioChannel / sizeof(int32)); return true; } return false; } bool OutputChannelThread::Thread_TransferAudioBuffer(const uint8* InBuffer, int32 InBufferSize) { bool bSuccess = true; ULWord AudioWrapOffset = 0; bSuccess &= GetDevice().GetAudioWrapAddress(AudioWrapOffset); if (CurrentAudioWriteOffset + InBufferSize < AudioWrapOffset) { // Simplest case, just write the whole data to the SDRAM. bSuccess &= GetDevice().DMAWriteAudio(AudioSystem, reinterpret_cast(InBuffer), CurrentAudioWriteOffset, InBufferSize); CurrentAudioWriteOffset += InBufferSize; } else { // Audio data will wrap, so write in two parts. ULWord BytesUntilBufferEnd = AudioWrapOffset - CurrentAudioWriteOffset; LWord RemainingBytes = InBufferSize - BytesUntilBufferEnd; // We might technically have space remaining, but we can't write there since it's too close to the wrap address. if (RemainingBytes < 0) { RemainingBytes = 0; } bSuccess &= GetDevice().DMAWriteAudio(AudioSystem, reinterpret_cast(InBuffer), CurrentAudioWriteOffset, BytesUntilBufferEnd); if (RemainingBytes > 0) { bSuccess &= GetDevice().DMAWriteAudio(AudioSystem, reinterpret_cast(InBuffer + BytesUntilBufferEnd), 0, RemainingBytes); } CurrentAudioWriteOffset = RemainingBytes; } return bSuccess; } bool OutputChannelThread::Thread_HandleAudio(const FString& OutputMethod, Frame* AvailableReadingFrame) { bool bAudioTransferWasSuccessful = true; bool bAudioSuccess = true; bool bAudioOutputRunning = false; bAudioSuccess &= GetDevice().IsAudioOutputRunning(AudioSystem, bAudioOutputRunning); bool bIsPaused = false; bAudioSuccess &= GetDevice().GetAudioOutputPause(AudioSystem, bIsPaused); if (bIsPaused && bAudioOutputRunning && !Thread_ShouldPauseAudioOutput()) { UE_LOG(LogAjaCore, Verbose, TEXT("%s: Resuming audio output."), *OutputMethod); bool bPausePlayback = false; bAudioSuccess &= GetDevice().SetAudioOutputPause(AudioSystem, bPausePlayback); bIsPaused = true; } else if (!bAudioOutputRunning) { // Only offset the start ptr if this is1 the first time we're starting the audio output. ULWord WrapOffset = 0; bAudioSuccess &= GetDevice().GetAudioWrapAddress(WrapOffset); int32 NumOffsetFrames = 1; int32 InitialOffset = Options.NumberOfAudioChannel * NumSamplesPerFrame * NumOffsetFrames * sizeof(int32); CurrentAudioWriteOffset = InitialOffset; UE_LOG(LogAjaCore, Verbose, TEXT("%s: Starting audio output."), *OutputMethod); bAudioSuccess &= GetDevice().StartAudioOutput(AudioSystem); // This resets the playhead to 0, we have to override that to something closer to the write head in order to reduce audio delay } if (bAudioSuccess) { double OffsetInSeconds = 0; bAudioSuccess &= Thread_GetAudioOffsetInSeconds(OffsetInSeconds); ULWord PlayHeadPosition = 0; bAudioSuccess &= GetDevice().ReadAudioLastOut(PlayHeadPosition, AudioSystem); AudioPlayheadLastPosition = PlayHeadPosition; SET_FLOAT_STAT(STAT_AjaMediaCapture_Audio_Delay, OffsetInSeconds); UE_LOG(LogAjaCore, Verbose, TEXT("%s: Play head: %d, Write Head: %d, Offset: %f"), *OutputMethod, AudioPlayheadLastPosition.load(), CurrentAudioWriteOffset.load(), OffsetInSeconds); bAudioTransferWasSuccessful &= Thread_TransferAudioBuffer(AvailableReadingFrame->AudioBuffer, AvailableReadingFrame->CopiedAudioBufferSize); int32 OffsetInSamples = 0; bAudioSuccess &= Thread_GetAudioOffsetInSamples(OffsetInSamples); if (bAudioSuccess && Thread_ShouldPauseAudioOutput()) { UE_LOG(LogAjaCore, Warning, TEXT("%s: Audio play head is catching up to write head for channel %d on device %S.\n"), *OutputMethod, uint32_t(Channel) + 1, GetDevice().GetDisplayName().c_str()); bool bPausePlayout = true; GetDevice().SetAudioOutputPause(AudioSystem, bPausePlayout); UE_LOG(LogAjaCore, Verbose, TEXT("%s Pausing audio output because the play head is catching up to the write head. Last playhead position: %d"), *OutputMethod, AudioPlayheadLastPosition.load()); } } else { UE_LOG(LogAjaCore, Verbose, TEXT("%s Audio output failed."), *OutputMethod); } return bAudioTransferWasSuccessful; } bool OutputChannelThread::DMAWriteAudio(const uint8_t* InAudioBuffer, int32_t BufferSize) { if (!Device || !Device->GetCard()) { return false; } TRACE_CPUPROFILER_EVENT_SCOPE(OutputChannelThread::DMAWriteAudio); FScopeLock DeviceLock(&DeviceCriticalSection); bool bAudioSuccess = true; bool bAudioOutputRunning = false; bAudioSuccess &= GetDevice().IsAudioOutputRunning(AudioSystem, bAudioOutputRunning); UE_CLOG(!bAudioSuccess, LogAjaCore, Verbose, TEXT("IsAudioOutputRunning returned false.")); bool bIsPaused = false; bAudioSuccess &= GetDevice().GetAudioOutputPause(AudioSystem, bIsPaused); UE_CLOG(!bAudioSuccess, LogAjaCore, Verbose, TEXT("GetAudioOutputPause returned false.")); // Start audio output as late as possible to reduce delay. if (!bAudioOutputRunning) { // Only offset the start ptr if this is the first time we're starting the audio output. ULWord WrapOffset = 0; bAudioSuccess &= GetDevice().GetAudioWrapAddress(WrapOffset); constexpr int32 NumOffsetFrames = 4; const int32 InitialOffset = Options.NumberOfAudioChannel * NumSamplesPerFrame * NumOffsetFrames * sizeof(int32); CurrentAudioWriteOffset = InitialOffset; UE_LOG(LogAjaCore, Verbose, TEXT("PingPong: Starting audio output.")); bAudioSuccess &= GetDevice().StartAudioOutput(AudioSystem); // This resets the playhead to 0, we have to override that to something closer to the write head in order to reduce audio delay } double OffsetInSeconds = 0; bAudioSuccess &= Thread_GetAudioOffsetInSeconds(OffsetInSeconds); UE_CLOG(!bAudioSuccess, LogAjaCore, Verbose, TEXT("Thread_GetAudioOffsetInSeconds returned false.")); ULWord PlayHeadPosition = 0; bAudioSuccess &= GetDevice().ReadAudioLastOut(PlayHeadPosition, AudioSystem); UE_CLOG(!bAudioSuccess, LogAjaCore, Verbose, TEXT("ReadAudioLastOut returned false.")); AudioPlayheadLastPosition = PlayHeadPosition; SET_FLOAT_STAT(STAT_AjaMediaCapture_Audio_Delay, OffsetInSeconds); UE_LOG(LogAjaCore, Verbose, TEXT("%s: Play head: %d, Write Head: %d, Offset: %f"), TEXT("PingPong"), AudioPlayheadLastPosition.load(), CurrentAudioWriteOffset.load(), OffsetInSeconds); bAudioSuccess &= Thread_TransferAudioBuffer(InAudioBuffer, BufferSize); return bAudioSuccess; } bool OutputChannelThread::Thread_HandleLostFrameAudio() { bool bAudioSuccess = true; bool bAudioOutputRunning = false; bAudioSuccess &= GetDevice().IsAudioOutputRunning(AudioSystem, bAudioOutputRunning); if (bAudioOutputRunning) { // When dropping frames, we have to keep writing audio data or else the play head will catch up to the write head which will induce a delay int32 OffsetInSeconds = 0; bAudioSuccess &= Thread_GetAudioOffsetInSamples(OffsetInSeconds); if (bAudioSuccess && Thread_ShouldPauseAudioOutput()) { constexpr bool bPausePlayout = true; bAudioSuccess &= GetDevice().SetAudioOutputPause(AudioSystem, bPausePlayout); UE_LOG(LogAjaCore, Warning, TEXT("Pausing audio output. Last playhead position: %d"), AudioPlayheadLastPosition.load()); } } return bAudioSuccess; } bool OutputChannelThread::Thread_ShouldPauseAudioOutput() { constexpr int32 AudioBufferZoneInFrames = 1; int32 OffsetInSamples = 0; Thread_GetAudioOffsetInSamples(OffsetInSamples); if (OffsetInSamples <= (int32) NumSamplesPerFrame * AudioBufferZoneInFrames) { return true; } return false; } void OutputChannelThread::OnVerticalInterrupt(uint32 SyncCount) const { uint32 FrameId = 0; uint8 BufferId = 0; if (BufferFreeIndex[0] == SyncCount) { FrameId = BufferFrameIdMapping[0]; BufferId = 0; } else if (BufferFreeIndex[1] == SyncCount) { FrameId = BufferFrameIdMapping[1]; BufferId = 1; } if (FrameId != 0) { TRACE_BOOKMARK(TEXT("Aja: Finish SDI transfer frame [%d] (Buffer: %d)"), FrameId, BufferId); } else if (BufferFrameIdMapping[0] != 0 && BufferFrameIdMapping[1] != 0) // Don't log drop frames when starting the output { TRACE_BOOKMARK(TEXT("Aja VBI [%d] (Dropped frame)"), SyncCount); } } } }