SHA256
340 lines
12 KiB
C#
340 lines
12 KiB
C#
using System;
|
||
using System.Buffers;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using ADOFAIRenderer.Core;
|
||
using ADOFAIRenderer.UI;
|
||
using FFmpeg.AutoGen;
|
||
using MelonLoader;
|
||
using Unity.Collections;
|
||
using Unity.Collections.LowLevel.Unsafe;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
|
||
namespace ADOFAIRenderer.Util
|
||
{
|
||
internal class FrameReader : IDisposable
|
||
{
|
||
private static readonly int Height = Shader.PropertyToID("Height");
|
||
private static readonly int Width = Shader.PropertyToID("Width");
|
||
private static readonly int DestinationBuffer = Shader.PropertyToID("DestinationBuffer");
|
||
private static readonly int SourceTexture = Shader.PropertyToID("SourceTexture");
|
||
private const int InitialBufferSize = 16;
|
||
|
||
private readonly Stack<ReadbackRequestState> _statePool = new();
|
||
private readonly List<ReadbackRequestState> _allocatedStates = new();
|
||
private readonly object _poolLock = new();
|
||
private bool _isInitialized;
|
||
private volatile bool _isDisposed;
|
||
|
||
private readonly ComputeShader? _computeShader;
|
||
private readonly int _kernelIndex = -1;
|
||
private readonly bool _useGPUConversion;
|
||
private readonly AVPixelFormat _expectedFormat;
|
||
|
||
public FrameReader(AVPixelFormat targetFormat)
|
||
{
|
||
_computeShader = ModAssets.ColorConversionShader;
|
||
if (_computeShader)
|
||
{
|
||
var kernelName = targetFormat switch
|
||
{
|
||
AVPixelFormat.AV_PIX_FMT_NV12 => "CSMain_NV12",
|
||
AVPixelFormat.AV_PIX_FMT_YUV420P => "CSMain_YUV420P",
|
||
_ => null
|
||
};
|
||
|
||
if (kernelName != null)
|
||
{
|
||
try { _kernelIndex = _computeShader.FindKernel(kernelName); }
|
||
catch { _kernelIndex = -1; }
|
||
}
|
||
}
|
||
|
||
_useGPUConversion = _kernelIndex >= 0;
|
||
_expectedFormat = _useGPUConversion ? targetFormat : AVPixelFormat.AV_PIX_FMT_RGBA;
|
||
|
||
InitializePool();
|
||
}
|
||
|
||
private void InitializePool()
|
||
{
|
||
lock (_poolLock)
|
||
{
|
||
if (_isInitialized) return;
|
||
for (var i = 0; i < InitialBufferSize; i++)
|
||
{
|
||
var state = new ReadbackRequestState(this);
|
||
_statePool.Push(state);
|
||
_allocatedStates.Add(state);
|
||
}
|
||
|
||
_isInitialized = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Bytes of the readback buffer of one frame: Width x Height x 3 / 2 when the GPU conversion to NV12 / YUV420P
|
||
/// is used, Width x Height x 4 (RGBA) otherwise. The same size as ReadbackRequestState.Setup uses.
|
||
/// </summary>
|
||
public long GetFrameBufferBytes(int width, int height)
|
||
{
|
||
var pixels = (long)width * height;
|
||
return _useGPUConversion ? pixels * 3 / 2 : pixels * 4;
|
||
}
|
||
|
||
public void RequestFrameCapture(Texture texture, long currentFrame)
|
||
{
|
||
if (!RenderManager.IsCapturingActive) return;
|
||
ReadbackRequestState state;
|
||
|
||
lock (_poolLock)
|
||
{
|
||
if (_statePool.Count == 0)
|
||
{
|
||
state = new ReadbackRequestState(this);
|
||
_allocatedStates.Add(state);
|
||
}
|
||
else
|
||
{
|
||
state = _statePool.Pop();
|
||
}
|
||
|
||
state.IsInFlight = true;
|
||
}
|
||
|
||
var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
|
||
state.Setup(currentFrame, bufferSize, _useGPUConversion, _expectedFormat, texture.width, texture.height);
|
||
|
||
if (_useGPUConversion && state.GPUBuffer != null && _computeShader)
|
||
{
|
||
_computeShader.SetTexture(_kernelIndex, SourceTexture, texture);
|
||
_computeShader.SetBuffer(_kernelIndex, DestinationBuffer, state.GPUBuffer);
|
||
_computeShader.SetInt(Width, texture.width);
|
||
_computeShader.SetInt(Height, texture.height);
|
||
|
||
var threadGroupsX = _expectedFormat == AVPixelFormat.AV_PIX_FMT_NV12
|
||
? (texture.width + 31) / 32
|
||
: (texture.width + 63) / 64;
|
||
var threadGroupsY = (texture.height + 15) / 16;
|
||
|
||
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||
|
||
AsyncGPUReadback.RequestIntoNativeArray(
|
||
ref state.NativeBuffer, state.GPUBuffer, state.CachedCallback
|
||
);
|
||
/*AsyncGPUReadback.Request(
|
||
state.GPUBuffer,
|
||
state.CachedCallback
|
||
);*/
|
||
}
|
||
else
|
||
{
|
||
AsyncGPUReadback.RequestIntoNativeArray(
|
||
ref state.NativeBuffer, texture, 0, state.CachedCallback
|
||
);
|
||
}
|
||
}
|
||
|
||
internal void OnReadbackComplete(ReadbackRequestState state, AsyncGPUReadbackRequest request)
|
||
{
|
||
var frame = state.FrameIndex;
|
||
|
||
lock (_poolLock)
|
||
{
|
||
state.IsInFlight = false;
|
||
if (_isDisposed)
|
||
{
|
||
state.Dispose();
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (RenderManager.Instance && RenderManager.Instance.IsRenderAborted)
|
||
{
|
||
RecycleState(state);
|
||
return;
|
||
}
|
||
|
||
if (request.hasError)
|
||
{
|
||
if (RenderManager.Instance)
|
||
{
|
||
RenderManager.Instance.readbackFailureCount++;
|
||
}
|
||
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
|
||
|
||
var blackFrame = CreateBlackFrame(state, out var byteSize);
|
||
var capFrame = new RenderManager.CapturedFrame
|
||
{
|
||
ManagedBuffer = blackFrame, Length = byteSize,
|
||
IsColorConverted = state.IsGPUConversion
|
||
};
|
||
PushToQueue(frame, capFrame);
|
||
RecycleState(state);
|
||
}
|
||
else
|
||
{
|
||
unsafe
|
||
{
|
||
/*
|
||
if (state.IsGPUConversion)
|
||
{
|
||
var data = request.GetData<byte>();
|
||
// Copy async request buffer data to persistent NativeBuffer memory
|
||
NativeArray<byte>.Copy(data, state.NativeBuffer, data.Length);
|
||
}
|
||
*/
|
||
|
||
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
||
var capFrame = new RenderManager.CapturedFrame
|
||
{
|
||
NativePointer = ptr, Length = state.NativeBuffer.Length,
|
||
ReadbackState = state, IsColorConverted = state.IsGPUConversion
|
||
};
|
||
PushToQueue(frame, capFrame);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Creates a black frame in the same format as the frames of this reader, from ArrayPool<byte>.Shared
|
||
/// (returned to the pool by the render thread after encoding).
|
||
///
|
||
/// GPU conversion (NV12 / YUV420P, full range): Width × Height × 3 / 2 bytes.
|
||
/// Y plane (Width × Height bytes) = 0, U/V data (the rest) = 128.
|
||
/// 128 is the zero chroma value of the 8-bit full range: the U/V offset 0.5 of RGB2YUV_BT709_Full
|
||
/// in ColorConversion.compute, which the encoder input of this path uses.
|
||
/// Both NV12 (interleaved UV) and YUV420P (U plane, then V plane) store all chroma after the Y plane,
|
||
/// so the same fill applies to both.
|
||
/// No GPU conversion (RGBA input, converted by the encoder): Width × Height × 4 bytes, all 0.
|
||
/// </summary>
|
||
private static byte[] CreateBlackFrame(ReadbackRequestState state, out int byteSize)
|
||
{
|
||
var lumaSize = state.Width * state.Height;
|
||
byteSize = state.IsGPUConversion ? lumaSize * 3 / 2 : lumaSize * 4;
|
||
|
||
var frame = ArrayPool<byte>.Shared.Rent(byteSize);
|
||
if (state.IsGPUConversion)
|
||
{
|
||
Array.Clear(frame, 0, lumaSize);
|
||
frame.AsSpan(lumaSize, byteSize - lumaSize).Fill(128);
|
||
}
|
||
else
|
||
{
|
||
Array.Clear(frame, 0, byteSize);
|
||
}
|
||
return frame;
|
||
}
|
||
|
||
internal void RecycleState(ReadbackRequestState state)
|
||
{
|
||
lock (_poolLock)
|
||
{
|
||
if (_isDisposed || !_isInitialized)
|
||
{
|
||
state.Dispose();
|
||
return;
|
||
}
|
||
|
||
state.Recycle();
|
||
_statePool.Push(state);
|
||
}
|
||
}
|
||
|
||
private static void PushToQueue(long frame, RenderManager.CapturedFrame capturedFrame)
|
||
{
|
||
if (!RenderManager.Instance) return;
|
||
lock (RenderManager.Instance.ImageQueue)
|
||
{
|
||
RenderManager.Instance.ImageQueue[frame] = capturedFrame;
|
||
}
|
||
RenderManager.Instance.FrameAvailableEvent.Set();
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
lock (_poolLock)
|
||
{
|
||
if (_isDisposed) return;
|
||
_isDisposed = true;
|
||
foreach (var state in _allocatedStates.Where(state => !state.IsInFlight))
|
||
{
|
||
state.Dispose();
|
||
}
|
||
_statePool.Clear();
|
||
_allocatedStates.Clear();
|
||
_isInitialized = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
internal class ReadbackRequestState : IDisposable
|
||
{
|
||
private readonly FrameReader _owner;
|
||
public long FrameIndex { get; private set; }
|
||
public NativeArray<byte> NativeBuffer;
|
||
public bool IsInFlight { get; internal set; }
|
||
|
||
public ComputeBuffer? GPUBuffer;
|
||
public bool IsGPUConversion { get; private set; }
|
||
public int Width { get; private set; }
|
||
public int Height { get; private set; }
|
||
|
||
public readonly Action<AsyncGPUReadbackRequest> CachedCallback;
|
||
|
||
public ReadbackRequestState(FrameReader owner)
|
||
{
|
||
_owner = owner;
|
||
CachedCallback = OnComplete;
|
||
FrameIndex = -1;
|
||
}
|
||
|
||
public void Setup(long frameIndex, int bufferSize, bool isGPUConversion, AVPixelFormat pixelFormat, int width, int height)
|
||
{
|
||
FrameIndex = frameIndex;
|
||
IsGPUConversion = isGPUConversion;
|
||
Width = width; Height = height;
|
||
|
||
var readbackSize = isGPUConversion ? (width * height * 3 / 2) : bufferSize;
|
||
if (!NativeBuffer.IsCreated || NativeBuffer.Length != readbackSize)
|
||
{
|
||
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
||
|
||
NativeBuffer = new NativeArray<byte>(readbackSize, Allocator.Persistent,
|
||
NativeArrayOptions.UninitializedMemory);
|
||
}
|
||
|
||
if (isGPUConversion)
|
||
{
|
||
var computeBufferSize = readbackSize / 4;
|
||
if (GPUBuffer != null && GPUBuffer.count == computeBufferSize) return;
|
||
|
||
GPUBuffer?.Dispose();
|
||
GPUBuffer = new ComputeBuffer(computeBufferSize, 4, ComputeBufferType.Raw);
|
||
}
|
||
else
|
||
{
|
||
GPUBuffer?.Dispose();
|
||
GPUBuffer = null;
|
||
}
|
||
}
|
||
|
||
private void OnComplete(AsyncGPUReadbackRequest request)
|
||
{
|
||
_owner.OnReadbackComplete(this, request);
|
||
}
|
||
|
||
public void Recycle()
|
||
{
|
||
FrameIndex = -1;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
||
GPUBuffer?.Dispose();
|
||
GPUBuffer = null;
|
||
}
|
||
}
|
||
} |