using System;
using System.Collections.Generic;
using System.ComponentModel;
using UnityEngine;
namespace UnityEditor.Recorder
{
internal enum ERecordingSessionStage
{
BeginRecording,
NewFrameStarting,
NewFrameReady,
SkipFrame,
FrameDone,
EndRecording,
SessionCreated
}
///
/// Base class for all Recorders. To create a new Recorder, extend .
///
public abstract class Recorder : ScriptableObject
{
static int sm_CaptureFrameRateCount;
bool m_ModifiedCaptureFR;
double m_FrameInterval;
bool m_TimePadDisabled;
float m_SessionStartTime;
double m_FramePadTime = 0; // Time padding seconds to fix JIRA REC-1105
private static bool s_asyncShaderCompileSetting;
private static bool s_asyncShaderCompileAlreadyRestored = false; // have we already restored the value of the setting?
private static bool s_asyncShaderCompileAlreadyDisabled = false; // have we already disabled the setting?
///
/// Indicates the number of frames of the current recording session.
///
protected internal int RecordedFramesCount { get; internal set; }
///
/// The list of inputs to the Recorder, representing the sources of the captured data.
///
protected List m_Inputs;
void Awake()
{
sm_CaptureFrameRateCount = 0;
}
protected internal virtual void Reset()
{
RecordedFramesCount = 0;
Recording = false;
}
void OnDestroy()
{
if (m_ModifiedCaptureFR)
{
sm_CaptureFrameRateCount--;
if (sm_CaptureFrameRateCount == 0)
{
Time.captureFramerate = 0;
if (RecorderOptions.VerboseMode)
Debug.Log("Recorder resetting 'CaptureFrameRate' to zero");
}
}
}
internal abstract RecorderSettings settings { get; set; }
protected internal void ConsoleLogMessage(string message, LogType logType)
{
string sAborted = "";
switch (logType)
{
case LogType.Warning:
sAborted = "Recording may cause slowdowns or generate an invalid file. ";
break;
case LogType.Error:
sAborted = "Recording failed. ";
break;
default:
break;
}
string logMessage = $"[{GetType().Name}: {settings.name}] {sAborted}{message}";
switch (logType)
{
case LogType.Log:
Debug.Log(logMessage);
break;
case LogType.Warning:
Debug.LogWarning(logMessage);
break;
case LogType.Error:
Debug.LogError(logMessage);
break;
default:
throw new InvalidEnumArgumentException($"Log type {logType} is not supported yet.");
}
}
protected internal virtual void SessionCreated(RecordingSession session)
{
if (RecorderOptions.VerboseMode)
ConsoleLogMessage("Session created", LogType.Log);
var fixedRate = settings.FrameRatePlayback == FrameRatePlayback.Constant ? settings.FrameRate : 0.0f;
if (fixedRate > 0)
{
var toComparePeriodSeconds = 1.0f / fixedRate; // Hz -> secs
#if RECORDER_DISABLE_FRAME_TIME_PADDING
m_FramePadTime = 0;
#else
m_FramePadTime = 1e-7; // Need to add some time to the Time.captureDeltaTime to prevent double frame capture (REC-1105)
#endif
if (Time.captureFramerate != 0 && Math.Abs(toComparePeriodSeconds - Time.captureDeltaTime) > (float.Epsilon + m_FramePadTime))
ConsoleLogMessage($"Recorder {GetType().Name} is set to record at a fixed rate and another component has already set a conflicting value for [Time.captureFramerate], new value being applied : {fixedRate}!", LogType.Error);
else if (Time.captureFramerate == 0 && RecorderOptions.VerboseMode)
ConsoleLogMessage($"Frame recorder set fixed frame rate to {fixedRate}", LogType.Log);
// Note that Time.captureDeltaTime will be modified by HDRP SubFrameManager
// to implement the accumulation motion blur/path tracer support.
m_SessionStartTime = Time.time;
m_FrameInterval = (1.0d / (double)fixedRate); // Store the expected frame interval
#if RECORDER_DISABLE_FRAME_TIME_PADDING
Time.captureDeltaTime = (float)(m_FrameInterval); // Do not use the "time padding" workaround (REC-1105)
#else
Time.captureDeltaTime = (float)(m_FrameInterval + m_FramePadTime); // Need to add some time to the Time.captureDeltaTime to prevent double frame capture (REC-1105)
#endif
sm_CaptureFrameRateCount++;
m_ModifiedCaptureFR = true;
}
m_Inputs = new List();
foreach (var inputSettings in settings.InputsSettings)
{
var input = (RecorderInput)Activator.CreateInstance(inputSettings.InputType);
input.settings = inputSettings;
m_Inputs.Add(input);
SignalInputsOfStage(ERecordingSessionStage.SessionCreated, session);
}
}
///
/// Starts a new recording session. Callback is invoked once when the recording session starts.
///
/// The newly created recording session.
/// True if recording can start, False otherwise.
/// Throws if there is already a recording session running.
protected internal virtual bool BeginRecording(RecordingSession session)
{
if (Recording)
throw new Exception("Already recording!");
// Log old warnings (non-blocking)
var oldWarnings = new List();
#pragma warning disable 618
if (!session.settings.ValidityCheck(oldWarnings))
#pragma warning restore 618
{
foreach (var w in oldWarnings)
ConsoleLogMessage(w, LogType.Warning);
}
// Log non-blocking warnings
var warnings = new List();
session.settings.GetWarnings(warnings);
foreach (var w in warnings)
ConsoleLogMessage(w, LogType.Warning);
// Log blocking errors and stop
var errors = new List();
session.settings.GetErrors(errors);
foreach (var w in errors)
ConsoleLogMessage(w, LogType.Error);
if (errors.Count > 0)
{
Recording = false;
return false;
}
if (RecorderOptions.VerboseMode)
ConsoleLogMessage($"Starting to record", LogType.Log);
DisableAsyncShaderCompil();
return Recording = true;
}
///
/// Ends the current recording session. Callback is invoked when the recording session ends.
///
/// The current recording session.
protected internal virtual void EndRecording(RecordingSession session)
{
if (!Recording)
return;
Recording = false;
if (m_ModifiedCaptureFR)
{
m_ModifiedCaptureFR = false;
sm_CaptureFrameRateCount--;
if (sm_CaptureFrameRateCount == 0)
{
Time.captureFramerate = 0;
if (RecorderOptions.VerboseMode)
ConsoleLogMessage("Recorder resetting 'CaptureFrameRate' to zero", LogType.Log);
}
}
foreach (var input in m_Inputs)
{
if (input != null)
input.Dispose();
}
if (RecorderOptions.VerboseMode)
ConsoleLogMessage($"Recording stopped, total frame count: {RecordedFramesCount}", LogType.Log);
RestoreAsynchronousShaderCompilation();
++settings.Take;
// When adding a file to Unity's assets directory, trigger a refresh so it is detected.
if (settings.fileNameGenerator.Root == OutputPath.Root.AssetsFolder || settings.fileNameGenerator.Root == OutputPath.Root.StreamingAssets)
AssetDatabase.Refresh();
}
///
/// Records a single frame. Callback is invoked for every frame during the recording session.
///
/// The current recording session.
protected internal abstract void RecordFrame(RecordingSession ctx);
///
/// Prepares a frame before recording it. Callback is invoked for every frame during the recording session, before RecordFrame.
///
/// The current recording session.
protected internal virtual void PrepareNewFrame(RecordingSession ctx)
{
#if !RECORDER_DISABLE_FRAME_TIME_PADDING
ResetDeltaTime();
#endif
}
///
/// To prevent capturing frames too early (resulting in double frames captured), due to
/// Time.captureDeltaTime rounding errors, Time.captureDeltaTime must be padded a bit
/// to ensure the sample is taken on the right side of the frame time boundary. This only
/// needs to be done on the first 2 frames it seems, after that reset Time.captureDeltaTime
/// back to the frame time interval. This addresses Jira issue REC-1105.
/// Use define symbol RECORDER_DISABLE_FRAME_TIME_PADDING to disable this.
///
internal void ResetDeltaTime()
{
if (m_TimePadDisabled && Time.captureDeltaTime >= (float)m_FrameInterval)
{
// Time Padding is disabled
Time.captureDeltaTime = (float)m_FrameInterval; // Set captureDeltaTime back to the frame interval without any padding since this workaround is only needed for the first few frames
m_FramePadTime = 0;
}
else
{
// Time Padding is enabled
m_TimePadDisabled = (Time.time - m_SessionStartTime > 0f) ? true : false; // Disable padding this after we get past time 0
}
}
internal virtual void RecordSubFrame(RecordingSession ctx)
{
}
///
/// Tests if a frame should be skipped before trying to record it. Callback is invoked for every frame during the recording session.
///
///
/// If this function returns True, RecordFrame will not be invoked.
///
/// The current recording session.
/// True if the frame should be skipped, False otherwise.
protected internal virtual bool SkipFrame(RecordingSession ctx)
{
// Compute the starting frame of the recording
int startFrame = settings.RecordMode == RecordMode.TimeInterval ? (int)(settings.StartTime * settings.FrameRate) : settings.StartFrame;
var skip_vfr = ctx.frameIndex % settings.captureEveryNthFrame != 0 &&
ctx.settings.FrameRatePlayback == FrameRatePlayback.Variable;
var skip_time = settings.RecordMode == RecordMode.TimeInterval && ctx.frameIndex < startFrame;
var skip_frame = settings.RecordMode == RecordMode.FrameInterval && ctx.frameIndex < startFrame;
var skip_single = settings.RecordMode == RecordMode.SingleFrame && ctx.frameIndex < startFrame;
var result = !Recording
|| skip_vfr
|| skip_time
|| skip_frame
|| skip_single;
return result;
}
///
/// Tests if a sub frame should be skipped before trying to record it. Callback is invoked for every frame during the recording session.
///
///
/// If this function returns True, RecordFrame will not be invoked.
///
/// The current recording session.
/// True if the sub frame should be skipped, False otherwise.
protected internal virtual bool SkipSubFrame(RecordingSession ctx)
{
if (!settings.IsAccumulationSupported())
return false;
IAccumulation accumulation = settings as IAccumulation;
AccumulationSettings accumulationSettings = null;
if (accumulation != null)
{
accumulationSettings = accumulation.GetAccumulationSettings();
}
int accumulationSamples = 1;
if (accumulationSettings != null && accumulationSettings.CaptureAccumulation)
{
accumulationSamples = accumulationSettings.Samples;
}
bool skip = (ctx.subFrameIndex) % accumulationSamples != 0;
return skip;
}
///
/// Tests if there is a recording session currently running.
///
/// True if a recording session is currently active, False otherwise.
public bool Recording { get; protected set; }
internal void SignalInputsOfStage(ERecordingSessionStage stage, RecordingSession session)
{
if (m_Inputs == null)
return;
switch (stage)
{
case ERecordingSessionStage.SessionCreated:
foreach (var input in m_Inputs)
input.SessionCreated(session);
break;
case ERecordingSessionStage.BeginRecording:
foreach (var input in m_Inputs)
input.BeginRecording(session);
break;
case ERecordingSessionStage.NewFrameStarting:
foreach (var input in m_Inputs)
input.NewFrameStarting(session);
break;
case ERecordingSessionStage.NewFrameReady:
foreach (var input in m_Inputs)
input.NewFrameReady(session);
break;
case ERecordingSessionStage.SkipFrame:
foreach (var input in m_Inputs)
input.SkipFrame(session);
break;
case ERecordingSessionStage.FrameDone:
foreach (var input in m_Inputs)
input.FrameDone(session);
break;
case ERecordingSessionStage.EndRecording:
foreach (var input in m_Inputs)
input.EndRecording(session);
break;
default:
throw new ArgumentOutOfRangeException("stage", stage, null);
}
}
///
/// Disable asynchronous shader compilation and save the status.
///
private static void DisableAsyncShaderCompil()
{
if (s_asyncShaderCompileAlreadyDisabled)
return;
// Save the async compile shader setting to restore it at the end of recording
s_asyncShaderCompileSetting = EditorSettings.asyncShaderCompilation;
// Disable async compile shader setting when recording
EditorSettings.asyncShaderCompilation = false;
s_asyncShaderCompileAlreadyRestored = false;
s_asyncShaderCompileAlreadyDisabled = true;
}
///
/// If we have not already restored the setting of asynchronous shader compilation, restore it.
///
private static void RestoreAsynchronousShaderCompilation()
{
if (s_asyncShaderCompileAlreadyRestored || !s_asyncShaderCompileAlreadyDisabled)
return;
EditorSettings.asyncShaderCompilation = s_asyncShaderCompileSetting;
s_asyncShaderCompileAlreadyRestored = true;
s_asyncShaderCompileAlreadyDisabled = false;
}
///
/// If a recording fails, clean up after it.
///
internal void CleanupFailedRecording()
{
RestoreAsynchronousShaderCompilation();
}
}
}