Document Render Core And API

This commit is contained in:
copecone committed 2026-10-15 16:17:13 +09:00
1 parent 354c42fdd9
commit 8deb64fd4d
22 files changed
+1241 -27

No files matched your search

+17
View File
@@ -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.&lt;platform&gt;.&lt;resourcePath&gt;" 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)
+7
View File
@@ -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);
+106
View File
@@ -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();
+42
View File
@@ -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();
+12
View File
@@ -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;
+11
View File
@@ -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)
{
+68
View File
@@ -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)
{