diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index 084f79f..03e0298 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -8,6 +8,7 @@ using ADOFAIRenderer.Data; using ADOFAIRenderer.UI; using ADOFAIRenderer.Util; using ADOFAIRenderer.Util.FFmpeg; +using FFmpeg.AutoGen; using HarmonyLib; using MelonLoader; using MelonLoader.Utils; @@ -396,8 +397,25 @@ namespace ADOFAIRenderer.Core frameCountBase = Time.frameCount; _frameReader?.Dispose(); - _frameReader = new FrameReader(); - StartFFmpegThread(); + _frameReader = null; + try + { + StartFFmpegThread(); + _frameReader = _activeVideoFFmpeg != null ? new FrameReader(_activeVideoFFmpeg.SelectedPixelFormat) : new FrameReader(AVPixelFormat.AV_PIX_FMT_YUV420P); + } + catch (Exception ex) + { + Melon.Logger.Error($"인코더 초기화 에러: {ex}"); + CommonWarningDialogUI.ShowDialog( + "인코더 오류", + $"비디오 인코더를 초기화할 수 없습니다.\n오류: {ex.Message}", + new DialogButton("확인", () => + { + ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor"); + }) + ); + yield break; + } StartCapturing(); scrController.instance.paused = scrController.instance.audioPaused = false; @@ -453,37 +471,53 @@ namespace ADOFAIRenderer.Core 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 { - FFmpegNativeEncoder? videoFFmpeg = null; - try + var effectiveVideoFps = Math.Min(videoFPS, renderFPS); + var settings = new FFmpegNativeEncoder.EncoderSettings { - 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, - ForceSoftware = Configuration.EncodingConfig.ForceSoftwareEncoder - }; + 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, + ForceSoftware = Configuration.EncodingConfig.ForceSoftwareEncoder + }; - videoFFmpeg = new FFmpegNativeEncoder(settings); - - SetupCaptureDirectory(); - - _activeVideoFFmpeg = videoFFmpeg; + videoFFmpeg = new FFmpegNativeEncoder(settings); + + SetupCaptureDirectory(); + + _activeVideoFFmpeg = videoFFmpeg; + lock (_allVideoFFmpegInstances) + { + _allVideoFFmpegInstances.Add(videoFFmpeg); + } + videoFFmpeg.Start(_currentOutPath); + } + catch (Exception) + { + if (videoFFmpeg != null) + { lock (_allVideoFFmpegInstances) { - _allVideoFFmpegInstances.Add(videoFFmpeg); + _allVideoFFmpegInstances.Remove(videoFFmpeg); } - videoFFmpeg.Start(_currentOutPath); + videoFFmpeg.Dispose(); + } + _activeVideoFFmpeg = null; + throw; + } + RenderThread = new Thread(() => + { + try + { while (true) { CapturedFrame frame = default; @@ -565,13 +599,10 @@ namespace ADOFAIRenderer.Core catch (Exception ex) { Melon.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}"); - videoFFmpeg?.Dispose(); - if (videoFFmpeg != null) + videoFFmpeg.Dispose(); + lock (_allVideoFFmpegInstances) { - lock (_allVideoFFmpegInstances) - { - _allVideoFFmpegInstances.Remove(videoFFmpeg); - } + _allVideoFFmpegInstances.Remove(videoFFmpeg); } _activeVideoFFmpeg = null; } diff --git a/RenderMod.cs b/RenderMod.cs index 5b3db62..04d8260 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -7,7 +7,7 @@ using MelonLoader.Utils; using UnityEngine; using UnityEngine.EventSystems; -[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.0.5-INDEV", "ADOFAI Editor Forum")] +[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.3-BETA", "ADOFAI Editor Forum")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")] namespace ADOFAIRenderer diff --git a/Util/FFmpeg/FFmpegNativeEncoder.cs b/Util/FFmpeg/FFmpegNativeEncoder.cs index 3339613..8a1cc26 100644 --- a/Util/FFmpeg/FFmpegNativeEncoder.cs +++ b/Util/FFmpeg/FFmpegNativeEncoder.cs @@ -158,6 +158,9 @@ namespace ADOFAIRenderer.Util.FFmpeg /// private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P; + /// 최종 선택되어 구동된 인코더의 픽셀 포맷입니다. + public AVPixelFormat SelectedPixelFormat => _dstPixFmt; + /// /// 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체. /// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다. diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs index 3e1db0b..6d7228c 100644 --- a/Util/FrameReader.cs +++ b/Util/FrameReader.cs @@ -3,7 +3,6 @@ using System.Buffers; using System.Collections.Generic; using System.Linq; using ADOFAIRenderer.Core; -using ADOFAIRenderer.Util.FFmpeg; using FFmpeg.AutoGen; using MelonLoader; using Unity.Collections; @@ -27,8 +26,11 @@ namespace ADOFAIRenderer.Util private bool _isInitialized; private volatile bool _isDisposed; - public FrameReader() + private readonly AVPixelFormat _targetFormat; + + public FrameReader(AVPixelFormat targetFormat) { + _targetFormat = targetFormat; InitializePool(); } @@ -48,24 +50,6 @@ namespace ADOFAIRenderer.Util } } - private static unsafe AVPixelFormat PredictPixelFormat(Codec codec) - { - var candidates = GPUUtil.GetPrioritizedEncoderCandidates(codec); - // ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator - foreach (var encoderName in candidates) - { - var pCodec = ffmpeg.avcodec_find_encoder_by_name(encoderName); - if (pCodec == null) continue; - - if (encoderName.Contains("qsv") || encoderName.Contains("nvenc") || encoderName.Contains("amf")) - { - return AVPixelFormat.AV_PIX_FMT_NV12; - } - return encoderName == "libvvenc" ? AVPixelFormat.AV_PIX_FMT_YUV420P10LE : AVPixelFormat.AV_PIX_FMT_YUV420P; - } - return AVPixelFormat.AV_PIX_FMT_YUV420P; - } - public void RequestFrameCapture(Texture texture, long currentFrame) { if (!RenderManager.IsCapturingActive) return; @@ -90,9 +74,9 @@ namespace ADOFAIRenderer.Util bool useGPUConversion = RenderMod.ColorConversionShader; AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; - if (useGPUConversion && RenderManager.Instance) + if (useGPUConversion) { - expectedFmt = PredictPixelFormat(RenderManager.Instance.Configuration.Codec); + expectedFmt = _targetFormat; // Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리 if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P) {