SHA256
303 lines
11 KiB
C#
303 lines
11 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>
|
|
/// </remarks>
|
|
[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;
|
|
|
|
/// <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.
|
|
/// </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 encoded.
|
|
/// </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.
|
|
/// </summary>
|
|
public long RenderFrameIndex { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The timestamp of the current frame in seconds relative to video start.
|
|
/// </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.
|
|
/// </summary>
|
|
public int Width => _currentSource.width;
|
|
|
|
/// <summary>
|
|
/// The height of the frame in pixels.
|
|
/// </summary>
|
|
public int Height => _currentSource.height;
|
|
|
|
/// <summary>
|
|
/// The configured render frames per second.
|
|
/// </summary>
|
|
public double RenderFPS { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The output video frames per second.
|
|
/// </summary>
|
|
public double VideoFPS { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The number of successful modification passes completed for the current frame.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Blits from <see cref="Source"/> to <see cref="Destination"/> using the specified <see cref="Material"/> and automatically swaps ping-pong buffers.
|
|
/// </summary>
|
|
/// <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>
|
|
/// <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.
|
|
/// </summary>
|
|
public void Copy()
|
|
{
|
|
Graphics.Blit(_currentSource, _currentDestination);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears <see cref="Destination"/> with the specified color.
|
|
/// </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>
|
|
/// <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>
|
|
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>
|
|
/// <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++;
|
|
}
|
|
}
|