diff --git a/ADOFAIRenderer.csproj b/ADOFAIRenderer.csproj
index 08e5b21..fa3b553 100644
--- a/ADOFAIRenderer.csproj
+++ b/ADOFAIRenderer.csproj
@@ -5,6 +5,7 @@
enable
MSB3277
false
+ true
diff --git a/Component/RenderManager.cs b/Component/RenderManager.cs
index 43546fb..9367fdc 100644
--- a/Component/RenderManager.cs
+++ b/Component/RenderManager.cs
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.IO;
using System.Threading;
using ADOFAIRenderer.Util;
+using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using MelonLoader.Utils;
using UnityEngine;
@@ -13,7 +14,7 @@ namespace ADOFAIRenderer.Component
public class RenderManager : MonoBehaviour
{
public static int Countdown;
- public static int FrameCount;
+ public static long FrameCount;
public static bool SkipPressToStart;
public static string LevelPath = "";
public static RenderManager Instance = null!;
@@ -52,12 +53,12 @@ namespace ADOFAIRenderer.Component
}
}
- public static SortedList ImageQueue = new SortedList();
+ public static SortedList ImageQueue = new SortedList();
public static Thread? RenderThread;
- public static int PassCount;
+ public static long PassCount;
public static bool EndRequested;
public static bool EndSuccessful;
- public static int WonFrame = -1;
+ public static long WonFrame = -1;
private static bool _renderSetup;
@@ -125,7 +126,7 @@ namespace ADOFAIRenderer.Component
WonFrame = FrameCount;
}
- var extraFrames = (int)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS);
+ var extraFrames = (long)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS);
if (FrameCount - WonFrame < extraFrames) return;
RenderMod.IsRendering = RenderMod.IsCapturing = false;
@@ -164,6 +165,8 @@ namespace ADOFAIRenderer.Component
FrameCount = 0;
+ RenderMod.FrameReader?.Dispose();
+ RenderMod.FrameReader = new FrameReader();
StartFFmpegThread();
RenderMod.IsCapturing = true;
@@ -258,7 +261,7 @@ namespace ADOFAIRenderer.Component
videoFFmpeg.Start(_currentOutPath, bitrate);
- var num = isMultiple ? (int)Math.Round(quotient) : 1;
+ var num = isMultiple ? (long)Math.Round(quotient) : 1L;
while (true)
{
@@ -267,7 +270,7 @@ namespace ADOFAIRenderer.Component
{
if (ImageQueue.Count > 0)
{
- int firstKey = ImageQueue.Keys[0];
+ var firstKey = ImageQueue.Keys[0];
if (firstKey == PassCount)
{
buffer = ImageQueue[firstKey];
diff --git a/Patch/RenderEnvironmentPatches.cs b/Patch/RenderEnvironmentPatches.cs
index 39812f6..a669813 100644
--- a/Patch/RenderEnvironmentPatches.cs
+++ b/Patch/RenderEnvironmentPatches.cs
@@ -87,7 +87,7 @@ namespace ADOFAIRenderer.Patch
public static bool Prefix(ref int __result)
{
if (!RenderMod.IsCapturing || !RenderPatch) return true;
- __result = RenderManager.FrameCount + RenderMod.FrameCountBase;
+ __result = (int)(RenderManager.FrameCount + RenderMod.FrameCountBase);
return false;
}
}
diff --git a/RenderMod.cs b/RenderMod.cs
index 5b567fe..db823a7 100644
--- a/RenderMod.cs
+++ b/RenderMod.cs
@@ -3,6 +3,7 @@ using System.Reflection;
using ADOFAIRenderer;
using ADOFAIRenderer.Component;
using ADOFAIRenderer.Util;
+using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using UnityEngine;
using UnityEngine.EventSystems;
@@ -38,6 +39,8 @@ namespace ADOFAIRenderer
public static double DspTimeBase;
public static int FrameCountBase;
+ public static FrameReader? FrameReader;
+
public static bool RenderCheck = true;
public static int SkipCount;
@@ -98,7 +101,7 @@ namespace ADOFAIRenderer
var targetFps = RenderManager.Configuration.TargetFPS;
var videoFps = RenderManager.Configuration.VideoFPS;
var quotient = targetFps / videoFps;
- var num = Math.Abs(quotient - Math.Round(quotient)) < 1e-9 ? (int)Math.Round(quotient) : 1;
+ var num = Math.Abs(quotient - Math.Round(quotient)) < 1e-9 ? (long)Math.Round(quotient) : 1L;
if (FrameCountForVideo % num == 0)
{
@@ -126,7 +129,7 @@ namespace ADOFAIRenderer
}
}
- public static int FrameCountForVideo;
+ public static long FrameCountForVideo;
public static int ReadbackFailureCount;
public static int ThrottleCount;
public static bool IsThrottling;
@@ -171,7 +174,10 @@ namespace ADOFAIRenderer
{
return;
}
+
+ FrameReader?.RequestFrameCapture(texture, currentFrame);
+ /*
AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req)
{
if (!IsCapturing) return;
@@ -202,7 +208,7 @@ namespace ADOFAIRenderer
{
RenderManager.ImageQueue[currentFrame] = rawBytes;
}
- });
+ });*/
}
public static void StartRender()
diff --git a/UI/RenderButton.cs b/UI/RenderButton.cs
index 012459c..2b9f4a9 100644
--- a/UI/RenderButton.cs
+++ b/UI/RenderButton.cs
@@ -1,5 +1,6 @@
using System.Linq;
using ADOFAIRenderer.Component;
+using ADOFAIRenderer.Util.FFmpeg;
using MelonLoader;
using TMPro;
using UnityEngine;
@@ -35,7 +36,7 @@ namespace ADOFAIRenderer.UI
if (codecDropdown == null) return;
codecDropdown.ClearOptions();
- var options = System.Enum.GetNames(typeof(Util.VideoFFmpeg.Codec)).ToList();
+ var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList();
codecDropdown.AddOptions(options);
}
diff --git a/Util/FFmpeg/FFmpegProcess.cs b/Util/FFmpeg/FFmpegProcess.cs
index 9e4de17..120dc49 100644
--- a/Util/FFmpeg/FFmpegProcess.cs
+++ b/Util/FFmpeg/FFmpegProcess.cs
@@ -52,15 +52,21 @@ namespace ADOFAIRenderer.Util.FFmpeg
_ffmpegProcess.WaitForExit();
}
- public static byte[] RentRawBytes(NativeArray arr) where T : struct
+ public static byte[] RentRawBytes(int size)
+ {
+ var array = ArrayPool.Shared.Rent(size);
+ return array;
+ }
+
+ /*public static byte[] RentRawBytes(NativeArray arr) where T : struct
{
var val = new NativeSlice(arr).SliceConvert();
var array = ArrayPool.Shared.Rent(val.Length);
val.CopyTo(array);
return array;
- }
+ }*/
- public static byte[] ToRawBytes(NativeArray arr) where T : struct
+ protected static byte[] ToRawBytes(NativeArray arr) where T : struct
{
var val = new NativeSlice(arr).SliceConvert();
var array = new byte[val.Length];
diff --git a/Util/FFmpeg/VideoFFmpeg.cs b/Util/FFmpeg/VideoFFmpeg.cs
index e37015a..3077835 100644
--- a/Util/FFmpeg/VideoFFmpeg.cs
+++ b/Util/FFmpeg/VideoFFmpeg.cs
@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using Unity.Collections;
-using UnityEngine;
namespace ADOFAIRenderer.Util.FFmpeg
{
diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs
index 36756b2..695d47d 100644
--- a/Util/FrameReader.cs
+++ b/Util/FrameReader.cs
@@ -1,7 +1,181 @@
-namespace ADOFAIRenderer.Util
+using System;
+using System.Collections.Generic;
+using ADOFAIRenderer.Component;
+using ADOFAIRenderer.Util.FFmpeg;
+using MelonLoader;
+using Unity.Collections;
+using Unity.Collections.LowLevel.Unsafe;
+using UnityEngine;
+using UnityEngine.Rendering;
+
+namespace ADOFAIRenderer.Util
{
- public class FrameReader
+ public class FrameReader : IDisposable
{
+ private const int InitialBufferSize = 16;
+ private readonly Stack _statePool = new Stack();
+ private readonly object _poolLock = new object();
+ private bool _isInitialized;
+
+ public FrameReader()
+ {
+ InitializePool();
+ }
+
+ private void InitializePool()
+ {
+
+ lock (_poolLock)
+ {
+ if (_isInitialized) return;
+ for (var i = 0; i < InitialBufferSize; i++)
+ {
+ _statePool.Push(new ReadbackRequestState(this));
+ }
+
+ _isInitialized = true;
+ }
+ }
+
+ public void RequestFrameCapture(Texture texture, long currentFrame)
+ {
+ if (!RenderMod.IsCapturing) return;
+ ReadbackRequestState state;
+
+ lock (_poolLock)
+ {
+ if (_statePool.Count == 0)
+ {
+ state = new ReadbackRequestState(this);
+ }
+ else
+ {
+ state = _statePool.Pop();
+ }
+ }
+
+ 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;
+
+ if (!RenderMod.IsCapturing)
+ {
+ RecycleState(state);
+ return;
+ }
+
+ if (request.hasError)
+ {
+ RenderMod.ReadbackFailureCount++;
+ Melon.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending an empty frame.");
+
+ PushToQueue(frame, Array.Empty());
+ }
+ else
+ {
+ unsafe
+ {
+ var ptr = state.NativeBuffer.GetUnsafeReadOnlyPtr();
+ var rawBytes = FFmpegProcess.RentRawBytes(state.NativeBuffer.Length);
+
+ fixed (byte* destPtr = rawBytes)
+ {
+ Buffer.MemoryCopy(ptr, destPtr, rawBytes.Length, state.NativeBuffer.Length);
+ }
+
+ PushToQueue(frame, rawBytes);
+ }
+ }
+
+ RecycleState(state);
+ }
+
+ private void RecycleState(ReadbackRequestState state)
+ {
+ state.Recycle();
+ lock (_poolLock)
+ {
+ _statePool.Push(state);
+ }
+ }
+
+ private static void PushToQueue(long frame, byte[] rawBytes)
+ {
+ lock (RenderManager.ImageQueue)
+ {
+ RenderManager.ImageQueue[frame] = rawBytes;
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_poolLock)
+ {
+ while (_statePool.Count > 0)
+ {
+ var state = _statePool.Pop();
+ state.Dispose();
+ }
+ _isInitialized = false;
+ }
+ }
+ }
+
+ internal class ReadbackRequestState : IDisposable
+ {
+ private readonly FrameReader _owner;
+ public long FrameIndex { get; private set; }
+ public NativeArray NativeBuffer;
+
+ public readonly Action 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(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();
+ }
}
}
\ No newline at end of file