Files
ADOFAIRenderer/RenderMod.cs
T

239 lines
8.8 KiB
C#

using System;
using System.Reflection;
using ADOFAIRenderer;
using ADOFAIRenderer.Component;
using ADOFAIRenderer.Util;
using MelonLoader;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using Object = UnityEngine.Object;
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.3-BETA", "ADOFAI Editor Forum")]
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
namespace ADOFAIRenderer
{
public class RenderMod : MelonMod
{
static RenderMod()
{
ThrottleCount = 0;
}
public static AssetBundle FontBundle = null!;
// ReSharper disable once MemberCanBePrivate.Global
public static AssetBundle SceneBundle = null!;
public static bool WatchGameLoaded;
public static string[] ModScenes = {};
private static double _beforeTime;
public static bool IsRendering = false;
public static bool IsCapturing = false;
public static double TimeBase;
public static double UnscaledTimeBase;
public static double DspTimeBase;
public static int FrameCountBase;
public static bool RenderCheck = true;
public static int SkipCount;
public override void OnInitializeMelon()
{
var renderManagerObj = new GameObject("Render Manager");
var updateHookerObj = new GameObject("Update Hooker");
Object.DontDestroyOnLoad( renderManagerObj );
Object.DontDestroyOnLoad( updateHookerObj );
renderManagerObj.AddComponent<RenderManager>();
updateHookerObj.AddComponent<FirstUpdateHook>();
LoadBundles();
}
private static void LoadBundles()
{
FontBundle = BundleLoader.GetBundle("fonts");
SceneBundle = BundleLoader.GetBundle("scenes");
ModScenes = SceneBundle.GetAllScenePaths();
}
public static void FixEventSystem()
{
if (EventSystem.current == null) return;
var currentModule = EventSystem.current.currentInputModule;
if (currentModule != null) return;
Melon<RenderMod>.Logger.Msg("Unity 6 인풋 모듈 이식.");
var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
newModule.enabled = true;
}
public static void OnFirstUpdate()
{
if (_beforeTime == 0) _beforeTime = Time.realtimeSinceStartupAsDouble;
var realDeltaTime = Time.realtimeSinceStartupAsDouble - _beforeTime;
_beforeTime = Time.realtimeSinceStartupAsDouble;
if (!IsRendering) return;
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)]
if (RenderCheck && IsCapturing)
{
if (SkipCount < 60)
{
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {RenderManager.FrameCount} rendering missed. Retrying... (Attempt {SkipCount + 1}/60)");
SkipCount++;
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도)
return; // 업데이트 진행하지 않고 대기
}
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {RenderManager.FrameCount} rendering failed after 60 retries. Supplying black frame.");
var targetFps = (int)RenderManager.Configuration.TargetFPS;
var videoFps = (int)RenderManager.Configuration.VideoFPS;
var num = targetFps % videoFps != 0 ? 1 : targetFps / videoFps;
if (FrameCountForVideo % num == 0)
{
int byteSize = RenderManager.Configuration.RenderWidth * RenderManager.Configuration.RenderHeight * 4;
byte[] blackFrame = System.Buffers.ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize);
lock (RenderManager.ImageQueue)
{
RenderManager.ImageQueue[FrameCountForVideo] = blackFrame;
}
}
FrameCountForVideo++;
RenderCheck = false; // 진행 허용
}
SkipCount = 0;
RenderCheck = true; // 다음 프레임 검사용으로 초기화
RenderManager.FrameCount++;
if (realDeltaTime > 0)
{
Time.timeScale = (float)(Time.deltaTime / realDeltaTime);
}
var array = Object.FindObjectsByType<ParticleSystem>(FindObjectsSortMode.None);
foreach (var particleSystem in array)
{
if (particleSystem.IsAlive(false))
{
particleSystem.Simulate(Time.deltaTime, false, false, false);
}
}
}
public static int FrameCountForVideo;
public static int ReadbackFailureCount;
public static int ThrottleCount;
public static bool IsThrottling;
public static void OnPostRender(RenderTexture? texture)
{
if (!IsCapturing || !texture) return;
RenderCheck = false; // 이번 프레임 정상 렌더링 완료
// [Backpressure Throttling (2번 작업)]
const int maxQueuedFrames = 60;
IsThrottling = false;
while (IsCapturing && IsRendering)
{
int count;
lock (RenderManager.ImageQueue)
{
count = RenderManager.ImageQueue.Count;
}
if (count <= maxQueuedFrames) break;
if (!IsThrottling)
{
IsThrottling = true;
ThrottleCount++;
}
System.Threading.Thread.Sleep(1);
}
IsThrottling = false;
var currentFrame = FrameCountForVideo++;
var targetFps = RenderManager.Configuration.TargetFPS;
var videoFps = RenderManager.Configuration.VideoFPS;
var num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
if (currentFrame % num != 0)
{
return;
}
AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req)
{
if (!IsCapturing) return;
byte[] rawBytes;
if (req.hasError)
{
ReadbackFailureCount++;
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {currentFrame}. Queue deadlock prevented by sending an empty frame.");
rawBytes = Array.Empty<byte>(); // 1번 작업: 빈 배열로 대체하여 정체 방지
}
else
{
try
{
var data = req.GetData<byte>();
rawBytes = FFmpegProcess.RentRawBytes(data); // 3번 작업: ArrayPool에서 대여
}
catch (Exception ex)
{
ReadbackFailureCount++;
Melon<RenderMod>.Logger.Error($"[OOM Prevent] Native conversion failed for frame {currentFrame}: {ex}");
rawBytes = Array.Empty<byte>();
}
}
lock (RenderManager.ImageQueue)
{
RenderManager.ImageQueue[currentFrame] = rawBytes;
}
});
}
public static void StartRender()
{
var targetMethod = typeof(scnEditor).GetMethod("TryApplicationQuit",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
if (targetMethod == null || scnEditor.instance == null) return;
if (ADOBase.levelPath.IsNullOrEmpty())
{
scnEditor.instance.TryQuitToMenu();
return;
}
RenderManager.LevelPath = ADOBase.levelPath;
scnEditor.instance.CheckUnsavedChanges(() =>
{
var quitDelegate =
(Func<bool>)Delegate.CreateDelegate(typeof(Func<bool>), scnEditor.instance, targetMethod);
Application.wantsToQuit -= quitDelegate;
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity");
});
}
}
}