You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
1327 lines
42 KiB
C++
1327 lines
42 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "NvVideoEncoder.h"
|
|
#include "Microsoft/AVEncoderMicrosoftCommon.h"
|
|
|
|
#if AVENCODER_SUPPORTED_MICROSOFT_PLATFORM
|
|
|
|
THIRD_PARTY_INCLUDES_START
|
|
#include "ThirdParty/NvEncoder/nvEncodeAPI.h"
|
|
THIRD_PARTY_INCLUDES_END
|
|
|
|
// This is mostly to use internally at Epic.
|
|
// Setting this to 1 will collect detailed timings in the `Timings` member array.
|
|
// It will also clear every frame with a solid colour before copying the backbuffer into it.
|
|
#define NVENC_VIDEO_ENCODER_DEBUG 0
|
|
|
|
#if NVENC_VIDEO_ENCODER_DEBUG
|
|
#include "ClearQuad.h"
|
|
#endif
|
|
|
|
#define CHECK_NV_RES(NvCall)\
|
|
{\
|
|
NVENCSTATUS Res = NvCall;\
|
|
if (Res != NV_ENC_SUCCESS)\
|
|
{\
|
|
check(false);\
|
|
UE_LOG(LogAVEncoder, Error, TEXT("`" #NvCall "` failed with error code: %d"), Res);\
|
|
return false;\
|
|
}\
|
|
}
|
|
|
|
namespace AVEncoder
|
|
{
|
|
|
|
namespace
|
|
{
|
|
constexpr const TCHAR* GetDllName()
|
|
{
|
|
#if defined PLATFORM_WINDOWS
|
|
#if defined _WIN64
|
|
return TEXT("nvEncodeAPI64.dll");
|
|
#else
|
|
return TEXT("nvEncodeAPI.dll");
|
|
#endif
|
|
#elif defined PLATFORM_LINUX
|
|
return TEXT("libnvidia-encode.so.1");
|
|
#else
|
|
return TEXT("");
|
|
#endif
|
|
}
|
|
|
|
DECLARE_STATS_GROUP(TEXT("NvEnc"), STATGROUP_NvEncVideoEncoder, STATCAT_Advanced);
|
|
|
|
DECLARE_CYCLE_STAT(TEXT("CopyTexture"), STAT_NvEnc_CopyTexture, STATGROUP_NvEncVideoEncoder);
|
|
DECLARE_CYCLE_STAT(TEXT("SubmitFrameToEncoder"), STAT_NvEnc_SubmitFrameToEncoder, STATGROUP_NvEncVideoEncoder);
|
|
DECLARE_CYCLE_STAT(TEXT("WaitForEncodeEvent"), STAT_NvEnc_WaitForEncodeEvent, STATGROUP_NvEncVideoEncoder);
|
|
DECLARE_CYCLE_STAT(TEXT("RetrieveEncodedFrame"), STAT_NvEnc_RetrieveEncodedFrame, STATGROUP_NvEncVideoEncoder);
|
|
DECLARE_CYCLE_STAT(TEXT("OnEncodedVideoFrameCallback"), STAT_NvEnc_OnEncodedVideoFrameCallback, STATGROUP_NvEncVideoEncoder);
|
|
|
|
NV_ENC_PARAMS_RC_MODE ToNvEncRcMode(FH264Settings::ERateControlMode RcMode)
|
|
{
|
|
switch(RcMode)
|
|
{
|
|
case FH264Settings::ERateControlMode::ConstQP:
|
|
return NV_ENC_PARAMS_RC_CONSTQP;
|
|
case FH264Settings::ERateControlMode::VBR:
|
|
return NV_ENC_PARAMS_RC_VBR;
|
|
case FH264Settings::ERateControlMode::CBR:
|
|
return NV_ENC_PARAMS_RC_CBR;
|
|
default:
|
|
UE_LOG(LogAVEncoder, Error, TEXT("Invalid rate control mode (%d) for nvenc"), (int)RcMode);
|
|
return NV_ENC_PARAMS_RC_CBR;
|
|
}
|
|
}
|
|
|
|
const TCHAR* ToString(NV_ENC_PARAMS_RC_MODE RcMode)
|
|
{
|
|
switch (RcMode)
|
|
{
|
|
case NV_ENC_PARAMS_RC_CONSTQP:
|
|
return TEXT("ConstQP");
|
|
case NV_ENC_PARAMS_RC_VBR:
|
|
return TEXT("VBR");
|
|
case NV_ENC_PARAMS_RC_CBR:
|
|
return TEXT("CBR");
|
|
case NV_ENC_PARAMS_RC_CBR_LOWDELAY_HQ:
|
|
return TEXT("CBR_LOWDELAY_HQ");
|
|
case NV_ENC_PARAMS_RC_CBR_HQ:
|
|
return TEXT("CBR_HQ");
|
|
case NV_ENC_PARAMS_RC_VBR_HQ:
|
|
return TEXT("VBR_HQ");
|
|
default:
|
|
checkNoEntry();
|
|
return TEXT("Unknown");
|
|
}
|
|
}
|
|
|
|
const TCHAR* ToString(NV_ENC_PIC_TYPE PicType)
|
|
{
|
|
switch (PicType)
|
|
{
|
|
case NV_ENC_PIC_TYPE_P:
|
|
return TEXT("NV_ENC_PIC_TYPE_P");
|
|
case NV_ENC_PIC_TYPE_B:
|
|
return TEXT("NV_ENC_PIC_TYPE_B");
|
|
case NV_ENC_PIC_TYPE_I:
|
|
return TEXT("NV_ENC_PIC_TYPE_I");
|
|
case NV_ENC_PIC_TYPE_IDR:
|
|
return TEXT("NV_ENC_PIC_TYPE_IDR");
|
|
case NV_ENC_PIC_TYPE_BI:
|
|
return TEXT("NV_ENC_PIC_TYPE_BI");
|
|
case NV_ENC_PIC_TYPE_SKIPPED:
|
|
return TEXT("NV_ENC_PIC_TYPE_SKIPPED");
|
|
case NV_ENC_PIC_TYPE_INTRA_REFRESH:
|
|
return TEXT("NV_ENC_PIC_TYPE_INTRA_REFRESH");
|
|
default:
|
|
checkNoEntry();
|
|
return TEXT("Unknown");
|
|
}
|
|
}
|
|
|
|
static bool D3D_ShouldCreateWithD3DDebug()
|
|
{
|
|
// Use a debug device if specified on the command line.
|
|
static bool bCreateWithD3DDebug =
|
|
FParse::Param(FCommandLine::Get(), TEXT("d3ddebug")) ||
|
|
FParse::Param(FCommandLine::Get(), TEXT("d3debug")) ||
|
|
FParse::Param(FCommandLine::Get(), TEXT("dxdebug"));
|
|
return bCreateWithD3DDebug;
|
|
}
|
|
|
|
static bool D3D_ShouldAllowAsyncResourceCreation()
|
|
{
|
|
static bool bAllowAsyncResourceCreation = !FParse::Param(FCommandLine::Get(), TEXT("nod3dasync"));
|
|
return bAllowAsyncResourceCreation;
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
// Video encoder implementation based on NVIDIA Video Codecs SDK: https://developer.nvidia.com/nvidia-video-codec-sdk
|
|
// Uses only encoder part
|
|
class FNvVideoEncoder : public FVideoEncoder
|
|
{
|
|
public:
|
|
|
|
FNvVideoEncoder();
|
|
~FNvVideoEncoder();
|
|
|
|
//
|
|
// FVideoEncoder interface
|
|
//
|
|
const TCHAR* GetName() const override;
|
|
const TCHAR* GetType() const override;
|
|
bool Initialize(const FVideoEncoderConfig& InConfig) override;
|
|
void Shutdown() override;
|
|
bool CopyTexture(FTexture2DRHIRef Texture, FTimespan CaptureTs, FTimespan Duration, FBufferId& OutBufferId, FIntPoint Resolution) override;
|
|
void Drop(FBufferId BufferId) override;
|
|
void Encode(FBufferId BufferId, bool bForceKeyFrame, uint32 Bitrate, TUniquePtr<FEncoderVideoFrameCookie> Cookie) override;
|
|
FVideoEncoderConfig GetConfig() const override;
|
|
bool SetBitrate(uint32 Bitrate) override;
|
|
bool SetFramerate(uint32 Framerate) override;
|
|
bool SetParameter(const FString& Parameter, const FString& Value) override;
|
|
|
|
private:
|
|
|
|
struct FInputFrame
|
|
{
|
|
FInputFrame() {}
|
|
UE_NONCOPYABLE(FInputFrame);
|
|
void* RegisteredResource = nullptr;
|
|
NV_ENC_INPUT_PTR MappedResource = nullptr;
|
|
NV_ENC_BUFFER_FORMAT BufferFormat;
|
|
FTexture2DRHIRef Texture;
|
|
ID3D11Texture2D* SharedTexture = nullptr;
|
|
bool bForceKeyFrame = false;
|
|
FTimespan CaptureTs;
|
|
FTimespan Duration;
|
|
FGPUFenceRHIRef CopyFence;
|
|
};
|
|
|
|
struct FOutputFrame
|
|
{
|
|
FOutputFrame() {}
|
|
UE_NONCOPYABLE(FOutputFrame);
|
|
NV_ENC_OUTPUT_PTR BitstreamBuffer = nullptr;
|
|
HANDLE EventHandle = nullptr;
|
|
TUniquePtr<FEncoderVideoFrameCookie> Cookie;
|
|
};
|
|
|
|
enum class EFrameState
|
|
{
|
|
Free,
|
|
Capturing,
|
|
Captured,
|
|
Encoding
|
|
};
|
|
|
|
struct FFrame
|
|
{
|
|
FFrame() {}
|
|
UE_NONCOPYABLE(FFrame);
|
|
|
|
// Array index of this FFrame. This is set at startup, and should never be changed
|
|
FBufferId Id = 0;
|
|
|
|
TAtomic<EFrameState> State = { EFrameState::Free };
|
|
// Bitrate requested at the time the video encoder asked us to encode this frame
|
|
// We save this, because we can't use it at the moment we receive it.
|
|
uint32 BitrateRequested = 0;
|
|
FInputFrame InputFrame;
|
|
FOutputFrame OutputFrame;
|
|
uint64 FrameIdx = 0;
|
|
|
|
// Some timestamps to track how long a frame spends in each step
|
|
FTimespan CopyBufferStartTs;
|
|
FTimespan CopyBufferFinishTs;
|
|
FTimespan EncodingStartTs;
|
|
FTimespan EncodingFinishTs;
|
|
};
|
|
|
|
bool InitFrameInputBuffer(FFrame& Frame, uint32 Width, uint32 Heigh);
|
|
bool InitializeResources();
|
|
void ReleaseFrameInputBuffer(FFrame& Frame);
|
|
void ReleaseResources();
|
|
void RegisterAsyncEvent(void** OutEvent);
|
|
void UnregisterAsyncEvent(void* Event);
|
|
FFrame* CheckForFinishedCopy();
|
|
|
|
bool UpdateFramerate();
|
|
|
|
/**
|
|
* Update some encoder settings
|
|
*
|
|
* @param ResolutionIf both X and Y are NOT 0, then set the encoder resolution
|
|
* @param Bitrate If not 0, then set the encoder average bitrate
|
|
*/
|
|
void UpdateNvEncConfig(FIntPoint Resolution, uint32 Bitrate);
|
|
|
|
void UpdateRes(FFrame& Frame, FIntPoint Resolution);
|
|
void CopyTexture(const FTexture2DRHIRef& Texture, FFrame& Frame, FIntPoint Resolution);
|
|
|
|
void EncoderCheckLoop();
|
|
|
|
void ProcessFrame(FFrame& Frame);
|
|
|
|
void UpdateSettings(FInputFrame& InputFrame, uint32 Bitrate);
|
|
void SubmitFrameToEncoder(FFrame& Frame);
|
|
|
|
bool bInitialized = false;
|
|
void* DllHandle = nullptr;
|
|
TUniquePtr<NV_ENCODE_API_FUNCTION_LIST> NvEncodeAPI;
|
|
void* EncoderInterface = nullptr;
|
|
NV_ENC_INITIALIZE_PARAMS NvEncInitializeParams;
|
|
|
|
// Used to atomically change NvEnc settings, so if the outside calls GetConfig, it
|
|
// gets a valid result, instead of something that was in the middle of being updated.
|
|
mutable FCriticalSection ConfigCS;
|
|
|
|
NV_ENC_CONFIG NvEncConfig;
|
|
uint32 CapturedFrameCount = 0; // of captured, not encoded frames
|
|
static constexpr uint32 NumBufferedFrames = 3;
|
|
FFrame BufferedFrames[NumBufferedFrames];
|
|
TUniquePtr<FThread> EncoderThread;
|
|
FThreadSafeBool bExitEncoderThread = false;
|
|
|
|
// Desired config.
|
|
// This is not applied immediately. It's applied when the next frame is sent to
|
|
// the encoder
|
|
FVideoEncoderConfig Config;
|
|
FH264Settings ConfigH264;
|
|
|
|
class FEncoderDevice
|
|
{
|
|
public:
|
|
FEncoderDevice();
|
|
|
|
TRefCountPtr<ID3D11Device> Device;
|
|
TRefCountPtr<ID3D11DeviceContext> DeviceContext;
|
|
};
|
|
|
|
TUniquePtr<FEncoderDevice> EncoderDevice;
|
|
|
|
#if NVENC_VIDEO_ENCODER_DEBUG
|
|
// This is just for debugging
|
|
void ClearFrame(FFrame& Frame);
|
|
// Timings in milliseconds. Just for debugging
|
|
struct FFrameTiming
|
|
{
|
|
// 0 : CopyBufferStart -> CopyBufferFinish
|
|
// 1 : CopyBufferStart -> EncodingStart
|
|
// 2 : CopyBufferStart -> EncodingFinish
|
|
double Total[3];
|
|
// 0 : CopyBufferStart -> CopyBufferFinish
|
|
// 1 : CopyBufferFinish -> EncodingStart
|
|
// 2 : EncodingStart -> EncodingFinish
|
|
double Steps[3];
|
|
};
|
|
TArray<FFrameTiming> Timings;
|
|
#endif
|
|
|
|
// When we receive an "Encode" call, we can't send to the encoder right away because maybe the
|
|
// texture copy hasn't completed yet.
|
|
// So, we put them in this queue and keep checking if the texture copy finished
|
|
TQueue<FFrame*> CopyingQueue;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FNvVideoEncoder implementation
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
FNvVideoEncoder::FEncoderDevice::FEncoderDevice()
|
|
{
|
|
if (GDynamicRHI)
|
|
{
|
|
FString RHIName = GDynamicRHI->GetName();
|
|
|
|
TRefCountPtr<IDXGIDevice> DXGIDevice;
|
|
uint32 DeviceFlags = D3D_ShouldAllowAsyncResourceCreation() ? 0 : D3D11_CREATE_DEVICE_SINGLETHREADED;
|
|
if (D3D_ShouldCreateWithD3DDebug())
|
|
DeviceFlags |= D3D11_CREATE_DEVICE_DEBUG;
|
|
D3D_FEATURE_LEVEL FeatureLevel = D3D_FEATURE_LEVEL_11_1;
|
|
TRefCountPtr<IDXGIAdapter> Adapter;
|
|
|
|
if (RHIName == TEXT("D3D11"))
|
|
{
|
|
auto UE4D3DDevice = static_cast<ID3D11Device*>(GDynamicRHI->RHIGetNativeDevice());
|
|
checkf(UE4D3DDevice != nullptr, TEXT("Cannot initialize NvEnc with invalid device"));
|
|
CHECK_HR_VOID(UE4D3DDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)DXGIDevice.GetInitReference()));
|
|
CHECK_HR_VOID(DXGIDevice->GetAdapter(Adapter.GetInitReference()));
|
|
FeatureLevel = D3D_FEATURE_LEVEL_11_0;
|
|
}
|
|
else if (RHIName == TEXT("D3D12"))
|
|
{
|
|
auto UE4D3DDevice = static_cast<ID3D12Device*>(GDynamicRHI->RHIGetNativeDevice());
|
|
checkf(UE4D3DDevice != nullptr, TEXT("Cannot initialize NvEnc with invalid device"));
|
|
LUID AdapterLuid = UE4D3DDevice->GetAdapterLuid();
|
|
TRefCountPtr<IDXGIFactory4> DXGIFactory;
|
|
CHECK_HR_VOID(CreateDXGIFactory(IID_PPV_ARGS(DXGIFactory.GetInitReference())));
|
|
// To use a shared texture from D3D12, we need to use a D3D 11.1 device, because we need the
|
|
// D3D11Device1::OpenSharedResource1 method
|
|
FeatureLevel = D3D_FEATURE_LEVEL_11_1;
|
|
CHECK_HR_VOID(DXGIFactory->EnumAdapterByLuid(AdapterLuid, IID_PPV_ARGS(Adapter.GetInitReference())));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAVEncoder, Fatal, TEXT("NvEnc requires D3D11/D3D12"));
|
|
return;
|
|
}
|
|
|
|
D3D_FEATURE_LEVEL ActualFeatureLevel;
|
|
|
|
CHECK_HR_VOID(D3D11CreateDevice(
|
|
Adapter,
|
|
D3D_DRIVER_TYPE_UNKNOWN,
|
|
NULL,
|
|
DeviceFlags,
|
|
&FeatureLevel,
|
|
1,
|
|
D3D11_SDK_VERSION,
|
|
Device.GetInitReference(),
|
|
&ActualFeatureLevel,
|
|
DeviceContext.GetInitReference()
|
|
));
|
|
|
|
// If we are using D3D12, make sure we got a 11.1 device
|
|
if (FeatureLevel == D3D_FEATURE_LEVEL_11_1 && ActualFeatureLevel!=D3D_FEATURE_LEVEL_11_1)
|
|
{
|
|
UE_LOG(LogAVEncoder, Fatal, TEXT("Failed to create a D3D 11.1 device. This is needed when using the D3D12 renderer."));
|
|
}
|
|
|
|
// Work-around for the EOS overlay to get an unwrapped, raw ID3D11Device
|
|
TRefCountPtr<ID3D11Device> UnwrappedDevice;
|
|
DeviceContext->GetDevice(UnwrappedDevice.GetInitReference());
|
|
Device = MoveTemp(UnwrappedDevice);
|
|
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAVEncoder, Error, TEXT("Attempting to create Encoder Device without existing RHI"));
|
|
}
|
|
}
|
|
|
|
const uint32 BitstreamSize = 1024 * 1024 * 2;
|
|
|
|
#define NV_RESULT(NvFunction) NvFunction == NV_ENC_SUCCESS
|
|
|
|
FNvVideoEncoder::FNvVideoEncoder()
|
|
{
|
|
DllHandle = FPlatformProcess::GetDllHandle(GetDllName());
|
|
checkf(DllHandle != nullptr, TEXT("Failed to load NvEncode dll"));
|
|
if (!DllHandle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
FNvVideoEncoder::~FNvVideoEncoder()
|
|
{
|
|
if (DllHandle)
|
|
{
|
|
UE_LOG(LogAVEncoder, Fatal, TEXT("FNvVideoEncoder Shutdown not called before destruction."));
|
|
}
|
|
}
|
|
|
|
const TCHAR* FNvVideoEncoder::GetName() const
|
|
{
|
|
return TEXT("h264.nvenc");
|
|
}
|
|
const TCHAR* FNvVideoEncoder::GetType() const
|
|
{
|
|
return TEXT("h264");
|
|
}
|
|
|
|
bool FNvVideoEncoder::Initialize(const FVideoEncoderConfig& InConfig)
|
|
{
|
|
check(!bInitialized);
|
|
|
|
Config = InConfig;
|
|
ConfigH264 = {};
|
|
ReadH264Settings(Config.Options, ConfigH264);
|
|
|
|
UE_LOG(LogAVEncoder, Log, TEXT("FNvVideoEncoder initialization with %u*%d, %u FPS"),
|
|
Config.Width, Config.Height, Config.Framerate);
|
|
|
|
EncoderDevice = MakeUnique<FEncoderDevice>();
|
|
|
|
_NVENCSTATUS Result;
|
|
|
|
// Load NvEnc dll and create an NvEncode API instance
|
|
{
|
|
// define a function pointer for creating an instance of nvEncodeAPI
|
|
typedef NVENCSTATUS(NVENCAPI *NVENCAPIPROC)(NV_ENCODE_API_FUNCTION_LIST*);
|
|
NVENCAPIPROC NvEncodeAPICreateInstanceFunc;
|
|
|
|
#if defined PLATFORM_WINDOWS
|
|
# pragma warning(push)
|
|
# pragma warning(disable: 4191) // https://stackoverflow.com/a/4215425/453271
|
|
NvEncodeAPICreateInstanceFunc = (NVENCAPIPROC)GetProcAddress((HMODULE)DllHandle, "NvEncodeAPICreateInstance");
|
|
# pragma warning(pop)
|
|
#else
|
|
NvEncodeAPICreateInstanceFunc = (NVENCAPIPROC)dlsym(DllHandle, "NvEncodeAPICreateInstance");
|
|
#endif
|
|
checkf(NvEncodeAPICreateInstanceFunc != nullptr, TEXT("NvEncodeAPICreateInstance failed"));
|
|
NvEncodeAPI.Reset(new NV_ENCODE_API_FUNCTION_LIST);
|
|
FMemory::Memzero(NvEncodeAPI.Get(), sizeof(NV_ENCODE_API_FUNCTION_LIST));
|
|
NvEncodeAPI->version = NV_ENCODE_API_FUNCTION_LIST_VER;
|
|
Result = NvEncodeAPICreateInstanceFunc(NvEncodeAPI.Get());
|
|
checkf(NV_RESULT(Result), TEXT("Unable to create NvEnc API function list: error %d"), Result);
|
|
}
|
|
// Open an encoding session
|
|
{
|
|
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS OpenEncodeSessionExParams;
|
|
FMemory::Memzero(OpenEncodeSessionExParams);
|
|
OpenEncodeSessionExParams.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
|
|
OpenEncodeSessionExParams.device = EncoderDevice->Device;
|
|
OpenEncodeSessionExParams.deviceType = NV_ENC_DEVICE_TYPE_DIRECTX; // Currently only DX11 is supported
|
|
OpenEncodeSessionExParams.apiVersion = NVENCAPI_VERSION;
|
|
Result = NvEncodeAPI->nvEncOpenEncodeSessionEx(&OpenEncodeSessionExParams, &EncoderInterface);
|
|
checkf(NV_RESULT(Result), TEXT("Unable to open NvEnc encoding session (status: %d)"), Result);
|
|
}
|
|
// Set initialization parameters
|
|
{
|
|
FMemory::Memzero(NvEncInitializeParams);
|
|
NvEncInitializeParams.version = NV_ENC_INITIALIZE_PARAMS_VER;
|
|
// hardcoded to FullHD for now, if actual resolution is different it will be changed dynamically
|
|
NvEncInitializeParams.encodeWidth = NvEncInitializeParams.darWidth = Config.Width;
|
|
NvEncInitializeParams.encodeHeight = NvEncInitializeParams.darHeight = Config.Height;
|
|
NvEncInitializeParams.encodeGUID = NV_ENC_CODEC_H264_GUID;
|
|
|
|
if (Config.Preset == FVideoEncoderConfig::EPreset::LowLatency)
|
|
NvEncInitializeParams.presetGUID = NV_ENC_PRESET_LOW_LATENCY_HQ_GUID;
|
|
else if (Config.Preset == FVideoEncoderConfig::EPreset::HighQuality)
|
|
NvEncInitializeParams.presetGUID = NV_ENC_PRESET_HQ_GUID;
|
|
else
|
|
{
|
|
check(false);
|
|
}
|
|
|
|
NvEncInitializeParams.frameRateNum = Config.Framerate;
|
|
NvEncInitializeParams.frameRateDen = 1;
|
|
NvEncInitializeParams.enablePTD = 1;
|
|
NvEncInitializeParams.reportSliceOffsets = 0;
|
|
NvEncInitializeParams.enableSubFrameWrite = 0;
|
|
NvEncInitializeParams.encodeConfig = &NvEncConfig;
|
|
NvEncInitializeParams.maxEncodeWidth = 3840;
|
|
NvEncInitializeParams.maxEncodeHeight = 2160;
|
|
FParse::Value(FCommandLine::Get(), TEXT("NvEncMaxEncodeWidth="), NvEncInitializeParams.maxEncodeWidth);
|
|
FParse::Value(FCommandLine::Get(), TEXT("NvEncMaxEncodeHeight="), NvEncInitializeParams.maxEncodeHeight);
|
|
}
|
|
// Get preset config and tweak it accordingly
|
|
{
|
|
NV_ENC_PRESET_CONFIG PresetConfig;
|
|
FMemory::Memzero(PresetConfig);
|
|
PresetConfig.version = NV_ENC_PRESET_CONFIG_VER;
|
|
PresetConfig.presetCfg.version = NV_ENC_CONFIG_VER;
|
|
Result = NvEncodeAPI->nvEncGetEncodePresetConfig(EncoderInterface, NvEncInitializeParams.encodeGUID, NvEncInitializeParams.presetGUID, &PresetConfig);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to select NVEncoder preset config (status: %d)"), Result);
|
|
FMemory::Memcpy(&NvEncConfig, &PresetConfig.presetCfg, sizeof(NV_ENC_CONFIG));
|
|
|
|
NvEncConfig.profileGUID = Config.Preset == FVideoEncoderConfig::EPreset::LowLatency ? NV_ENC_H264_PROFILE_BASELINE_GUID : NV_ENC_H264_PROFILE_MAIN_GUID;
|
|
|
|
NvEncConfig.gopLength = NvEncInitializeParams.frameRateNum; // once a sec
|
|
|
|
NV_ENC_RC_PARAMS& RcParams = NvEncConfig.rcParams;
|
|
RcParams.rateControlMode = ToNvEncRcMode(ConfigH264.RcMode);
|
|
|
|
RcParams.enableMinQP = true;
|
|
RcParams.minQP = { 20, 20, 20 };
|
|
|
|
RcParams.maxBitRate = Config.MaxBitrate;
|
|
RcParams.averageBitRate = FMath::Min(Config.Bitrate, RcParams.maxBitRate);
|
|
|
|
NvEncConfig.encodeCodecConfig.h264Config.idrPeriod = NvEncConfig.gopLength;
|
|
|
|
// configure "entire frame as a single slice"
|
|
// seems WebRTC implementation doesn't work well with slicing, default mode
|
|
// (Mode=3/ModeData=4 - 4 slices per frame) produces (rarely) grey full screen or just top half of it.
|
|
// it also can be related with our handling of slices in proxy's FakeVideoEncoder
|
|
if (Config.Preset == FVideoEncoderConfig::EPreset::LowLatency)
|
|
{
|
|
NvEncConfig.encodeCodecConfig.h264Config.sliceMode = 0;
|
|
NvEncConfig.encodeCodecConfig.h264Config.sliceModeData = 0;
|
|
}
|
|
else
|
|
{
|
|
NvEncConfig.encodeCodecConfig.h264Config.sliceMode = 3;
|
|
NvEncConfig.encodeCodecConfig.h264Config.sliceModeData = 1;
|
|
}
|
|
|
|
// let encoder slice encoded frame so they can fit into RTP packets
|
|
// commented out because at some point it started to produce immediately visible visual artifacts on players
|
|
//NvEncConfig.encodeCodecConfig.h264Config.sliceMode = 1;
|
|
//NvEncConfig.encodeCodecConfig.h264Config.sliceModeData = 1100; // max bytes per slice
|
|
|
|
// repeat SPS/PPS with each key-frame for a case when the first frame (with mandatory SPS/PPS)
|
|
// was dropped by WebRTC
|
|
NvEncConfig.encodeCodecConfig.h264Config.repeatSPSPPS = 1;
|
|
|
|
// maybe doesn't have an effect, high level is chosen because we aim at high bitrate
|
|
NvEncConfig.encodeCodecConfig.h264Config.level = Config.Preset==FVideoEncoderConfig::EPreset::LowLatency ? NV_ENC_LEVEL_H264_52 : NV_ENC_LEVEL_H264_51;
|
|
|
|
}
|
|
|
|
// Get encoder capability
|
|
{
|
|
NV_ENC_CAPS_PARAM CapsParam;
|
|
FMemory::Memzero(CapsParam);
|
|
CapsParam.version = NV_ENC_CAPS_PARAM_VER;
|
|
CapsParam.capsToQuery = NV_ENC_CAPS_ASYNC_ENCODE_SUPPORT;
|
|
int32 AsyncMode = 0;
|
|
Result = NvEncodeAPI->nvEncGetEncodeCaps(EncoderInterface, NvEncInitializeParams.encodeGUID, &CapsParam, &AsyncMode);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to get NVEncoder capability params (status: %d)"), Result);
|
|
if (AsyncMode == 0)
|
|
{
|
|
UE_LOG(LogAVEncoder, Fatal, TEXT("NvEnc doesn't support async mode"));
|
|
return false;
|
|
}
|
|
|
|
NvEncInitializeParams.enableEncodeAsync = true;
|
|
}
|
|
|
|
Result = NvEncodeAPI->nvEncInitializeEncoder(EncoderInterface, &NvEncInitializeParams);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to initialize NVEncoder (status: %d)"), Result);
|
|
|
|
FBufferId Id = 0;
|
|
for(FFrame& Frame : BufferedFrames)
|
|
{
|
|
Frame.Id = Id++;
|
|
}
|
|
|
|
if (!InitializeResources())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
EncoderThread = MakeUnique<FThread>(TEXT("NvVideoEncoder"), [this]() { EncoderCheckLoop(); });
|
|
|
|
UE_LOG(LogAVEncoder, Log, TEXT("NvEnc initialised"));
|
|
|
|
bInitialized = true;
|
|
return true;
|
|
}
|
|
|
|
void FNvVideoEncoder::Shutdown()
|
|
{
|
|
if (!DllHandle)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (EncoderThread)
|
|
{
|
|
bExitEncoderThread = true;
|
|
|
|
// Exit encoder runnable thread before shutting down NvEnc interface
|
|
EncoderThread->Join();
|
|
}
|
|
|
|
ReleaseResources();
|
|
|
|
if (EncoderInterface)
|
|
{
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncDestroyEncoder(EncoderInterface);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to destroy NvEnc interface (status: %d)"), Result);
|
|
EncoderInterface = nullptr;
|
|
}
|
|
|
|
#if defined PLATFORM_WINDOWS
|
|
FPlatformProcess::FreeDllHandle(DllHandle);
|
|
#else
|
|
dlclose(DllHandle);
|
|
#endif
|
|
DllHandle = nullptr;
|
|
}
|
|
|
|
FVideoEncoderConfig FNvVideoEncoder::GetConfig() const
|
|
{
|
|
FVideoEncoderConfig Cfg;
|
|
FScopeLock ScopedLock(&ConfigCS);
|
|
Cfg.Bitrate = NvEncConfig.rcParams.averageBitRate;
|
|
Cfg.Framerate = NvEncInitializeParams.frameRateNum;
|
|
Cfg.Width = NvEncInitializeParams.encodeWidth;
|
|
Cfg.Height = NvEncInitializeParams.encodeHeight;
|
|
return Cfg;
|
|
}
|
|
|
|
bool FNvVideoEncoder::SetBitrate(uint32 Bitrate)
|
|
{
|
|
Config.Bitrate = Bitrate;
|
|
return true;
|
|
}
|
|
|
|
bool FNvVideoEncoder::SetFramerate(uint32 Framerate)
|
|
{
|
|
Config.Framerate = Framerate;
|
|
return true;
|
|
}
|
|
|
|
bool FNvVideoEncoder::SetParameter(const FString& Parameter, const FString& Value)
|
|
{
|
|
if (Parameter == TEXT("fillerdata"))
|
|
{
|
|
Config.bFillerDataHack = Value == TEXT("0") ? false : true;
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return ReadH264Setting(Parameter, Value, ConfigH264);
|
|
}
|
|
}
|
|
|
|
bool FNvVideoEncoder::CopyTexture(const FTexture2DRHIRef Texture, FTimespan CaptureTs, FTimespan Duration, FBufferId& OutBufferId, FIntPoint Resolution)
|
|
{
|
|
check(IsInRenderingThread());
|
|
|
|
// Find a free slot we can use
|
|
FFrame* Frame = nullptr;
|
|
for (FFrame& Slot : BufferedFrames)
|
|
{
|
|
if (Slot.State.Load() == EFrameState::Free)
|
|
{
|
|
Frame = &Slot;
|
|
OutBufferId = Slot.Id;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!Frame)
|
|
{
|
|
UE_LOG(LogAVEncoder, Verbose, TEXT("Frame dropped because NvEnc queue is full"));
|
|
return false;
|
|
}
|
|
|
|
Frame->FrameIdx = CapturedFrameCount++;
|
|
Frame->InputFrame.CaptureTs = CaptureTs;
|
|
Frame->InputFrame.Duration = Duration;
|
|
Frame->CopyBufferStartTs = FTimespan::FromSeconds(FPlatformTime::Seconds());
|
|
|
|
#if NVENC_VIDEO_ENCODER_DEBUG
|
|
// By clearing the frame at this point, we can catch the occasional glimpse of a solid color
|
|
// frame in PixelStreaming if there are any bugs detecting when the copy finished
|
|
ClearFrame(*Frame);
|
|
#endif
|
|
|
|
CopyTexture(Texture, *Frame, Resolution);
|
|
|
|
UE_LOG(LogAVEncoder, Verbose, TEXT("Buffer #%d (%d) captured"), Frame->FrameIdx, OutBufferId);
|
|
Frame->State = EFrameState::Capturing;
|
|
|
|
return true;
|
|
}
|
|
|
|
void FNvVideoEncoder::CopyTexture(const FTexture2DRHIRef& Texture, FFrame& Frame, FIntPoint Resolution)
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_NvEnc_CopyTexture);
|
|
UpdateRes(Frame, Resolution.Size() ? Resolution : Texture->GetSizeXY());
|
|
CopyTextureImpl(Texture, Frame.InputFrame.Texture, Frame.InputFrame.CopyFence);
|
|
}
|
|
|
|
FNvVideoEncoder::FFrame* FNvVideoEncoder::CheckForFinishedCopy()
|
|
{
|
|
FFrame* Frame;
|
|
if (!CopyingQueue.Peek(Frame))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
{
|
|
EFrameState State = Frame->State.Load();
|
|
checkf(State == EFrameState::Capturing, TEXT("Buffer %d : Expected state %d, but found %d"), Frame->Id, (int)EFrameState::Captured, (int)State);
|
|
}
|
|
|
|
if (Frame->InputFrame.CopyFence->Poll())
|
|
{
|
|
CopyingQueue.Pop();
|
|
Frame->State = EFrameState::Captured;
|
|
Frame->CopyBufferFinishTs = FTimespan::FromSeconds(FPlatformTime::Seconds());
|
|
return Frame;
|
|
}
|
|
else
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
}
|
|
|
|
void FNvVideoEncoder::Encode(FBufferId BufferId, bool bForceKeyFrame, uint32 Bitrate, TUniquePtr<FEncoderVideoFrameCookie> Cookie)
|
|
{
|
|
FFrame& Frame = BufferedFrames[BufferId];
|
|
|
|
{
|
|
EFrameState State = Frame.State.Load();
|
|
checkf(State == EFrameState::Capturing, TEXT("Buffer %d : Expected state %d, but found %d"), BufferId, (int)EFrameState::Captured, (int)State);
|
|
}
|
|
|
|
Frame.InputFrame.bForceKeyFrame = bForceKeyFrame;
|
|
Frame.BitrateRequested = Bitrate;
|
|
Frame.OutputFrame.Cookie = MoveTemp(Cookie);
|
|
CopyingQueue.Enqueue(&Frame);
|
|
}
|
|
|
|
void FNvVideoEncoder::Drop(FBufferId BufferId)
|
|
{
|
|
FFrame& Frame = BufferedFrames[BufferId];
|
|
|
|
{
|
|
EFrameState State = Frame.State.Load();
|
|
checkf(State == EFrameState::Capturing, TEXT("Buffer %d: Expected state %d, found %d")
|
|
, BufferId, (int)EFrameState::Capturing, (int)State);
|
|
}
|
|
|
|
Frame.State = EFrameState::Free;
|
|
|
|
UE_LOG(LogAVEncoder, Log, TEXT("Buffer #%d (%d) dropped"), BufferedFrames[BufferId].FrameIdx, BufferId);
|
|
}
|
|
|
|
void FNvVideoEncoder::UpdateNvEncConfig(FIntPoint Resolution, uint32 Bitrate)
|
|
{
|
|
bool bSettingsChanged = false;
|
|
bool bResolutionChanged = false;
|
|
|
|
// Putting all this in a single scope, so we can put changes to the config struct
|
|
// inside a big lock
|
|
{
|
|
FScopeLock ScopedLock(&ConfigCS);
|
|
|
|
//
|
|
// If an explicit Bitrate was specified, use that one, if not, use the one
|
|
// from the Config struct
|
|
//
|
|
if (Bitrate)
|
|
{
|
|
if (NvEncConfig.rcParams.averageBitRate != Bitrate)
|
|
{
|
|
NvEncConfig.rcParams.averageBitRate = Bitrate;
|
|
Config.Bitrate = Bitrate;
|
|
|
|
// #TODO : This is mostly an hack for PixelStreaming.
|
|
if (Config.bFillerDataHack)
|
|
{
|
|
// Bitrate generated by NVENC is not very stable when the scene doesnt have a lot of movement
|
|
// outputPictureTimingSEI enables the filling data when using CBR so that the bitrate generated
|
|
// is much closer to the requested one and bandwidth estimation algorithms can work better.
|
|
// Otherwise in a static scene it can send 50kbps when configuring 300kbps and it will never ramp up.
|
|
if (NvEncConfig.rcParams.averageBitRate < 5000000)
|
|
{
|
|
NvEncConfig.encodeCodecConfig.h264Config.outputPictureTimingSEI = 1;
|
|
NvEncConfig.rcParams.enableMinQP = 0;
|
|
}
|
|
else
|
|
{
|
|
NvEncConfig.encodeCodecConfig.h264Config.outputPictureTimingSEI = 0;
|
|
NvEncConfig.rcParams.enableMinQP = 1;
|
|
NvEncConfig.rcParams.minQP = { 20, 20, 20 };
|
|
}
|
|
}
|
|
|
|
bSettingsChanged = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (NvEncConfig.rcParams.averageBitRate != Config.Bitrate)
|
|
{
|
|
NvEncConfig.rcParams.averageBitRate = Config.Bitrate;
|
|
bSettingsChanged = true;
|
|
}
|
|
}
|
|
|
|
if (NvEncConfig.rcParams.minQP.qpIntra != ConfigH264.QP)
|
|
{
|
|
NvEncConfig.rcParams.minQP.qpIntra = NvEncConfig.rcParams.minQP.qpInterP = NvEncConfig.rcParams.minQP.qpInterB = ConfigH264.QP;
|
|
UE_LOG(LogAVEncoder, Log, TEXT("MinQP %u"), ConfigH264.QP);
|
|
bSettingsChanged = true;
|
|
}
|
|
|
|
NV_ENC_PARAMS_RC_MODE RcMode = ToNvEncRcMode(ConfigH264.RcMode);
|
|
if (RcMode != NvEncConfig.rcParams.rateControlMode)
|
|
{
|
|
NvEncConfig.rcParams.rateControlMode = RcMode;
|
|
UE_LOG(LogAVEncoder, Log, TEXT("Rate Control mode %s"), ToString(RcMode));
|
|
bSettingsChanged = true;
|
|
}
|
|
|
|
if (UpdateFramerate())
|
|
{
|
|
bSettingsChanged = true;
|
|
}
|
|
|
|
// Only try and change resolution if required
|
|
if (Resolution.X && Resolution.Y)
|
|
{
|
|
if (Resolution.X != NvEncInitializeParams.encodeWidth ||
|
|
Resolution.Y != NvEncInitializeParams.encodeHeight)
|
|
{
|
|
NvEncInitializeParams.encodeWidth = NvEncInitializeParams.darWidth = Resolution.X;
|
|
NvEncInitializeParams.encodeHeight = NvEncInitializeParams.darHeight = Resolution.Y;
|
|
|
|
bSettingsChanged = true;
|
|
bResolutionChanged = true;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
if (bSettingsChanged)
|
|
{
|
|
NV_ENC_RECONFIGURE_PARAMS NvEncReconfigureParams;
|
|
FMemory::Memcpy(&NvEncReconfigureParams.reInitEncodeParams, &NvEncInitializeParams, sizeof(NvEncInitializeParams));
|
|
NvEncReconfigureParams.version = NV_ENC_RECONFIGURE_PARAMS_VER;
|
|
NvEncReconfigureParams.forceIDR = bResolutionChanged;
|
|
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncReconfigureEncoder(EncoderInterface, &NvEncReconfigureParams);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to reconfigure encoder (status: %d)"), Result);
|
|
}
|
|
}
|
|
|
|
bool FNvVideoEncoder::UpdateFramerate()
|
|
{
|
|
|
|
if (NvEncInitializeParams.frameRateNum != Config.Framerate)
|
|
{
|
|
NvEncInitializeParams.frameRateNum = Config.Framerate;
|
|
UE_LOG(LogAVEncoder, Log, TEXT("NvEnc reconfigured to %d FPS"), NvEncInitializeParams.frameRateNum);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// checks if resolution changed, either the game res changed or new streaming resolution was specified by the console var
|
|
void FNvVideoEncoder::UpdateRes(FFrame& Frame, FIntPoint Resolution)
|
|
{
|
|
check(IsInRenderingThread());
|
|
|
|
FInputFrame& InputFrame = Frame.InputFrame;
|
|
|
|
// check if target resolution matches our currently allocated `InputFrame.Texture` resolution
|
|
if (InputFrame.Texture->GetSizeX() == Resolution.X && InputFrame.Texture->GetSizeY() == Resolution.Y)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// reallocate and re-register InputFrame with NvEnc
|
|
ReleaseFrameInputBuffer(Frame);
|
|
verify(InitFrameInputBuffer(Frame, Resolution.X, Resolution.Y));
|
|
}
|
|
|
|
void FNvVideoEncoder::SubmitFrameToEncoder(FFrame& Frame)
|
|
{
|
|
check(Frame.State.Load() == EFrameState::Captured);
|
|
|
|
SCOPE_CYCLE_COUNTER(STAT_NvEnc_SubmitFrameToEncoder);
|
|
|
|
Frame.State = EFrameState::Encoding;
|
|
Frame.EncodingStartTs = FTimespan::FromSeconds(FPlatformTime::Seconds());
|
|
|
|
NV_ENC_PIC_PARAMS PicParams;
|
|
FMemory::Memzero(PicParams);
|
|
PicParams.version = NV_ENC_PIC_PARAMS_VER;
|
|
PicParams.inputBuffer = Frame.InputFrame.MappedResource;
|
|
PicParams.bufferFmt = Frame.InputFrame.BufferFormat;
|
|
PicParams.inputWidth = NvEncInitializeParams.encodeWidth;
|
|
PicParams.inputHeight = NvEncInitializeParams.encodeHeight;
|
|
PicParams.outputBitstream = Frame.OutputFrame.BitstreamBuffer;
|
|
PicParams.completionEvent = Frame.OutputFrame.EventHandle;
|
|
PicParams.inputTimeStamp = Frame.FrameIdx;
|
|
PicParams.pictureStruct = NV_ENC_PIC_STRUCT_FRAME;
|
|
|
|
if (Frame.InputFrame.bForceKeyFrame)
|
|
PicParams.encodePicFlags |= NV_ENC_PIC_FLAG_FORCEIDR;
|
|
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncEncodePicture(EncoderInterface, &PicParams);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to encode frame (status: %d)"), Result);
|
|
}
|
|
|
|
void FNvVideoEncoder::EncoderCheckLoop()
|
|
{
|
|
// This thread will both encode frames and will also wait for the next frame
|
|
// to finish encoding.
|
|
TQueue<FFrame*> CurrentlyEncodingQueue;
|
|
|
|
while (!bExitEncoderThread)
|
|
{
|
|
//
|
|
// Check if any frames finished copying so we can submit then to the encoder
|
|
//
|
|
while(true)
|
|
{
|
|
FFrame* Frame = CheckForFinishedCopy();
|
|
if (!Frame)
|
|
{
|
|
break;
|
|
}
|
|
UpdateNvEncConfig(Frame->InputFrame.Texture->GetSizeXY(), Frame->BitrateRequested);
|
|
SubmitFrameToEncoder(*Frame);
|
|
CurrentlyEncodingQueue.Enqueue(Frame);
|
|
}
|
|
|
|
//
|
|
// Check for finished encoding work
|
|
//
|
|
if (!CurrentlyEncodingQueue.IsEmpty())
|
|
{
|
|
HANDLE Handle = (**CurrentlyEncodingQueue.Peek()).OutputFrame.EventHandle;
|
|
DWORD Result = WaitForSingleObject(Handle, 2);
|
|
if (Result == WAIT_OBJECT_0)
|
|
{
|
|
FFrame* Frame = nullptr;
|
|
verify(CurrentlyEncodingQueue.Dequeue(Frame));
|
|
ResetEvent(Frame->OutputFrame.EventHandle);
|
|
UE_LOG(LogAVEncoder, Verbose, TEXT("Buffer #%d (%d) encoded"), Frame->FrameIdx, Frame->Id);
|
|
ProcessFrame(*Frame);
|
|
}
|
|
else if (Result == WAIT_TIMEOUT)
|
|
{
|
|
// Nothing to do. This is expected
|
|
}
|
|
else
|
|
{
|
|
check(0 && "Unexpected code path");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FNvVideoEncoder::ProcessFrame(FFrame& Frame)
|
|
{
|
|
check(Frame.State.Load() == EFrameState::Encoding);
|
|
|
|
FOutputFrame& OutputFrame = Frame.OutputFrame;
|
|
|
|
FAVPacket Packet(EPacketType::Video);
|
|
NV_ENC_PIC_TYPE PicType;
|
|
// Retrieve encoded frame from output buffer
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_NvEnc_RetrieveEncodedFrame);
|
|
|
|
NV_ENC_LOCK_BITSTREAM LockBitstream;
|
|
FMemory::Memzero(LockBitstream);
|
|
LockBitstream.version = NV_ENC_LOCK_BITSTREAM_VER;
|
|
LockBitstream.outputBitstream = OutputFrame.BitstreamBuffer;
|
|
LockBitstream.doNotWait = NvEncInitializeParams.enableEncodeAsync;
|
|
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncLockBitstream(EncoderInterface, &LockBitstream);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to lock bitstream (status: %d)"), Result);
|
|
|
|
PicType = LockBitstream.pictureType;
|
|
checkf(PicType == NV_ENC_PIC_TYPE_IDR || Frame.InputFrame.bForceKeyFrame==false, TEXT("key frame requested by but not provided by NvEnc. NvEnc provided %d"), (int)PicType);
|
|
Packet.Video.bKeyFrame = (PicType == NV_ENC_PIC_TYPE_IDR) ? true : false;
|
|
Packet.Video.FrameAvgQP = LockBitstream.frameAvgQP;
|
|
Packet.Data = TArray<uint8>(reinterpret_cast<const uint8*>(LockBitstream.bitstreamBufferPtr), LockBitstream.bitstreamSizeInBytes);
|
|
Result = NvEncodeAPI->nvEncUnlockBitstream(EncoderInterface, Frame.OutputFrame.BitstreamBuffer);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to unlock bitstream (status: %d)"), Result);
|
|
}
|
|
|
|
Frame.EncodingFinishTs = FTimespan::FromSeconds(FPlatformTime::Seconds());
|
|
|
|
Packet.Timestamp = Frame.InputFrame.CaptureTs;
|
|
Packet.Duration = Frame.InputFrame.Duration;
|
|
Packet.Video.Width = Frame.InputFrame.Texture->GetSizeX();
|
|
Packet.Video.Height = Frame.InputFrame.Texture->GetSizeY();
|
|
Packet.Video.Framerate = NvEncInitializeParams.frameRateNum;
|
|
Packet.Timings.EncodeStartTs = Frame.EncodingStartTs;
|
|
Packet.Timings.EncodeFinishTs = Frame.EncodingFinishTs;
|
|
|
|
#if NVENC_VIDEO_ENCODER_DEBUG
|
|
{
|
|
FFrameTiming Timing;
|
|
Timing.Total[0] = (Frame.CopyBufferFinishTs - Frame.CopyBufferStartTs).GetTotalMilliseconds();
|
|
Timing.Total[1] = (Frame.EncodingStartTs - Frame.CopyBufferStartTs).GetTotalMilliseconds();
|
|
Timing.Total[2] = (Frame.EncodingFinishTs - Frame.CopyBufferStartTs).GetTotalMilliseconds();
|
|
|
|
Timing.Steps[0] = (Frame.CopyBufferFinishTs - Frame.CopyBufferStartTs).GetTotalMilliseconds();
|
|
Timing.Steps[1] = (Frame.EncodingStartTs - Frame.CopyBufferFinishTs).GetTotalMilliseconds();
|
|
Timing.Steps[2] = (Frame.EncodingFinishTs - Frame.EncodingStartTs).GetTotalMilliseconds();
|
|
Timings.Add(Timing);
|
|
// Limit the array size
|
|
if (Timings.Num()>1000)
|
|
{
|
|
Timings.RemoveAt(0);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
UE_LOG(LogAVEncoder, VeryVerbose, TEXT("encoded %s ts %lld, %d bytes")
|
|
, ToString(PicType)
|
|
, Packet.Timestamp.GetTicks()
|
|
, (int)Packet.Data.Num());
|
|
|
|
{
|
|
SCOPE_CYCLE_COUNTER(STAT_NvEnc_OnEncodedVideoFrameCallback);
|
|
OnEncodedVideoFrame(Packet, MoveTemp(Frame.OutputFrame.Cookie));
|
|
}
|
|
|
|
Frame.State = EFrameState::Free;
|
|
}
|
|
|
|
bool FNvVideoEncoder::InitFrameInputBuffer(FFrame& Frame, uint32 Width, uint32 Height)
|
|
{
|
|
FInputFrame& InputFrame = Frame.InputFrame;
|
|
|
|
// Create (if necessary) and clear the GPU Fence so we can detect when the copy finished
|
|
if (!InputFrame.CopyFence)
|
|
{
|
|
FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
|
|
InputFrame.CopyFence = RHICmdList.CreateGPUFence(*FString::Printf(TEXT("PixelStreamingCopy_%d"), Frame.Id));
|
|
}
|
|
|
|
// Create resolved back buffer texture
|
|
{
|
|
// Make sure format used here is compatible with NV_ENC_BUFFER_FORMAT specified later in NV_ENC_REGISTER_RESOURCE bufferFormat
|
|
FRHIResourceCreateInfo CreateInfo(TEXT("InputFrameTexture"));
|
|
InputFrame.Texture = RHICreateTexture2D(Width, Height, EPixelFormat::PF_B8G8R8A8, 1, 1, TexCreate_RenderTargetable | TexCreate_Shared, CreateInfo);
|
|
}
|
|
|
|
// Share this texture with the encoder device.
|
|
FString RHIName = GDynamicRHI->GetName();
|
|
|
|
if (RHIName == TEXT("D3D11"))
|
|
{
|
|
ID3D11Texture2D* ResolvedTexture = (ID3D11Texture2D*)InputFrame.Texture->GetTexture2D()->GetNativeResource();
|
|
|
|
TRefCountPtr<IDXGIResource> DXGIResource;
|
|
CHECK_HR_DEFAULT(ResolvedTexture->QueryInterface(IID_PPV_ARGS(DXGIResource.GetInitReference())));
|
|
|
|
//
|
|
// NOTE : The HANDLE IDXGIResource::GetSharedHandle gives us is NOT an NT Handle, and therefre we should not call CloseHandle on it
|
|
//
|
|
HANDLE SharedHandle;
|
|
CHECK_HR_DEFAULT(DXGIResource->GetSharedHandle(&SharedHandle));
|
|
CHECK_HR_DEFAULT(EncoderDevice->Device->OpenSharedResource(SharedHandle, __uuidof(ID3D11Texture2D), (LPVOID*)&InputFrame.SharedTexture));
|
|
}
|
|
else if (RHIName == TEXT("D3D12"))
|
|
{
|
|
auto UE4D3DDevice = static_cast<ID3D12Device*>(GDynamicRHI->RHIGetNativeDevice());
|
|
static uint32 NamingIdx = 0;
|
|
ID3D12Resource* ResolvedTexture = (ID3D12Resource*)InputFrame.Texture->GetTexture2D()->GetNativeResource();
|
|
|
|
//
|
|
// NOTE: ID3D12Device::CreateSharedHandle gives as an NT Handle, and so we need to call CloseHandle on it
|
|
//
|
|
HANDLE SharedHandle;
|
|
HRESULT Res1 = UE4D3DDevice->CreateSharedHandle(ResolvedTexture, NULL, GENERIC_ALL, *FString::Printf(TEXT("PixelStreaming_NvEnc_%u"), NamingIdx++), &SharedHandle);
|
|
CHECK_HR_DEFAULT(Res1);
|
|
|
|
TRefCountPtr <ID3D11Device1> Device1;
|
|
CHECK_HR_DEFAULT(EncoderDevice->Device->QueryInterface(__uuidof(ID3D11Device1), (void**)Device1.GetInitReference()));
|
|
CHECK_HR_DEFAULT(Device1->OpenSharedResource1(SharedHandle, __uuidof(ID3D11Texture2D), (LPVOID*)&InputFrame.SharedTexture));
|
|
verify(CloseHandle(SharedHandle));
|
|
}
|
|
|
|
// Register input back buffer
|
|
{
|
|
NV_ENC_REGISTER_RESOURCE RegisterResource;
|
|
FMemory::Memzero(RegisterResource);
|
|
EPixelFormat PixelFormat = InputFrame.Texture->GetFormat();
|
|
|
|
RegisterResource.version = NV_ENC_REGISTER_RESOURCE_VER;
|
|
RegisterResource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX;
|
|
RegisterResource.resourceToRegister = (void*)InputFrame.SharedTexture;
|
|
RegisterResource.width = Width;
|
|
RegisterResource.height = Height;
|
|
RegisterResource.bufferFormat = NV_ENC_BUFFER_FORMAT_ABGR; // Make sure ResolvedTexture is created with a compatible format
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncRegisterResource(EncoderInterface, &RegisterResource);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to register input back buffer (status: %d)"), Result);
|
|
|
|
InputFrame.RegisteredResource = RegisterResource.registeredResource;
|
|
InputFrame.BufferFormat = RegisterResource.bufferFormat;
|
|
}
|
|
// Map input buffer resource
|
|
{
|
|
NV_ENC_MAP_INPUT_RESOURCE MapInputResource;
|
|
FMemory::Memzero(MapInputResource);
|
|
MapInputResource.version = NV_ENC_MAP_INPUT_RESOURCE_VER;
|
|
MapInputResource.registeredResource = InputFrame.RegisteredResource;
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncMapInputResource(EncoderInterface, &MapInputResource);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to map NvEnc input resource (status: %d)"), Result);
|
|
InputFrame.MappedResource = MapInputResource.mappedResource;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FNvVideoEncoder::InitializeResources()
|
|
{
|
|
for (uint32 i = 0; i < NumBufferedFrames; ++i)
|
|
{
|
|
FFrame& Frame = BufferedFrames[i];
|
|
|
|
if (!InitFrameInputBuffer(Frame, NvEncInitializeParams.encodeWidth, NvEncInitializeParams.encodeHeight))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FMemory::Memzero(Frame.OutputFrame);
|
|
// Create output bitstream buffer
|
|
{
|
|
NV_ENC_CREATE_BITSTREAM_BUFFER CreateBitstreamBuffer;
|
|
FMemory::Memzero(CreateBitstreamBuffer);
|
|
CreateBitstreamBuffer.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER;
|
|
CreateBitstreamBuffer.size = BitstreamSize;
|
|
CreateBitstreamBuffer.memoryHeap = NV_ENC_MEMORY_HEAP_SYSMEM_CACHED;
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncCreateBitstreamBuffer(EncoderInterface, &CreateBitstreamBuffer);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to create NvEnc bitstream buffer (status: %d)"), Result);
|
|
Frame.OutputFrame.BitstreamBuffer = CreateBitstreamBuffer.bitstreamBuffer;
|
|
}
|
|
|
|
RegisterAsyncEvent(&Frame.OutputFrame.EventHandle);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void FNvVideoEncoder::ReleaseFrameInputBuffer(FFrame& Frame)
|
|
{
|
|
FInputFrame& InputFrame = Frame.InputFrame;
|
|
|
|
if (InputFrame.MappedResource)
|
|
{
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncUnmapInputResource(EncoderInterface, InputFrame.MappedResource);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to unmap input resource (status: %d)"), Result);
|
|
InputFrame.MappedResource = nullptr;
|
|
}
|
|
|
|
if (InputFrame.RegisteredResource)
|
|
{
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncUnregisterResource(EncoderInterface, InputFrame.RegisteredResource);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to unregister input buffer resource (status: %d)"), Result);
|
|
InputFrame.RegisteredResource = nullptr;
|
|
}
|
|
|
|
InputFrame.Texture.SafeRelease();
|
|
if (InputFrame.SharedTexture)
|
|
{
|
|
InputFrame.SharedTexture->Release();
|
|
InputFrame.SharedTexture = nullptr;
|
|
}
|
|
|
|
if (InputFrame.CopyFence)
|
|
{
|
|
InputFrame.CopyFence.SafeRelease();
|
|
}
|
|
}
|
|
|
|
void FNvVideoEncoder::ReleaseResources()
|
|
{
|
|
for (uint32 i = 0; i < NumBufferedFrames; ++i)
|
|
{
|
|
FFrame& Frame = BufferedFrames[i];
|
|
|
|
ReleaseFrameInputBuffer(Frame);
|
|
|
|
if (Frame.OutputFrame.BitstreamBuffer)
|
|
{
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncDestroyBitstreamBuffer(EncoderInterface, Frame.OutputFrame.BitstreamBuffer);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to destroy output buffer bitstream (status: %d)"), Result);
|
|
Frame.OutputFrame.BitstreamBuffer = nullptr;
|
|
}
|
|
|
|
if (Frame.OutputFrame.EventHandle)
|
|
{
|
|
UnregisterAsyncEvent(Frame.OutputFrame.EventHandle);
|
|
::CloseHandle(Frame.OutputFrame.EventHandle);
|
|
Frame.OutputFrame.EventHandle = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
void FNvVideoEncoder::RegisterAsyncEvent(void** OutEvent)
|
|
{
|
|
NV_ENC_EVENT_PARAMS EventParams;
|
|
FMemory::Memzero(EventParams);
|
|
EventParams.version = NV_ENC_EVENT_PARAMS_VER;
|
|
#if defined PLATFORM_WINDOWS
|
|
EventParams.completionEvent = CreateEvent(nullptr, false, false, nullptr);
|
|
#endif
|
|
_NVENCSTATUS Result = NvEncodeAPI->nvEncRegisterAsyncEvent(EncoderInterface, &EventParams);
|
|
checkf(NV_RESULT(Result), TEXT("Failed to register async event (status: %d)"), Result);
|
|
*OutEvent = EventParams.completionEvent;
|
|
}
|
|
|
|
void FNvVideoEncoder::UnregisterAsyncEvent(void* Event)
|
|
{
|
|
if (Event)
|
|
{
|
|
NV_ENC_EVENT_PARAMS EventParams;
|
|
FMemory::Memzero(EventParams);
|
|
EventParams.version = NV_ENC_EVENT_PARAMS_VER;
|
|
EventParams.completionEvent = Event;
|
|
bool Result = NV_RESULT(NvEncodeAPI->nvEncUnregisterAsyncEvent(EncoderInterface, &EventParams));
|
|
checkf(Result, TEXT("Failed to unregister async event"));
|
|
}
|
|
}
|
|
|
|
#if NVENC_VIDEO_ENCODER_DEBUG
|
|
// Fills with a solid colour
|
|
void FNvVideoEncoder::ClearFrame(FFrame& Frame)
|
|
{
|
|
check(IsInRenderingThread());
|
|
|
|
FRHICommandListImmediate& RHICmdList = FRHICommandListExecutor::GetImmediateCommandList();
|
|
|
|
static_assert(NumBufferedFrames == 3, "Unexpected number of slots. Please update the array to match.");
|
|
FLinearColor Colors[NumBufferedFrames] =
|
|
{
|
|
FLinearColor(1,0,0),
|
|
FLinearColor(0,1,0),
|
|
FLinearColor(0,0,1)
|
|
};
|
|
|
|
FRHIRenderPassInfo RPInfo(Frame.InputFrame.Texture, ERenderTargetActions::Load_Store);
|
|
TransitionRenderPassTargets(RHICmdList, RPInfo);
|
|
RHICmdList.BeginRenderPass(RPInfo, TEXT("ClearCanvas"));
|
|
RHICmdList.SetViewport(0, 0, 0.0f
|
|
, Frame.InputFrame.Texture->GetSizeXY().X
|
|
, Frame.InputFrame.Texture->GetSizeXY().Y, 1.0f);
|
|
|
|
DrawClearQuad(RHICmdList, Colors[Frame.Id]);
|
|
RHICmdList.EndRenderPass();
|
|
}
|
|
#endif
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// FNvVideoEncoderFactory implementation
|
|
//////////////////////////////////////////////////////////////////////////
|
|
|
|
FNvVideoEncoderFactory::FNvVideoEncoderFactory()
|
|
{
|
|
}
|
|
|
|
FNvVideoEncoderFactory::~FNvVideoEncoderFactory()
|
|
{
|
|
}
|
|
|
|
const TCHAR* FNvVideoEncoderFactory::GetName() const
|
|
{
|
|
return TEXT("nvenc");
|
|
}
|
|
|
|
TArray<FString> FNvVideoEncoderFactory::GetSupportedCodecs() const
|
|
{
|
|
TArray<FString> Codecs;
|
|
|
|
if (!IsRHIDeviceNVIDIA())
|
|
{
|
|
UE_LOG(LogAVEncoder, Log, TEXT("No NvEnc because no NVidia card found"));
|
|
return Codecs;
|
|
}
|
|
|
|
void* Handle = FPlatformProcess::GetDllHandle(GetDllName());
|
|
if (Handle == nullptr)
|
|
{
|
|
UE_LOG(LogAVEncoder, Error, TEXT("NVidia card found, but no NvEnc DLL installed."));
|
|
return Codecs;
|
|
}
|
|
else
|
|
{
|
|
FPlatformProcess::FreeDllHandle(Handle);
|
|
}
|
|
|
|
Codecs.Add(TEXT("h264"));
|
|
return Codecs;
|
|
}
|
|
|
|
TUniquePtr<FVideoEncoder> FNvVideoEncoderFactory::CreateEncoder(const FString& Codec)
|
|
{
|
|
if (Codec=="h264")
|
|
{
|
|
return TUniquePtr<FVideoEncoder>(new FNvVideoEncoder());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAVEncoder, Error, TEXT("FNvVideoEncoderFactory doesn't support the %s codec"), *Codec);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
|
|
} // namespace AVEncoder
|
|
|
|
|
|
#endif //AVENCODER_SUPPORTED_MICROSOFT_PLATFORM
|