SHA256
Add Custom Frame Modification Hook API for Video Rendering
This commit is contained in:
5 files changed
+961
-1
No files matched your search
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using ADOFAIRenderer.Core;
|
||||
using ADOFAIRenderer.Data;
|
||||
using ADOFAIRenderer.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace ADOFAIRenderer.API;
|
||||
|
||||
@@ -11,8 +13,47 @@ namespace ADOFAIRenderer.API;
|
||||
/// Fluent builder for configuring and starting a render session.
|
||||
/// </summary>
|
||||
[SuppressMessage("ReSharper", "UnusedType.Global")]
|
||||
[SuppressMessage("ReSharper", "UnusedMember.Global")]
|
||||
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
|
||||
public class RenderSession
|
||||
{
|
||||
private sealed class GlobalHookRegistration : IDisposable
|
||||
{
|
||||
public object Key { get; }
|
||||
public Action<FrameHookContext> Action { get; }
|
||||
private bool _isDisposed;
|
||||
|
||||
public GlobalHookRegistration(object key, Action<FrameHookContext> action)
|
||||
{
|
||||
Key = key;
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
_isDisposed = true;
|
||||
RemoveGlobalRegistration(this);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SessionHookEntry
|
||||
{
|
||||
public object Key { get; }
|
||||
public Action<FrameHookContext> Action { get; }
|
||||
|
||||
public SessionHookEntry(object key, Action<FrameHookContext> action)
|
||||
{
|
||||
Key = key;
|
||||
Action = action;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly List<GlobalHookRegistration> GlobalFrameHooks = new();
|
||||
private static readonly object GlobalHooksLock = new();
|
||||
private static int _globalHooksVersion;
|
||||
|
||||
private readonly List<SessionHookEntry> _frameHooks = new();
|
||||
private bool _auto = true;
|
||||
|
||||
/// <summary>
|
||||
@@ -126,6 +167,331 @@ public class RenderSession
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom frame modification hook to be executed for each encoded video frame.
|
||||
/// The hook can modify the rendered frame using GPU ping-pong double buffering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Execution Frequency Note:</b> This hook is invoked only for frames that are captured for the output video.
|
||||
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
|
||||
/// so this hook will <b>not</b> be called on every game frame.
|
||||
/// </remarks>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
_frameHooks.Add(new SessionHookEntry(hook, hook));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a material-based post-processing pass to be executed for each encoded video frame.
|
||||
/// Automatically performs a GPU blit from source to destination and swaps buffers.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(Material material, int pass = -1)
|
||||
{
|
||||
if (material is null) throw new ArgumentNullException(nameof(material));
|
||||
_frameHooks.Add(new SessionHookEntry(material, context => context.Blit(material, pass)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a custom frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public RenderSession AddFrameHook(IFrameHook hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
_frameHooks.Add(new SessionHookEntry(hook, hook.OnRenderFrame));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(Action{FrameHookContext})"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(Action<FrameHookContext> hook) => AddFrameHook(hook);
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(Material, int)"/>.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(Material material, int pass = -1) => AddFrameHook(material, pass);
|
||||
|
||||
/// <summary>
|
||||
/// Alias for <see cref="AddFrameHook(IFrameHook)"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession WithFrameHook(IFrameHook hook) => AddFrameHook(hook);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added frame modification hook callback from this session.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook action to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook) || Equals(e.Action, hook));
|
||||
if (idx >= 0) _frameHooks.RemoveAt(idx);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added material-based post-processing pass from this session.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(Material material)
|
||||
{
|
||||
if (material)
|
||||
{
|
||||
for (var i = _frameHooks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (ReferenceEquals(_frameHooks[i].Key, material))
|
||||
{
|
||||
_frameHooks.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously added <see cref="IFrameHook"/> from this session.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to remove.</param>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession RemoveFrameHook(IFrameHook hook)
|
||||
{
|
||||
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook));
|
||||
if (idx >= 0) _frameHooks.RemoveAt(idx);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all frame modification hooks configured for this render session.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
||||
public RenderSession ClearFrameHooks()
|
||||
{
|
||||
_frameHooks.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global frame modification hook that will execute on every render session.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Execution Frequency Note:</b> Global frame hooks are executed only for frames that are captured for the output video.
|
||||
/// When <c>RenderFPS</c> is higher than <c>VideoFPS</c>, intermediate game render frames are skipped,
|
||||
/// so this hook will <b>not</b> be called on every game frame.
|
||||
/// </remarks>
|
||||
/// <param name="hook">The callback action that receives a <see cref="FrameHookContext"/>.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
var reg = new GlobalHookRegistration(hook, hook);
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global material-based post-processing pass that will execute on every render session.
|
||||
/// </summary>
|
||||
/// <param name="material">The material containing the shader to apply.</param>
|
||||
/// <param name="pass">The shader pass index to use, or -1 to execute all passes.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="material"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(Material material, int pass = -1)
|
||||
{
|
||||
if (material is null) throw new ArgumentNullException(nameof(material));
|
||||
var reg = new GlobalHookRegistration(material, context => context.Blit(material, pass));
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a global frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to execute.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> registration handle that unregisters the hook when disposed.</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="hook"/> is null.</exception>
|
||||
public static IDisposable AddGlobalFrameHook(IFrameHook hook)
|
||||
{
|
||||
if (hook == null) throw new ArgumentNullException(nameof(hook));
|
||||
var reg = new GlobalHookRegistration(hook, hook.OnRenderFrame);
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
GlobalFrameHooks.Add(reg);
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return reg;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global frame modification hook.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook action to remove.</param>
|
||||
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(Action<FrameHookContext> hook)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook) || Equals(r.Action, hook));
|
||||
if (idx >= 0)
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(idx);
|
||||
_globalHooksVersion++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global material-based post-processing pass.
|
||||
/// </summary>
|
||||
/// <param name="material">The material to remove.</param>
|
||||
/// <returns><c>true</c> if any hooks matching <paramref name="material"/> were removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(Material material)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var removed = false;
|
||||
for (var i = GlobalFrameHooks.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (ReferenceEquals(GlobalFrameHooks[i].Key, material))
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
if (removed)
|
||||
{
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a previously registered global frame modification hook implementing <see cref="IFrameHook"/>.
|
||||
/// </summary>
|
||||
/// <param name="hook">The hook instance to remove.</param>
|
||||
/// <returns><c>true</c> if the hook was successfully removed; otherwise, <c>false</c>.</returns>
|
||||
public static bool RemoveGlobalFrameHook(IFrameHook hook)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
var idx = GlobalFrameHooks.FindIndex(r => ReferenceEquals(r.Key, hook));
|
||||
if (idx >= 0)
|
||||
{
|
||||
GlobalFrameHooks.RemoveAt(idx);
|
||||
_globalHooksVersion++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void RemoveGlobalRegistration(GlobalHookRegistration registration)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
if (GlobalFrameHooks.Remove(registration))
|
||||
{
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all registered global frame modification hooks.
|
||||
/// </summary>
|
||||
public static void ClearGlobalFrameHooks()
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
if (GlobalFrameHooks.Count > 0)
|
||||
{
|
||||
GlobalFrameHooks.Clear();
|
||||
_globalHooksVersion++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether any global frame modification hooks are currently registered.
|
||||
/// </summary>
|
||||
public static bool HasGlobalFrameHooks
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return GlobalFrameHooks.Count > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of currently registered global frame modification hooks.
|
||||
/// </summary>
|
||||
public static int GlobalFrameHookCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return GlobalFrameHooks.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static int GlobalHooksVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
return _globalHooksVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void CopyGlobalFrameHooksTo(List<Action<FrameHookContext>> destination)
|
||||
{
|
||||
lock (GlobalHooksLock)
|
||||
{
|
||||
for (var i = 0; i < GlobalFrameHooks.Count; i++)
|
||||
{
|
||||
destination.Add(GlobalFrameHooks[i].Action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the configured flags and initiates the render session.
|
||||
/// </summary>
|
||||
@@ -143,6 +509,15 @@ public class RenderSession
|
||||
if (_endCondition is not null) RenderContext.EndCondition = _endCondition;
|
||||
if (_onComplete is not null) RenderContext.OnComplete = _onComplete;
|
||||
if (_onLastUpdate is not null) RenderContext.OnLastUpdate = _onLastUpdate;
|
||||
|
||||
RenderContext.SessionFrameHooks.Clear();
|
||||
if (_frameHooks.Count > 0)
|
||||
{
|
||||
foreach (var t in _frameHooks)
|
||||
{
|
||||
RenderContext.SessionFrameHooks.Add(t.Action);
|
||||
}
|
||||
}
|
||||
|
||||
RenderManager.Instance.levelPath = levelPath;
|
||||
RenderManager.Instance.Configuration = renderConfig;
|
||||
|
||||
Reference in new issue
Block a user