Replace Throttle Busy Wait With Dequeue Signal And Byte Limit

This commit is contained in:
copecone committed 2026-10-13 19:05:39 +09:00
1 parent e802b6a08a
commit aefa1409c1
3 files changed
+59 -6

No files matched your search

+48 -5
View File
@@ -5,7 +5,6 @@ using System.IO;
using System.Threading;
using System.Buffers;
using System.Diagnostics;
using System.Linq;
using ADOFAIRenderer.API;
using ADOFAIRenderer.Core.Data;
using ADOFAIRenderer.Data;
@@ -81,7 +80,27 @@ internal class RenderManager : MonoBehaviour
public static bool IsActive => Instance && Instance.stage >= RenderStage.OnScene;
public static bool IsCapturingActive => Instance && Instance.stage >= RenderStage.Render;
/// <summary>
/// Maximum number of frames in ImageQueue. The main thread waits (throttles) while the queue holds more frames.
/// </summary>
public const int MaxQueuedFrames = 64;
/// <summary>
/// Upper bound of the readback buffer bytes of the frames in ImageQueue (4 GiB).
/// The frame limit becomes min(MaxQueuedFrames, MaxQueuedBytes / bytes per frame) (see ComputeQueueFrameLimit),
/// so it only lowers the frame count at resolutions where MaxQueuedFrames frames exceed this size.
/// </summary>
public const long MaxQueuedBytes = 4L * 1024 * 1024 * 1024;
/// <summary>
/// Wait time of the throttle loop (OnPostFrame) for the render thread to take a frame from ImageQueue.
/// When it elapses without a dequeue, AsyncGPUReadback.WaitAllRequests() is called once so the readbacks
/// still in flight complete (the render thread may be waiting for one of them to fill a gap in ImageQueue).
/// </summary>
private const int ThrottleWaitMilliseconds = 10;
/// <summary>Frame limit of ImageQueue applied by the last throttle check (MaxQueuedFrames or lower).</summary>
public int QueueFrameLimit { get; private set; } = MaxQueuedFrames;
// Statistics
public long framesSent;
@@ -213,6 +232,8 @@ internal class RenderManager : MonoBehaviour
// Thread and Queue State
internal SortedList<long, CapturedFrame> ImageQueue = new();
internal AutoResetEvent FrameAvailableEvent = new(false);
// Set by the render thread each time it takes a frame from ImageQueue; the throttle loop waits on it.
internal AutoResetEvent FrameDequeuedEvent = new(false);
public Thread? RenderThread;
public long passCount;
public volatile bool endRequested;
@@ -439,14 +460,31 @@ internal class RenderManager : MonoBehaviour
_holdFloorsSourceCount = floors.Count;
}
/// <summary>
/// Frame limit of ImageQueue for the given capture texture: MaxQueuedBytes divided by the readback bytes of
/// one frame (YUV 4:2:0 when the GPU conversion is active, RGBA otherwise), within 4 to MaxQueuedFrames.
/// </summary>
private int ComputeQueueFrameLimit(RenderTexture texture)
{
var frameBytes = _frameReader?.GetFrameBufferBytes(texture.width, texture.height)
?? (long)texture.width * texture.height * 4;
if (frameBytes <= 0) return MaxQueuedFrames;
return (int)Math.Min(MaxQueuedFrames, Math.Max(4, MaxQueuedBytes / frameBytes));
}
// ReSharper disable once Unity.IncorrectMethodSignature
public void OnPostFrame(RenderTexture? texture)
{
if (Instance.stage < RenderStage.Render || !texture) return;
// [Backpressure Throttling]
// The main thread waits while ImageQueue holds more than QueueFrameLimit frames. Only the render thread
// removes frames, and it signals FrameDequeuedEvent for each one. The wait has a timeout so that the loop
// re-checks the render thread state and pumps pending readback completions (see ThrottleWaitMilliseconds).
isThrottling = false;
var frameLimit = QueueFrameLimit = ComputeQueueFrameLimit(texture!);
while (Instance.stage >= RenderStage.Render)
{
if (RenderThread is not { IsAlive: true })
@@ -461,7 +499,7 @@ internal class RenderManager : MonoBehaviour
count = ImageQueue.Count;
}
if (count <= MaxQueuedFrames) break;
if (count <= frameLimit) break;
if (!isThrottling)
{
@@ -469,7 +507,10 @@ internal class RenderManager : MonoBehaviour
throttleCount++;
}
AsyncGPUReadback.WaitAllRequests();
if (!FrameDequeuedEvent.WaitOne(ThrottleWaitMilliseconds))
{
AsyncGPUReadback.WaitAllRequests();
}
}
isThrottling = false;
@@ -649,6 +690,7 @@ internal class RenderManager : MonoBehaviour
endRequested = false;
endSuccessful = false;
FrameAvailableEvent.Reset();
FrameDequeuedEvent.Reset();
var renderFPS = Configuration.RenderFPS;
var videoFPS = Configuration.VideoFPS;
@@ -712,7 +754,7 @@ internal class RenderManager : MonoBehaviour
throw;
}
var encoder = videoFFmpeg!;
var encoder = videoFFmpeg;
RenderThread = new Thread(() => RunRenderThread(encoder))
{
IsBackground = true,
@@ -775,6 +817,7 @@ internal class RenderManager : MonoBehaviour
if (TryDequeueNextFrame(out var frame))
{
FrameDequeuedEvent.Set();
SendVideoFrame(videoFFmpeg, frame);
WriteAudioForFrame(videoFFmpeg);
continue;
+1 -1
View File
@@ -164,7 +164,7 @@ internal class DebugMonitorUI : MonoBehaviour
}
}
sb.AppendLine($"• Queue: <b>{queueCount}</b> / {RenderManager.MaxQueuedFrames} frames");
sb.AppendLine($"• Queue: <b>{queueCount}</b> / {(RenderManager.Instance ? RenderManager.Instance.QueueFrameLimit : RenderManager.MaxQueuedFrames)} frames");
sb.AppendLine($"• Rendered Frames: <b>{(RenderManager.Instance ? RenderManager.Instance.frameCount : 0)}</b>");
sb.AppendLine($"• Frames Sent: <b>{(RenderManager.Instance ? Interlocked.Read(ref RenderManager.Instance.framesSent) : 0)}</b>");
+10
View File
@@ -73,6 +73,16 @@ namespace ADOFAIRenderer.Util
}
}
/// <summary>
/// Bytes of the readback buffer of one frame: Width x Height x 3 / 2 when the GPU conversion to NV12 / YUV420P
/// is used, Width x Height x 4 (RGBA) otherwise. The same size as ReadbackRequestState.Setup uses.
/// </summary>
public long GetFrameBufferBytes(int width, int height)
{
var pixels = (long)width * height;
return _useGPUConversion ? pixels * 3 / 2 : pixels * 4;
}
public void RequestFrameCapture(Texture texture, long currentFrame)
{
if (!RenderManager.IsCapturingActive) return;