SHA256
219 lines
6.6 KiB
C#
219 lines
6.6 KiB
C#
using System;
|
|
using System.Buffers;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using ADOFAIRenderer.Core;
|
|
using MelonLoader;
|
|
using Unity.Collections;
|
|
using Unity.Collections.LowLevel.Unsafe;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace ADOFAIRenderer.Util
|
|
{
|
|
public class FrameReader : IDisposable
|
|
{
|
|
private const int InitialBufferSize = 16;
|
|
|
|
private readonly Stack<ReadbackRequestState> _statePool = new Stack<ReadbackRequestState>();
|
|
private readonly List<ReadbackRequestState> _allocatedStates = new List<ReadbackRequestState>();
|
|
private readonly object _poolLock = new object();
|
|
private bool _isInitialized;
|
|
private volatile bool _isDisposed;
|
|
|
|
public FrameReader()
|
|
{
|
|
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);
|
|
|
|
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.NativeBuffer.Length;
|
|
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
|
|
Array.Clear(blackFrame, 0, byteSize);
|
|
|
|
var capFrame = new RenderManager.CapturedFrame
|
|
{
|
|
ManagedBuffer = blackFrame,
|
|
Length = byteSize
|
|
};
|
|
PushToQueue(frame, capFrame);
|
|
RecycleState(state);
|
|
}
|
|
else
|
|
{
|
|
unsafe
|
|
{
|
|
var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
|
|
var capFrame = new RenderManager.CapturedFrame
|
|
{
|
|
NativePointer = ptr,
|
|
Length = state.NativeBuffer.Length,
|
|
ReadbackState = state
|
|
};
|
|
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;
|
|
}
|
|
}
|
|
|
|
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 readonly Action<AsyncGPUReadbackRequest> CachedCallback;
|
|
|
|
public ReadbackRequestState(FrameReader owner)
|
|
{
|
|
_owner = owner;
|
|
CachedCallback = OnComplete;
|
|
FrameIndex = -1;
|
|
}
|
|
|
|
public void Setup(long frameIndex, int bufferSize)
|
|
{
|
|
FrameIndex = frameIndex;
|
|
|
|
// 버퍼가 생성되지 않았거나 크기가 다른 경우 재생성
|
|
if (!NativeBuffer.IsCreated || NativeBuffer.Length != bufferSize)
|
|
{
|
|
// 이전 메모리 해제
|
|
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
|
|
|
NativeBuffer = new NativeArray<byte>(bufferSize, Allocator.Persistent,
|
|
NativeArrayOptions.UninitializedMemory);
|
|
}
|
|
}
|
|
|
|
private void OnComplete(AsyncGPUReadbackRequest request)
|
|
{
|
|
_owner.OnReadbackComplete(this, request);
|
|
}
|
|
|
|
public void Recycle()
|
|
{
|
|
FrameIndex = -1;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
|
}
|
|
}
|
|
} |