Improve Code Consistency Between RenderMod and RenderManager

This commit is contained in:
copecone committed 2026-05-28 14:27:02 +09:00
1 parent d4b9e4b89c
commit ddd0cc1d39
18 files changed
+225 -269

No files matched your search

+4 -1
View File
@@ -7,7 +7,10 @@ namespace ADOFAIRenderer.Component
{ {
private void Update() private void Update()
{ {
RenderMod.OnFirstUpdate(); if (RenderManager.Instance)
{
RenderManager.Instance.OnPreRenderUpdate();
}
} }
} }
} }
+4 -1
View File
@@ -17,7 +17,10 @@ namespace ADOFAIRenderer.Component
private void LateUpdate() private void LateUpdate()
{ {
var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance); var val = (MeshRenderer)_camQuadMeshField!.GetValue(scrCamera.instance);
RenderMod.OnPostRender(val.material.mainTexture as RenderTexture); if (RenderManager.Instance)
{
RenderManager.Instance.OnPostRender(val.material.mainTexture as RenderTexture);
}
} }
} }
} }
+165 -27
View File
@@ -17,8 +17,10 @@ namespace ADOFAIRenderer.Component
{ {
// Singleton Accessors // Singleton Accessors
public static RenderManager Instance { get; private set; } = null!; public static RenderManager Instance { get; private set; } = null!;
public static bool IsActive => Instance && Instance.isRendering;
public static bool IsCapturingActive => Instance && Instance.isCapturing;
// Core Render Configuration and Path (OOP Instance Fields) // Core Render Configuration and Path
public int countdown; public int countdown;
public long frameCount; public long frameCount;
public bool skipPressToStart; public bool skipPressToStart;
@@ -26,6 +28,25 @@ namespace ADOFAIRenderer.Component
public bool isRenderCancelled; public bool isRenderCancelled;
public RenderSettings Configuration; public RenderSettings Configuration;
// Render Sessions State (OOP Instance Fields)
public bool isRendering;
public bool isCapturing;
public double timeBase;
public double unscaledTimeBase;
public double dspTimeBase;
public int frameCountBase;
private FrameReader? _frameReader;
public bool renderCheck = true;
public int skipCount;
public long frameCountForVideo;
public long lastVideoFrameIndex = -1;
public long lastRequestedVideoFrameIndex = -1;
public int readbackFailureCount;
public int throttleCount;
public bool isThrottling;
public struct RenderSettings public struct RenderSettings
{ {
public double TargetFPS; public double TargetFPS;
@@ -54,7 +75,7 @@ namespace ADOFAIRenderer.Component
} }
} }
// Thread and Queue State (OOP Instance Fields) // Thread and Queue State
public SortedList<long, byte[]> ImageQueue = new SortedList<long, byte[]>(); public SortedList<long, byte[]> ImageQueue = new SortedList<long, byte[]>();
public Thread? RenderThread; public Thread? RenderThread;
public long passCount; public long passCount;
@@ -100,12 +121,12 @@ namespace ADOFAIRenderer.Component
scrController.instance.noFailInfiniteMargin = true; scrController.instance.noFailInfiniteMargin = true;
} }
RenderMod.TimeBase = Time.timeAsDouble; timeBase = Time.timeAsDouble;
RenderMod.UnscaledTimeBase = Time.unscaledTimeAsDouble; unscaledTimeBase = Time.unscaledTimeAsDouble;
RenderMod.DspTimeBase = AudioSettings.dspTime; dspTimeBase = AudioSettings.dspTime;
RenderMod.FrameCountBase = Time.frameCount; frameCountBase = Time.frameCount;
RenderMod.IsRendering = true; isRendering = true;
countdown = 3; countdown = 3;
wonFrame = -1; wonFrame = -1;
@@ -120,10 +141,10 @@ namespace ADOFAIRenderer.Component
scrController.instance!.StartCoroutine(SetupPlay()); scrController.instance!.StartCoroutine(SetupPlay());
} }
if (!RenderMod.IsRendering) return; if (!isRendering) return;
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2) if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
{ {
RenderMod.IsRendering = RenderMod.IsCapturing = false; isRendering = isCapturing = false;
StopFFmpeg(false); StopFFmpeg(false);
EndRender(); EndRender();
} }
@@ -137,11 +158,131 @@ namespace ADOFAIRenderer.Component
var extraFrames = (long)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; isRendering = isCapturing = false;
StopFFmpeg(true); StopFFmpeg(true);
} }
} }
public void OnPreRenderUpdate()
{
if (!isRendering) return;
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)]
if (renderCheck && isCapturing)
{
if (skipCount < 60)
{
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/60)");
skipCount++;
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도)
return; // 업데이트 진행하지 않고 대기
}
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {frameCount} rendering failed after 60 retries. Supplying black frame.");
var currentFrame = frameCountForVideo++;
var targetFps = Configuration.TargetFPS;
var videoFps = Configuration.VideoFPS;
var effectiveVideoFps = Math.Min(videoFps, targetFps);
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps);
if (videoFrameIndex > lastVideoFrameIndex)
{
lastVideoFrameIndex = videoFrameIndex;
lastRequestedVideoFrameIndex = videoFrameIndex;
var byteSize = Configuration.RenderWidth * Configuration.RenderHeight * 4;
var blackFrame = System.Buffers.ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize);
lock (ImageQueue)
{
ImageQueue[videoFrameIndex] = blackFrame;
}
}
renderCheck = false; // 진행 허용
}
skipCount = 0;
renderCheck = true; // 다음 프레임 검사용으로 초기화
frameCount++;
}
// ReSharper disable once Unity.IncorrectMethodSignature
public 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 (ImageQueue)
{
count = ImageQueue.Count;
}
if (count <= maxQueuedFrames) break;
if (!isThrottling)
{
isThrottling = true;
throttleCount++;
}
Thread.Sleep(1);
}
isThrottling = false;
var currentFrame = frameCountForVideo++;
var targetFps = Configuration.TargetFPS;
var videoFps = Configuration.VideoFPS;
var effectiveVideoFps = Math.Min(videoFps, targetFps);
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps);
if (videoFrameIndex > lastVideoFrameIndex)
{
lastVideoFrameIndex = videoFrameIndex;
lastRequestedVideoFrameIndex = videoFrameIndex;
_frameReader?.RequestFrameCapture(texture, videoFrameIndex);
}
}
public static void StartRender()
{
var targetMethod = typeof(scnEditor).GetMethod("TryApplicationQuit",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
if (targetMethod == null || scnEditor.instance == null) return;
if (ADOBase.levelPath.IsNullOrEmpty())
{
scnEditor.instance.TryQuitToMenu();
return;
}
if (Instance != null)
{
Instance.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");
});
}
public void SetupCaptureDirectory() public void SetupCaptureDirectory()
{ {
var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render"); var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
@@ -173,11 +314,11 @@ namespace ADOFAIRenderer.Component
frameCount = 0; frameCount = 0;
RenderMod.FrameReader?.Dispose(); _frameReader?.Dispose();
RenderMod.FrameReader = new FrameReader(); _frameReader = new FrameReader();
StartFFmpegThread(); StartFFmpegThread();
RenderMod.IsCapturing = true; isCapturing = true;
scrController.instance.paused = scrController.instance.audioPaused = false; scrController.instance.paused = scrController.instance.audioPaused = false;
scrController.instance.levelWasSkipped = true; scrController.instance.levelWasSkipped = true;
@@ -226,12 +367,11 @@ namespace ADOFAIRenderer.Component
} }
passCount = 0; passCount = 0;
isRenderCancelled = false; isRenderCancelled = false;
RenderMod.IsRenderCancelled = false; frameCountForVideo = 0;
RenderMod.FrameCountForVideo = 0; lastVideoFrameIndex = -1;
RenderMod.LastVideoFrameIndex = -1; lastRequestedVideoFrameIndex = -1;
RenderMod.LastRequestedVideoFrameIndex = -1; renderCheck = true;
RenderMod.RenderCheck = true; skipCount = 0;
RenderMod.SkipCount = 0;
endRequested = false; endRequested = false;
endSuccessful = false; endSuccessful = false;
@@ -280,7 +420,7 @@ namespace ADOFAIRenderer.Component
{ {
var firstKey = ImageQueue.Keys[0]; var firstKey = ImageQueue.Keys[0];
if (firstKey > passCount && !RenderMod.IsCapturing) if (firstKey > passCount && !isCapturing)
{ {
passCount = firstKey; passCount = firstKey;
} }
@@ -305,14 +445,14 @@ namespace ADOFAIRenderer.Component
continue; continue;
} }
if (isRenderCancelled || RenderMod.IsRenderCancelled) if (isRenderCancelled)
{ {
break; break;
} }
if (endRequested) if (endRequested)
{ {
if (passCount > RenderMod.LastRequestedVideoFrameIndex) if (passCount > lastRequestedVideoFrameIndex)
{ {
break; break;
} }
@@ -335,9 +475,8 @@ namespace ADOFAIRenderer.Component
public void CancelRender() public void CancelRender()
{ {
RenderMod.IsRendering = RenderMod.IsCapturing = false; isRendering = isCapturing = false;
isRenderCancelled = true; isRenderCancelled = true;
RenderMod.IsRenderCancelled = true;
StopFFmpeg(false); StopFFmpeg(false);
EndRender(); EndRender();
@@ -349,7 +488,6 @@ namespace ADOFAIRenderer.Component
if (!success) if (!success)
{ {
isRenderCancelled = true; isRenderCancelled = true;
RenderMod.IsRenderCancelled = true;
} }
skipPressToStart = false; skipPressToStart = false;
_renderSetup = false; _renderSetup = false;
@@ -379,7 +517,7 @@ namespace ADOFAIRenderer.Component
ImageQueue.Clear(); ImageQueue.Clear();
} }
RenderMod.FrameReader?.Dispose(); _frameReader?.Dispose();
// 디버그 모니터 UI 정리 // 디버그 모니터 UI 정리
if (_debugMonitorObj) if (_debugMonitorObj)
@@ -450,7 +588,7 @@ namespace ADOFAIRenderer.Component
GCS.customLevelPaths = null; GCS.customLevelPaths = null;
GCS.customLevelId = null; GCS.customLevelId = null;
RenderMod.FrameReader?.Dispose(); _frameReader?.Dispose();
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
} }
} }
+2 -1
View File
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
namespace ADOFAIRenderer.Patch.ClassPatch namespace ADOFAIRenderer.Patch.ClassPatch
@@ -12,7 +13,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool PrefixShow(EndscreenLanterns __instance, ref int __result) public static bool PrefixShow(EndscreenLanterns __instance, ref int __result)
{ {
if (!RenderMod.IsRendering) return true; if (!RenderManager.IsActive) return true;
if (__instance != null) if (__instance != null)
{ {
__instance.Hide(); __instance.Hide();
+2 -1
View File
@@ -1,3 +1,4 @@
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
namespace ADOFAIRenderer.Patch.ClassPatch namespace ADOFAIRenderer.Patch.ClassPatch
@@ -8,7 +9,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static void Postfix(ref SkipIntroBehavior __result) public static void Postfix(ref SkipIntroBehavior __result)
{ {
if (RenderMod.IsRendering && if (RenderManager.IsActive &&
scrConductor.instance != null && scrConductor.instance != null &&
!scrConductor.instance.hasSongStarted) !scrConductor.instance.hasSongStarted)
{ {
+2 -1
View File
@@ -1,3 +1,4 @@
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@@ -22,7 +23,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
var button = buttonObj.GetComponent<Button>()!; var button = buttonObj.GetComponent<Button>()!;
// Clear persistent and non-persistent listeners from the original button // Clear persistent and non-persistent listeners from the original button
button.onClick = new Button.ButtonClickedEvent(); button.onClick = new Button.ButtonClickedEvent();
button.onClick.AddListener(RenderMod.StartRender); button.onClick.AddListener(RenderManager.StartRender);
// The button root has an Image component for the background (rounded box). // The button root has an Image component for the background (rounded box).
// We want to change the icon sprite, which is on a child GameObject. // We want to change the icon sprite, which is on a child GameObject.
+6 -6
View File
@@ -1,5 +1,5 @@
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
using MelonLoader;
namespace ADOFAIRenderer.Patch.ClassPatch namespace ADOFAIRenderer.Patch.ClassPatch
{ {
@@ -11,35 +11,35 @@ namespace ADOFAIRenderer.Patch.ClassPatch
[HarmonyPatch("TogglePauseGame")] [HarmonyPatch("TogglePauseGame")]
public static bool PreventPause() public static bool PreventPause()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch("PortalTravelAction")] [HarmonyPatch("PortalTravelAction")]
public static bool PreventPortalTravel() public static bool PreventPortalTravel()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch("FailAction")] [HarmonyPatch("FailAction")]
public static bool PreventFailAction() public static bool PreventFailAction()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch("Fail2Action")] [HarmonyPatch("Fail2Action")]
public static bool PreventFail2Action() public static bool PreventFail2Action()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch("OnPlayerDied")] [HarmonyPatch("OnPlayerDied")]
public static bool PreventPlayerDied() public static bool PreventPlayerDied()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
} }
} }
+2 -1
View File
@@ -1,3 +1,4 @@
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
namespace ADOFAIRenderer.Patch.ClassPatch namespace ADOFAIRenderer.Patch.ClassPatch
@@ -10,7 +11,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
[HarmonyPatch("SaveCustom")] [HarmonyPatch("SaveCustom")]
public static bool PreventSaveCustom() public static bool PreventSaveCustom()
{ {
return !RenderMod.IsRendering; return !RenderManager.IsActive;
} }
} }
} }
+1 -1
View File
@@ -12,7 +12,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
private static bool IgnoreInput(ref bool __result) private static bool IgnoreInput(ref bool __result)
{ {
if (!RenderMod.IsRendering) return true; if (!RenderManager.IsActive) return true;
__result = RenderManager.Instance && RenderManager.Instance.frameCount == 0 && RenderManager.Instance.countdown == 0; __result = RenderManager.Instance && RenderManager.Instance.frameCount == 0 && RenderManager.Instance.countdown == 0;
return false; return false;
+1 -1
View File
@@ -15,7 +15,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
private static void IgnoreInput(Text ___txt, Color ___defaultColor) private static void IgnoreInput(Text ___txt, Color ___defaultColor)
{ {
if (!RenderMod.IsRendering) return; if (!RenderManager.IsActive) return;
___txt.text = "Render"; ___txt.text = "Render";
if (RenderManager.Instance) if (RenderManager.Instance)
+2 -1
View File
@@ -1,3 +1,4 @@
using ADOFAIRenderer.Component;
using HarmonyLib; using HarmonyLib;
namespace ADOFAIRenderer.Patch.ClassPatch namespace ADOFAIRenderer.Patch.ClassPatch
@@ -11,7 +12,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static void PreventEndscreenLanternsPostfix(scrUIController __instance) public static void PreventEndscreenLanternsPostfix(scrUIController __instance)
{ {
if (RenderMod.IsRendering && __instance != null) if (RenderManager.IsActive && __instance != null)
{ {
__instance.HideEndscreenLanterns(); __instance.HideEndscreenLanterns();
} }
+18 -19
View File
@@ -15,8 +15,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref float __result) public static bool Prefix(ref float __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderMod.TimeBase); __result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.timeBase);
return false; return false;
} }
} }
@@ -27,8 +27,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref double __result) public static bool Prefix(ref double __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderMod.TimeBase; __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.timeBase;
return false; return false;
} }
} }
@@ -39,8 +39,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref float __result) public static bool Prefix(ref float __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderMod.UnscaledTimeBase); __result = (float)(RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.unscaledTimeBase);
return false; return false;
} }
} }
@@ -51,8 +51,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref double __result) public static bool Prefix(ref double __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderMod.UnscaledTimeBase; __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.unscaledTimeBase;
return false; return false;
} }
} }
@@ -63,10 +63,9 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref double __result) public static bool Prefix(ref double __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderMod.DspTimeBase; __result = RenderManager.Instance.frameCount / RenderManager.Instance.Configuration.TargetFPS + RenderManager.Instance.dspTimeBase;
return false; return false;
} }
} }
@@ -76,7 +75,7 @@ namespace ADOFAIRenderer.Patch
// ReSharper disable once InconsistentNaming // ReSharper disable once InconsistentNaming
public static bool Prefix(AudioSource __instance, double time) public static bool Prefix(AudioSource __instance, double time)
{ {
return !RenderMod.IsRendering || !RenderPatch; return !RenderManager.IsActive || !RenderPatch;
} }
} }
@@ -86,8 +85,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref int __result) public static bool Prefix(ref int __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = (int)(RenderManager.Instance.frameCount + RenderMod.FrameCountBase); __result = (int)(RenderManager.Instance.frameCount + RenderManager.Instance.frameCountBase);
return false; return false;
} }
} }
@@ -98,8 +97,8 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref float __result) public static bool Prefix(ref float __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (RenderManager.Instance && (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance)) return true;
__result = (float)(1 / RenderManager.Instance.Configuration.TargetFPS); __result = (float)(1 / RenderManager.Instance!.Configuration.TargetFPS);
return false; return false;
} }
} }
@@ -110,7 +109,7 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref float __result) public static bool Prefix(ref float __result)
{ {
if (!RenderMod.IsCapturing || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsCapturingActive || !RenderPatch || !RenderManager.Instance) return true;
__result = (float)(1 / RenderManager.Instance.Configuration.TargetFPS); __result = (float)(1 / RenderManager.Instance.Configuration.TargetFPS);
return false; return false;
} }
@@ -122,7 +121,7 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref int __result) public static bool Prefix(ref int __result)
{ {
if (!RenderMod.IsRendering || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsActive || !RenderPatch || !RenderManager.Instance) return true;
__result = RenderManager.Instance.Configuration.RenderWidth; __result = RenderManager.Instance.Configuration.RenderWidth;
return false; return false;
} }
@@ -134,7 +133,7 @@ namespace ADOFAIRenderer.Patch
[SuppressMessage("ReSharper", "InconsistentNaming")] [SuppressMessage("ReSharper", "InconsistentNaming")]
public static bool Prefix(ref int __result) public static bool Prefix(ref int __result)
{ {
if (!RenderMod.IsRendering || !RenderPatch || !RenderManager.Instance) return true; if (!RenderManager.IsActive || !RenderPatch || !RenderManager.Instance) return true;
__result = RenderManager.Instance.Configuration.RenderHeight; __result = RenderManager.Instance.Configuration.RenderHeight;
return false; return false;
} }
+1 -1
View File
@@ -8,7 +8,7 @@ namespace ADOFAIRenderer.Patch
{ {
public static bool Prefix() public static bool Prefix()
{ {
return !RenderMod.IsRendering || (RenderManager.Instance && RenderManager.Instance.Configuration.ShowHitText); return !RenderManager.IsActive || RenderManager.Instance is { Configuration: { ShowHitText: true } };
} }
} }
} }
+2 -197
View File
@@ -1,52 +1,27 @@
using System;
using System.Reflection;
using ADOFAIRenderer; using ADOFAIRenderer;
using ADOFAIRenderer.Component; using ADOFAIRenderer.Component;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
using MelonLoader; using MelonLoader;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; using UnityEngine.EventSystems;
using Object = UnityEngine.Object;
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.5-BETA", "ADOFAI Editor Forum")] [assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.5-BETA", "ADOFAI Editor Forum")]
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
namespace ADOFAIRenderer namespace ADOFAIRenderer
{ {
public class RenderMod : MelonMod public class RenderMod : MelonMod
{ {
static RenderMod()
{
ThrottleCount = 0;
}
public static AssetBundle FontBundle = null!; public static AssetBundle FontBundle = null!;
// ReSharper disable once MemberCanBePrivate.Global
public static AssetBundle SceneBundle = null!; public static AssetBundle SceneBundle = null!;
public static string[] ModScenes = {}; 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 FrameReader? FrameReader;
public static bool RenderCheck = true;
public static int SkipCount;
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
var renderManagerObj = new GameObject("Render Manager"); var renderManagerObj = new GameObject("Render Manager");
var updateHookerObj = new GameObject("Update Hooker"); var updateHookerObj = new GameObject("Update Hooker");
Object.DontDestroyOnLoad( renderManagerObj ); UnityEngine.Object.DontDestroyOnLoad(renderManagerObj);
Object.DontDestroyOnLoad( updateHookerObj ); UnityEngine.Object.DontDestroyOnLoad(updateHookerObj);
renderManagerObj.AddComponent<RenderManager>(); renderManagerObj.AddComponent<RenderManager>();
updateHookerObj.AddComponent<FirstUpdateHook>(); updateHookerObj.AddComponent<FirstUpdateHook>();
@@ -71,175 +46,5 @@ namespace ADOFAIRenderer
var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>(); var newModule = EventSystem.current.gameObject.AddComponent<StandaloneInputModule>();
newModule.enabled = true; 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.Instance.frameCount} rendering missed. Retrying... (Attempt {SkipCount + 1}/60)");
SkipCount++;
Time.timeScale = 0f; // 진행 시간 멈추기 (재시도)
return; // 업데이트 진행하지 않고 대기
}
// 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지
Melon<RenderMod>.Logger.Error($"[Render Skipped] Frame {RenderManager.Instance.frameCount} rendering failed after 60 retries. Supplying black frame.");
var currentFrame = FrameCountForVideo++;
var targetFps = RenderManager.Instance.Configuration.TargetFPS;
var videoFps = RenderManager.Instance.Configuration.VideoFPS;
var effectiveVideoFps = Math.Min(videoFps, targetFps);
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps);
if (videoFrameIndex > LastVideoFrameIndex)
{
LastVideoFrameIndex = videoFrameIndex;
LastRequestedVideoFrameIndex = videoFrameIndex;
var byteSize = RenderManager.Instance.Configuration.RenderWidth * RenderManager.Instance.Configuration.RenderHeight * 4;
var blackFrame = System.Buffers.ArrayPool<byte>.Shared.Rent(byteSize);
Array.Clear(blackFrame, 0, byteSize);
lock (RenderManager.Instance.ImageQueue)
{
RenderManager.Instance.ImageQueue[videoFrameIndex] = blackFrame;
}
}
RenderCheck = false; // 진행 허용
}
SkipCount = 0;
RenderCheck = true; // 다음 프레임 검사용으로 초기화
RenderManager.Instance.frameCount++;
if (realDeltaTime > 0)
{
// Time.timeScale = (float)(1 / RenderManager.Configuration.TargetFPS / realDeltaTime);
}
}
public static long FrameCountForVideo;
public static long LastVideoFrameIndex = -1;
public static long LastRequestedVideoFrameIndex = -1;
public static bool IsRenderCancelled = false;
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.Instance.ImageQueue)
{
count = RenderManager.Instance.ImageQueue.Count;
}
if (count <= maxQueuedFrames) break;
if (!IsThrottling)
{
IsThrottling = true;
ThrottleCount++;
}
System.Threading.Thread.Sleep(1);
}
IsThrottling = false;
var currentFrame = FrameCountForVideo++;
var targetFps = RenderManager.Instance.Configuration.TargetFPS;
var videoFps = RenderManager.Instance.Configuration.VideoFPS;
var effectiveVideoFps = Math.Min(videoFps, targetFps);
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps);
if (videoFrameIndex > LastVideoFrameIndex)
{
LastVideoFrameIndex = videoFrameIndex;
LastRequestedVideoFrameIndex = videoFrameIndex;
FrameReader?.RequestFrameCapture(texture, videoFrameIndex);
}
/*
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;
}
if (RenderManager.Instance)
{
RenderManager.Instance.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");
});
}
} }
} }
+5 -5
View File
@@ -187,7 +187,7 @@ namespace ADOFAIRenderer.UI
var sb = new StringBuilder(); var sb = new StringBuilder();
sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>"); sb.AppendLine("<b><color=#00ccff>ADOFAI Renderer Realtime Monitor</color></b>");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine($"• Rendered Progress: <b>{(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}</b> frames"); sb.AppendLine($"• Rendered Progress: <b>{(RenderManager.Instance != null ? RenderManager.Instance.frameCount : 0)}</b> frames");
var queueCount = 0; var queueCount = 0;
if (RenderManager.Instance) if (RenderManager.Instance)
@@ -199,13 +199,13 @@ namespace ADOFAIRenderer.UI
} }
sb.AppendLine($"• Queued Buffers: <b>{queueCount}</b> / 60 frames"); sb.AppendLine($"• Queued Buffers: <b>{queueCount}</b> / 60 frames");
sb.AppendLine(RenderMod.IsThrottling sb.AppendLine((RenderManager.Instance && RenderManager.Instance.isThrottling)
? "• Status: <b><color=yellow>THROTTLING (Safe Mode)</color></b>" ? "• Status: <b><color=yellow>THROTTLING (Safe Mode)</color></b>"
: "• Status: <b><color=green>NORMAL (Encoding)</color></b>"); : "• Status: <b><color=green>NORMAL (Encoding)</color></b>");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine($"• Throttling Controls: {RenderMod.ThrottleCount} times"); sb.AppendLine($"• Throttling Controls: {(RenderManager.Instance ? RenderManager.Instance.throttleCount : 0)} times");
sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{RenderMod.ReadbackFailureCount}</color> times"); sb.AppendLine($"• Readback Hard Failures: <color=#ff3333>{(RenderManager.Instance ? RenderManager.Instance.readbackFailureCount : 0)}</color> times");
sb.AppendLine($"• Render Skip-Retry: {RenderMod.SkipCount} / 60"); sb.AppendLine($"• Render Skip-Retry: {(RenderManager.Instance ? RenderManager.Instance.skipCount : 0)} / 60");
var memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0); var memoryMb = GC.GetTotalMemory(false) / (1024.0 * 1024.0);
sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB"); sb.AppendLine($"• Mono Heap Memory: {memoryMb:F2} MB");
+1 -1
View File
@@ -75,7 +75,7 @@ namespace ADOFAIRenderer.UI
private void Update() private void Update()
{ {
// 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴 // 렌더링이 더 이상 활성화되어 있지 않으면 자가 파괴
if (!RenderMod.IsRendering) if (!RenderManager.IsActive)
{ {
Destroy(gameObject); Destroy(gameObject);
return; return;
+1 -1
View File
@@ -51,7 +51,7 @@ namespace ADOFAIRenderer.UI
public void OnClick() public void OnClick()
{ {
if (IsAnyFieldEmpty()) return; if (IsAnyFieldEmpty()) return;
if (RenderMod.IsRendering) return; if (RenderManager.IsActive) return;
if (!double.TryParse(targetFPSInput.text, out var targetFPS) || targetFPS <= 0) if (!double.TryParse(targetFPSInput.text, out var targetFPS) || targetFPS <= 0)
{ {
+6 -3
View File
@@ -42,7 +42,7 @@ namespace ADOFAIRenderer.Util
public void RequestFrameCapture(Texture texture, long currentFrame) public void RequestFrameCapture(Texture texture, long currentFrame)
{ {
if (!RenderMod.IsCapturing) return; if (!RenderManager.IsCapturingActive) return;
ReadbackRequestState state; ReadbackRequestState state;
lock (_poolLock) lock (_poolLock)
@@ -74,7 +74,7 @@ namespace ADOFAIRenderer.Util
} }
} }
if (RenderMod.IsRenderCancelled) if (RenderManager.Instance && RenderManager.Instance.isRenderCancelled)
{ {
RecycleState(state); RecycleState(state);
return; return;
@@ -82,7 +82,10 @@ namespace ADOFAIRenderer.Util
if (request.hasError) if (request.hasError)
{ {
RenderMod.ReadbackFailureCount++; if (RenderManager.Instance)
{
RenderManager.Instance.readbackFailureCount++;
}
Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame."); Melon<RenderMod>.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame.");
var byteSize = state.NativeBuffer.Length; var byteSize = state.NativeBuffer.Length;