SHA256
Fix FFmpegNativeEncoder to Work With QSV Properly
This commit is contained in:
1 parent
8dd33c1807
commit
32dccd4298
4 files changed
+893
-115
No files matched your search
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user