SHA256
Document Render Core And API
This commit is contained in:
1 parent
354c42fdd9
commit
8deb64fd4d
22 files changed
+1241
-27
No files matched your search
@@ -6,9 +6,16 @@ using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Loads the Unity asset bundles (fonts, shaders) embedded in the mod DLL as manifest resources. One bundle set is
|
||||
/// embedded per platform (Bundle/Win64, Bundle/OSX, Bundle/Linux).
|
||||
/// Main thread (AssetBundle loading).
|
||||
/// </summary>
|
||||
internal static class BundleLoader
|
||||
{
|
||||
/// <summary>Resource folder of the running platform: "Win64" on Windows, "OSX" on macOS, "Linux" otherwise.</summary>
|
||||
private static readonly string BundlePlatform;
|
||||
/// <summary>The mod assembly that contains the bundle resources.</summary>
|
||||
private static readonly Assembly? ExecutingAssembly;
|
||||
|
||||
static BundleLoader()
|
||||
@@ -22,6 +29,16 @@ internal static class BundleLoader
|
||||
ExecutingAssembly = Assembly.GetExecutingAssembly();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads the bundle stored as manifest resource "ADOFAIRenderer.Bundle.<platform>.<resourcePath>" with
|
||||
/// AssetBundle.LoadFromStream. The resource stream is disposed when this method returns. The caller owns the returned
|
||||
/// bundle.
|
||||
/// Reference: https://docs.unity3d.com/ScriptReference/AssetBundle.LoadFromStream.html
|
||||
/// </summary>
|
||||
/// <param name="resourcePath">Bundle name inside the platform folder, for example "fonts" or "shaders".</param>
|
||||
/// <returns>The loaded bundle, or null when Unity fails to load it.</returns>
|
||||
/// <exception cref="MissingManifestResourceException">The resource is not embedded in the DLL.</exception>
|
||||
/// <exception cref="InvalidOperationException">The executing assembly is not available.</exception>
|
||||
public static AssetBundle GetBundle(string resourcePath)
|
||||
{
|
||||
if (ExecutingAssembly != null)
|
||||
|
||||
@@ -2,8 +2,15 @@
|
||||
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
/// <summary>File system helpers.</summary>
|
||||
internal static class FileUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates <paramref name="path"/> and its missing parent directories when it does not exist.
|
||||
/// Exceptions of Directory.CreateDirectory are not caught (for example ArgumentException for an empty path,
|
||||
/// IOException or UnauthorizedAccessException when the directory cannot be created).
|
||||
/// </summary>
|
||||
/// <param name="path">Directory path, absolute or relative to the current directory.</param>
|
||||
public static void CreateDirectory(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
|
||||
|
||||
@@ -13,25 +13,61 @@ using UnityEngine.Rendering;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
{
|
||||
/// <summary>
|
||||
/// GPU capture of rendered frames for the encoder. For each requested frame it optionally converts the RGBA texture to
|
||||
/// the encoder's YUV 4:2:0 layout with a compute shader (ColorConversion.compute from the mod's asset bundle, kernels
|
||||
/// CSMain_NV12 / CSMain_YUV420P), then reads the result back asynchronously (AsyncGPUReadback.RequestIntoNativeArray)
|
||||
/// into a persistent native buffer and adds it to RenderManager.ImageQueue under its video frame index.
|
||||
/// When the shader or the kernel for the pixel format is unavailable, the RGBA texture is read back unchanged and the
|
||||
/// encoder converts it (IsColorConverted = false).
|
||||
/// <para>
|
||||
/// Each request uses a <see cref="ReadbackRequestState"/> from a pool (16 created up front, more allocated when the pool
|
||||
/// is empty). A state stays out of the pool from the request until the render thread has sent its frame
|
||||
/// (RenderManager.SendVideoFrame, then <see cref="RecycleState"/>) or the frame was dropped.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Threads: requests and readback completions run on the main thread; <see cref="RecycleState"/> is also called by
|
||||
/// the render thread. The pool and the disposed flag are guarded by _poolLock. One instance per render, created in
|
||||
/// RenderManager.SetupPlay with the pixel format of the opened encoder.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal class FrameReader : IDisposable
|
||||
{
|
||||
// Property IDs of the compute shader parameters: source texture, output buffer and frame size in pixels.
|
||||
private static readonly int Height = Shader.PropertyToID("Height");
|
||||
private static readonly int Width = Shader.PropertyToID("Width");
|
||||
private static readonly int DestinationBuffer = Shader.PropertyToID("DestinationBuffer");
|
||||
private static readonly int SourceTexture = Shader.PropertyToID("SourceTexture");
|
||||
/// <summary>Number of readback states created by InitializePool.</summary>
|
||||
private const int InitialBufferSize = 16;
|
||||
|
||||
/// <summary>States that are free for a new request. Guarded by _poolLock.</summary>
|
||||
private readonly Stack<ReadbackRequestState> _statePool = new();
|
||||
/// <summary>Every state created by this reader (free, in flight or holding a queued frame), for Dispose. Guarded by _poolLock.</summary>
|
||||
private readonly List<ReadbackRequestState> _allocatedStates = new();
|
||||
/// <summary>Lock for _statePool, _allocatedStates, _isInitialized, _isDisposed and ReadbackRequestState.IsInFlight.</summary>
|
||||
private readonly object _poolLock = new();
|
||||
/// <summary>True after InitializePool; false again after Dispose (RecycleState then disposes returned states).</summary>
|
||||
private bool _isInitialized;
|
||||
/// <summary>Set by Dispose. Completions and recycles that arrive afterwards dispose their state.</summary>
|
||||
private volatile bool _isDisposed;
|
||||
|
||||
/// <summary>Color conversion compute shader from the asset bundle (ModAssets.ColorConversionShader); null when not loaded.</summary>
|
||||
private readonly ComputeShader? _computeShader;
|
||||
/// <summary>Kernel index for the target pixel format; -1 when the format has no kernel or the kernel was not found.</summary>
|
||||
private readonly int _kernelIndex = -1;
|
||||
/// <summary>True when frames are converted to YUV on the GPU (shader and kernel available).</summary>
|
||||
private readonly bool _useGPUConversion;
|
||||
/// <summary>Pixel format of the read-back data: the target format with GPU conversion, AV_PIX_FMT_RGBA otherwise.</summary>
|
||||
private readonly AVPixelFormat _expectedFormat;
|
||||
|
||||
/// <summary>
|
||||
/// Selects the conversion kernel for <paramref name="targetFormat"/> and fills the state pool.
|
||||
/// </summary>
|
||||
/// <param name="targetFormat">
|
||||
/// Pixel format of the encoder input (FFmpegNativeEncoder.SelectedPixelFormat). NV12 and YUV420P use GPU conversion;
|
||||
/// any other format falls back to RGBA readback.
|
||||
/// </param>
|
||||
public FrameReader(AVPixelFormat targetFormat)
|
||||
{
|
||||
_computeShader = ModAssets.ColorConversionShader;
|
||||
@@ -57,6 +93,7 @@ namespace ADOFAIRenderer.Util
|
||||
InitializePool();
|
||||
}
|
||||
|
||||
/// <summary>Creates InitialBufferSize states once. Their buffers are allocated on first use (ReadbackRequestState.Setup).</summary>
|
||||
private void InitializePool()
|
||||
{
|
||||
lock (_poolLock)
|
||||
@@ -83,6 +120,15 @@ namespace ADOFAIRenderer.Util
|
||||
return _useGPUConversion ? pixels * 3 / 2 : pixels * 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the capture of <paramref name="texture"/> as video frame <paramref name="currentFrame"/>. Does nothing
|
||||
/// outside the Render stage. Main thread.
|
||||
/// With GPU conversion, the compute kernel writes the YUV data of the texture into the state's ComputeBuffer and that
|
||||
/// buffer is read back; otherwise the texture itself is read back as RGBA. The buffers are sized from the texture
|
||||
/// dimensions of this call. The result arrives in OnReadbackComplete.
|
||||
/// </summary>
|
||||
/// <param name="texture">Frame to capture (game frame or the output of the frame hooks).</param>
|
||||
/// <param name="currentFrame">Video frame index; the key of the frame in ImageQueue.</param>
|
||||
public void RequestFrameCapture(Texture texture, long currentFrame)
|
||||
{
|
||||
if (!RenderManager.IsCapturingActive) return;
|
||||
@@ -113,6 +159,8 @@ namespace ADOFAIRenderer.Util
|
||||
_computeShader.SetInt(Width, texture.width);
|
||||
_computeShader.SetInt(Height, texture.height);
|
||||
|
||||
// Thread group counts cover the frame in tiles of 32 x 16 pixels (NV12) or 64 x 16 pixels (YUV420P), rounded up.
|
||||
// The tile size is the area one thread group of the ColorConversion.compute kernels processes.
|
||||
var threadGroupsX = _expectedFormat == AVPixelFormat.AV_PIX_FMT_NV12
|
||||
? (texture.width + 31) / 32
|
||||
: (texture.width + 63) / 64;
|
||||
@@ -120,6 +168,8 @@ namespace ADOFAIRenderer.Util
|
||||
|
||||
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||
|
||||
// The native buffer must not be disposed until the request completes; Dispose skips states that are in flight.
|
||||
// Reference: https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadback.RequestIntoNativeArray.html
|
||||
AsyncGPUReadback.RequestIntoNativeArray(
|
||||
ref state.NativeBuffer, state.GPUBuffer, state.CachedCallback
|
||||
);
|
||||
@@ -136,6 +186,17 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Completion of a readback request (through ReadbackRequestState.CachedCallback).
|
||||
/// <list type="bullet">
|
||||
/// <item>Reader disposed: the state is disposed.</item>
|
||||
/// <item>Render aborted: the state is returned to the pool and nothing is queued.</item>
|
||||
/// <item>Readback error: a black frame (managed buffer) is queued under the same index, so the render thread does not
|
||||
/// wait forever for the missing index, and the state is returned to the pool.</item>
|
||||
/// <item>Success: the state's native buffer is queued without copying; the state stays out of the pool until the
|
||||
/// render thread has encoded the frame.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal void OnReadbackComplete(ReadbackRequestState state, AsyncGPUReadbackRequest request)
|
||||
{
|
||||
var frame = state.FrameIndex;
|
||||
@@ -227,6 +288,10 @@ namespace ADOFAIRenderer.Util
|
||||
return frame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a state to the pool after its frame was sent or dropped, or disposes it when the reader is disposed.
|
||||
/// Called by the render thread (after sending) and by the main thread (abort, errors, queue cleanup).
|
||||
/// </summary>
|
||||
internal void RecycleState(ReadbackRequestState state)
|
||||
{
|
||||
lock (_poolLock)
|
||||
@@ -242,6 +307,10 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the frame to RenderManager.ImageQueue under <paramref name="frame"/> (replacing a frame with the same index)
|
||||
/// and wakes the render thread.
|
||||
/// </summary>
|
||||
private static void PushToQueue(long frame, RenderManager.CapturedFrame capturedFrame)
|
||||
{
|
||||
if (!RenderManager.Instance) return;
|
||||
@@ -252,6 +321,11 @@ namespace ADOFAIRenderer.Util
|
||||
RenderManager.Instance.FrameAvailableEvent.Set();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes every state that is not in flight and clears the pool. States in flight are disposed when their
|
||||
/// readback completes. States still referenced by frames in ImageQueue are disposed too, so callers clear the queue
|
||||
/// first (RenderManager.StopFFmpeg / RestoreRenderEnvironment). Main thread.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_poolLock)
|
||||
@@ -269,20 +343,38 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// One reusable capture slot of <see cref="FrameReader"/>: the persistent native buffer the readback writes into, the
|
||||
/// ComputeBuffer the conversion kernel writes (GPU conversion only), and the metadata of the frame it currently holds.
|
||||
/// The buffers are kept between frames and reallocated only when the frame size or conversion mode changes.
|
||||
/// </summary>
|
||||
internal class ReadbackRequestState : IDisposable
|
||||
{
|
||||
/// <summary>Reader that receives the completion and owns the pool.</summary>
|
||||
private readonly FrameReader _owner;
|
||||
/// <summary>Video frame index of the current request; -1 while the state is free.</summary>
|
||||
public long FrameIndex { get; private set; }
|
||||
/// <summary>
|
||||
/// Persistent native buffer receiving the readback: Width x Height x 3 / 2 bytes of YUV 4:2:0 with GPU conversion,
|
||||
/// Width x Height x 4 bytes of RGBA otherwise. Read by the render thread through CapturedFrame.NativePointer.
|
||||
/// </summary>
|
||||
public NativeArray<byte> NativeBuffer;
|
||||
/// <summary>True between the request and its completion. Written under FrameReader._poolLock.</summary>
|
||||
public bool IsInFlight { get; internal set; }
|
||||
|
||||
/// <summary>Raw ComputeBuffer (4-byte elements) the conversion kernel writes; null without GPU conversion.</summary>
|
||||
public ComputeBuffer? GPUBuffer;
|
||||
/// <summary>True when the current frame is converted to YUV on the GPU.</summary>
|
||||
public bool IsGPUConversion { get; private set; }
|
||||
/// <summary>Width of the current frame in pixels.</summary>
|
||||
public int Width { get; private set; }
|
||||
/// <summary>Height of the current frame in pixels.</summary>
|
||||
public int Height { get; private set; }
|
||||
|
||||
/// <summary>Completion delegate created once per state, so a request does not allocate a new delegate.</summary>
|
||||
public readonly Action<AsyncGPUReadbackRequest> CachedCallback;
|
||||
|
||||
/// <summary>Creates a free state without buffers.</summary>
|
||||
public ReadbackRequestState(FrameReader owner)
|
||||
{
|
||||
_owner = owner;
|
||||
@@ -290,6 +382,17 @@ namespace ADOFAIRenderer.Util
|
||||
FrameIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares the state for a request: stores the frame metadata and (re)allocates the buffers when their size differs.
|
||||
/// The native buffer holds width x height x 3 / 2 bytes with GPU conversion, otherwise <paramref name="bufferSize"/>.
|
||||
/// The ComputeBuffer holds the same number of bytes as 4-byte elements (byte count / 4, rounded down).
|
||||
/// </summary>
|
||||
/// <param name="frameIndex">Video frame index of the request.</param>
|
||||
/// <param name="bufferSize">Readback size in bytes for RGBA readback (width x height x 4).</param>
|
||||
/// <param name="isGPUConversion">Whether the kernel converts the frame to YUV.</param>
|
||||
/// <param name="pixelFormat">Pixel format of the read-back data (not used by this method).</param>
|
||||
/// <param name="width">Frame width in pixels.</param>
|
||||
/// <param name="height">Frame height in pixels.</param>
|
||||
public void Setup(long frameIndex, int bufferSize, bool isGPUConversion, AVPixelFormat pixelFormat, int width, int height)
|
||||
{
|
||||
FrameIndex = frameIndex;
|
||||
@@ -320,16 +423,19 @@ namespace ADOFAIRenderer.Util
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Readback completion; forwards to FrameReader.OnReadbackComplete.</summary>
|
||||
private void OnComplete(AsyncGPUReadbackRequest request)
|
||||
{
|
||||
_owner.OnReadbackComplete(this, request);
|
||||
}
|
||||
|
||||
/// <summary>Marks the state as free (FrameIndex -1). The buffers are kept for the next request.</summary>
|
||||
public void Recycle()
|
||||
{
|
||||
FrameIndex = -1;
|
||||
}
|
||||
|
||||
/// <summary>Releases the native buffer and the ComputeBuffer.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (NativeBuffer.IsCreated) NativeBuffer.Dispose();
|
||||
|
||||
@@ -10,27 +10,48 @@ using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
/// <summary>
|
||||
/// GPU vendor, used to try the hardware encoders of the active GPU first (VideoEncoder.Vendor). <c>Unknown</c> is
|
||||
/// also the vendor of the software encoders.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
internal enum GPUVendor
|
||||
{
|
||||
Unknown, NVIDIA, AMD, Intel, Apple
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detection of the active GPU vendor and the NVIDIA driver version, and the encoder try order derived from the vendor.
|
||||
/// Results are cached for the game session. Main thread (SystemInfo is read on first use).
|
||||
/// </summary>
|
||||
internal static class GPUUtil
|
||||
{
|
||||
// Device name patterns (SystemInfo.graphicsDeviceName) of the GPU families recognized per vendor. A name that matches
|
||||
// none of them gives GPUVendor.Unknown (Apple on macOS), which only affects the encoder try order.
|
||||
// NVIDIA: "NVIDIA GeForce RTX/GTX <number>" with optional SUPER / Ti / Ti SUPER, memory size, "Laptop GPU" and a
|
||||
// "/PCIe/SSE2" suffix.
|
||||
private const string NvidiaPattern =
|
||||
@"^NVIDIA GeForce [RG]TX [0-9]+(?> SUPER| Ti(?> SUPER)?)?(?> [0-9]+GB)?(?> Laptop GPU)?(?>/PCIe/SSE2)?$";
|
||||
|
||||
// Intel: "Intel(R) UHD/HD Graphics <number>" or "Intel(R) Arc(TM) <A-C><number> Graphics", with an optional
|
||||
// parenthesized suffix.
|
||||
private const string IntelPattern =
|
||||
@"^Intel(?>\(R\))? (?>U?HD Graphics [0-9]+|Arc\(TM\) [A-C][0-9]+ Graphics)(?> \([^)]+\))?$";
|
||||
|
||||
// AMD: "AMD Radeon RX <number>" with an optional " XT".
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private const string AMDPattern =
|
||||
@"^AMD Radeon RX [0-9]+(?> XT)?$";
|
||||
|
||||
/// <summary>Vendor found by the first GetActiveGPUVendor call.</summary>
|
||||
private static GPUVendor _cachedVendor = GPUVendor.Unknown;
|
||||
/// <summary>True once _cachedVendor is set.</summary>
|
||||
private static bool _hasCachedVendor;
|
||||
|
||||
/// <summary>
|
||||
/// Vendor of the GPU Unity renders with: the device name is matched against the NVIDIA, Intel and AMD patterns in
|
||||
/// that order; on macOS an unmatched name gives Apple; otherwise Unknown. Logs the device name once.
|
||||
/// </summary>
|
||||
private static GPUVendor GetActiveGPUVendor()
|
||||
{
|
||||
if (_hasCachedVendor) return _cachedVendor;
|
||||
@@ -72,9 +93,21 @@ internal static class GPUUtil
|
||||
return _cachedVendor;
|
||||
}
|
||||
|
||||
/// <summary>Result of the first GetNvidiaDriverVersion call (null when not available).</summary>
|
||||
private static double? _cachedNvidiaVersion;
|
||||
/// <summary>True once _cachedNvidiaVersion is set.</summary>
|
||||
private static bool _hasCachedNvidiaVersion;
|
||||
|
||||
/// <summary>
|
||||
/// NVIDIA driver version in NVIDIA's numbering (for example 561.09), or null when the active GPU is not detected as
|
||||
/// NVIDIA or no version can be found. The result is cached.
|
||||
/// <para>
|
||||
/// The version is derived from a four-part Windows driver version a.b.c.d as ((c mod 10) x 10000 + d) / 100,
|
||||
/// for example 31.0.15.6109 gives 561.09. It is read from SystemInfo.graphicsDeviceVersion when that string contains a
|
||||
/// four-part number, otherwise (Windows only) from the DriverVersion value of the first display adapter entry whose
|
||||
/// ProviderName contains "NVIDIA" under the display adapter class key of the registry.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static double? GetNvidiaDriverVersion()
|
||||
{
|
||||
if (_hasCachedNvidiaVersion) return _cachedNvidiaVersion;
|
||||
@@ -103,6 +136,9 @@ internal static class GPUUtil
|
||||
|
||||
// 2. Fallback to Windows Registry (only on Windows NT platforms via RegistryUtil)
|
||||
if (System.Environment.OSVersion.Platform != System.PlatformID.Win32NT) return null;
|
||||
// Device class key of display adapters (class GUID {4d36e968-e325-11ce-bfc1-08002be10318}); each numbered subkey is
|
||||
// one installed adapter driver.
|
||||
// Reference: https://learn.microsoft.com/en-us/windows-hardware/drivers/install/system-defined-device-setup-classes-available-to-vendors
|
||||
const string classKey = @"SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}";
|
||||
var subkeys = RegistryUtil.GetHKLMSubKeyNames(classKey);
|
||||
if (subkeys == null) return null;
|
||||
@@ -130,6 +166,12 @@ internal static class GPUUtil
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encoders for <paramref name="codec"/> in the order they are tried: the encoders of the active GPU vendor first,
|
||||
/// then all others in VideoEncoder.All order (hardware encoders of other vendors and the software encoders).
|
||||
/// </summary>
|
||||
/// <param name="codec">Video codec.</param>
|
||||
/// <param name="forceSoftware">When true, only the software encoders of the codec are returned.</param>
|
||||
public static List<VideoEncoder> GetPrioritizedEncoders(Codec codec, bool forceSoftware = false)
|
||||
{
|
||||
var encoders = VideoEncoder.All.Where(e => e.Codec == codec).ToList();
|
||||
|
||||
@@ -4,10 +4,22 @@ using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
{
|
||||
/// <summary>Particle system helpers.</summary>
|
||||
internal static class ParticleUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Renderer found for each particle system. Weak keys: an entry is dropped when its ParticleSystem object is collected.
|
||||
/// Main thread.
|
||||
/// </summary>
|
||||
private static readonly ConditionalWeakTable<ParticleSystem, ParticleSystemRenderer> Cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the ParticleSystemRenderer that belongs to <paramref name="ps"/>. When a GameObject has several particle
|
||||
/// systems and renderers, the renderer with the same component index as the particle system is used; otherwise the
|
||||
/// first renderer of the GameObject. The result is cached per particle system. Main thread.
|
||||
/// </summary>
|
||||
/// <param name="ps">Particle system; null or destroyed returns null.</param>
|
||||
/// <returns>The renderer, or null when the GameObject has none.</returns>
|
||||
public static ParticleSystemRenderer? GetRenderer(this ParticleSystem ps)
|
||||
{
|
||||
if (!ps) return null;
|
||||
|
||||
@@ -2,8 +2,19 @@ using System;
|
||||
|
||||
namespace ADOFAIRenderer.Util
|
||||
{
|
||||
/// <summary>Conversion of floating-point rates (frame rates) to integer fractions.</summary>
|
||||
internal static class RationalUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Best rational approximation Numerator / Denominator of <paramref name="value"/> by continued fraction expansion:
|
||||
/// the last convergent whose numerator fits in int and whose denominator does not exceed
|
||||
/// <paramref name="maxDenominator"/>. Exact for values with a short expansion (for example 60 gives 60/1). The sign is
|
||||
/// carried by the numerator; 0 gives 0/1. A magnitude above int.MaxValue has no valid convergent and gives 1/0.
|
||||
/// Thread-safe (pure function).
|
||||
/// </summary>
|
||||
/// <param name="value">Value to convert, for example a frame rate in frames per second.</param>
|
||||
/// <param name="maxDenominator">Largest allowed denominator (default 100000000).</param>
|
||||
/// <returns>Numerator and denominator (positive for magnitudes up to int.MaxValue).</returns>
|
||||
public static (int Numerator, int Denominator) Double2MaxPrecisionInt(double value,
|
||||
int maxDenominator = 100000000)
|
||||
{
|
||||
|
||||
@@ -9,9 +9,25 @@ using FFmpeg.AutoGen;
|
||||
|
||||
namespace ADOFAIRenderer.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Plays a custom level's background video with FFmpeg (FFmpegNativeDecoder) in place of Unity's VideoPlayer while a
|
||||
/// render is active, so the video frame shown always matches the render clock. Attached to the scnGame.videoBG object
|
||||
/// by scnGamePatch.ReplaceVideo, which disables the native VideoPlayer; removed again by that patch when no render is
|
||||
/// active.
|
||||
/// <para>
|
||||
/// Every LateUpdate it computes the video time from the conductor (the same formula scrVfxPlus uses to start and
|
||||
/// position the native video), decodes frames up to that time and draws the newest one either into the VideoPlayer's
|
||||
/// target RenderTexture or onto a quad at the far plane of the background camera.
|
||||
/// Decoded YUV frames are converted to RGBA on the GPU (ColorConversion.compute, kernels CSMain_NV12ToRGBA /
|
||||
/// CSMain_YUV420PToRGBA); other pixel formats, or a missing shader, use the decoder's RGBA output (CPU conversion).
|
||||
/// Main thread only.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
internal class VideoBGPlayer : MonoBehaviour
|
||||
{
|
||||
// Property IDs of the compute shader parameters: input planes, output texture, frame size in pixels,
|
||||
// ColorRange (1 = full range, 0 = limited range) and ColorMatrix (FFmpegNativeDecoder.ColorMatrix).
|
||||
private static readonly int YTex = Shader.PropertyToID("_YTex");
|
||||
private static readonly int UVTex = Shader.PropertyToID("_UVTex");
|
||||
private static readonly int Result = Shader.PropertyToID("_Result");
|
||||
@@ -22,6 +38,10 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
private static readonly int ColorRange = Shader.PropertyToID("ColorRange");
|
||||
private static readonly int ColorMatrix = Shader.PropertyToID("ColorMatrix");
|
||||
|
||||
/// <summary>
|
||||
/// Conversion path of the current video: <c>None</c> = CPU RGBA texture, <c>YUV420P</c> = separate Y, U, V planes,
|
||||
/// <c>NV12</c> = Y plane and interleaved UV plane.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "InconsistentNaming")]
|
||||
private enum GPUMode
|
||||
{
|
||||
@@ -30,6 +50,7 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
NV12
|
||||
}
|
||||
|
||||
/// <summary>Decoder of the current video; null when not initialized or after CleanUp.</summary>
|
||||
private FFmpegNativeDecoder? _decoder;
|
||||
|
||||
// CPU fallback rendering
|
||||
@@ -47,16 +68,34 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
private RenderTexture? _renderTexture;
|
||||
private int _kernelIndex = -1;
|
||||
|
||||
/// <summary>Presentation time in seconds of the frame currently shown (video time, 0 = start of the file).</summary>
|
||||
private double _currentFrameTimestamp;
|
||||
/// <summary>Video time in seconds from which the next frame is decoded (current timestamp + 1 / FPS).</summary>
|
||||
private double _nextFrameTimestamp;
|
||||
/// <summary>Conversion path selected by Init.</summary>
|
||||
private GPUMode _gpuMode = GPUMode.None;
|
||||
/// <summary>Target RenderTexture of the native VideoPlayer; null when the video is shown on the background quad.</summary>
|
||||
private RenderTexture? _targetTexture;
|
||||
/// <summary>Whether the level loops the video (miscSettings "loopVideo").</summary>
|
||||
private bool _isLooping;
|
||||
/// <summary>True after a successful Init; set to false by CleanUp or a playback error (playback then stops).</summary>
|
||||
private bool _isInitialized;
|
||||
|
||||
/// <summary>Background quad created by SetupBackgroundQuad when there is no target texture; hidden until the first frame.</summary>
|
||||
private GameObject? _quad;
|
||||
/// <summary>Material of the background quad; its main texture is the converted frame.</summary>
|
||||
private Material? _material;
|
||||
|
||||
/// <summary>
|
||||
/// Releases any previous video and opens <paramref name="videoPath"/>. Allocates the plane textures for the GPU path
|
||||
/// of the decoder's pixel format (chroma planes are (width + 1) / 2 x (height + 1) / 2), or an RGBA texture for the
|
||||
/// CPU path. Errors are logged; the player then stays uninitialized and shows nothing.
|
||||
/// </summary>
|
||||
/// <param name="videoPath">Full path of the video file.</param>
|
||||
/// <param name="targetTexture">RenderTexture the native VideoPlayer renders into, or null to draw on a background quad.</param>
|
||||
/// <param name="loop">Whether the video loops.</param>
|
||||
/// <param name="targetCamera">Camera the background quad is attached to when <paramref name="targetTexture"/> is null.</param>
|
||||
/// <param name="videoBGGo">VideoPlayer object; its layer is given to the background quad.</param>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public void Init(string videoPath, RenderTexture? targetTexture, bool loop, Camera? targetCamera, GameObject videoBGGo)
|
||||
{
|
||||
@@ -169,12 +208,19 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets clamp wrapping and bilinear filtering.</summary>
|
||||
private static void ConfigureTexture(Texture2D tex)
|
||||
{
|
||||
tex.wrapMode = TextureWrapMode.Clamp;
|
||||
tex.filterMode = FilterMode.Bilinear;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main thread, every frame after the song has started. Computes the video time, seeks when it moved backwards or more
|
||||
/// than 1 s ahead of the shown frame, decodes frames until the next frame lies after that time, and draws the last
|
||||
/// decoded frame. Before the video starts (negative time) the target texture is cleared to black and the quad hidden.
|
||||
/// When decoding fails at the end of the file, a looping video seeks to 0; otherwise the last frame stays.
|
||||
/// </summary>
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!_isInitialized || _decoder == null) return;
|
||||
@@ -182,6 +228,9 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
var cond = scrConductor.instance;
|
||||
if (!cond || !cond.hasSongStarted) return;
|
||||
|
||||
// Song position in seconds of song time, as scrConductor computes songposition_minusi from dspTime with the input
|
||||
// offset calibration_i = 0 (the render sets the input offset to 0), minus the separate countdown length.
|
||||
// scrVfxPlus starts the native video at songposition_minusi - countdown + vidOffset (scrVfxPlus.cs).
|
||||
var pitch = cond.song.pitch;
|
||||
var separateCountdownOffset = cond.separateCountdownTime ?
|
||||
cond.crotchetAtStart * cond.adjustedCountdownTicks : 0.0;
|
||||
@@ -215,6 +264,7 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
}
|
||||
|
||||
|
||||
// scrVfxPlus hides the tutorial (editor) background when the native video starts (scrVfxPlus.cs).
|
||||
if (ADOBase.customLevel)
|
||||
{
|
||||
ADOBase.customLevel.ShowTutorialBackground(false);
|
||||
@@ -222,6 +272,7 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
|
||||
try
|
||||
{
|
||||
// A seek is cheaper than decoding many frames when the time jumped forward by more than 1 s.
|
||||
if (targetVideoTime < _currentFrameTimestamp || targetVideoTime - _currentFrameTimestamp > 1.0)
|
||||
{
|
||||
_decoder.SeekToTime(targetVideoTime);
|
||||
@@ -298,6 +349,10 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uploads the decoded planes, converts them to RGBA with the compute shader (or uploads the RGBA texture), and shows
|
||||
/// the result: blitted into the target texture, or set as the quad's main texture (the quad is activated).
|
||||
/// </summary>
|
||||
private void RenderToTarget()
|
||||
{
|
||||
if (_decoder == null) return;
|
||||
@@ -318,6 +373,7 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
|
||||
_computeShader.SetInt(ColorMatrix, _decoder.ColorMatrix);
|
||||
|
||||
// One thread group per 8 x 8 output pixels, rounded up (group size expected by the ColorConversion.compute kernels).
|
||||
int threadGroupsX = (w + 7) / 8;
|
||||
int threadGroupsY = (h + 7) / 8;
|
||||
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||
@@ -353,6 +409,7 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
_computeShader.SetInt(ColorRange, _decoder.ColorRange == AVColorRange.AVCOL_RANGE_JPEG ? 1 : 0);
|
||||
_computeShader.SetInt(ColorMatrix, _decoder.ColorMatrix);
|
||||
|
||||
// One thread group per 8 x 8 output pixels, rounded up (group size expected by the ColorConversion.compute kernels).
|
||||
int threadGroupsX = (w + 7) / 8;
|
||||
int threadGroupsY = (h + 7) / 8;
|
||||
_computeShader.Dispatch(_kernelIndex, threadGroupsX, threadGroupsY, 1);
|
||||
@@ -394,11 +451,16 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases the decoder and GPU resources when the component is destroyed.</summary>
|
||||
private void OnDestroy()
|
||||
{
|
||||
CleanUp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the decoder and destroys the textures, the RenderTexture, the background quad and its material.
|
||||
/// The player is uninitialized afterwards. Called by Init, on errors and on destroy.
|
||||
/// </summary>
|
||||
public void CleanUp()
|
||||
{
|
||||
_isInitialized = false;
|
||||
@@ -446,6 +508,12 @@ internal class VideoBGPlayer : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a quad as a child of <paramref name="targetCamera"/>, 0.5 units in front of its far clip plane, scaled to
|
||||
/// fill the view (orthographic size or field of view), on the layer of <paramref name="videoBGGo"/>. Its shader is
|
||||
/// taken from scrCamera's camera quad material when available, otherwise the first of Sprites/Default, Unlit/Texture
|
||||
/// and Standard that exists. The quad starts inactive.
|
||||
/// </summary>
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private void SetupBackgroundQuad(Camera targetCamera, GameObject videoBGGo)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user