Merge branch 'reduce_memcopy'

This commit is contained in:
copecone committed 2026-06-01 12:42:47 +09:00
commit 54a4d2e7bb
4 files changed
+132 -32

No files matched your search

+57 -12
View File
@@ -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 // Thread and Queue State
public SortedList<long, byte[]> ImageQueue = new SortedList<long, byte[]>(); internal SortedList<long, CapturedFrame> ImageQueue = new SortedList<long, CapturedFrame>();
public Thread? RenderThread; public Thread? RenderThread;
public long passCount; public long passCount;
public bool endRequested; public bool endRequested;
@@ -251,7 +259,11 @@ namespace ADOFAIRenderer.Core
lock (ImageQueue) lock (ImageQueue)
{ {
ImageQueue[videoFrameIndex] = blackFrame; ImageQueue[videoFrameIndex] = new CapturedFrame
{
ManagedBuffer = blackFrame,
Length = byteSize
};
} }
} }
@@ -470,7 +482,8 @@ namespace ADOFAIRenderer.Core
while (true) while (true)
{ {
byte[]? buffer = null; CapturedFrame frame = default;
bool hasFrame = false;
lock (ImageQueue) lock (ImageQueue)
{ {
if (ImageQueue.Count > 0) if (ImageQueue.Count > 0)
@@ -484,20 +497,32 @@ namespace ADOFAIRenderer.Core
if (firstKey == passCount) if (firstKey == passCount)
{ {
buffer = ImageQueue[firstKey]; frame = ImageQueue[firstKey];
ImageQueue.RemoveAt(0); ImageQueue.RemoveAt(0);
passCount++; passCount++;
hasFrame = true;
} }
} }
} }
if (buffer != null) if (hasFrame)
{ {
if (buffer.Length > 0) if (frame.ManagedBuffer != null)
{ {
var bufferSize = Configuration.RenderWidth * Configuration.RenderHeight * 4; if (frame.ManagedBuffer.Length > 0)
videoFFmpeg.SendDataFromBuffer(buffer, bufferSize); {
ArrayPool<byte>.Shared.Return(buffer); videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length);
ArrayPool<byte>.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; continue;
} }
@@ -583,11 +608,15 @@ namespace ADOFAIRenderer.Core
// 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어 // 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
lock (ImageQueue) 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<byte>.Shared.Return(kvp.Value); ArrayPool<byte>.Shared.Return(kvp.ManagedBuffer);
}
if (kvp.ReadbackState != null)
{
_frameReader?.RecycleState(kvp.ReadbackState);
} }
} }
ImageQueue.Clear(); ImageQueue.Clear();
@@ -664,6 +693,22 @@ namespace ADOFAIRenderer.Core
GCS.customLevelPaths = null; GCS.customLevelPaths = null;
GCS.customLevelId = null; GCS.customLevelId = null;
lock (ImageQueue)
{
foreach (var kvp in ImageQueue.Values)
{
if (kvp.ManagedBuffer != null && kvp.ManagedBuffer.Length > 0)
{
ArrayPool<byte>.Shared.Return(kvp.ManagedBuffer);
}
if (kvp.ReadbackState != null)
{
_frameReader?.RecycleState(kvp.ReadbackState);
}
}
ImageQueue.Clear();
}
_frameReader?.Dispose(); _frameReader?.Dispose();
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
} }
+7
View File
@@ -44,6 +44,13 @@ namespace ADOFAIRenderer.Util.FFmpeg
_inputStream.Flush(); _inputStream.Flush();
} }
public void SendDataFromNativeBuffer(IntPtr nativePtr, int length)
{
if (_inputStream == null) return;
StreamUtil.WriteNativePtrToStream(nativePtr, length, _inputStream);
_inputStream.Flush();
}
public void Close() public void Close()
{ {
if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return; if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return;
+39 -20
View File
@@ -1,8 +1,8 @@
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader; using MelonLoader;
using Unity.Collections; using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe; using Unity.Collections.LowLevel.Unsafe;
@@ -16,6 +16,7 @@ namespace ADOFAIRenderer.Util
private const int InitialBufferSize = 16; private const int InitialBufferSize = 16;
private readonly Stack<ReadbackRequestState> _statePool = new Stack<ReadbackRequestState>(); private readonly Stack<ReadbackRequestState> _statePool = new Stack<ReadbackRequestState>();
private readonly List<ReadbackRequestState> _allocatedStates = new List<ReadbackRequestState>();
private readonly object _poolLock = new object(); private readonly object _poolLock = new object();
private bool _isInitialized; private bool _isInitialized;
private volatile bool _isDisposed; private volatile bool _isDisposed;
@@ -27,13 +28,14 @@ namespace ADOFAIRenderer.Util
private void InitializePool() private void InitializePool()
{ {
lock (_poolLock) lock (_poolLock)
{ {
if (_isInitialized) return; if (_isInitialized) return;
for (var i = 0; i < InitialBufferSize; i++) 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; _isInitialized = true;
@@ -47,7 +49,17 @@ namespace ADOFAIRenderer.Util
lock (_poolLock) 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 var bufferSize = texture.width * texture.height * 4; // RGBA = 4 channel
@@ -67,6 +79,7 @@ namespace ADOFAIRenderer.Util
lock (_poolLock) lock (_poolLock)
{ {
state.IsInFlight = false;
if (_isDisposed) if (_isDisposed)
{ {
state.Dispose(); state.Dispose();
@@ -91,28 +104,32 @@ namespace ADOFAIRenderer.Util
var byteSize = state.NativeBuffer.Length; var byteSize = state.NativeBuffer.Length;
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize); var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize); Array.Clear(blackFrame, 0, byteSize);
PushToQueue(frame, blackFrame);
var capFrame = new RenderManager.CapturedFrame
{
ManagedBuffer = blackFrame,
Length = byteSize
};
PushToQueue(frame, capFrame);
RecycleState(state);
} }
else else
{ {
unsafe unsafe
{ {
var ptr = state.NativeBuffer.GetUnsafeReadOnlyPtr(); var ptr = (IntPtr)state.NativeBuffer.GetUnsafeReadOnlyPtr();
var rawBytes = FFmpegProcess.RentRawBytes(state.NativeBuffer.Length); var capFrame = new RenderManager.CapturedFrame
fixed (byte* destPtr = rawBytes)
{ {
Buffer.MemoryCopy(ptr, destPtr, rawBytes.Length, state.NativeBuffer.Length); NativePointer = ptr,
Length = state.NativeBuffer.Length,
ReadbackState = state
};
PushToQueue(frame, capFrame);
} }
PushToQueue(frame, rawBytes);
} }
} }
RecycleState(state); internal void RecycleState(ReadbackRequestState state)
}
private void RecycleState(ReadbackRequestState state)
{ {
lock (_poolLock) 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; if (!RenderManager.Instance) return;
lock (RenderManager.Instance.ImageQueue) lock (RenderManager.Instance.ImageQueue)
{ {
RenderManager.Instance.ImageQueue[frame] = rawBytes; RenderManager.Instance.ImageQueue[frame] = capturedFrame;
} }
} }
@@ -142,11 +159,12 @@ namespace ADOFAIRenderer.Util
{ {
if (_isDisposed) return; if (_isDisposed) return;
_isDisposed = true; _isDisposed = true;
while (_statePool.Count > 0) foreach (var state in _allocatedStates.Where(state => !state.IsInFlight))
{ {
var state = _statePool.Pop();
state.Dispose(); state.Dispose();
} }
_statePool.Clear();
_allocatedStates.Clear();
_isInitialized = false; _isInitialized = false;
} }
} }
@@ -157,6 +175,7 @@ namespace ADOFAIRenderer.Util
private readonly FrameReader _owner; private readonly FrameReader _owner;
public long FrameIndex { get; private set; } public long FrameIndex { get; private set; }
public NativeArray<byte> NativeBuffer; public NativeArray<byte> NativeBuffer;
public bool IsInFlight { get; internal set; }
public readonly Action<AsyncGPUReadbackRequest> CachedCallback; public readonly Action<AsyncGPUReadbackRequest> CachedCallback;
+29
View File
@@ -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<byte>((void*)nativePtr, length);
destinationStream.Write(span);
}
catch (IOException err)
{
Melon<RenderMod>.Logger.Error($"FFmpeg 파이프가 예상치 못하게 닫혔습니다: {err.Message}");
}
}
}
}
}