Files
ADOFAIRenderer/Util/FrameReader.cs
T

303 lines
10 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;
}
}
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.IsRenderCancelled)
{
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 byteSize = state.Width * state.Height * 3 / 2;
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize);
var capFrame = new RenderManager.CapturedFrame
{
ManagedBuffer = blackFrame, Length = byteSize,
IsColorConverted = true
};
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);
}
}
}
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;
}
}
}