Fix FFmpegNativeEncoder to Work With QSV Properly

This commit is contained in:
copecone committed 2026-06-03 17:18:27 +09:00
1 parent 8dd33c1807
commit 32dccd4298
4 files changed
+990 -212

No files matched your search

+1
View File
@@ -6,6 +6,7 @@
<NoWarn>MSB3277</NoWarn>
<EnableSourceControlManagerQueries>false</EnableSourceControlManagerQueries>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
<ItemGroup>
+46 -55
View File
@@ -111,8 +111,8 @@ namespace ADOFAIRenderer.Core
public long wonFrame = -1;
// Private Instance Fields
private VideoFFmpeg? _activeVideoFFmpeg;
private readonly List<VideoFFmpeg> _allVideoFFmpegInstances = new List<VideoFFmpeg>();
private FFmpegNativeEncoder? _activeVideoFFmpeg;
private readonly List<FFmpegNativeEncoder> _allVideoFFmpegInstances = new List<FFmpegNativeEncoder>();
private int _originalInputOffset;
private GameObject? _debugMonitorObj;
private GameObject? _escapeHandlerObj;
@@ -136,8 +136,8 @@ namespace ADOFAIRenderer.Core
if (_activeVideoFFmpeg != null)
{
Melon<RenderMod>.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 렌더 프로세스를 강제 종료합니다.");
_activeVideoFFmpeg.Kill();
Melon<RenderMod>.Logger.Msg("게임 종료 또는 RenderManager 파괴 감지: FFmpeg 네이티브 인코더 리소스를 해제합니다.");
_activeVideoFFmpeg.Dispose();
_activeVideoFFmpeg = null;
}
@@ -145,12 +145,12 @@ namespace ADOFAIRenderer.Core
{
if (_allVideoFFmpegInstances.Count > 0)
{
Melon<RenderMod>.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 프로세스({_allVideoFFmpegInstances.Count}개)를 강제 수거 및 종료합니다.");
Melon<RenderMod>.Logger.Msg($"게임 종료 감지: 생성된 모든 FFmpeg 인코더 인스턴스({_allVideoFFmpegInstances.Count}개)를 수거 및 해제합니다.");
foreach (var instance in _allVideoFFmpegInstances)
{
try
{
instance.Kill();
instance.Dispose();
}
catch
{
@@ -400,33 +400,8 @@ namespace ADOFAIRenderer.Core
scrConductor.instance.song.time = -1000;
}
private string FindFFmpegPath()
{
var isWindows = Application.platform == RuntimePlatform.WindowsPlayer;
var 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);
return File.Exists(path3) ? path3 : "";
}
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");
@@ -456,18 +431,14 @@ namespace ADOFAIRenderer.Core
var qp = Configuration.QP;
var codecStr = Configuration.Codec;
var selectedCodec = VideoFFmpeg.Codec.H264_Default;
if (Enum.TryParse<VideoFFmpeg.Codec>(codecStr, true, out var parsedCodec))
var selectedCodec = Codec.H264;
if (Enum.TryParse<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)
if (selectedCodec == Codec.AV1 || selectedCodec == Codec.VP9)
{
ext = ".webm";
}
@@ -478,11 +449,23 @@ namespace ADOFAIRenderer.Core
RenderThread = new Thread(() =>
{
VideoFFmpeg? videoFFmpeg = null;
FFmpegNativeEncoder? videoFFmpeg = null;
try
{
var effectiveVideoFps = Math.Min(videoFps, targetFps);
videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, effectiveVideoFps, videoFps, selectedCodec);
var settings = new FFmpegNativeEncoder.EncoderSettings
{
Width = Configuration.RenderWidth,
Height = Configuration.RenderHeight,
FPS = effectiveVideoFps,
Codec = selectedCodec,
Mode = bitrateMode,
QualityValue = qp,
TargetBitrate = bitrate,
MaxBitrate = maxBitrate
};
videoFFmpeg = new FFmpegNativeEncoder(settings);
SetupCaptureDirectory();
@@ -491,7 +474,7 @@ namespace ADOFAIRenderer.Core
{
_allVideoFFmpegInstances.Add(videoFFmpeg);
}
videoFFmpeg.Start(_currentOutPath, bitrateMode, bitrate, maxBitrate, qp);
videoFFmpeg.Start(_currentOutPath);
while (true)
{
@@ -550,8 +533,15 @@ namespace ADOFAIRenderer.Core
Thread.Sleep(1);
}
if (isRenderCancelled) videoFFmpeg.Kill();
else videoFFmpeg.Close();
if (isRenderCancelled)
{
videoFFmpeg.Dispose();
}
else
{
videoFFmpeg.End();
videoFFmpeg.Dispose();
}
lock (_allVideoFFmpegInstances)
{
@@ -564,7 +554,7 @@ namespace ADOFAIRenderer.Core
catch (Exception ex)
{
Melon<RenderMod>.Logger.Error($"FFmpeg 렌더링 스레드 오류: {ex}");
_activeVideoFFmpeg?.Kill();
videoFFmpeg?.Dispose();
if (videoFFmpeg != null)
{
lock (_allVideoFFmpegInstances)
@@ -612,13 +602,20 @@ namespace ADOFAIRenderer.Core
{
Melon<RenderMod>.Logger.Error("렌더링이 실패했거나 취소되었습니다. 상태 및 리소스를 안전하게 청소합니다.");
// 1. 렌더링 백그라운드 스레드가 큐에 유입된 잔여 프레임 처리를 마무리하고 종료될 때까지 대기
if (RenderThread is { IsAlive: true })
{
RenderThread.Join(2000);
}
// 2. 스레드 종료 보장 후, 안전하게 FFmpeg 네이티브 인코더 리소스 해제
if (_activeVideoFFmpeg != null)
{
_activeVideoFFmpeg.Kill(16384);
_activeVideoFFmpeg.Dispose();
_activeVideoFFmpeg = null;
}
// 1. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
// 3. 메모리 누수 방지: 큐에 렌트된 임시 프레임 버퍼 풀 반환 및 클리어
lock (ImageQueue)
{
foreach (var kvp in ImageQueue.Values)
@@ -644,13 +641,7 @@ namespace ADOFAIRenderer.Core
_debugMonitorObj = null;
}
// 2. 렌더링 스레드가 큐 비어 있음을 감지하고 break 하도록 대기
if (RenderThread is { IsAlive: true })
{
RenderThread.Join(2000);
}
// 3. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복
// 4. 인게임 카메라 및 렌더링용 RTCam 상태 안전 원복
if (scrCamera.instance)
{
scrCamera.instance.forceRTCam = false;
@@ -660,14 +651,14 @@ namespace ADOFAIRenderer.Core
if (hook) Destroy(hook);
}
// 4. 인게임 실패 방지 플래그 원상 복구
// 5. 인게임 실패 방지 플래그 원상 복구
if (scrController.instance)
{
scrController.instance.noFail = false;
scrController.instance.noFailInfiniteMargin = false;
}
// 5. 취소된 미완성 임시 동영상 파일 즉각 삭제
// 6. 취소된 미완성 임시 동영상 파일 즉각 삭제
if (!string.IsNullOrEmpty(_currentOutPath) && File.Exists(_currentOutPath))
{
try
+4 -4
View File
@@ -177,18 +177,18 @@ namespace ADOFAIRenderer.UI
if (bitrateMode == RateControlMode.CBR || bitrateMode == RateControlMode.VBR)
{
if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0)
if (!int.TryParse(_bitrateInput.text, out bitrate) || bitrate <= 0 || bitrate > 1_000_000)
{
Melon<RenderMod>.Logger.Error("올바르지 않은 Bitrate 값입니다. 0보다 큰 정수여야 합니다.");
Melon<RenderMod>.Logger.Error("올바르지 않은 Bitrate 값입니다. 1에서 1,000,000 (kbps) 사이의 정수여야 합니다.");
return;
}
}
if (bitrateMode == RateControlMode.VBR)
{
if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0)
if (!int.TryParse(_maxBitrateInput.text, out maxBitrate) || maxBitrate <= 0 || maxBitrate > 1_000_000)
{
Melon<RenderMod>.Logger.Error("올바르지 않은 Max Bitrate 값입니다. 0보다 큰 정수여야 합니다.");
Melon<RenderMod>.Logger.Error("올바르지 않은 Max Bitrate 값입니다. 1에서 1,000,000 (kbps) 사이의 정수여야 합니다.");
return;
}
if (maxBitrate < bitrate)
File diff suppressed because it is too large. Load diff