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
+893 -115

No files matched your search

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