diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index 4671169..0f05758 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -83,8 +83,16 @@ namespace ADOFAIRenderer.Core } } + internal struct CapturedFrame + { + public IntPtr NativePointer; + public int Length; + public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame) + public ReadbackRequestState? ReadbackState; // Associated state to recycle + } + // Thread and Queue State - public SortedList ImageQueue = new SortedList(); + internal SortedList ImageQueue = new SortedList(); public Thread? RenderThread; public long passCount; public bool endRequested; @@ -251,7 +259,11 @@ namespace ADOFAIRenderer.Core lock (ImageQueue) { - ImageQueue[videoFrameIndex] = blackFrame; + ImageQueue[videoFrameIndex] = new CapturedFrame + { + ManagedBuffer = blackFrame, + Length = byteSize + }; } } @@ -470,7 +482,8 @@ namespace ADOFAIRenderer.Core while (true) { - byte[]? buffer = null; + CapturedFrame frame = default; + bool hasFrame = false; lock (ImageQueue) { if (ImageQueue.Count > 0) @@ -484,20 +497,32 @@ namespace ADOFAIRenderer.Core if (firstKey == passCount) { - buffer = ImageQueue[firstKey]; + frame = ImageQueue[firstKey]; ImageQueue.RemoveAt(0); passCount++; + hasFrame = true; } } } - if (buffer != null) + if (hasFrame) { - if (buffer.Length > 0) + if (frame.ManagedBuffer != null) { - var bufferSize = Configuration.RenderWidth * Configuration.RenderHeight * 4; - videoFFmpeg.SendDataFromBuffer(buffer, bufferSize); - ArrayPool.Shared.Return(buffer); + if (frame.ManagedBuffer.Length > 0) + { + videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length); + ArrayPool.Shared.Return(frame.ManagedBuffer); + } + } + else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0) + { + videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length); + + if (frame.ReadbackState != null) + { + _frameReader?.RecycleState(frame.ReadbackState); + } } continue; } @@ -583,11 +608,15 @@ namespace ADOFAIRenderer.Core // 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어 lock (ImageQueue) { - foreach (var kvp in ImageQueue) + foreach (var kvp in ImageQueue.Values) { - if (kvp.Value != null && kvp.Value.Length > 0) + if (kvp.ManagedBuffer != null && kvp.ManagedBuffer.Length > 0) { - ArrayPool.Shared.Return(kvp.Value); + ArrayPool.Shared.Return(kvp.ManagedBuffer); + } + if (kvp.ReadbackState != null) + { + _frameReader?.RecycleState(kvp.ReadbackState); } } ImageQueue.Clear(); @@ -664,6 +693,22 @@ namespace ADOFAIRenderer.Core GCS.customLevelPaths = null; GCS.customLevelId = null; + lock (ImageQueue) + { + foreach (var kvp in ImageQueue.Values) + { + if (kvp.ManagedBuffer != null && kvp.ManagedBuffer.Length > 0) + { + ArrayPool.Shared.Return(kvp.ManagedBuffer); + } + if (kvp.ReadbackState != null) + { + _frameReader?.RecycleState(kvp.ReadbackState); + } + } + ImageQueue.Clear(); + } + _frameReader?.Dispose(); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); } diff --git a/Util/FFmpeg/FFmpegProcess.cs b/Util/FFmpeg/FFmpegProcess.cs index 4e3c300..065c180 100644 --- a/Util/FFmpeg/FFmpegProcess.cs +++ b/Util/FFmpeg/FFmpegProcess.cs @@ -44,6 +44,13 @@ namespace ADOFAIRenderer.Util.FFmpeg _inputStream.Flush(); } + public void SendDataFromNativeBuffer(IntPtr nativePtr, int length) + { + if (_inputStream == null) return; + StreamUtil.WriteNativePtrToStream(nativePtr, length, _inputStream); + _inputStream.Flush(); + } + public void Close() { if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return; diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs index 6a05c91..9b05308 100644 --- a/Util/FrameReader.cs +++ b/Util/FrameReader.cs @@ -1,8 +1,8 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using ADOFAIRenderer.Core; -using ADOFAIRenderer.Util.FFmpeg; using MelonLoader; using Unity.Collections; using Unity.Collections.LowLevel.Unsafe; @@ -16,6 +16,7 @@ namespace ADOFAIRenderer.Util private const int InitialBufferSize = 16; private readonly Stack _statePool = new Stack(); + private readonly List _allocatedStates = new List(); private readonly object _poolLock = new object(); private bool _isInitialized; private volatile bool _isDisposed; @@ -27,13 +28,14 @@ namespace ADOFAIRenderer.Util private void InitializePool() { - lock (_poolLock) { if (_isInitialized) return; for (var i = 0; i < InitialBufferSize; i++) { - _statePool.Push(new ReadbackRequestState(this)); + var state = new ReadbackRequestState(this); + _statePool.Push(state); + _allocatedStates.Add(state); } _isInitialized = true; @@ -47,7 +49,17 @@ namespace ADOFAIRenderer.Util lock (_poolLock) { - state = _statePool.Count == 0 ? new ReadbackRequestState(this) : _statePool.Pop(); + 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 @@ -67,6 +79,7 @@ namespace ADOFAIRenderer.Util lock (_poolLock) { + state.IsInFlight = false; if (_isDisposed) { state.Dispose(); @@ -91,28 +104,32 @@ namespace ADOFAIRenderer.Util var byteSize = state.NativeBuffer.Length; var blackFrame = ArrayPool.Shared.Rent(byteSize); Array.Clear(blackFrame, 0, byteSize); - PushToQueue(frame, blackFrame); + + var capFrame = new RenderManager.CapturedFrame + { + ManagedBuffer = blackFrame, + Length = byteSize + }; + PushToQueue(frame, capFrame); + RecycleState(state); } else { unsafe { - var ptr = state.NativeBuffer.GetUnsafeReadOnlyPtr(); - var rawBytes = FFmpegProcess.RentRawBytes(state.NativeBuffer.Length); - - fixed (byte* destPtr = rawBytes) + var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr(); + var capFrame = new RenderManager.CapturedFrame { - Buffer.MemoryCopy(ptr, destPtr, rawBytes.Length, state.NativeBuffer.Length); - } - - PushToQueue(frame, rawBytes); + NativePointer = ptr, + Length = state.NativeBuffer.Length, + ReadbackState = state + }; + PushToQueue(frame, capFrame); } } - - RecycleState(state); } - private void RecycleState(ReadbackRequestState state) + internal void RecycleState(ReadbackRequestState state) { lock (_poolLock) { @@ -127,12 +144,12 @@ namespace ADOFAIRenderer.Util } } - private static void PushToQueue(long frame, byte[] rawBytes) + private static void PushToQueue(long frame, RenderManager.CapturedFrame capturedFrame) { if (!RenderManager.Instance) return; lock (RenderManager.Instance.ImageQueue) { - RenderManager.Instance.ImageQueue[frame] = rawBytes; + RenderManager.Instance.ImageQueue[frame] = capturedFrame; } } @@ -142,11 +159,12 @@ namespace ADOFAIRenderer.Util { if (_isDisposed) return; _isDisposed = true; - while (_statePool.Count > 0) + foreach (var state in _allocatedStates.Where(state => !state.IsInFlight)) { - var state = _statePool.Pop(); state.Dispose(); } + _statePool.Clear(); + _allocatedStates.Clear(); _isInitialized = false; } } @@ -157,6 +175,7 @@ namespace ADOFAIRenderer.Util private readonly FrameReader _owner; public long FrameIndex { get; private set; } public NativeArray NativeBuffer; + public bool IsInFlight { get; internal set; } public readonly Action CachedCallback; diff --git a/Util/StreamUtil.cs b/Util/StreamUtil.cs new file mode 100644 index 0000000..fc21487 --- /dev/null +++ b/Util/StreamUtil.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; +using MelonLoader; + +namespace ADOFAIRenderer.Util +{ + public static class StreamUtil + { + public static void WriteNativePtrToStream(IntPtr nativePtr, int length, Stream destinationStream) + { + if (nativePtr == IntPtr.Zero) + throw new ArgumentNullException(nameof(nativePtr)); + if (length <= 0) return; + + unsafe + { + try + { + var span = new ReadOnlySpan((void*)nativePtr, length); + destinationStream.Write(span); + } + catch (IOException err) + { + Melon.Logger.Error($"FFmpeg 파이프가 예상치 못하게 닫혔습니다: {err.Message}"); + } + } + } + } +} \ No newline at end of file