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++; } }