SHA256
363 lines
16 KiB
C#
363 lines
16 KiB
C#
using System;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
namespace ADOFAIRenderer.API;
|
|
|
|
/// <summary>
|
|
/// Execution context provided to frame modification hooks, managing GPU ping-pong double buffering.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Zero-Allocation Notice:</b> For maximum performance on the render hot path, the pipeline reuses context instances
|
|
/// across frames. Do not cache the context instance itself across frames.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Execution Frequency Notice:</b> This hook is executed only when a frame is captured for video encoding.
|
|
/// When <c>RenderFPS</c> exceeds <c>VideoFPS</c>, intermediate game rendering frames are skipped and the hook
|
|
/// will <b>not</b> be invoked on every rendered game frame.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Ping-pong model:</b> the renderer owns two RenderTextures with the size and format of the game frame.
|
|
/// <see cref="Source"/> is the current frame, <see cref="Destination"/> is the buffer the next pass writes.
|
|
/// A pass draws from <see cref="Source"/> into <see cref="Destination"/> and then calls <see cref="Swap"/>, which makes
|
|
/// the written buffer the new <see cref="Source"/>. <see cref="Blit(Material, int)"/>, and by default
|
|
/// <see cref="DrawToDestination"/> and <see cref="ExecuteCommandBuffer"/>, swap automatically. The texture in
|
|
/// <see cref="Current"/> after the last hook is encoded. When no hook swaps or replaces the frame, the game frame is
|
|
/// encoded unchanged. The first <see cref="Source"/> of every frame is the game's own frame texture
|
|
/// (<see cref="OriginalSource"/>); do not write into it.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Threading and lifetime:</b> hooks run on the main Unity thread at the end of the frame, after the game has
|
|
/// rendered. The textures are valid only during the hook call: do not keep references to <see cref="Source"/>,
|
|
/// <see cref="Destination"/> or the context. <c>RenderTexture.active</c> is restored after all hooks have run.
|
|
/// </para>
|
|
/// </remarks>
|
|
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
|
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
|
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
|
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
|
|
public class FrameHookContext
|
|
{
|
|
/// <summary>Game frame of this capture (camera quad texture); never written by the context.</summary>
|
|
private RenderTexture _originalSource = null!;
|
|
|
|
/// <summary>First ping-pong buffer, owned by FrameHookPipeline.</summary>
|
|
private RenderTexture _pingRT = null!;
|
|
|
|
/// <summary>Second ping-pong buffer, owned by FrameHookPipeline.</summary>
|
|
private RenderTexture _pongRT = null!;
|
|
|
|
/// <summary>Current frame: the original source, a ping-pong buffer, or a texture set by ReplaceCurrent.</summary>
|
|
private RenderTexture _currentSource = null!;
|
|
|
|
/// <summary>Buffer the next pass writes: always the ping or pong buffer.</summary>
|
|
private RenderTexture _currentDestination = null!;
|
|
|
|
/// <summary>True after the first Swap or ReplaceCurrent of this frame.</summary>
|
|
private bool _hasModified;
|
|
|
|
/// <summary>
|
|
/// The input <see cref="RenderTexture"/> containing the rendered frame before the current pass.
|
|
/// </summary>
|
|
public RenderTexture Source => _currentSource;
|
|
|
|
/// <summary>
|
|
/// The writable destination <see cref="RenderTexture"/> for the current pass.
|
|
/// </summary>
|
|
public RenderTexture Destination => _currentDestination;
|
|
|
|
/// <summary>
|
|
/// The active <see cref="RenderTexture"/> representing the current state of the modified frame.
|
|
/// Same texture as <see cref="Source"/>; after the last hook it is the frame that gets encoded.
|
|
/// </summary>
|
|
public RenderTexture Current => _currentSource;
|
|
|
|
/// <summary>
|
|
/// The original unmodified render texture captured from the game for the current frame before any hooks were applied.
|
|
/// </summary>
|
|
public RenderTexture OriginalSource => _originalSource;
|
|
|
|
/// <summary>
|
|
/// Indicates whether any modifications or swaps have been applied to this frame.
|
|
/// </summary>
|
|
public bool HasModified => _hasModified;
|
|
|
|
/// <summary>
|
|
/// The sequential 0-based index of the video frame currently being captured (its position in the output video).
|
|
/// </summary>
|
|
public long FrameIndex { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Alias for <see cref="FrameIndex"/>. The sequential 0-based index of the video frame.
|
|
/// </summary>
|
|
public long VideoFrameIndex => FrameIndex;
|
|
|
|
/// <summary>
|
|
/// The sequential 0-based index of the game render frame, counted from the start of capture.
|
|
/// With <c>RenderFPS</c> above the video frame rate it advances by more than one between hook calls.
|
|
/// </summary>
|
|
public long RenderFrameIndex { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The timestamp of the current frame in seconds relative to video start (<see cref="FrameIndex"/> / <see cref="VideoFPS"/>).
|
|
/// </summary>
|
|
public double VideoTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The delta time duration in seconds of a single video frame (equivalent to 1.0 / VideoFPS).
|
|
/// </summary>
|
|
public double DeltaTime => VideoFPS > 0 ? 1.0 / VideoFPS : 0.0;
|
|
|
|
/// <summary>
|
|
/// The width of the frame in pixels (width of the current <see cref="Source"/>).
|
|
/// </summary>
|
|
public int Width => _currentSource.width;
|
|
|
|
/// <summary>
|
|
/// The height of the frame in pixels (height of the current <see cref="Source"/>).
|
|
/// </summary>
|
|
public int Height => _currentSource.height;
|
|
|
|
/// <summary>
|
|
/// The configured render frames per second (game frames simulated per second of render time).
|
|
/// </summary>
|
|
public double RenderFPS { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The output video frames per second: the effective rate, min(configured <c>VideoFPS</c>, <see cref="RenderFPS"/>).
|
|
/// </summary>
|
|
public double VideoFPS { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The number of modification passes completed for the current frame (calls of <see cref="Swap"/> and
|
|
/// <see cref="ReplaceCurrent"/>, including the automatic swaps of the helper methods). 0 at the start of every frame.
|
|
/// </summary>
|
|
public int PassCount { get; private set; }
|
|
|
|
/// <summary>Creates a context for the first frame; see <see cref="Reset"/> for the parameters.</summary>
|
|
internal FrameHookContext(
|
|
RenderTexture originalSource,
|
|
RenderTexture pingRT,
|
|
RenderTexture pongRT,
|
|
RenderTexture firstDestination,
|
|
long frameIndex,
|
|
long renderFrameIndex,
|
|
double videoTime,
|
|
double renderFps,
|
|
double videoFps)
|
|
{
|
|
Reset(originalSource, pingRT, pongRT, firstDestination, frameIndex, renderFrameIndex, videoTime, renderFps, videoFps);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prepares the context for a new frame: Source = <paramref name="originalSource"/>, Destination =
|
|
/// <paramref name="firstDestination"/> (ping or pong), not modified, PassCount 0, and the frame metadata.
|
|
/// </summary>
|
|
internal void Reset(
|
|
RenderTexture originalSource,
|
|
RenderTexture pingRT,
|
|
RenderTexture pongRT,
|
|
RenderTexture firstDestination,
|
|
long frameIndex,
|
|
long renderFrameIndex,
|
|
double videoTime,
|
|
double renderFps,
|
|
double videoFps)
|
|
{
|
|
_originalSource = originalSource;
|
|
_pingRT = pingRT;
|
|
_pongRT = pongRT;
|
|
|
|
_currentSource = originalSource;
|
|
_currentDestination = firstDestination;
|
|
_hasModified = false;
|
|
PassCount = 0;
|
|
|
|
FrameIndex = frameIndex;
|
|
RenderFrameIndex = renderFrameIndex;
|
|
VideoTime = videoTime;
|
|
RenderFPS = renderFps;
|
|
VideoFPS = videoFps;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Blits from <see cref="Source"/> to <see cref="Destination"/> using the specified <see cref="Material"/> and automatically swaps ping-pong buffers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Uses <c>Graphics.Blit</c>, which binds <see cref="Source"/> as the material's <c>_MainTex</c> and changes
|
|
/// <c>RenderTexture.active</c>.
|
|
/// Reference: https://docs.unity3d.com/ScriptReference/Graphics.Blit.html
|
|
/// </remarks>
|
|
/// <param name="material">The material containing the shader to apply, or <c>null</c> for a standard copy.</param>
|
|
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
|
public void Blit(Material? material = null, int pass = -1)
|
|
{
|
|
if (material != null)
|
|
{
|
|
if (pass >= 0)
|
|
Graphics.Blit(_currentSource, _currentDestination, material, pass);
|
|
else
|
|
Graphics.Blit(_currentSource, _currentDestination, material);
|
|
}
|
|
else
|
|
{
|
|
Graphics.Blit(_currentSource, _currentDestination);
|
|
}
|
|
|
|
Swap();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Blits from a custom source <see cref="Texture"/> to <see cref="Destination"/> using the specified <see cref="Material"/> and automatically swaps ping-pong buffers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The current <see cref="Source"/> is not read; the result replaces the frame. <paramref name="customSource"/> is
|
|
/// stretched to the size of <see cref="Destination"/>.
|
|
/// </remarks>
|
|
/// <param name="customSource">The custom source texture to blit from.</param>
|
|
/// <param name="material">The material containing the shader to apply, or <c>null</c> for a standard copy.</param>
|
|
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="customSource"/> is null.</exception>
|
|
public void Blit(Texture customSource, Material? material = null, int pass = -1)
|
|
{
|
|
if (customSource == null) throw new ArgumentNullException(nameof(customSource));
|
|
|
|
if (material != null)
|
|
{
|
|
if (pass >= 0)
|
|
Graphics.Blit(customSource, _currentDestination, material, pass);
|
|
else
|
|
Graphics.Blit(customSource, _currentDestination, material);
|
|
}
|
|
else
|
|
{
|
|
Graphics.Blit(customSource, _currentDestination);
|
|
}
|
|
|
|
Swap();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Copies the contents of <see cref="Source"/> into <see cref="Destination"/> without swapping buffers.
|
|
/// Call this before drawing custom overlays into <see cref="Destination"/> if you wish to preserve the background.
|
|
/// Does not count as a modification; call <see cref="Swap"/> after drawing to make the result the current frame.
|
|
/// </summary>
|
|
public void Copy()
|
|
{
|
|
Graphics.Blit(_currentSource, _currentDestination);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears <see cref="Destination"/> with the specified color (color only; no depth buffer is cleared).
|
|
/// Does not swap; the previous <c>RenderTexture.active</c> is restored.
|
|
/// </summary>
|
|
/// <param name="clearColor">The color to clear with, or <c>null</c> for <see cref="Color.clear"/>.</param>
|
|
public void ClearDestination(Color? clearColor = null)
|
|
{
|
|
var prev = RenderTexture.active;
|
|
try
|
|
{
|
|
RenderTexture.active = _currentDestination;
|
|
GL.Clear(false, true, clearColor ?? Color.clear);
|
|
}
|
|
finally
|
|
{
|
|
RenderTexture.active = prev;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Temporarily activates <see cref="Destination"/> as <see cref="RenderTexture.active"/> to perform custom draw calls,
|
|
/// safely restoring the previous active RenderTexture afterwards.
|
|
/// </summary>
|
|
/// <param name="drawAction">The action that performs custom draw operations.</param>
|
|
/// <param name="autoSwap">If <c>true</c> (default), automatically calls <see cref="Swap"/> after drawing.</param>
|
|
/// <param name="preserveBackground">If <c>true</c>, copies <see cref="Source"/> into <see cref="Destination"/> before executing <paramref name="drawAction"/>.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="drawAction"/> is null.</exception>
|
|
public void DrawToDestination(Action drawAction, bool autoSwap = true, bool preserveBackground = false)
|
|
{
|
|
if (drawAction == null) throw new ArgumentNullException(nameof(drawAction));
|
|
|
|
if (preserveBackground)
|
|
{
|
|
Copy();
|
|
}
|
|
|
|
var prev = RenderTexture.active;
|
|
try
|
|
{
|
|
RenderTexture.active = _currentDestination;
|
|
drawAction();
|
|
}
|
|
finally
|
|
{
|
|
RenderTexture.active = prev;
|
|
}
|
|
|
|
if (autoSwap)
|
|
{
|
|
Swap();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes a <see cref="CommandBuffer"/> against the GPU and optionally swaps buffers.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The context does not set a render target for the command buffer: the commands must render into
|
|
/// <see cref="Destination"/> themselves (for example with <c>SetRenderTarget</c> or <c>Blit(Source, Destination)</c>),
|
|
/// so build or update the buffer inside the hook with the current <see cref="Source"/> and <see cref="Destination"/>.
|
|
/// </remarks>
|
|
/// <param name="commandBuffer">The command buffer to execute.</param>
|
|
/// <param name="autoSwap">If <c>true</c> (default), automatically calls <see cref="Swap"/> after execution.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="commandBuffer"/> is null.</exception>
|
|
public void ExecuteCommandBuffer(CommandBuffer commandBuffer, bool autoSwap = true)
|
|
{
|
|
if (commandBuffer == null) throw new ArgumentNullException(nameof(commandBuffer));
|
|
Graphics.ExecuteCommandBuffer(commandBuffer);
|
|
if (autoSwap)
|
|
{
|
|
Swap();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Swaps the active <see cref="Source"/> and <see cref="Destination"/> ping-pong buffers.
|
|
/// Call this after manually writing or rendering into <see cref="Destination"/> so that subsequent hooks or the video encoder read the updated frame.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// After the call, <see cref="Source"/> is the buffer that was <see cref="Destination"/>, and <see cref="Destination"/>
|
|
/// is the other ping-pong buffer (its content is left over from an earlier pass or frame; the next pass overwrites it).
|
|
/// A texture set by <see cref="ReplaceCurrent"/> is dropped by a swap.
|
|
/// </remarks>
|
|
public void Swap()
|
|
{
|
|
_hasModified = true;
|
|
PassCount++;
|
|
|
|
var previousDestination = _currentDestination;
|
|
_currentSource = previousDestination;
|
|
_currentDestination = (previousDestination == _pingRT) ? _pongRT : _pingRT;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Replaces the current modified frame texture with a custom <see cref="RenderTexture"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The texture becomes <see cref="Source"/> / <see cref="Current"/>; <see cref="Destination"/> is unchanged.
|
|
/// The texture is read back for encoding when it is still current after the last hook, so it must stay alive and
|
|
/// unchanged until the end of the hook chain, and it should have the frame's width, height and a format the capture
|
|
/// can read: the capture sizes its buffers from the texture, and the encoder expects the render resolution.
|
|
/// </remarks>
|
|
/// <param name="customTexture">The custom texture to set as the active frame.</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when <paramref name="customTexture"/> is null.</exception>
|
|
public void ReplaceCurrent(RenderTexture customTexture)
|
|
{
|
|
_currentSource = customTexture ?? throw new ArgumentNullException(nameof(customTexture));
|
|
_hasModified = true;
|
|
PassCount++;
|
|
}
|
|
}
|