From fee7077b16ef075e4661b3de075ad94073552d0b2bb0e2e43e0159afc473b4a6 Mon Sep 17 00:00:00 2001 From: copecone Date: Mon, 12 Oct 2026 02:03:45 +0900 Subject: [PATCH] Split Render Thread Body Into Methods --- Core/RenderManager.cs | 287 ++++++++++++++++++++++++------------------ 1 file changed, 164 insertions(+), 123 deletions(-) diff --git a/Core/RenderManager.cs b/Core/RenderManager.cs index 0532887..9afecb8 100644 --- a/Core/RenderManager.cs +++ b/Core/RenderManager.cs @@ -682,129 +682,8 @@ internal class RenderManager : MonoBehaviour throw; } - RenderThread = new Thread(() => - { - try - { - while (true) - { - CapturedFrame frame = default; - bool hasFrame = false; - lock (ImageQueue) - { - if (ImageQueue.Count > 0) - { - var firstKey = ImageQueue.Keys[0]; - - if (firstKey > passCount && Instance.stage < RenderStage.Render) - { - passCount = firstKey; - } - - if (firstKey == passCount) - { - frame = ImageQueue[firstKey]; - ImageQueue.RemoveAt(0); - passCount++; - hasFrame = true; - } - } - } - - if (hasFrame) - { - if (frame.ManagedBuffer != null) - { - if (frame.ManagedBuffer.Length > 0) - { - videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted); - Interlocked.Increment(ref framesSent); - - ArrayPool.Shared.Return(frame.ManagedBuffer); - } - } - else if (frame.NativePointer != IntPtr.Zero && frame.Length > 0) - { - videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted); - Interlocked.Increment(ref framesSent); - - if (frame.ReadbackState != null) - { - _frameReader?.RecycleState(frame.ReadbackState); - } - } - - if (Configuration.AudioConfig.RenderAudio && SyncCalculator is { } syncCalculator) - { - var frameIdx = (int)(passCount - 1); - var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS); - var frameTime = frameIdx / effectiveVideoFps; - - var channels = Configuration.AudioConfig.AudioChannels; - var samplesNeeded = syncCalculator.GetNextFrameSamples(); - - var audioBuffer = ArrayPool.Shared.Rent(samplesNeeded * channels); - try - { - AudioMixer?.GenerateAudioForFrame(audioBuffer, samplesNeeded, frameTime, dspTimeBase); - videoFFmpeg.WriteAudioSamples(audioBuffer, samplesNeeded); - } - finally - { - ArrayPool.Shared.Return(audioBuffer); - } - } - continue; - } - - if (IsRenderAborted) break; - - if (endRequested) - { - if (passCount > Volatile.Read(ref lastRequestedVideoFrameIndex)) break; - } - - FrameAvailableEvent.WaitOne(50); - } - - if (!IsRenderAborted) - { - // End throws when the file cannot be finished (e.g. the trailer cannot be written), - // which is handled as a render failure by the catch block below. - videoFFmpeg.End(); - Volatile.Write(ref _incompleteOutputPath, null); - } - videoFFmpeg.Dispose(); - - lock (_allVideoFFmpegInstances) - { - _allVideoFFmpegInstances.Remove(videoFFmpeg); - } - - _activeVideoFFmpeg = null; - endSuccessful = !IsRenderAborted; - } - catch (Exception ex) - { - Melon.Logger.Error($"FFmpeg rendering thread error: {ex}"); - - MarkFailed("encoder thread error. See the log for details."); - try - { - videoFFmpeg.Dispose(); - } - catch (Exception disposeEx) - { - Melon.Logger.Error($"Encoder dispose after thread error also failed: {disposeEx.Message}"); - } - - lock (_allVideoFFmpegInstances) - { - _allVideoFFmpegInstances.Remove(videoFFmpeg); - } - _activeVideoFFmpeg = null; - } - }) + var encoder = videoFFmpeg!; + RenderThread = new Thread(() => RunRenderThread(encoder)) { IsBackground = true, Priority = ThreadPriority.AboveNormal @@ -813,6 +692,168 @@ internal class RenderManager : MonoBehaviour RenderThread.Start(); } + /// + /// Body of the render thread. Sends queued frames and their audio to the encoder until the render ends, + /// finishes the output file, and releases the encoder. An exception is handled as a render failure. + /// + private void RunRenderThread(FFmpegNativeEncoder videoFFmpeg) + { + try + { + EncodeQueuedFrames(videoFFmpeg); + + if (!IsRenderAborted) + { + // End throws when the file cannot be finished (e.g. the trailer cannot be written), + // which is handled as a render failure by the catch block below. + videoFFmpeg.End(); + Volatile.Write(ref _incompleteOutputPath, null); + } + videoFFmpeg.Dispose(); + UnregisterEncoder(videoFFmpeg); + + endSuccessful = !IsRenderAborted; + } + catch (Exception ex) + { + Melon.Logger.Error($"FFmpeg rendering thread error: {ex}"); + + MarkFailed("encoder thread error. See the log for details."); + try + { + videoFFmpeg.Dispose(); + } + catch (Exception disposeEx) + { + Melon.Logger.Error($"Encoder dispose after thread error also failed: {disposeEx.Message}"); + } + UnregisterEncoder(videoFFmpeg); + } + } + + /// + /// Takes frames from ImageQueue in passCount order and sends each one to the encoder, followed by the audio + /// samples of that frame. Waits on FrameAvailableEvent (50ms) while no frame is ready. + /// Returns when the render is aborted or all requested frames have been sent after endRequested. + /// + private void EncodeQueuedFrames(FFmpegNativeEncoder videoFFmpeg) + { + while (true) + { + if (TryDequeueNextFrame(out var frame)) + { + SendVideoFrame(videoFFmpeg, frame); + WriteAudioForFrame(videoFFmpeg); + continue; + } + + if (ShouldStopEncodeLoop()) return; + + FrameAvailableEvent.WaitOne(50); + } + } + + /// + /// Removes the frame whose key equals passCount from ImageQueue and advances passCount. + /// Before the Render stage, passCount jumps to the first queued key (the first captured frame index is not 0). + /// Returns false when the queue is empty or the next frame has not arrived yet. + /// + private bool TryDequeueNextFrame(out CapturedFrame frame) + { + frame = default; + lock (ImageQueue) + { + if (ImageQueue.Count == 0) return false; + + var firstKey = ImageQueue.Keys[0]; + if (firstKey > passCount && Instance.stage < RenderStage.Render) + { + passCount = firstKey; + } + + if (firstKey != passCount) return false; + + frame = ImageQueue[firstKey]; + ImageQueue.RemoveAt(0); + passCount++; + return true; + } + } + + /// + /// Sends one captured frame to the encoder and releases its buffer (managed: ArrayPool, native: readback state). + /// A frame without data is skipped. + /// + private void SendVideoFrame(FFmpegNativeEncoder videoFFmpeg, in CapturedFrame frame) + { + if (frame.ManagedBuffer != null) + { + if (frame.ManagedBuffer.Length == 0) return; + + videoFFmpeg.SendDataFromBuffer(frame.ManagedBuffer, frame.Length, frame.IsColorConverted); + Interlocked.Increment(ref framesSent); + + ArrayPool.Shared.Return(frame.ManagedBuffer); + return; + } + + if (frame.NativePointer == IntPtr.Zero || frame.Length <= 0) return; + + videoFFmpeg.SendDataFromNativeBuffer(frame.NativePointer, frame.Length, frame.IsColorConverted); + Interlocked.Increment(ref framesSent); + + if (frame.ReadbackState != null) + { + _frameReader?.RecycleState(frame.ReadbackState); + } + } + + /// + /// Mixes the audio of the frame that was just sent (index passCount - 1) and writes it to the encoder. + /// Does nothing when audio rendering is off. + /// + private void WriteAudioForFrame(FFmpegNativeEncoder videoFFmpeg) + { + if (!Configuration.AudioConfig.RenderAudio || SyncCalculator is not { } syncCalculator) return; + + var frameIdx = (int)(passCount - 1); + var effectiveVideoFps = Math.Min(Configuration.VideoFPS, Configuration.RenderFPS); + var frameTime = frameIdx / effectiveVideoFps; + + var channels = Configuration.AudioConfig.AudioChannels; + var samplesNeeded = syncCalculator.GetNextFrameSamples(); + + var audioBuffer = ArrayPool.Shared.Rent(samplesNeeded * channels); + try + { + AudioMixer?.GenerateAudioForFrame(audioBuffer, samplesNeeded, frameTime, dspTimeBase); + videoFFmpeg.WriteAudioSamples(audioBuffer, samplesNeeded); + } + finally + { + ArrayPool.Shared.Return(audioBuffer); + } + } + + /// + /// True when the render is aborted, or when the end was requested and every requested frame has been sent. + /// + private bool ShouldStopEncodeLoop() + { + if (IsRenderAborted) return true; + + return endRequested && passCount > Volatile.Read(ref lastRequestedVideoFrameIndex); + } + + private void UnregisterEncoder(FFmpegNativeEncoder videoFFmpeg) + { + lock (_allVideoFFmpegInstances) + { + _allVideoFFmpegInstances.Remove(videoFFmpeg); + } + _activeVideoFFmpeg = null; + } + public void CancelRender() { ResetRenderState(success: false);