SHA256
720 lines
26 KiB
C#
720 lines
26 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Threading;
|
|
using System.Buffers;
|
|
using ADOFAIRenderer.Data;
|
|
using ADOFAIRenderer.UI;
|
|
using ADOFAIRenderer.Util;
|
|
using ADOFAIRenderer.Util.FFmpeg;
|
|
using MelonLoader;
|
|
using MelonLoader.Utils;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using Random = UnityEngine.Random;
|
|
using ThreadPriority = System.Threading.ThreadPriority;
|
|
|
|
namespace ADOFAIRenderer.Core
|
|
{
|
|
public class RenderManager : MonoBehaviour
|
|
{
|
|
// Singleton Accessors
|
|
public static RenderManager Instance { get; private set; } = null!;
|
|
public static bool IsActive => Instance && Instance.isRendering;
|
|
public static bool IsCapturingActive => Instance && Instance.isCapturing;
|
|
|
|
public const int MaxQueuedFrames = 64;
|
|
public const int MaxRenderRetry = 64;
|
|
|
|
// Core Render Configuration and Path
|
|
public int countdown;
|
|
public long frameCount;
|
|
public bool skipPressToStart;
|
|
public string levelPath = "";
|
|
public bool isRenderCancelled { get; private set; }
|
|
public RenderConfig Configuration = null!;
|
|
|
|
// Render Sessions State (OOP Instance Fields)
|
|
public bool overrideLoading { get; private set; }
|
|
public bool isRendering { get; private set; }
|
|
public bool isCapturing { get; private set; }
|
|
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;
|
|
|
|
internal struct CapturedFrame
|
|
{
|
|
public IntPtr NativePointer;
|
|
public int Length;
|
|
public byte[]? ManagedBuffer; // Non-null if it's a managed buffer (e.g. black frame)
|
|
public ReadbackRequestState? ReadbackState; // Associated state to recycle
|
|
}
|
|
|
|
// Thread and Queue State
|
|
internal SortedList<long, CapturedFrame> ImageQueue = new();
|
|
internal AutoResetEvent FrameAvailableEvent = new(false);
|
|
public Thread? RenderThread;
|
|
public long passCount;
|
|
public bool endRequested;
|
|
public bool endSuccessful;
|
|
public long wonFrame = -1;
|
|
|
|
// Private Instance Fields
|
|
private FFmpegNativeEncoder? _activeVideoFFmpeg;
|
|
private readonly List<FFmpegNativeEncoder> _allVideoFFmpegInstances = new();
|
|
private int _originalInputOffset;
|
|
private GameObject? _debugMonitorObj;
|
|
private GameObject? _escapeHandlerObj;
|
|
private string? _currentOutPath;
|
|
private bool _renderSetup;
|
|
|
|
private void Awake()
|
|
{
|
|
Instance = this;
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
CleanUpActiveRender();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets overrideLoading to true when starting to load the render scene.
|
|
/// </summary>
|
|
public void StartLoadingOverride()
|
|
{
|
|
overrideLoading = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets isRendering to true when starting the rendering setup.
|
|
/// </summary>
|
|
public void StartRendering()
|
|
{
|
|
isRendering = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets isCapturing to true and resets the cancellation flag when starting the actual frame capture.
|
|
/// </summary>
|
|
public void StartCapturing()
|
|
{
|
|
isCapturing = true;
|
|
isRenderCancelled = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resets all rendering-related states.
|
|
/// </summary>
|
|
public void ResetRenderState(bool success)
|
|
{
|
|
isRendering = false;
|
|
isCapturing = false;
|
|
overrideLoading = false;
|
|
isRenderCancelled = !success;
|
|
}
|
|
|
|
public void CleanUpActiveRender()
|
|
{
|
|
ResetRenderState(success: false);
|
|
|
|
AudioSpectrumGenerator.Clear();
|
|
|
|
if (_activeVideoFFmpeg != null)
|
|
{
|
|
Melon<RenderMod>.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 네이티브 인코더 리소스를 해제합니다.");
|
|
_activeVideoFFmpeg.Dispose();
|
|
_activeVideoFFmpeg = null;
|
|
}
|
|
|
|
lock (_allVideoFFmpegInstances)
|
|
{
|
|
if (_allVideoFFmpegInstances.Count > 0)
|
|
{
|
|
Melon<RenderMod>.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 인코더 인스턴스({_allVideoFFmpegInstances.Count}개)를 수거 및 해제합니다.");
|
|
foreach (var instance in _allVideoFFmpegInstances)
|
|
{
|
|
try
|
|
{
|
|
instance.Dispose();
|
|
}
|
|
catch
|
|
{
|
|
// Ignore
|
|
}
|
|
}
|
|
_allVideoFFmpegInstances.Clear();
|
|
}
|
|
}
|
|
|
|
if (RenderThread is { IsAlive: true })
|
|
{
|
|
RenderThread.Join(200);
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
timeBase = Time.timeAsDouble;
|
|
unscaledTimeBase = Time.unscaledTimeAsDouble;
|
|
dspTimeBase = AudioSettings.dspTime;
|
|
frameCountBase = Time.frameCount;
|
|
|
|
StartRendering();
|
|
countdown = 3;
|
|
wonFrame = -1;
|
|
|
|
if (!_escapeHandlerObj)
|
|
{
|
|
_escapeHandlerObj = new GameObject("EscapeHandlerCanvas");
|
|
_escapeHandlerObj.AddComponent<EscapeHandlerUI>();
|
|
DontDestroyOnLoad(_escapeHandlerObj);
|
|
}
|
|
|
|
Time.captureDeltaTime = (float)(1 / Configuration.RenderFPS);
|
|
scrController.instance!.StartCoroutine(SetupPlay());
|
|
}
|
|
|
|
if (!isRendering) return;
|
|
if (!scrController.instance || scrController.instance.state == States.Fail || scrController.instance.state == States.Fail2)
|
|
{
|
|
ResetRenderState(success: false);
|
|
StopFFmpeg(false);
|
|
EndRender();
|
|
}
|
|
else if (scrController.instance.state == States.Won)
|
|
{
|
|
if (wonFrame == -1)
|
|
{
|
|
wonFrame = frameCount;
|
|
}
|
|
|
|
var extraFrames = (long)Math.Ceiling(Configuration.DelayAfterLevelEnd * Configuration.RenderFPS);
|
|
|
|
if (frameCount - wonFrame < extraFrames) return;
|
|
ResetRenderState(success: true);
|
|
StopFFmpeg(true);
|
|
}
|
|
}
|
|
|
|
public void OnPreRenderUpdate()
|
|
{
|
|
if (!isRendering) return;
|
|
|
|
// [렉으로 인한 프레임 렌더링 스킵 시 재시도 로직 (원본 모드 이식)]
|
|
if (renderCheck && isCapturing)
|
|
{
|
|
if (skipCount < MaxRenderRetry)
|
|
{
|
|
Melon<RenderMod>.Logger.Warning($"[Render Skipped] Frame {frameCount} rendering missed. Retrying... (Attempt {skipCount + 1}/{MaxRenderRetry})");
|
|
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 renderFPS = Configuration.RenderFPS;
|
|
var videoFPS = Configuration.VideoFPS;
|
|
var effectiveVideoFps = Math.Min(videoFPS, renderFPS);
|
|
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / renderFPS);
|
|
|
|
if (videoFrameIndex > lastVideoFrameIndex)
|
|
{
|
|
lastVideoFrameIndex = videoFrameIndex;
|
|
lastRequestedVideoFrameIndex = videoFrameIndex;
|
|
|
|
var byteSize = Configuration.RenderWidth * Configuration.RenderHeight * 4;
|
|
var blackFrame = ArrayPool<byte>.Shared.Rent(byteSize);
|
|
Array.Clear(blackFrame, 0, byteSize);
|
|
|
|
lock (ImageQueue)
|
|
{
|
|
ImageQueue[videoFrameIndex] = new CapturedFrame
|
|
{
|
|
ManagedBuffer = blackFrame,
|
|
Length = byteSize
|
|
};
|
|
}
|
|
FrameAvailableEvent.Set();
|
|
}
|
|
|
|
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번 작업)]
|
|
isThrottling = false;
|
|
|
|
while (isCapturing && isRendering)
|
|
{
|
|
int count;
|
|
lock (ImageQueue)
|
|
{
|
|
count = ImageQueue.Count;
|
|
}
|
|
|
|
if (count <= MaxQueuedFrames) break;
|
|
|
|
if (!isThrottling)
|
|
{
|
|
isThrottling = true;
|
|
throttleCount++;
|
|
}
|
|
|
|
AsyncGPUReadback.WaitAllRequests();
|
|
}
|
|
isThrottling = false;
|
|
|
|
var currentFrame = frameCountForVideo++;
|
|
|
|
var renderFPS = Configuration.RenderFPS;
|
|
var videoFps = Configuration.VideoFPS;
|
|
var effectiveVideoFps = Math.Min(videoFps, renderFPS);
|
|
|
|
var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / renderFPS);
|
|
|
|
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) Instance.levelPath = ADOBase.levelPath;
|
|
scnEditor.instance.CheckUnsavedChanges(() =>
|
|
{
|
|
var quitDelegate =
|
|
(Func<bool>)Delegate.CreateDelegate(typeof(Func<bool>), scnEditor.instance, targetMethod);
|
|
|
|
if (Instance) Instance.StartLoadingOverride();
|
|
Application.wantsToQuit -= quitDelegate;
|
|
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "Assets/Scenes/RenderSettingsScene.unity");
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
if (Configuration.RandomSeed.HasValue) Random.InitState(Configuration.RandomSeed.Value);
|
|
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnGame");
|
|
skipPressToStart = true;
|
|
_renderSetup = true;
|
|
}
|
|
|
|
private IEnumerator SetupPlay()
|
|
{
|
|
for (; countdown > 0; countdown--)
|
|
{
|
|
yield return new WaitForSecondsRealtime(1);
|
|
}
|
|
|
|
frameCount = 0;
|
|
|
|
_frameReader?.Dispose();
|
|
_frameReader = new FrameReader();
|
|
StartFFmpegThread();
|
|
|
|
StartCapturing();
|
|
scrController.instance.paused = scrController.instance.audioPaused = false;
|
|
scrController.instance.levelWasSkipped = true;
|
|
|
|
if (!scrConductor.instance || !scrConductor.instance.song) yield break;
|
|
if (Configuration.ProcessAudio)
|
|
{
|
|
AudioSpectrumGenerator.Initialize(scrConductor.instance.song.clip);
|
|
}
|
|
|
|
scrConductor.instance.song.clip = null;
|
|
scrConductor.instance.song.time = -1000;
|
|
}
|
|
|
|
private void StartFFmpegThread()
|
|
{
|
|
if (!_debugMonitorObj)
|
|
{
|
|
_debugMonitorObj = new GameObject("DebugMonitorCanvas");
|
|
_debugMonitorObj.AddComponent<DebugMonitorUI>();
|
|
DontDestroyOnLoad(_debugMonitorObj);
|
|
}
|
|
|
|
lock (ImageQueue)
|
|
{
|
|
ImageQueue.Clear();
|
|
}
|
|
passCount = 0;
|
|
isRenderCancelled = false;
|
|
frameCountForVideo = 0;
|
|
lastVideoFrameIndex = -1;
|
|
lastRequestedVideoFrameIndex = -1;
|
|
renderCheck = true;
|
|
skipCount = 0;
|
|
endRequested = false;
|
|
endSuccessful = false;
|
|
FrameAvailableEvent.Reset();
|
|
|
|
var renderFPS = Configuration.RenderFPS;
|
|
var videoFPS = Configuration.VideoFPS;
|
|
|
|
var selectedCodec = Configuration.Codec;
|
|
|
|
var ext = selectedCodec switch
|
|
{
|
|
Codec.AV1 or Codec.VP9 => ".webm",
|
|
Codec.VVC => ".mkv",
|
|
_ => ".mp4"
|
|
};
|
|
|
|
var startTime = DateTime.Now;
|
|
var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + ext;
|
|
_currentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
|
|
|
|
RenderThread = new Thread(() =>
|
|
{
|
|
FFmpegNativeEncoder? videoFFmpeg = null;
|
|
try
|
|
{
|
|
var effectiveVideoFps = Math.Min(videoFPS, renderFPS);
|
|
var settings = new FFmpegNativeEncoder.EncoderSettings
|
|
{
|
|
Width = Configuration.RenderWidth,
|
|
Height = Configuration.RenderHeight,
|
|
FPS = effectiveVideoFps,
|
|
Codec = selectedCodec,
|
|
Mode = Configuration.EncodingConfig!.RateControlMode,
|
|
QualityValue = Configuration.EncodingConfig.QP,
|
|
TargetBitrate = Configuration.EncodingConfig.Bitrate,
|
|
MaxBitrate = Configuration.EncodingConfig.MaxBitrate,
|
|
GopSize = Configuration.EncodingConfig.KeyframeInterval
|
|
};
|
|
|
|
videoFFmpeg = new FFmpegNativeEncoder(settings);
|
|
|
|
SetupCaptureDirectory();
|
|
|
|
_activeVideoFFmpeg = videoFFmpeg;
|
|
lock (_allVideoFFmpegInstances)
|
|
{
|
|
_allVideoFFmpegInstances.Add(videoFFmpeg);
|
|
}
|
|
videoFFmpeg.Start(_currentOutPath);
|
|
|
|
while (true)
|
|
{
|
|
CapturedFrame frame = default;
|
|
bool hasFrame = false;
|
|
lock (ImageQueue)
|
|
{
|
|
if (ImageQueue.Count > 0)
|
|
{
|
|
var firstKey = ImageQueue.Keys[0];
|
|
|
|
if (firstKey > passCount && !isCapturing)
|
|
{
|
|
passCount = firstKey;
|
|
}
|
|
|
|
if (firstKey == passCount)
|
|
{
|
|
frame = ImageQueue[firstKey];
|
|
ImageQueue.RemoveAt(0);
|
|
passCount++;
|
|
hasFrame = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasFrame)
|
|
{
|
|
if (frame.ManagedBuffer != null)
|
|
{
|
|
if (frame.ManagedBuffer.Length > 0)
|
|
{
|
|
videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length);
|
|
ArrayPool<byte>.Shared.Return(frame.ManagedBuffer);
|
|
}
|
|
}
|
|
else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0)
|
|
{
|
|
videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length);
|
|
|
|
if (frame.ReadbackState != null)
|
|
{
|
|
_frameReader?.RecycleState(frame.ReadbackState);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isRenderCancelled) break;
|
|
|
|
if (endRequested)
|
|
{
|
|
if (passCount > lastRequestedVideoFrameIndex) break;
|
|
}
|
|
|
|
FrameAvailableEvent.WaitOne(50);
|
|
}
|
|
|
|
if (isRenderCancelled)
|
|
{
|
|
videoFFmpeg.Dispose();
|
|
}
|
|
else
|
|
{
|
|
videoFFmpeg.End();
|
|
videoFFmpeg.Dispose();
|
|
}
|
|
|
|
lock (_allVideoFFmpegInstances)
|
|
{
|
|
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
|
}
|
|
|
|
_activeVideoFFmpeg = null;
|
|
endSuccessful = !isRenderCancelled;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
|
|
videoFFmpeg?.Dispose();
|
|
if (videoFFmpeg != null)
|
|
{
|
|
lock (_allVideoFFmpegInstances)
|
|
{
|
|
_allVideoFFmpegInstances.Remove(videoFFmpeg);
|
|
}
|
|
}
|
|
_activeVideoFFmpeg = null;
|
|
}
|
|
})
|
|
{
|
|
IsBackground = true,
|
|
Priority = ThreadPriority.AboveNormal
|
|
};
|
|
|
|
RenderThread.Start();
|
|
}
|
|
|
|
public void CancelRender()
|
|
{
|
|
ResetRenderState(success: false);
|
|
|
|
StopFFmpeg(false);
|
|
EndRender();
|
|
}
|
|
|
|
private void StopFFmpeg(bool success)
|
|
{
|
|
endRequested = true;
|
|
if (!success)
|
|
{
|
|
isRenderCancelled = true;
|
|
}
|
|
FrameAvailableEvent.Set();
|
|
skipPressToStart = false;
|
|
_renderSetup = false;
|
|
if (_escapeHandlerObj)
|
|
{
|
|
Destroy(_escapeHandlerObj);
|
|
_escapeHandlerObj = null;
|
|
}
|
|
|
|
if (success) StartCoroutine(StopRenderCoroutine());
|
|
else
|
|
{
|
|
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다.");
|
|
|
|
// 1. 렌더링 백그라운드 스레드가 큐에 유입된 잔여 프레임 처리를 마무리하고 종료될 때까지 대기
|
|
if (RenderThread is { IsAlive: true })
|
|
{
|
|
RenderThread.Join(2000);
|
|
}
|
|
|
|
// 2. 스레드 종료 보장 후, 안전하게 FFmpeg 네이티브 인코더 리소스 해제
|
|
if (_activeVideoFFmpeg != null)
|
|
{
|
|
_activeVideoFFmpeg.Dispose();
|
|
_activeVideoFFmpeg = null;
|
|
}
|
|
|
|
// 3. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
|
|
lock (ImageQueue)
|
|
{
|
|
foreach (var kvp in ImageQueue.Values)
|
|
{
|
|
if (kvp.ManagedBuffer is { Length: > 0 })
|
|
{
|
|
ArrayPool<byte>.Shared.Return(kvp.ManagedBuffer);
|
|
}
|
|
if (kvp.ReadbackState != null)
|
|
{
|
|
_frameReader?.RecycleState(kvp.ReadbackState);
|
|
}
|
|
}
|
|
ImageQueue.Clear();
|
|
}
|
|
|
|
_frameReader?.Dispose();
|
|
|
|
// 디버그 모니터 UI 정리
|
|
if (_debugMonitorObj)
|
|
{
|
|
Destroy(_debugMonitorObj);
|
|
_debugMonitorObj = null;
|
|
}
|
|
|
|
// 4. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복
|
|
if (scrCamera.instance)
|
|
{
|
|
scrCamera.instance.forceRTCam = false;
|
|
scrCamera.instance.SetupRTCam(false);
|
|
|
|
var hook = scrCamera.instance.GetComponent<PostRenderHook>();
|
|
if (hook) Destroy(hook);
|
|
}
|
|
|
|
// 5. 인게임 실패 방지 플래그 원상 복구
|
|
if (scrController.instance)
|
|
{
|
|
scrController.instance.noFail = false;
|
|
scrController.instance.noFailInfiniteMargin = false;
|
|
}
|
|
|
|
// 6. 취소된 미완성 임시 동영상 파일 즉각 삭제
|
|
if (!string.IsNullOrEmpty(_currentOutPath) && File.Exists(_currentOutPath))
|
|
{
|
|
try
|
|
{
|
|
File.Delete(_currentOutPath);
|
|
Melon<RenderMod>.Logger.Msg($"[File Cleanup] 미완성 동영상 삭제 완료: {_currentOutPath}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Melon<RenderMod>.Logger.Error($"[File Cleanup] 임시 동영상 삭제 실패: {ex.Message}");
|
|
}
|
|
_currentOutPath = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private IEnumerator StopRenderCoroutine()
|
|
{
|
|
yield return new WaitUntil(() => endSuccessful);
|
|
EndRender();
|
|
}
|
|
|
|
private void EndRender()
|
|
{
|
|
Time.timeScale = 1f;
|
|
Time.captureDeltaTime = 0f;
|
|
|
|
if (_debugMonitorObj)
|
|
{
|
|
Destroy(_debugMonitorObj);
|
|
_debugMonitorObj = null;
|
|
}
|
|
|
|
RDC.auto = false;
|
|
scrConductor.currentPreset.inputOffset = _originalInputOffset;
|
|
GCS.customLevelPaths = null;
|
|
GCS.customLevelId = null;
|
|
|
|
lock (ImageQueue)
|
|
{
|
|
foreach (var kvp in ImageQueue.Values)
|
|
{
|
|
if (kvp.ManagedBuffer is { Length: > 0 })
|
|
{
|
|
ArrayPool<byte>.Shared.Return(kvp.ManagedBuffer);
|
|
}
|
|
if (kvp.ReadbackState != null)
|
|
{
|
|
_frameReader?.RecycleState(kvp.ReadbackState);
|
|
}
|
|
}
|
|
ImageQueue.Clear();
|
|
}
|
|
|
|
_frameReader?.Dispose();
|
|
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
|
|
}
|
|
}
|
|
} |