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 _statePool = new(); private readonly List _allocatedStates = new(); private readonly object _poolLock = new(); private bool _isInitialized; private volatile bool _isDisposed; private readonly AVPixelFormat _targetFormat; public FrameReader(AVPixelFormat targetFormat) { _targetFormat = targetFormat; 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; } // Check if GPU color conversion is available var expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; var kernelIndex = -1; if (ModAssets.ColorConversionShader) { var kernalName = _targetFormat switch { 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 state.Setup(currentFrame, bufferSize, useGPUConversion, expectedFmt, texture.width, texture.height); if (useGPUConversion && state.GPUBuffer != null && ModAssets.ColorConversionShader) { ModAssets.ColorConversionShader.SetTexture(kernelIndex, SourceTexture, texture); ModAssets.ColorConversionShader.SetBuffer(kernelIndex, DestinationBuffer, state.GPUBuffer); ModAssets.ColorConversionShader.SetInt(Width, texture.width); ModAssets.ColorConversionShader.SetInt(Height, texture.height); var threadGroupsX = expectedFmt == AVPixelFormat.AV_PIX_FMT_NV12 ? (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, 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.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.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(); // Copy async request buffer data to persistent NativeBuffer memory NativeArray.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 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 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(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; } } }