SHA256
344 lines
12 KiB
C#
344 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;
|
|
public 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)
|
|
{
|
|
_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;
|
|
|
|
var targetFps = Configuration.TargetFPS;
|
|
var videoFps = Configuration.VideoFPS;
|
|
float bitrate = Configuration.Bitrate;
|
|
var codecStr = Configuration.Codec;
|
|
|
|
VideoFFmpeg.Codec selectedCodec = VideoFFmpeg.Codec.H264_Default;
|
|
if (Enum.TryParse<VideoFFmpeg.Codec>(codecStr, true, out var parsedCodec))
|
|
{
|
|
selectedCodec = parsedCodec;
|
|
}
|
|
|
|
var 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";
|
|
}
|
|
|
|
var startTime = DateTime.Now;
|
|
var fileName = startTime.ToString("yyyy-MM-dd HH_mm_ss") + ext;
|
|
|
|
RenderThread = new Thread(() =>
|
|
{
|
|
try
|
|
{
|
|
var workingFps = (float)(targetFps % videoFps == 0.0 ? videoFps : targetFps);
|
|
var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, (int)workingFps, (int)videoFps, selectedCodec);
|
|
|
|
var outPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
|
|
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)
|
|
{
|
|
var 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");
|
|
}
|
|
}
|
|
} |