From ddd0cc1d39fc95567bba28cea8caa6c393d3f745c586f5952a007218a536c217 Mon Sep 17 00:00:00 2001 From: copecone Date: Thu, 28 May 2026 14:27:02 +0900 Subject: [PATCH] Improve Code Consistency Between `RenderMod` and `RenderManager` --- Component/FirstUpdateHook.cs | 5 +- Component/PostRenderHook.cs | 5 +- Component/RenderManager.cs | 192 ++++++++++++++++--- Patch/ClassPatch/EndscreenLanternsPatch.cs | 3 +- Patch/ClassPatch/PersistencePatch.cs | 3 +- Patch/ClassPatch/scnEditorPatch.cs | 3 +- Patch/ClassPatch/scrControllerPatch.cs | 12 +- Patch/ClassPatch/scrMistakesManagerPatch.cs | 3 +- Patch/ClassPatch/scrPlayerManagerPatch.cs | 2 +- Patch/ClassPatch/scrShowIfDebugPatch.cs | 2 +- Patch/ClassPatch/scrUIControllerPatch.cs | 3 +- Patch/RenderEnvironmentPatches.cs | 37 ++-- Patch/ShowHitTextPatch.cs | 2 +- RenderMod.cs | 199 +------------------- UI/DebugMonitorUI.cs | 10 +- UI/EscapeHandlerUI.cs | 2 +- UI/RenderButton.cs | 2 +- Util/FrameReader.cs | 9 +- 18 files changed, 225 insertions(+), 269 deletions(-) diff --git a/Component/FirstUpdateHook.cs b/Component/FirstUpdateHook.cs index 3fc11a4..37b9188 100644 --- a/Component/FirstUpdateHook.cs +++ b/Component/FirstUpdateHook.cs @@ -7,7 +7,10 @@ namespace ADOFAIRenderer.Component { private void Update() { - RenderMod.OnFirstUpdate(); + if (RenderManager.Instance) + { + RenderManager.Instance.OnPreRenderUpdate(); + } } } } \ No newline at end of file diff --git a/Component/PostRenderHook.cs b/Component/PostRenderHook.cs index b13aac9..6e15d8a 100644 --- a/Component/PostRenderHook.cs +++ b/Component/PostRenderHook.cs @@ -17,7 +17,10 @@ namespace ADOFAIRenderer.Component private void LateUpdate() { 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); + } } } } diff --git a/Component/RenderManager.cs b/Component/RenderManager.cs index fa00389..7b54701 100644 --- a/Component/RenderManager.cs +++ b/Component/RenderManager.cs @@ -17,8 +17,10 @@ namespace ADOFAIRenderer.Component { // Singleton Accessors 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 long frameCount; public bool skipPressToStart; @@ -26,6 +28,25 @@ namespace ADOFAIRenderer.Component public bool isRenderCancelled; 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 double TargetFPS; @@ -54,7 +75,7 @@ namespace ADOFAIRenderer.Component } } - // Thread and Queue State (OOP Instance Fields) + // Thread and Queue State public SortedList ImageQueue = new SortedList(); public Thread? RenderThread; public long passCount; @@ -100,12 +121,12 @@ namespace ADOFAIRenderer.Component scrController.instance.noFailInfiniteMargin = true; } - RenderMod.TimeBase = Time.timeAsDouble; - RenderMod.UnscaledTimeBase = Time.unscaledTimeAsDouble; - RenderMod.DspTimeBase = AudioSettings.dspTime; - RenderMod.FrameCountBase = Time.frameCount; + timeBase = Time.timeAsDouble; + unscaledTimeBase = Time.unscaledTimeAsDouble; + dspTimeBase = AudioSettings.dspTime; + frameCountBase = Time.frameCount; - RenderMod.IsRendering = true; + isRendering = true; countdown = 3; wonFrame = -1; @@ -120,10 +141,10 @@ namespace ADOFAIRenderer.Component scrController.instance!.StartCoroutine(SetupPlay()); } - if (!RenderMod.IsRendering) return; + if (!isRendering) return; if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2) { - RenderMod.IsRendering = RenderMod.IsCapturing = false; + isRendering = isCapturing = false; StopFFmpeg(false); EndRender(); } @@ -137,11 +158,131 @@ namespace ADOFAIRenderer.Component var extraFrames = (long)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS); if (frameCount - wonFrame < extraFrames) return; - RenderMod.IsRendering = RenderMod.IsCapturing = false; + isRendering = isCapturing = false; StopFFmpeg(true); } } + public void OnPreRenderUpdate() + { + if (!isRendering) return; + + // [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)] + if (renderCheck && isCapturing) + { + if (skipCount < 60) + { + Melon.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/60)"); + skipCount++; + Time.timeScale = 0f; // 진행 시간 멈추기 (재시도) + return; // 업데이트 진행하지 않고 대기 + } + + // 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지 + Melon.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.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)Delegate.CreateDelegate(typeof(Func), scnEditor.instance, targetMethod); + + Application.wantsToQuit -= quitDelegate; + ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity"); + }); + } + public void SetupCaptureDirectory() { var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render"); @@ -173,11 +314,11 @@ namespace ADOFAIRenderer.Component frameCount = 0; - RenderMod.FrameReader?.Dispose(); - RenderMod.FrameReader = new FrameReader(); + _frameReader?.Dispose(); + _frameReader = new FrameReader(); StartFFmpegThread(); - RenderMod.IsCapturing = true; + isCapturing = true; scrController.instance.paused = scrController.instance.audioPaused = false; scrController.instance.levelWasSkipped = true; @@ -226,12 +367,11 @@ namespace ADOFAIRenderer.Component } passCount = 0; isRenderCancelled = false; - RenderMod.IsRenderCancelled = false; - RenderMod.FrameCountForVideo = 0; - RenderMod.LastVideoFrameIndex = -1; - RenderMod.LastRequestedVideoFrameIndex = -1; - RenderMod.RenderCheck = true; - RenderMod.SkipCount = 0; + frameCountForVideo = 0; + lastVideoFrameIndex = -1; + lastRequestedVideoFrameIndex = -1; + renderCheck = true; + skipCount = 0; endRequested = false; endSuccessful = false; @@ -280,7 +420,7 @@ namespace ADOFAIRenderer.Component { var firstKey = ImageQueue.Keys[0]; - if (firstKey > passCount && !RenderMod.IsCapturing) + if (firstKey > passCount && !isCapturing) { passCount = firstKey; } @@ -305,14 +445,14 @@ namespace ADOFAIRenderer.Component continue; } - if (isRenderCancelled || RenderMod.IsRenderCancelled) + if (isRenderCancelled) { break; } if (endRequested) { - if (passCount > RenderMod.LastRequestedVideoFrameIndex) + if (passCount > lastRequestedVideoFrameIndex) { break; } @@ -335,9 +475,8 @@ namespace ADOFAIRenderer.Component public void CancelRender() { - RenderMod.IsRendering = RenderMod.IsCapturing = false; + isRendering = isCapturing = false; isRenderCancelled = true; - RenderMod.IsRenderCancelled = true; StopFFmpeg(false); EndRender(); @@ -349,7 +488,6 @@ namespace ADOFAIRenderer.Component if (!success) { isRenderCancelled = true; - RenderMod.IsRenderCancelled = true; } skipPressToStart = false; _renderSetup = false; @@ -379,7 +517,7 @@ namespace ADOFAIRenderer.Component ImageQueue.Clear(); } - RenderMod.FrameReader?.Dispose(); + _frameReader?.Dispose(); // 디버그 모니터 UI 정리 if (_debugMonitorObj) @@ -450,7 +588,7 @@ namespace ADOFAIRenderer.Component GCS.customLevelPaths = null; GCS.customLevelId = null; - RenderMod.FrameReader?.Dispose(); + _frameReader?.Dispose(); ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); } } diff --git a/Patch/ClassPatch/EndscreenLanternsPatch.cs b/Patch/ClassPatch/EndscreenLanternsPatch.cs index d8a286b..2e463d6 100644 --- a/Patch/ClassPatch/EndscreenLanternsPatch.cs +++ b/Patch/ClassPatch/EndscreenLanternsPatch.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using ADOFAIRenderer.Component; using HarmonyLib; namespace ADOFAIRenderer.Patch.ClassPatch @@ -12,7 +13,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch [SuppressMessage("ReSharper", "InconsistentNaming")] public static bool PrefixShow(EndscreenLanterns __instance, ref int __result) { - if (!RenderMod.IsRendering) return true; + if (!RenderManager.IsActive) return true; if (__instance != null) { __instance.Hide(); diff --git a/Patch/ClassPatch/PersistencePatch.cs b/Patch/ClassPatch/PersistencePatch.cs index 3fca96c..c6062d6 100644 --- a/Patch/ClassPatch/PersistencePatch.cs +++ b/Patch/ClassPatch/PersistencePatch.cs @@ -1,3 +1,4 @@ +using ADOFAIRenderer.Component; using HarmonyLib; namespace ADOFAIRenderer.Patch.ClassPatch @@ -8,7 +9,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch // ReSharper disable once InconsistentNaming public static void Postfix(ref SkipIntroBehavior __result) { - if (RenderMod.IsRendering && + if (RenderManager.IsActive && scrConductor.instance != null && !scrConductor.instance.hasSongStarted) { diff --git a/Patch/ClassPatch/scnEditorPatch.cs b/Patch/ClassPatch/scnEditorPatch.cs index 7924506..e331043 100644 --- a/Patch/ClassPatch/scnEditorPatch.cs +++ b/Patch/ClassPatch/scnEditorPatch.cs @@ -1,3 +1,4 @@ +using ADOFAIRenderer.Component; using HarmonyLib; using UnityEngine; using UnityEngine.UI; @@ -22,7 +23,7 @@ namespace ADOFAIRenderer.Patch.ClassPatch var button = buttonObj.GetComponent