SHA256
534 lines
20 KiB
C#
534 lines
20 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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>
|
|
/// Specifies the types of patches that can be configured for a render session.
|
|
/// </summary>
|
|
public enum PatchType
|
|
{
|
|
/// <summary>
|
|
/// Time-related patch controlling frame timing and clock synchronization.
|
|
/// </summary>
|
|
Time,
|
|
|
|
/// <summary>
|
|
/// Screen-related patch controlling screen resolution.
|
|
/// </summary>
|
|
Screen
|
|
}
|
|
|
|
private bool _timePatch = true;
|
|
private bool _screenPatch = true;
|
|
private string? _outputPath;
|
|
|
|
private bool _notification = true;
|
|
|
|
private Func<bool>? _endCondition;
|
|
private Action<RenderResult>? _onComplete;
|
|
private Action? _onLastUpdate;
|
|
|
|
/// <summary>
|
|
/// Sets whether auto mode is enabled.
|
|
/// </summary>
|
|
/// <param name="enabled"><c>true</c> to enable auto mode; otherwise, <c>false</c>. Default is <c>true</c>.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession WithAuto(bool enabled = true)
|
|
{
|
|
_auto = enabled;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets whether a specific patch type is enabled for the render session.
|
|
/// </summary>
|
|
/// <param name="patchType">The <see cref="PatchType"/> to configure.</param>
|
|
/// <param name="enabled"><c>true</c> to enable the specified patch; otherwise, <c>false</c>. Default is <c>true</c>.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
/// <exception cref="ArgumentOutOfRangeException">Thrown when an unsupported <see cref="PatchType"/> is provided.</exception>
|
|
public RenderSession WithPatch(PatchType patchType, bool enabled = true)
|
|
{
|
|
switch (patchType)
|
|
{
|
|
case PatchType.Time: _timePatch = enabled; break;
|
|
case PatchType.Screen: _screenPatch = enabled; break;
|
|
default: throw new ArgumentOutOfRangeException(nameof(patchType), patchType, null);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the custom condition function that determines when the render session should end.
|
|
/// </summary>
|
|
/// <param name="endCondition">A delegate returning <c>true</c> when the session end condition is met.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession WithEndCondition(Func<bool> endCondition)
|
|
{
|
|
_endCondition = endCondition;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the custom output path for the rendered file.
|
|
/// </summary>
|
|
/// <param name="outputPath">The file path where the output will be saved, or <c>null</c> to use the default capture directory.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession WithOutputPath(string? outputPath)
|
|
{
|
|
_outputPath = outputPath;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets whether desktop notifications should be displayed when rendering completes.
|
|
/// </summary>
|
|
/// <param name="enabled"><c>true</c> to enable desktop notifications; otherwise, <c>false</c>.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession WithNotification(bool enabled)
|
|
{
|
|
_notification = enabled;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a callback action to be invoked when the render session completes or fails.
|
|
/// </summary>
|
|
/// <param name="callback">The callback action that receives the resulting <see cref="RenderResult"/>.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession OnComplete(Action<RenderResult> callback)
|
|
{
|
|
_onComplete += callback;
|
|
return this;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a callback action to be invoked on the last update of each frame in the render session.
|
|
/// </summary>
|
|
/// <param name="callback">The callback action to execute.</param>
|
|
/// <returns>The current <see cref="RenderSession"/> instance for method chaining.</returns>
|
|
public RenderSession OnLastUpdate(Action callback)
|
|
{
|
|
_onLastUpdate += callback;
|
|
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>
|
|
/// <param name="renderConfig">The configuration settings for the render session.</param>
|
|
/// <param name="levelPath">The file path of the level to render.</param>
|
|
/// <exception cref="InvalidOperationException">Thrown when the output directory cannot be determined from the output path.</exception>
|
|
public void Start(RenderConfig renderConfig, string levelPath)
|
|
{
|
|
RenderContext.Auto = _auto;
|
|
RenderContext.TimePatch = _timePatch;
|
|
RenderContext.ScreenPatch = _screenPatch;
|
|
RenderContext.OutputPath = _outputPath;
|
|
RenderContext.Notification = _notification;
|
|
|
|
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;
|
|
// The render uses its own copy of the configuration: values adjusted during the render
|
|
// (e.g. the audio sample rate in SetupPlay) stay in this copy and do not change the caller's object.
|
|
RenderManager.Instance.Configuration = renderConfig.Clone();
|
|
|
|
RenderManager.Instance.stage = RenderStage.Load;
|
|
|
|
if (_outputPath is null) RenderManager.Instance.SetupCaptureDirectory();
|
|
else FileUtil.CreateDirectory(Path.GetDirectoryName(_outputPath) ?? throw new InvalidOperationException());
|
|
|
|
RenderManager.Instance.SetupGameScene();
|
|
}
|
|
} |