SHA256
Compare commits
3
Commits
| Author | SHA256 | Date | |
|---|---|---|---|
|
|
27e7327c1e
|
||
|
|
2152625e45
|
||
|
|
aefa1409c1
|
No files matched your search
+47
-4
@@ -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,8 +80,28 @@ 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;
|
||||
private readonly Stopwatch _renderStopwatch = new();
|
||||
@@ -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,13 +460,30 @@ 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)
|
||||
{
|
||||
@@ -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
@@ -9,7 +9,7 @@ using MelonLoader.Utils;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.2.8-INDEV", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonInfo(typeof(RenderMod), "ADOFAI Renderer", "0.4.0-BETA", "ADOFAI Editor Forum")]
|
||||
[assembly: MelonGame("7th Beat Games", "A Dance of Fire and Ice")]
|
||||
|
||||
namespace ADOFAIRenderer;
|
||||
|
||||
@@ -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>");
|
||||
|
||||
|
||||
@@ -201,6 +201,12 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
/// </summary>
|
||||
private AVPixelFormat _dstPixFmt = AVPixelFormat.AV_PIX_FMT_YUV420P;
|
||||
|
||||
/// <summary>
|
||||
/// Color range of the encoder input and output (codec context color_range of the selected encoder).
|
||||
/// Full range (JPEG) for every encoder except libvvenc, limited range (MPEG) for libvvenc.
|
||||
/// </summary>
|
||||
private AVColorRange _colorRange = AVColorRange.AVCOL_RANGE_JPEG;
|
||||
|
||||
/// <summary>Pixel format of the finally selected and running encoder.</summary>
|
||||
public AVPixelFormat SelectedPixelFormat => _dstPixFmt;
|
||||
|
||||
@@ -399,7 +405,19 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
// ── Basic video parameters configuration ──
|
||||
_pCodecContext->width = _settings.Width;
|
||||
_pCodecContext->height = _settings.Height;
|
||||
_pCodecContext->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
||||
|
||||
// ── Output color tags ──
|
||||
// Matrix: BT.709, the RGB→YUV matrix of both conversion paths
|
||||
// (GPU: RGB2YUV_BT709_Full in ColorConversion.compute, CPU: ConfigureSwsColorspace).
|
||||
// Players use this tag to convert the YUV frames back to RGB.
|
||||
// Range: full (0-255) for every encoder except libvvenc, limited (16-235) for libvvenc.
|
||||
// The FFmpeg libvvenc wrapper passes the matrix/primaries/transfer tags to VVenC but not the range,
|
||||
// so a VVC stream is read as limited range; its input is converted to limited range to match.
|
||||
// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libavcodec/libvvenc.c (vvenc_set_color_format)
|
||||
_pCodecContext->color_range = encoder == VideoEncoder.LibVvenc
|
||||
? AVColorRange.AVCOL_RANGE_MPEG
|
||||
: AVColorRange.AVCOL_RANGE_JPEG;
|
||||
_pCodecContext->colorspace = AVColorSpace.AVCOL_SPC_BT709;
|
||||
// thread_count = 0: FFmpeg automatically allocates threads according to the system CPU core count.
|
||||
// For libvvenc, the FFmpeg wrapper passes thread_count as the VVenC internal thread count,
|
||||
// so setting 0 (auto) is required for parallel encoding utilizing all cores.
|
||||
@@ -756,6 +774,7 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
var accelStatus = isHardware ? "Hardware Acceleration Enabled" : "Software CPU Usage";
|
||||
Melon<RenderMod>.Logger.Msg($"Video encoder successfully initialized: {encoder.Name} ({accelStatus})");
|
||||
_dstPixFmt = selectedPixFmt;
|
||||
_colorRange = _pCodecContext->color_range;
|
||||
openedSuccessfully = true;
|
||||
break;
|
||||
}
|
||||
@@ -855,6 +874,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
if (_pSwsContext == null)
|
||||
throw new Exception("Failed to initialize software scaler.");
|
||||
|
||||
ConfigureSwsColorspace();
|
||||
|
||||
_isInitialized = true;
|
||||
_loggedConversionMode = false;
|
||||
}
|
||||
@@ -1240,7 +1261,51 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the format, size and color range of the destination frame (_pDstFrame) and allocates a new pixel buffer for it.
|
||||
/// Sets the RGB→YUV conversion of the CPU conversion path (_pSwsContext) to the BT.709 matrix
|
||||
/// and the color range of the encoder (_colorRange), so that it produces the same colors as the GPU path
|
||||
/// (RGB2YUV_BT709_Full in ColorConversion.compute) and as the tags of the output stream.
|
||||
/// A new libswscale context uses the BT.601 matrix (SWS_CS_DEFAULT = SWS_CS_ITU601) and the limited range.
|
||||
///
|
||||
/// For RGB input, "table" holds the coefficients of the output YUV space and dstRange its range (1 = full, 0 = limited).
|
||||
/// The source (RGB) values, brightness, contrast and saturation keep the values of the context.
|
||||
///
|
||||
/// Reference: https://ffmpeg.org/doxygen/trunk/group__libsws.html (sws_setColorspaceDetails, sws_getCoefficients)
|
||||
/// Reference: https://github.com/FFmpeg/FFmpeg/blob/release/8.0/libswscale/swscale.h (SWS_CS_ITU709, SWS_CS_DEFAULT)
|
||||
/// </summary>
|
||||
private void ConfigureSwsColorspace()
|
||||
{
|
||||
int* currentInvTable;
|
||||
int* currentTable;
|
||||
int srcRange, dstRange, brightness, contrast, saturation;
|
||||
|
||||
var ret = ffmpeg.sws_getColorspaceDetails(_pSwsContext, ¤tInvTable, &srcRange, ¤tTable, &dstRange,
|
||||
&brightness, &contrast, &saturation);
|
||||
if (ret < 0)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"[Encoder] Cannot read the colorspace details of the software scaler: {GetFFmpegErrorString(ret)} (Code: {ret}). The CPU conversion path keeps the libswscale defaults.");
|
||||
return;
|
||||
}
|
||||
|
||||
var coefficients = ffmpeg.sws_getCoefficients(ffmpeg.SWS_CS_ITU709);
|
||||
var invTable = new int_array4();
|
||||
var table = new int_array4();
|
||||
for (uint i = 0; i < 4; i++)
|
||||
{
|
||||
invTable[i] = currentInvTable[i];
|
||||
table[i] = coefficients[i];
|
||||
}
|
||||
|
||||
var fullRange = _colorRange == AVColorRange.AVCOL_RANGE_JPEG;
|
||||
ret = ffmpeg.sws_setColorspaceDetails(_pSwsContext, invTable, srcRange, table, fullRange ? 1 : 0,
|
||||
brightness, contrast, saturation);
|
||||
if (ret < 0)
|
||||
{
|
||||
Melon<RenderMod>.Logger.Warning($"[Encoder] Cannot set the colorspace details of the software scaler: {GetFFmpegErrorString(ret)} (Code: {ret}). The CPU conversion path keeps the libswscale defaults.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the format, size, color range and matrix of the destination frame (_pDstFrame) and allocates a new pixel buffer for it.
|
||||
/// av_frame_get_buffer allocates memory matching format, width and height and sets the data[]/linesize[] pointers.
|
||||
/// Second argument 0 means default alignment.
|
||||
/// Reference: https://ffmpeg.org/doxygen/trunk/group__lavu__frame.html (av_frame_get_buffer)
|
||||
@@ -1250,7 +1315,8 @@ namespace ADOFAIRenderer.Util.FFmpeg
|
||||
_pDstFrame->format = (int)_dstPixFmt;
|
||||
_pDstFrame->width = _settings.Width;
|
||||
_pDstFrame->height = _settings.Height;
|
||||
_pDstFrame->color_range = AVColorRange.AVCOL_RANGE_JPEG;
|
||||
_pDstFrame->color_range = _colorRange;
|
||||
_pDstFrame->colorspace = AVColorSpace.AVCOL_SPC_BT709;
|
||||
|
||||
var ret = ffmpeg.av_frame_get_buffer(_pDstFrame, 0);
|
||||
if (ret < 0)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user