Fix Color Space Issue

This commit is contained in:
copecone committed 2026-06-23 19:01:08 +09:00
1 parent 189612dbeb
commit 18fe547603
4 files changed
+73 -55

No files matched your search

+63 -32
View File
@@ -8,6 +8,7 @@ using ADOFAIRenderer.Data;
using ADOFAIRenderer.UI; using ADOFAIRenderer.UI;
using ADOFAIRenderer.Util; using ADOFAIRenderer.Util;
using ADOFAIRenderer.Util.FFmpeg; using ADOFAIRenderer.Util.FFmpeg;
using FFmpeg.AutoGen;
using HarmonyLib; using HarmonyLib;
using MelonLoader; using MelonLoader;
using MelonLoader.Utils; using MelonLoader.Utils;
@@ -396,8 +397,25 @@ namespace ADOFAIRenderer.Core
frameCountBase = Time.frameCount; frameCountBase = Time.frameCount;
_frameReader?.Dispose(); _frameReader?.Dispose();
_frameReader = new FrameReader(); _frameReader = null;
StartFFmpegThread(); try
{
StartFFmpegThread();
_frameReader = _activeVideoFFmpeg != null ? new FrameReader(_activeVideoFFmpeg.SelectedPixelFormat) : new FrameReader(AVPixelFormat.AV_PIX_FMT_YUV420P);
}
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"인코더 초기화 에러: {ex}");
CommonWarningDialogUI.ShowDialog(
"인코더 오류",
$"비디오 인코더를 초기화할 수 없습니다.\n오류: {ex.Message}",
new DialogButton("확인", () =>
{
ADOBase.loader.LoadSceneWithTransition(WipeDirection.StartsFromRight, "scnEditor");
})
);
yield break;
}
StartCapturing(); StartCapturing();
scrController.instance.paused = scrController.instance.audioPaused = false; 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; var fileName = startTime.ToString("yyyy-MM-dd HH-mm-ss") + ext;
_currentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName); _currentOutPath = Path.Combine(MelonEnvironment.GameRootDirectory, "Render", fileName);
RenderThread = new Thread(() => FFmpegNativeEncoder? videoFFmpeg = null;
try
{ {
FFmpegNativeEncoder? videoFFmpeg = null; var effectiveVideoFps = Math.Min(videoFPS, renderFPS);
try var settings = new FFmpegNativeEncoder.EncoderSettings
{ {
var effectiveVideoFps = Math.Min(videoFPS, renderFPS); Width = Configuration.RenderWidth,
var settings = new FFmpegNativeEncoder.EncoderSettings Height = Configuration.RenderHeight,
{ FPS = effectiveVideoFps,
Width = Configuration.RenderWidth, Codec = selectedCodec,
Height = Configuration.RenderHeight, Mode = Configuration.EncodingConfig!.RateControlMode,
FPS = effectiveVideoFps, QualityValue = Configuration.EncodingConfig.QP,
Codec = selectedCodec, TargetBitrate = Configuration.EncodingConfig.Bitrate,
Mode = Configuration.EncodingConfig!.RateControlMode, MaxBitrate = Configuration.EncodingConfig.MaxBitrate,
QualityValue = Configuration.EncodingConfig.QP, GopSize = Configuration.EncodingConfig.KeyframeInterval,
TargetBitrate = Configuration.EncodingConfig.Bitrate, ForceSoftware = Configuration.EncodingConfig.ForceSoftwareEncoder
MaxBitrate = Configuration.EncodingConfig.MaxBitrate, };
GopSize = Configuration.EncodingConfig.KeyframeInterval,
ForceSoftware = Configuration.EncodingConfig.ForceSoftwareEncoder
};
videoFFmpeg = new FFmpegNativeEncoder(settings); videoFFmpeg = new FFmpegNativeEncoder(settings);
SetupCaptureDirectory(); SetupCaptureDirectory();
_activeVideoFFmpeg = videoFFmpeg; _activeVideoFFmpeg = videoFFmpeg;
lock (_allVideoFFmpegInstances)
{
_allVideoFFmpegInstances.Add(videoFFmpeg);
}
videoFFmpeg.Start(_currentOutPath);
}
catch (Exception)
{
if (videoFFmpeg != null)
{
lock (_allVideoFFmpegInstances) lock (_allVideoFFmpegInstances)
{ {
_allVideoFFmpegInstances.Add(videoFFmpeg); _allVideoFFmpegInstances.Remove(videoFFmpeg);
} }
videoFFmpeg.Start(_currentOutPath); videoFFmpeg.Dispose();
}
_activeVideoFFmpeg = null;
throw;
}
RenderThread = new Thread(() =>
{
try
{
while (true) while (true)
{ {
CapturedFrame frame = default; CapturedFrame frame = default;
@@ -565,13 +599,10 @@ namespace ADOFAIRenderer.Core
catch (Exception ex) catch (Exception ex)
{ {
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}"); Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
videoFFmpeg?.Dispose(); videoFFmpeg.Dispose();
if (videoFFmpeg != null) lock (_allVideoFFmpegInstances)
{ {
lock (_allVideoFFmpegInstances) _allVideoFFmpegInstances.Remove(videoFFmpeg);
{
_allVideoFFmpegInstances.Remove(videoFFmpeg);
}
} }
_activeVideoFFmpeg = null; _activeVideoFFmpeg = null;
} }
+1 -1
View File
@@ -7,7 +7,7 @@ using MelonLoader.Utils;
using UnityEngine; using UnityEngine;
using UnityEngine.EventSystems; 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")] [assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
namespace ADOFAIRenderer namespace ADOFAIRenderer
+3
View File
@@ -158,6 +158,9 @@ namespace ADOFAIRenderer.Util.FFmpeg
/// </summary> /// </summary>
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P; private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
/// <summary>최종 선택되어 구동된 인코더의 픽셀 포맷입니다.</summary>
public AVPixelFormat SelectedPixelFormat => _dstPixFmt;
/// <summary> /// <summary>
/// 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체. /// 인코딩된 패킷 데이터를 임시로 담을 패킷 구조체.
/// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다. /// 매 프레임마다 할당/해제하는 오버헤드를 방지하기 위해 멤버 변수로 캐싱합니다.
+6 -22
View File
@@ -3,7 +3,6 @@ using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using ADOFAIRenderer.Core; using ADOFAIRenderer.Core;
using ADOFAIRenderer.Util.FFmpeg;
using FFmpeg.AutoGen; using FFmpeg.AutoGen;
using MelonLoader; using MelonLoader;
using Unity.Collections; using Unity.Collections;
@@ -27,8 +26,11 @@ namespace ADOFAIRenderer.Util
private bool _isInitialized; private bool _isInitialized;
private volatile bool _isDisposed; private volatile bool _isDisposed;
public FrameReader() private readonly AVPixelFormat _targetFormat;
public FrameReader(AVPixelFormat targetFormat)
{ {
_targetFormat = targetFormat;
InitializePool(); 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) public void RequestFrameCapture(Texture texture, long currentFrame)
{ {
if (!RenderManager.IsCapturingActive) return; if (!RenderManager.IsCapturingActive) return;
@@ -90,9 +74,9 @@ namespace ADOFAIRenderer.Util
bool useGPUConversion = RenderMod.ColorConversionShader; bool useGPUConversion = RenderMod.ColorConversionShader;
AVPixelFormat expectedFmt = AVPixelFormat.AV_PIX_FMT_RGBA; 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 폴백 처리 // Compute Shader는 NV12와 YUV420P만 지원하므로 그 외 포맷은 CPU 폴백 처리
if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P) if (expectedFmt != AVPixelFormat.AV_PIX_FMT_NV12 && expectedFmt != AVPixelFormat.AV_PIX_FMT_YUV420P)
{ {