diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index d71a2c2..d25c6c1 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -88,6 +88,8 @@ namespace ADOFAIRenderer.Core public long wonFrame = -1; // Private Instance Fields + private VideoFFmpeg? _activeVideoFFmpeg; + private readonly List _allVideoFFmpegInstances = new List(); private int _originalInputOffset; private GameObject? _debugMonitorObj; private GameObject? _escapeHandlerObj; @@ -99,6 +101,49 @@ namespace ADOFAIRenderer.Core Instance = this; } + private void OnDestroy() + { + CleanUpActiveRender(); + } + + public void CleanUpActiveRender() + { + isRendering = isCapturing = false; + isRenderCancelled = true; + + if (_activeVideoFFmpeg != null) + { + Melon.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 렌더 프로세스를 강제 종료합니다."); + _activeVideoFFmpeg.Kill(); + _activeVideoFFmpeg = null; + } + + lock (_allVideoFFmpegInstances) + { + if (_allVideoFFmpegInstances.Count > 0) + { + Melon.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 프로세스({_allVideoFFmpegInstances.Count}개)를 강제 수거 및 종료합니다."); + foreach (var instance in _allVideoFFmpegInstances) + { + try + { + instance.Kill(); + } + catch + { + // Ignore + } + } + _allVideoFFmpegInstances.Clear(); + } + } + + if (RenderThread is { IsAlive: true }) + { + RenderThread.Join(200); + } + } + private void Update() { if (_renderSetup && ADOBase.lm) @@ -405,13 +450,19 @@ namespace ADOFAIRenderer.Core RenderThread = new Thread(() => { + VideoFFmpeg? videoFFmpeg = null; try { var effectiveVideoFps = Math.Min(videoFps, targetFps); - var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, effectiveVideoFps, videoFps, selectedCodec); + videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, effectiveVideoFps, videoFps, selectedCodec); SetupCaptureDirectory(); + _activeVideoFFmpeg = videoFFmpeg; + lock (_allVideoFFmpegInstances) + { + _allVideoFFmpegInstances.Add(videoFFmpeg); + } videoFFmpeg.Start(_currentOutPath, bitrate); while (true) @@ -464,12 +515,35 @@ namespace ADOFAIRenderer.Core Thread.Sleep(1); } - videoFFmpeg.Close(); - endSuccessful = true; + if (isRenderCancelled) + { + videoFFmpeg.Kill(); + } + else + { + videoFFmpeg.Close(); + } + + lock (_allVideoFFmpegInstances) + { + _allVideoFFmpegInstances.Remove(videoFFmpeg); + } + + _activeVideoFFmpeg = null; + endSuccessful = !isRenderCancelled; } catch (Exception ex) { Melon.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}"); + _activeVideoFFmpeg?.Kill(); + if (videoFFmpeg != null) + { + lock (_allVideoFFmpegInstances) + { + _allVideoFFmpegInstances.Remove(videoFFmpeg); + } + } + _activeVideoFFmpeg = null; } }) { @@ -511,6 +585,12 @@ namespace ADOFAIRenderer.Core { Melon.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다."); + if (_activeVideoFFmpeg != null) + { + _activeVideoFFmpeg.Kill(); + _activeVideoFFmpeg = null; + } + // 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어 lock (ImageQueue) { diff --git a/RenderMod.cs b/RenderMod.cs index c718459..37cce66 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -46,5 +46,12 @@ namespace ADOFAIRenderer var newModule = EventSystem.current.gameObject.AddComponent(); newModule.enabled = true; } + + public override void OnApplicationQuit() + { + if (RenderManager.Instance == null) return; + Melon.Logger.Msg("MelonLoader 종료 감지: RenderManager의 활성 프로세스를 강제 수거 및 종료합니다."); + RenderManager.Instance.CleanUpActiveRender(); + } } } \ No newline at end of file diff --git a/Util/FFmpeg/FFmpegProcess.cs b/Util/FFmpeg/FFmpegProcess.cs index 5424d45..4e3c300 100644 --- a/Util/FFmpeg/FFmpegProcess.cs +++ b/Util/FFmpeg/FFmpegProcess.cs @@ -6,7 +6,7 @@ using Unity.Collections; namespace ADOFAIRenderer.Util.FFmpeg { - public class FFmpegProcess + public class FFmpegProcess : IDisposable { public static string FFmpegPath = ""; @@ -47,9 +47,58 @@ namespace ADOFAIRenderer.Util.FFmpeg public void Close() { if (_ffmpegProcess == null || _ffmpegProcess.HasExited || _inputStream == null) return; - _inputStream.Flush(); - _inputStream.Close(); - _ffmpegProcess.WaitForExit(); + try + { + _inputStream.Flush(); + _inputStream.Close(); + + // Allow FFmpeg to finish encoding and exit gracefully without any timeout to prevent video corruption + _ffmpegProcess.WaitForExit(); + } + catch (Exception) + { + // Ignore any issues during termination + } + finally + { + _ffmpegProcess.Dispose(); + _ffmpegProcess = null; + _inputStream = null; + } + } + + public void Kill() + { + try + { + if (_ffmpegProcess != null && !_ffmpegProcess.HasExited) + { + _ffmpegProcess.Kill(); + } + } + catch (Exception) + { + // Ignore process already exited or access denied errors during cleanup + } + finally + { + try + { + _inputStream?.Close(); + } + catch + { + // Ignore + } + _ffmpegProcess?.Dispose(); + _ffmpegProcess = null; + _inputStream = null; + } + } + + public void Dispose() + { + Kill(); } public static byte[] RentRawBytes(int size)