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