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.1-BETA", "ADOFAI Editor Forum")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] namespace ADOFAIRenderer { public class RenderMod : MelonMod { 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 = 0; public override void OnInitializeMelon() { var renderManagerObj = new GameObject("Render Manager"); var updateHookerObj = new GameObject("Update Hooker"); Object.DontDestroyOnLoad( renderManagerObj ); Object.DontDestroyOnLoad( updateHookerObj ); renderManagerObj.AddComponent(); updateHookerObj.AddComponent(); 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.Logger.Msg("Unity 6 인풋 모듈 이식."); var newModule = EventSystem.current.gameObject.AddComponent(); 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) { if (SkipCount < 60) { Melon.Logger.Warning($"[Render Skipped] Frame {RenderManager.FrameCount} rendering missed. Retrying... (Attempt {SkipCount + 1}/60)"); SkipCount++; Time.timeScale = 0f; // 진행 시간 멈추기 (재시도) return; // 업데이트 진행하지 않고 대기 } // 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지 Melon.Logger.Error($"[Render Skipped] Frame {RenderManager.FrameCount} rendering failed after 60 retries. Supplying black frame."); int targetFps = (int)RenderManager.Configuration.TargetFPS; int videoFps = (int)RenderManager.Configuration.VideoFPS; int 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.Shared.Rent(byteSize); Array.Clear(blackFrame, 0, byteSize); lock (RenderManager.ImageQueue) { RenderManager.ImageQueue[FrameCountForVideo] = blackFrame; } } FrameCountForVideo++; RenderCheck = false; // 진행 허용 } SkipCount = 0; RenderCheck = true; // 다음 프레임 검사용으로 초기화 RenderManager.FrameCount++; Time.timeScale = (float)(Time.deltaTime / realDeltaTime); } public static int FrameCountForVideo = 0; public static int ReadbackFailureCount = 0; public static int ThrottleCount = 0; public static bool IsThrottling = false; 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.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {currentFrame}. Queue deadlock prevented by sending an empty frame."); rawBytes = Array.Empty(); // 1번 작업: 빈 배열로 대체하여 정체 방지 } else { try { var data = req.GetData(); rawBytes = FFmpegProcess.RentRawBytes(data); // 3번 작업: ArrayPool에서 대여 } catch (Exception ex) { ReadbackFailureCount++; Melon.Logger.Error($"[OOM Prevent] Native conversion failed for frame {currentFrame}: {ex}"); rawBytes = Array.Empty(); } } 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)Delegate.CreateDelegate(typeof(Func), scnEditor.instance, targetMethod); Application.wantsToQuit -= quitDelegate; ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity"); }); } } }