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;
///
/// Fluent builder for configuring and starting a render session.
///
[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 Action { get; }
private bool _isDisposed;
public GlobalHookRegistration(object key, Action 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 Action { get; }
public SessionHookEntry(object key, Action action)
{
Key = key;
Action = action;
}
}
private static readonly List GlobalFrameHooks = new();
private static readonly object GlobalHooksLock = new();
private static int _globalHooksVersion;
private readonly List _frameHooks = new();
private bool _auto = true;
///
/// Specifies the types of patches that can be configured for a render session.
///
public enum PatchType
{
///
/// Time-related patch controlling frame timing and clock synchronization.
///
Time,
///
/// Screen-related patch controlling screen resolution.
///
Screen
}
private bool _timePatch = true;
private bool _screenPatch = true;
private string? _outputPath;
private bool _notification = true;
private Func? _endCondition;
private Action? _onComplete;
private Action? _onLastUpdate;
///
/// Sets whether auto mode is enabled.
///
/// true to enable auto mode; otherwise, false. Default is true.
/// The current instance for method chaining.
public RenderSession WithAuto(bool enabled = true)
{
_auto = enabled;
return this;
}
///
/// Sets whether a specific patch type is enabled for the render session.
///
/// The to configure.
/// true to enable the specified patch; otherwise, false. Default is true.
/// The current instance for method chaining.
/// Thrown when an unsupported is provided.
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;
}
///
/// Sets the custom condition function that determines when the render session should end.
///
/// A delegate returning true when the session end condition is met.
/// The current instance for method chaining.
public RenderSession WithEndCondition(Func endCondition)
{
_endCondition = endCondition;
return this;
}
///
/// Sets the custom output path for the rendered file.
///
/// The file path where the output will be saved, or null to use the default capture directory.
/// The current instance for method chaining.
public RenderSession WithOutputPath(string? outputPath)
{
_outputPath = outputPath;
return this;
}
///
/// Sets whether desktop notifications should be displayed when rendering completes.
///
/// true to enable desktop notifications; otherwise, false.
/// The current instance for method chaining.
public RenderSession WithNotification(bool enabled)
{
_notification = enabled;
return this;
}
///
/// Adds a callback action to be invoked when the render session completes or fails.
///
/// The callback action that receives the resulting .
/// The current instance for method chaining.
public RenderSession OnComplete(Action callback)
{
_onComplete += callback;
return this;
}
///
/// Adds a callback action to be invoked on the last update of each frame in the render session.
///
/// The callback action to execute.
/// The current instance for method chaining.
public RenderSession OnLastUpdate(Action callback)
{
_onLastUpdate += callback;
return this;
}
///
/// 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.
///
///
/// Execution Frequency Note: This hook is invoked only for frames that are captured for the output video.
/// When RenderFPS is higher than VideoFPS, intermediate game render frames are skipped,
/// so this hook will not be called on every game frame.
///
/// The callback action that receives a .
/// The current instance for method chaining.
/// Thrown when is null.
public RenderSession AddFrameHook(Action hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
_frameHooks.Add(new SessionHookEntry(hook, hook));
return this;
}
///
/// 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.
///
/// The material containing the shader to apply.
/// The shader pass index to use, or -1 to execute all passes.
/// The current instance for method chaining.
/// Thrown when is null.
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;
}
///
/// Adds a custom frame modification hook implementing .
///
/// The hook instance to execute.
/// The current instance for method chaining.
/// Thrown when is null.
public RenderSession AddFrameHook(IFrameHook hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
_frameHooks.Add(new SessionHookEntry(hook, hook.OnRenderFrame));
return this;
}
///
/// Alias for .
///
/// The callback action that receives a .
/// The current instance for method chaining.
public RenderSession WithFrameHook(Action hook) => AddFrameHook(hook);
///
/// Alias for .
///
/// The material containing the shader to apply.
/// The shader pass index to use, or -1 to execute all passes.
/// The current instance for method chaining.
public RenderSession WithFrameHook(Material material, int pass = -1) => AddFrameHook(material, pass);
///
/// Alias for .
///
/// The hook instance to execute.
/// The current instance for method chaining.
public RenderSession WithFrameHook(IFrameHook hook) => AddFrameHook(hook);
///
/// Removes a previously added frame modification hook callback from this session.
///
/// The hook action to remove.
/// The current instance for method chaining.
public RenderSession RemoveFrameHook(Action hook)
{
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook) || Equals(e.Action, hook));
if (idx >= 0) _frameHooks.RemoveAt(idx);
return this;
}
///
/// Removes a previously added material-based post-processing pass from this session.
///
/// The material to remove.
/// The current instance for method chaining.
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;
}
///
/// Removes a previously added from this session.
///
/// The hook instance to remove.
/// The current instance for method chaining.
public RenderSession RemoveFrameHook(IFrameHook hook)
{
var idx = _frameHooks.FindIndex(e => ReferenceEquals(e.Key, hook));
if (idx >= 0) _frameHooks.RemoveAt(idx);
return this;
}
///
/// Clears all frame modification hooks configured for this render session.
///
/// The current instance for method chaining.
public RenderSession ClearFrameHooks()
{
_frameHooks.Clear();
return this;
}
///
/// Registers a global frame modification hook that will execute on every render session.
///
///
/// Execution Frequency Note: Global frame hooks are executed only for frames that are captured for the output video.
/// When RenderFPS is higher than VideoFPS, intermediate game render frames are skipped,
/// so this hook will not be called on every game frame.
///
/// The callback action that receives a .
/// An registration handle that unregisters the hook when disposed.
/// Thrown when is null.
public static IDisposable AddGlobalFrameHook(Action hook)
{
if (hook == null) throw new ArgumentNullException(nameof(hook));
var reg = new GlobalHookRegistration(hook, hook);
lock (GlobalHooksLock)
{
GlobalFrameHooks.Add(reg);
_globalHooksVersion++;
}
return reg;
}
///
/// Registers a global material-based post-processing pass that will execute on every render session.
///
/// The material containing the shader to apply.
/// The shader pass index to use, or -1 to execute all passes.
/// An registration handle that unregisters the hook when disposed.
/// Thrown when is null.
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;
}
///
/// Registers a global frame modification hook implementing .
///
/// The hook instance to execute.
/// An registration handle that unregisters the hook when disposed.
/// Thrown when is null.
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;
}
///
/// Unregisters a previously registered global frame modification hook.
///
/// The hook action to remove.
/// true if the hook was successfully removed; otherwise, false.
public static bool RemoveGlobalFrameHook(Action 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;
}
}
///
/// Unregisters a previously registered global material-based post-processing pass.
///
/// The material to remove.
/// true if any hooks matching were removed; otherwise, false.
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;
}
}
///
/// Unregisters a previously registered global frame modification hook implementing .
///
/// The hook instance to remove.
/// true if the hook was successfully removed; otherwise, false.
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++;
}
}
}
///
/// Clears all registered global frame modification hooks.
///
public static void ClearGlobalFrameHooks()
{
lock (GlobalHooksLock)
{
if (GlobalFrameHooks.Count > 0)
{
GlobalFrameHooks.Clear();
_globalHooksVersion++;
}
}
}
///
/// Indicates whether any global frame modification hooks are currently registered.
///
public static bool HasGlobalFrameHooks
{
get
{
lock (GlobalHooksLock)
{
return GlobalFrameHooks.Count > 0;
}
}
}
///
/// The number of currently registered global frame modification hooks.
///
public static int GlobalFrameHookCount
{
get
{
lock (GlobalHooksLock)
{
return GlobalFrameHooks.Count;
}
}
}
internal static int GlobalHooksVersion
{
get
{
lock (GlobalHooksLock)
{
return _globalHooksVersion;
}
}
}
internal static void CopyGlobalFrameHooksTo(List> destination)
{
lock (GlobalHooksLock)
{
for (var i = 0; i < GlobalFrameHooks.Count; i++)
{
destination.Add(GlobalFrameHooks[i].Action);
}
}
}
///
/// Applies the configured flags and initiates the render session.
///
/// The configuration settings for the render session.
/// The file path of the level to render.
/// Thrown when the output directory cannot be determined from the output path.
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();
}
}