From ec06f996035aac5733b6d00a1bb5c7dcf6ce62e4b946256337ce8e6276c226a2 Mon Sep 17 00:00:00 2001 From: copecone Date: Thu, 28 May 2026 11:33:47 +0900 Subject: [PATCH] Implement Filter-Before-Capture Structure --- Component/RenderManager.cs | 33 +++++++++++++++++++------------ RenderMod.cs | 33 +++++++++++++++++++------------ Util/FFmpeg/FFmpegProcess.cs | 4 ++-- Util/FrameReader.cs | 38 +++++++++++++++++++++++++----------- 4 files changed, 70 insertions(+), 38 deletions(-) diff --git a/Component/RenderManager.cs b/Component/RenderManager.cs index d3ad625..a72758d 100644 --- a/Component/RenderManager.cs +++ b/Component/RenderManager.cs @@ -219,7 +219,10 @@ namespace ADOFAIRenderer.Component } PassCount = 0; IsRenderCancelled = false; + RenderMod.IsRenderCancelled = false; RenderMod.FrameCountForVideo = 0; + RenderMod.LastVideoFrameIndex = -1; + RenderMod.LastRequestedVideoFrameIndex = -1; RenderMod.RenderCheck = true; RenderMod.SkipCount = 0; EndRequested = false; @@ -254,17 +257,13 @@ namespace ADOFAIRenderer.Component { try { - var quotient = targetFps / videoFps; - var isMultiple = Math.Abs(quotient - Math.Round(quotient)) < 1e-9; - var workingFps = isMultiple ? videoFps : targetFps; - var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, workingFps, videoFps, selectedCodec); + var effectiveVideoFps = Math.Min(videoFps, targetFps); + var videoFFmpeg = new VideoFFmpeg(Configuration.RenderWidth, Configuration.RenderHeight, effectiveVideoFps, videoFps, selectedCodec); SetupCaptureDirectory(); videoFFmpeg.Start(_currentOutPath, bitrate); - var num = isMultiple ? (long)Math.Round(quotient) : 1L; - while (true) { byte[]? buffer = null; @@ -273,11 +272,17 @@ namespace ADOFAIRenderer.Component if (ImageQueue.Count > 0) { var firstKey = ImageQueue.Keys[0]; + + if (firstKey > PassCount && !RenderMod.IsCapturing) + { + PassCount = firstKey; + } + if (firstKey == PassCount) { buffer = ImageQueue[firstKey]; ImageQueue.RemoveAt(0); - PassCount += num; + PassCount++; } } } @@ -293,19 +298,17 @@ namespace ADOFAIRenderer.Component continue; } - if (IsRenderCancelled) + if (IsRenderCancelled || RenderMod.IsRenderCancelled) { break; } if (EndRequested) { - bool hasMore; - lock (ImageQueue) + if (PassCount > RenderMod.LastRequestedVideoFrameIndex) { - hasMore = ImageQueue.Count > 0; + break; } - if (!hasMore) break; } Thread.Sleep(1); @@ -327,6 +330,7 @@ namespace ADOFAIRenderer.Component { RenderMod.IsRendering = RenderMod.IsCapturing = false; IsRenderCancelled = true; + RenderMod.IsRenderCancelled = true; StopFFmpeg(false); EndRender(); @@ -335,6 +339,11 @@ namespace ADOFAIRenderer.Component private void StopFFmpeg(bool success) { EndRequested = true; + if (!success) + { + IsRenderCancelled = true; + RenderMod.IsRenderCancelled = true; + } SkipPressToStart = false; _renderSetup = false; if (_escapeHandlerObj) diff --git a/RenderMod.cs b/RenderMod.cs index db823a7..32c5a44 100644 --- a/RenderMod.cs +++ b/RenderMod.cs @@ -98,24 +98,27 @@ namespace ADOFAIRenderer // 60회 초과 시 최후의 블랙 프레임 삽입하여 전체 파이프라인 마비 방지 Melon.Logger.Error($"[Render Skipped] Frame {RenderManager.FrameCount} rendering failed after 60 retries. Supplying black frame."); + var currentFrame = FrameCountForVideo++; var targetFps = RenderManager.Configuration.TargetFPS; var videoFps = RenderManager.Configuration.VideoFPS; - var quotient = targetFps / videoFps; - var num = Math.Abs(quotient - Math.Round(quotient)) < 1e-9 ? (long)Math.Round(quotient) : 1L; + var effectiveVideoFps = Math.Min(videoFps, targetFps); + var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps); - if (FrameCountForVideo % num == 0) + if (videoFrameIndex > LastVideoFrameIndex) { + LastVideoFrameIndex = videoFrameIndex; + LastRequestedVideoFrameIndex = videoFrameIndex; + var byteSize = RenderManager.Configuration.RenderWidth * RenderManager.Configuration.RenderHeight * 4; var blackFrame = System.Buffers.ArrayPool.Shared.Rent(byteSize); Array.Clear(blackFrame, 0, byteSize); lock (RenderManager.ImageQueue) { - RenderManager.ImageQueue[FrameCountForVideo] = blackFrame; + RenderManager.ImageQueue[videoFrameIndex] = blackFrame; } } - FrameCountForVideo++; RenderCheck = false; // 진행 허용 } @@ -130,6 +133,9 @@ namespace ADOFAIRenderer } public static long FrameCountForVideo; + public static long LastVideoFrameIndex = -1; + public static long LastRequestedVideoFrameIndex = -1; + public static bool IsRenderCancelled = false; public static int ReadbackFailureCount; public static int ThrottleCount; public static bool IsThrottling; @@ -167,15 +173,16 @@ namespace ADOFAIRenderer var targetFps = RenderManager.Configuration.TargetFPS; var videoFps = RenderManager.Configuration.VideoFPS; - var quotient = targetFps / videoFps; - var num = Math.Abs(quotient - Math.Round(quotient)) < 1e-9 ? (int)Math.Round(quotient) : 1; - - if (currentFrame % num != 0) - { - return; - } + var effectiveVideoFps = Math.Min(videoFps, targetFps); - FrameReader?.RequestFrameCapture(texture, currentFrame); + var videoFrameIndex = (long)(currentFrame * effectiveVideoFps / targetFps); + + if (videoFrameIndex > LastVideoFrameIndex) + { + LastVideoFrameIndex = videoFrameIndex; + LastRequestedVideoFrameIndex = videoFrameIndex; + FrameReader?.RequestFrameCapture(texture, videoFrameIndex); + } /* AsyncGPUReadback.Request(texture, 0, delegate(AsyncGPUReadbackRequest req) diff --git a/Util/FFmpeg/FFmpegProcess.cs b/Util/FFmpeg/FFmpegProcess.cs index 120dc49..6472263 100644 --- a/Util/FFmpeg/FFmpegProcess.cs +++ b/Util/FFmpeg/FFmpegProcess.cs @@ -15,7 +15,7 @@ namespace ADOFAIRenderer.Util.FFmpeg public bool IsEnd => _ffmpegProcess is { HasExited: true }; - public void Execute(string command) + protected void Execute(string command) { if (string.IsNullOrEmpty(command)) { @@ -39,7 +39,7 @@ namespace ADOFAIRenderer.Util.FFmpeg public void SendDataFromBuffer(byte[] buffer, int length = -1) { if (_inputStream == null) return; - int writeLength = length < 0 ? buffer.Length : length; + var writeLength = length < 0 ? buffer.Length : length; _inputStream.Write(buffer, 0, writeLength); _inputStream.Flush(); } diff --git a/Util/FrameReader.cs b/Util/FrameReader.cs index 53d28bd..3e19f36 100644 --- a/Util/FrameReader.cs +++ b/Util/FrameReader.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Buffers; using System.Collections.Generic; using ADOFAIRenderer.Component; using ADOFAIRenderer.Util.FFmpeg; @@ -17,6 +18,7 @@ namespace ADOFAIRenderer.Util private readonly Stack _statePool = new Stack(); private readonly object _poolLock = new object(); private bool _isInitialized; + private volatile bool _isDisposed; public FrameReader() { @@ -63,7 +65,16 @@ namespace ADOFAIRenderer.Util { var frame = state.FrameIndex; - if (RenderManager.IsRenderCancelled) + lock (_poolLock) + { + if (_isDisposed) + { + state.Dispose(); + return; + } + } + + if (RenderMod.IsRenderCancelled) { RecycleState(state); return; @@ -72,9 +83,12 @@ namespace ADOFAIRenderer.Util if (request.hasError) { RenderMod.ReadbackFailureCount++; - Melon.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending an empty frame."); + Melon.Logger.Warning($"[OOM Prevent] GPU Readback failed for frame {frame}. Queue deadlock prevented by sending a black frame."); - PushToQueue(frame, Array.Empty()); + var byteSize = state.NativeBuffer.Length; + var blackFrame = ArrayPool.Shared.Rent(byteSize); + Array.Clear(blackFrame, 0, byteSize); + PushToQueue(frame, blackFrame); } else { @@ -97,15 +111,15 @@ namespace ADOFAIRenderer.Util private void RecycleState(ReadbackRequestState state) { - if (!_isInitialized) - { - state.Dispose(); - return; - } - - state.Recycle(); lock (_poolLock) { + if (_isDisposed || !_isInitialized) + { + state.Dispose(); + return; + } + + state.Recycle(); _statePool.Push(state); } } @@ -122,6 +136,8 @@ namespace ADOFAIRenderer.Util { lock (_poolLock) { + if (_isDisposed) return; + _isDisposed = true; while (_statePool.Count > 0) { var state = _statePool.Pop();