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