Optimize Frame Readback and Encoding Pipeline

This commit is contained in:
copecone committed 2026-10-07 15:48:09 +09:00
1 parent 892f1ca7b9
commit 86c8a87a0f
3 files changed
+57 -110

No files matched your search

+3 -2
View File
@@ -117,6 +117,7 @@ internal class RenderManager : MonoBehaviour
public int Length; public int Length;
public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame) public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame)
public ReadbackRequestState? ReadbackState; // Associated state to recycle public ReadbackRequestState? ReadbackState; // Associated state to recycle
public bool IsColorConverted;
} }
// Thread and Queue State // Thread and Queue State
@@ -546,7 +547,7 @@ internal class RenderManager : MonoBehaviour
{ {
if (frame.ManagedBuffer.Length > 0) if (frame.ManagedBuffer.Length > 0)
{ {
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length); videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent); Interlocked.Increment(ref framesSent);
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer); ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
@@ -554,7 +555,7 @@ internal class RenderManager : MonoBehaviour
} }
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0) else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
{ {
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length); videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted);
Interlocked.Increment(ref framesSent); Interlocked.Increment(ref framesSent);
if (frame.ReadbackState != null) if (frame.ReadbackState != null)
+15 -51
View File
@@ -974,29 +974,22 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// Encodes a frame from IntPtr (native pointer). /// Encodes a frame from IntPtr (native pointer).
/// Can be used from external native plugins or custom capture sources. /// Can be used from external native plugins or custom capture sources.
/// </summary> /// </summary>
public void SendDataFromNativeBuffer(IntPtr pointer, int length) public void SendDataFromNativeBuffer(IntPtr pointer, int length, bool isColorConverted)
{ {
lock (_lock) lock (_lock)
{ {
if (_disposed || !_isInitialized) return; if (_disposed || !_isInitialized) return;
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
if (!_loggedConversionMode) if (!_loggedConversionMode)
{ {
_loggedConversionMode = true; _loggedConversionMode = true;
Melon<RenderMod>.Logger.Msg(length == preConvertedSize Melon<RenderMod>.Logger.Msg(isColorConverted
? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)" ? "[Encoder] GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
: "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)"); : "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)");
} }
if (length == preConvertedSize) if (isColorConverted) WritePreConvertedFrame((byte*)pointer.ToPointer());
{ else WriteFrameFromBuffer((byte*)pointer.ToPointer());
WritePreConvertedFrame((byte*)pointer.ToPointer());
}
else
{
WriteFrameFromBuffer((byte*)pointer.ToPointer());
}
} }
} }
@@ -1004,31 +997,23 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// Encodes a frame from a managed byte[]. /// Encodes a frame from a managed byte[].
/// Pins the array using fixed keyword to prevent GC from moving it before passing the pointer. /// Pins the array using fixed keyword to prevent GC from moving it before passing the pointer.
/// </summary> /// </summary>
public void SendDataFromBuffer(byte[] buffer, int length) public void SendDataFromBuffer(byte[] buffer, int length, bool isColorConverted)
{ {
lock (_lock) lock (_lock)
{ {
if (_disposed || !_isInitialized) return; if (_disposed || !_isInitialized) return;
fixed (byte* pBuffer = buffer) fixed (byte* pBuffer = buffer)
{ {
var preConvertedSize = _settings.Width * _settings.Height * 3 / 2;
if (!_loggedConversionMode) if (!_loggedConversionMode)
{ {
_loggedConversionMode = true; _loggedConversionMode = true;
Melon<RenderMod>.Logger.Msg(length == preConvertedSize Melon<RenderMod>.Logger.Msg(isColorConverted
? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)" ? "[Encoder] High-speed GPU color conversion enabled (NV12/YUV420P Direct Write, bypassing sws_scale)"
: "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)"); : "[Encoder] CPU color conversion fallback enabled (RGBA Input, using sws_scale)");
} }
if (length == preConvertedSize) if (isColorConverted) WritePreConvertedFrame(pBuffer);
{ else WriteFrameFromBuffer(pBuffer);
WritePreConvertedFrame(pBuffer);
}
else
{
WriteFrameFromBuffer(pBuffer);
}
} }
} }
} }
@@ -1145,33 +1130,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
throw new NotSupportedException($"This pixel format ({_dstPixFmt}) does not support direct copying of pre-converted buffers."); throw new NotSupportedException($"This pixel format ({_dstPixFmt}) does not support direct copying of pre-converted buffers.");
} }
// QSV acceleration handling or encoder send SubmitDstFrame();
if (_isQsvEncoder)
{
ffmpeg.av_frame_unref(_pHwFrame);
try
{
var ret = ffmpeg.av_hwframe_get_buffer(_pCodecContext->hw_frames_ctx, _pHwFrame, 0);
if (ret < 0)
throw new Exception($"Failed to allocate QSV hardware frame buffer — {GetFFmpegErrorString(ret)} (Code: {ret})");
ret = ffmpeg.av_hwframe_transfer_data(_pHwFrame, _pDstFrame, 0);
if (ret < 0)
throw new Exception($"Failed to upload QSV frame — {GetFFmpegErrorString(ret)} (Code: {ret})");
_pHwFrame->pts = _frameIndex++;
SendFrameAndWritePackets(_pHwFrame);
}
finally
{
ffmpeg.av_frame_unref(_pHwFrame);
}
}
else
{
_pDstFrame->pts = _frameIndex++;
SendFrameAndWritePackets(_pDstFrame);
}
} }
/// <summary> /// <summary>
@@ -1209,6 +1168,11 @@ namespace ADOFAIRenderer.Util.FFmpeg
_pDstFrame->data, _pDstFrame->linesize _pDstFrame->data, _pDstFrame->linesize
); );
SubmitDstFrame();
}
private void SubmitDstFrame()
{
if (_isQsvEncoder) if (_isQsvEncoder)
{ {
// ── QSV hw_frames_ctx mode: System memory → GPU surface upload ── // ── QSV hw_frames_ctx mode: System memory → GPU surface upload ──
+39 -57
View File
@@ -71,72 +71,55 @@ namespace ADOFAIRenderer.Util
state.IsInFlight = true; state.IsInFlight = true;
} }
// Check if GPU color conversion is enabled // Check if GPU color conversion is available
bool useGPUConversion = ModAssets.ColorConversionShader; var expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA;
if (useGPUConversion) var kernelIndex = -1;
if (ModAssets.ColorConversionShader)
{ {
expectedFmt = _targetFormat; var kernalName = _targetFormat switch
// Compute Shader only supports NV12 and YUV420P; fallback to CPU for other formats
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
{ {
useGPUConversion = false; AVPixelFormat.AV_PIX_FMT_NV12 => "CSMain_NV12",
} AVPixelFormat.AV_PIX_FMT_YUV420P => "CSMain_YUV420P",
_ => null
};
if (kernalName != null)
kernelIndex = ModAssets.ColorConversionShader.FindKernel(kernalName);
} }
var useGPUConversion = kernelIndex >= 0;
if (useGPUConversion) expectedFmt = _targetFormat;
var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height); state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height);
if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader) if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader)
{ {
int kernelIndex = -1; ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture);
if (expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12) ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer);
{ ModAssets.ColorConversionShader.SetInt(Width, texture.width);
kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_NV12"); ModAssets.ColorConversionShader.SetInt(Height, texture.height);
}
else if (expectedFmt == AVPixelFormat.AV_PIX_FMT_YUV420P)
{
kernelIndex = ModAssets.ColorConversionShader.FindKernel("CSMain_YUV420P");
}
if (kernelIndex >= 0) var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12
{ ? (texture.width + 31) / 32
ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); : (texture.width + 63) / 64;
ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); var threadGroupsY = (texture.height + 15) / 16;
ModAssets.ColorConversionShader.SetInt(Width, texture.width);
ModAssets.ColorConversionShader.SetInt(Height, texture.height);
var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12 ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1);
? (texture.width + 31) / 32
: (texture.width + 63) / 64;
var threadGroupsY = (texture.height + 15) / 16;
ModAssets.ColorConversionShader.Dispatch(kernelIndex, threadGroupsX, threadGroupsY, 1); AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer, state.GPUBuffer, state.CachedCallback
AsyncGPUReadback.Request( );
state.GPUBuffer, /*AsyncGPUReadback.Request(
state.CachedCallback state.GPUBuffer,
); state.CachedCallback
} );*/
else
{
// If kernel is not found, fallback to standard RGBA texture readback
AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer,
texture,
0,
state.CachedCallback
);
}
} }
else else
{ {
AsyncGPUReadback.RequestIntoNativeArray( AsyncGPUReadback.RequestIntoNativeArray(
ref state.NativeBuffer, ref state.NativeBuffer, texture, 0, state.CachedCallback
texture,
0,
state.CachedCallback
); );
} }
} }
@@ -169,16 +152,14 @@ namespace ADOFAIRenderer.Util
} }
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame."); Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
var byteSize = state.IsGPUConversion var byteSize = state.Width * state.Height * 3 / 2;
? state.Width * state.Height * 3 / 2
: state.Width * state.Height * 4;
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize); var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize); Array.Clear(blackFrame, 0, byteSize);
var capFrame = new RenderManager.CapturedFrame var capFrame = new RenderManager.CapturedFrame
{ {
ManagedBuffer = blackFrame, ManagedBuffer = blackFrame, Length = byteSize,
Length = byteSize IsColorConverted = true
}; };
PushToQueue(frame, capFrame); PushToQueue(frame, capFrame);
RecycleState(state); RecycleState(state);
@@ -187,19 +168,20 @@ namespace ADOFAIRenderer.Util
{ {
unsafe unsafe
{ {
/*
if (state.IsGPUConversion) if (state.IsGPUConversion)
{ {
var data = request.GetData<byte>(); var data = request.GetData<byte>();
// Copy async request buffer data to persistent NativeBuffer memory // Copy async request buffer data to persistent NativeBuffer memory
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length); NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
} }
*/
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
var capFrame = new RenderManager.CapturedFrame var capFrame = new RenderManager.CapturedFrame
{ {
NativePointer = ptr, NativePointer = ptr, Length = state.NativeBuffer.Length,
Length = state.NativeBuffer.Length, ReadbackState = state, IsColorConverted = state.IsGPUConversion
ReadbackState = state
}; };
PushToQueue(frame, capFrame); PushToQueue(frame, capFrame);
} }