SHA256
Apply Optimized GPU Readback
This commit is contained in:
1 parent
3797cb7077
commit
9454226306
8 files changed
+208
-18
No files matched your search
@@ -5,6 +5,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<NoWarn>MSB3277</NoWarn>
|
<NoWarn>MSB3277</NoWarn>
|
||||||
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
|
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ADOFAIRenderer.Util;
|
using ADOFAIRenderer.Util;
|
||||||
|
using ADOFAIRenderer.Util.FFmpeg;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using MelonLoader.Utils;
|
using MelonLoader.Utils;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -13,7 +14,7 @@ namespace ADOFAIRenderer.Component
|
|||||||
public class RenderManager : MonoBehaviour
|
public class RenderManager : MonoBehaviour
|
||||||
{
|
{
|
||||||
public static int Countdown;
|
public static int Countdown;
|
||||||
public static int FrameCount;
|
public static long FrameCount;
|
||||||
public static bool SkipPressToStart;
|
public static bool SkipPressToStart;
|
||||||
public static string LevelPath = "";
|
public static string LevelPath = "";
|
||||||
public static RenderManager Instance = null!;
|
public static RenderManager Instance = null!;
|
||||||
@@ -52,12 +53,12 @@ namespace ADOFAIRenderer.Component
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SortedList<int, byte[]> ImageQueue = new SortedList<int, byte[]>();
|
public static SortedList<long, byte[]> ImageQueue = new SortedList<long, byte[]>();
|
||||||
public static Thread? RenderThread;
|
public static Thread? RenderThread;
|
||||||
public static int PassCount;
|
public static long PassCount;
|
||||||
public static bool EndRequested;
|
public static bool EndRequested;
|
||||||
public static bool EndSuccessful;
|
public static bool EndSuccessful;
|
||||||
public static int WonFrame = -1;
|
public static long WonFrame = -1;
|
||||||
|
|
||||||
private static bool _renderSetup;
|
private static bool _renderSetup;
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ namespace ADOFAIRenderer.Component
|
|||||||
WonFrame = FrameCount;
|
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;
|
if (FrameCount - WonFrame < extraFrames) return;
|
||||||
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
RenderMod.IsRendering = RenderMod.IsCapturing = false;
|
||||||
@@ -164,6 +165,8 @@ namespace ADOFAIRenderer.Component
|
|||||||
|
|
||||||
FrameCount = 0;
|
FrameCount = 0;
|
||||||
|
|
||||||
|
RenderMod.FrameReader?.Dispose();
|
||||||
|
RenderMod.FrameReader = new FrameReader();
|
||||||
StartFFmpegThread();
|
StartFFmpegThread();
|
||||||
|
|
||||||
RenderMod.IsCapturing = true;
|
RenderMod.IsCapturing = true;
|
||||||
@@ -258,7 +261,7 @@ namespace ADOFAIRenderer.Component
|
|||||||
|
|
||||||
videoFFmpeg.Start(_currentOutPath, bitrate);
|
videoFFmpeg.Start(_currentOutPath, bitrate);
|
||||||
|
|
||||||
var num = isMultiple ? (int)Math.Round(quotient) : 1;
|
var num = isMultiple ? (long)Math.Round(quotient) : 1L;
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -267,7 +270,7 @@ namespace ADOFAIRenderer.Component
|
|||||||
{
|
{
|
||||||
if (ImageQueue.Count > 0)
|
if (ImageQueue.Count > 0)
|
||||||
{
|
{
|
||||||
int firstKey = ImageQueue.Keys[0];
|
var firstKey = ImageQueue.Keys[0];
|
||||||
if (firstKey == PassCount)
|
if (firstKey == PassCount)
|
||||||
{
|
{
|
||||||
buffer = ImageQueue[firstKey];
|
buffer = ImageQueue[firstKey];
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ namespace ADOFAIRenderer.Patch
|
|||||||
public static bool Prefix(ref int __result)
|
public static bool Prefix(ref int __result)
|
||||||
{
|
{
|
||||||
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
if (!RenderMod.IsCapturing || !RenderPatch) return true;
|
||||||
__result = RenderManager.FrameCount + RenderMod.FrameCountBase;
|
__result = (int)(RenderManager.FrameCount + RenderMod.FrameCountBase);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-3
@@ -3,6 +3,7 @@ using System.Reflection;
|
|||||||
using ADOFAIRenderer;
|
using ADOFAIRenderer;
|
||||||
using ADOFAIRenderer.Component;
|
using ADOFAIRenderer.Component;
|
||||||
using ADOFAIRenderer.Util;
|
using ADOFAIRenderer.Util;
|
||||||
|
using ADOFAIRenderer.Util.FFmpeg;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
@@ -38,6 +39,8 @@ namespace ADOFAIRenderer
|
|||||||
public static double DspTimeBase;
|
public static double DspTimeBase;
|
||||||
public static int FrameCountBase;
|
public static int FrameCountBase;
|
||||||
|
|
||||||
|
public static FrameReader? FrameReader;
|
||||||
|
|
||||||
public static bool RenderCheck = true;
|
public static bool RenderCheck = true;
|
||||||
public static int SkipCount;
|
public static int SkipCount;
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ namespace ADOFAIRenderer
|
|||||||
var targetFps = RenderManager.Configuration.TargetFPS;
|
var targetFps = RenderManager.Configuration.TargetFPS;
|
||||||
var videoFps = RenderManager.Configuration.VideoFPS;
|
var videoFps = RenderManager.Configuration.VideoFPS;
|
||||||
var quotient = targetFps / 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)
|
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 ReadbackFailureCount;
|
||||||
public static int ThrottleCount;
|
public static int ThrottleCount;
|
||||||
public static bool IsThrottling;
|
public static bool IsThrottling;
|
||||||
@@ -171,7 +174,10 @@ namespace ADOFAIRenderer
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FrameReader?.RequestFrameCapture(texture, currentFrame);
|
||||||
|
|
||||||
|
/*
|
||||||
AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req)
|
AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req)
|
||||||
{
|
{
|
||||||
if (!IsCapturing) return;
|
if (!IsCapturing) return;
|
||||||
@@ -202,7 +208,7 @@ namespace ADOFAIRenderer
|
|||||||
{
|
{
|
||||||
RenderManager.ImageQueue[currentFrame] = rawBytes;
|
RenderManager.ImageQueue[currentFrame] = rawBytes;
|
||||||
}
|
}
|
||||||
});
|
});*/
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void StartRender()
|
public static void StartRender()
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ADOFAIRenderer.Component;
|
using ADOFAIRenderer.Component;
|
||||||
|
using ADOFAIRenderer.Util.FFmpeg;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using TMPro;
|
using TMPro;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@@ -35,7 +36,7 @@ namespace ADOFAIRenderer.UI
|
|||||||
if (codecDropdown == null) return;
|
if (codecDropdown == null) return;
|
||||||
codecDropdown.ClearOptions();
|
codecDropdown.ClearOptions();
|
||||||
|
|
||||||
var options = System.Enum.GetNames(typeof(Util.VideoFFmpeg.Codec)).ToList();
|
var options = System.Enum.GetNames(typeof(VideoFFmpeg.Codec)).ToList();
|
||||||
codecDropdown.AddOptions(options);
|
codecDropdown.AddOptions(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,15 +52,21 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
|||||||
_ffmpegProcess.WaitForExit();
|
_ffmpegProcess.WaitForExit();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static byte[] RentRawBytes<T>(NativeArray<T> arr) where T : struct
|
public static byte[] RentRawBytes(int size)
|
||||||
|
{
|
||||||
|
var array = ArrayPool<byte>.Shared.Rent(size);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*public static byte[] RentRawBytes<T>(NativeArray<T> arr) where T : struct
|
||||||
{
|
{
|
||||||
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
||||||
var array = ArrayPool<byte>.Shared.Rent(val.Length);
|
var array = ArrayPool<byte>.Shared.Rent(val.Length);
|
||||||
val.CopyTo(array);
|
val.CopyTo(array);
|
||||||
return array;
|
return array;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
public static byte[] ToRawBytes<T>(NativeArray<T> arr) where T : struct
|
protected static byte[] ToRawBytes<T>(NativeArray<T> arr) where T : struct
|
||||||
{
|
{
|
||||||
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
var val = new NativeSlice<T>(arr).SliceConvert<byte>();
|
||||||
var array = new byte[val.Length];
|
var array = new byte[val.Length];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace ADOFAIRenderer.Util.FFmpeg
|
namespace ADOFAIRenderer.Util.FFmpeg
|
||||||
{
|
{
|
||||||
|
|||||||
+176
-2
@@ -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<ReadbackRequestState> _statePool = new Stack<ReadbackRequestState>();
|
||||||
|
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<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending an empty frame.");
|
||||||
|
|
||||||
|
PushToQueue(frame, Array.Empty<byte>());
|
||||||
|
}
|
||||||
|
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<byte> NativeBuffer;
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user