Files
ADOFAIRenderer/Component/RenderManager.cs
T
2026-05-22 19:31:44 +09:00

342 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using ADOFAIRenderer.Util;
using MelonLoader;
using MelonLoader.Utils;
using UnityEngine;
namespace ADOFAIRenderer.Component
{
public class RenderManager : MonoBehaviour
{
public static int Countdown;
public static int FrameCount;
public static bool SkipPressToStart;
public static string LevelPath = "";
public static RenderManager Instance = null!;
private static int _originalInputOffset;
private static GameObject? _debugMonitorObj;
public static RenderSettings Configuration;
public struct RenderSettings
{
public double TargetFPS;
public double VideoFPS;
public int RenderWidth;
public int RenderHeight;
public int Bitrate;
public string Codec;
public double EndWaitTime;
public RenderSettings(double targetFPS, double videoFPS, int renderWidth, int renderHeight, int bitrate, string codec, double endWaitTime)
{
TargetFPS = targetFPS;
VideoFPS = videoFPS;
RenderWidth = renderWidth;
RenderHeight = renderHeight;
Bitrate = bitrate;
Codec = codec;
EndWaitTime = endWaitTime;
}
}
public static SortedList<int, byte[]> ImageQueue = new SortedList<int, byte[]>();
public static Thread? RenderThread;
public static int PassCount = 0;
public static bool EndRequested = false;
public static bool EndSuccessful = false;
private static int _wonFrame = -1;
private static bool _renderSetup;
private void Awake()
{
Instance = this;
}
private void Update()
{
if (_renderSetup && ADOBase.lm)
{
ADOBase.lm.hideDifficultyUI = true;
}
if (_renderSetup && scnGame.instance && !scnGame.instance.isLoading)
{
_renderSetup = false;
scrCamera.instance.forceRTCam = true;
scrCamera.instance.SetupRTCam(true);
if (!scrCamera.instance.gameObject.GetComponent<PostRenderHook>())
scrCamera.instance.gameObject.AddComponent<PostRenderHook>();
_originalInputOffset = scrConductor.currentPreset.inputOffset;
scrConductor.currentPreset.inputOffset = 0;
RDC.auto = true;
if (scrController.instance)
{
scrController.instance.noFail = true;
scrController.instance.noFailInfiniteMargin = true;
}
RenderMod.TimeBase = Time.timeAsDouble;
RenderMod.UnscaledTimeBase = Time.unscaledTimeAsDouble;
RenderMod.DspTimeBase = AudioSettings.dspTime;
RenderMod.FrameCountBase = Time.frameCount;
RenderMod.IsRendering = true;
Countdown = 3;
scrController.instance!.StartCoroutine(SetupPlay());
}
if (!RenderMod.IsRendering) return;
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
{
RenderMod.IsRendering = RenderMod.IsCapturing = false;
Time.timeScale = 1f;
scrConductor.currentPreset.inputOffset = _originalInputOffset;
StopFFmpeg(false);
}
else if (scrController.instance.state == States.Won)
{
if (_wonFrame == -1)
{
_wonFrame = FrameCount;
}
var extraFrames = (int)Math.Ceiling(Configuration.EndWaitTime * Configuration.TargetFPS);
if (FrameCount - _wonFrame < extraFrames) return;
RenderMod.IsRendering = RenderMod.IsCapturing = false;
Time.timeScale = 1f;
scrConductor.currentPreset.inputOffset = _originalInputOffset;
StopFFmpeg(true);
}
}
public void SetupCaptureDirectory()
{
var renderDir = Path.Combine(MelonEnvironment.GameRootDirectory, "Render");
if (!Directory.Exists(renderDir))
{
Directory.CreateDirectory(renderDir);
}
}
public void SetupGameScene()
{
GCS.customLevelPaths = new[] { LevelPath };
GCS.checkpointNum = 0;
GCS.customLevelId = "";
GCS.nextSpeedRun = 1;
GCS.customLevelIndex = 0;
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnGame");
SkipPressToStart = true;
_renderSetup = true;
}
private IEnumerator SetupPlay()
{
for (; Countdown > 0; Countdown--)
{
yield return new WaitForSecondsRealtime(1);
}
FrameCount = 0;
StartFFmpegThread();
RenderMod.IsCapturing = true;
scrController.instance.paused = scrController.instance.audioPaused = false;
scrController.instance.levelWasSkipped = true;
scrConductor.instance.song.clip = null;
scrConductor.instance.song.time = -1000;
}
private string FindFFmpegPath()
{
bool isWindows = Application.platform == RuntimePlatform.WindowsPlayer;
string ffmpegName = isWindows ? "ffmpeg.exe" : "ffmpeg";
// 1. Mods/ADOFAIRenderer/ffmpeg
var path1 = Path.Combine(MelonEnvironment.ModsDirectory, "ADOFAIRenderer", ffmpegName);
if (File.Exists(path1)) return path1;
// 2. Mods/ffmpeg
var path2 = Path.Combine(MelonEnvironment.ModsDirectory, ffmpegName);
if (File.Exists(path2)) return path2;
// 3. GameRootDirectory/ffmpeg
var path3 = Path.Combine(MelonEnvironment.GameRootDirectory, ffmpegName);
if (File.Exists(path3)) return path3;
return "";
}
private void StartFFmpegThread()
{
var ffmpegPath = FindFFmpegPath();
if (string.IsNullOrEmpty(ffmpegPath))
{
Melon<RenderMod>.Logger.Error("ffmpeg 바이너리를 찾을 수 없습니다. Mods/ADOFAIRenderer/ 에 ffmpeg를 넣어주세요.");
return;
}
if (_debugMonitorObj == null)
{
_debugMonitorObj = new GameObject("DebugMonitorCanvas");
_debugMonitorObj.AddComponent<DebugMonitorUI>();
DontDestroyOnLoad(_debugMonitorObj);
}
FFmpegProcess.FFmpegPath = ffmpegPath;
lock (ImageQueue)
{
ImageQueue.Clear();
}
PassCount = 0;
RenderMod.FrameCountForVideo = 0;
RenderMod.RenderCheck = true;
RenderMod.SkipCount = 0;
EndRequested = false;
EndSuccessful = false;
_wonFrame = -1;
double targetFps = Configuration.TargetFPS;
double videoFps = Configuration.VideoFPS;
float bitrate = Configuration.Bitrate;
string codecStr = Configuration.Codec;
VideoFFmpeg.Codec selectedCodec = VideoFFmpeg.Codec.H264_Default;
if (Enum.TryParse<VideoFFmpeg.Codec>(codecStr, true, out var parsedCodec))
{
selectedCodec = parsedCodec;
}
string ext = ".mp4";
if (selectedCodec == VideoFFmpeg.Codec.AV1_Default ||
selectedCodec == VideoFFmpeg.Codec.AV1_NVIDIA ||
selectedCodec == VideoFFmpeg.Codec.AV1_QSV ||
selectedCodec == VideoFFmpeg.Codec.VP9_Default ||
selectedCodec == VideoFFmpeg.Codec.VP9_NVIDIA)
{
ext = ".webm";
}
RenderThread = new Thread(() =>
{
try
{
float workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps);
VideoFFmpeg videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec);
var outPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", "out" + ext);
SetupCaptureDirectory();
videoFFmpeg.Start(outPath, bitrate);
int num = targetFps % videoFps != 0.0 ? 1 : (int)(targetFps / videoFps);
while (true)
{
byte[]? buffer = null;
lock (ImageQueue)
{
if (ImageQueue.Count > 0)
{
int firstKey = ImageQueue.Keys[0];
if (firstKey == PassCount)
{
buffer = ImageQueue[firstKey];
ImageQueue.RemoveAt(0);
PassCount += num;
}
}
}
if (buffer != null)
{
if (buffer.Length > 0)
{
int bufferSize = Configuration.RenderWidth * Configuration.RenderHeight * 4;
videoFFmpeg.SendDataFromBuffer(buffer, bufferSize);
System.Buffers.ArrayPool<byte>.Shared.Return(buffer);
}
continue;
}
if (EndRequested)
{
bool hasMore;
lock (ImageQueue)
{
hasMore = ImageQueue.Count > 0;
}
if (!hasMore) break;
}
Thread.Sleep(1);
}
videoFFmpeg.Close();
EndSuccessful = true;
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
}
});
RenderThread.Start();
}
private void StopFFmpeg(bool success)
{
EndRequested = true;
if (success)
{
StartCoroutine(StopRenderCoroutine());
}
else
{
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다.");
if (_debugMonitorObj)
{
Destroy(_debugMonitorObj);
_debugMonitorObj = null;
}
if (RenderThread != null && RenderThread.IsAlive)
{
RenderThread.Join(2000);
}
}
}
private static IEnumerator StopRenderCoroutine()
{
yield return new WaitUntil(() => EndSuccessful);
if (_debugMonitorObj)
{
Destroy(_debugMonitorObj);
_debugMonitorObj = null;
}
RDC.auto = false;
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
}
}
}