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.
///
///
[SuppressMessage("ReSharper", "UnusedType.Global")]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global")]
public class FrameHookContext
{
private RenderTexture _originalSource = null!;
private RenderTexture _pingRT = null!;
private RenderTexture _pongRT = null!;
private RenderTexture _currentSource = null!;
private RenderTexture _currentDestination = null!;
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.
///
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 encoded.
///
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.
///
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.
///
public int Width => _currentSource.width;
///
/// The height of the frame in pixels.
///
public int Height => _currentSource.height;
///
/// The configured render frames per second.
///
public double RenderFPS { get; private set; }
///
/// The output video frames per second.
///
public double VideoFPS { get; private set; }
///
/// The number of successful modification passes completed for the current frame.
///
public int PassCount { get; private set; }
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);
}
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.
///
/// 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 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.
///
public void Copy()
{
Graphics.Blit(_currentSource, _currentDestination);
}
///
/// Clears with the specified color.
///
/// 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 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.
///
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 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++;
}
}