VR4RoboticArm2/VR4RoboticArm/Library/PackageCache/com.meta.xr.sdk.movement/Runtime/Native/Scripts/Serialization/SequencePlaybackManager.cs
IonutMocanu 48cccc22ad Main2
2025-09-08 11:13:29 +03:00

481 lines
19 KiB
C#

// Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Assertions;
using static Meta.XR.Movement.MSDKUtility;
namespace Meta.XR.Movement.Playback
{
/// <summary>
/// Manages the state of a snapshot file used for playback.
/// </summary>
public class SequencePlaybackManager
{
/// <summary>
/// Number of snapshots available.
/// </summary>
public int NumSnapshots => _startHeader.NumSnapshots;
/// <summary>
/// Start network time.
/// </summary>
public double StartNetworkTime => _startHeader.StartNetworkTime;
/// <summary>
/// Num buffered snapshots used for serialization.
/// </summary>
public int NumBufferedSnapshots => _startHeader.NumBufferedSnapshots;
/// <summary>
/// If has file open for playback.
/// </summary>
public bool HasActivePlaybackFile => (_playbackFile != null);
/// <summary>
/// If read through all snapshots or not.
/// </summary>
public bool ReadAllSnapshots => (_snapshotIndex == _startHeader.NumSnapshots - 1);
/// <summary>
/// Snapshot index currently being read.
/// </summary>
public int SnapshotIndex => _snapshotIndex;
/// <summary>
/// Current playback network time of serialized data.
/// </summary>
public float NetworkTime
{
get => _networkTime;
set => _networkTime = value;
}
/// <summary>
/// Last network time.
/// </summary>
public float LastTimeStamp
{
get => _lastTimestamp;
set => _lastTimestamp = value;
}
/// <summary>
/// Delegate for processing a snapshot.
/// </summary>
/// <param name="snapshotBytes">Snapshot bytes to process.</param>
/// <returns>The timestamp of the snapshot.</returns>
public delegate float ProcessSnapshotGetTimestamp(byte[] snapshotBytes);
/// <summary>
/// Delegate for deserializing a snapshot.
/// </summary>
/// <param name="snapshotBytes">Snapshot bytes.</param>
/// <returns>If the snapshot was deserialized or not.</returns>
public delegate bool DeserializeSnapshot(byte[] snapshotBytes);
/// <summary>
/// Deserialize delegate.
/// </summary>
/// <param name="snapshotBytes">Snapshot bytes.</param>
/// <returns>True if deserialized, false if not.</returns>
public delegate bool Deserialize(byte[] snapshotBytes);
/// <summary>
/// Lerp delegate.
/// </summary>
public delegate void LerpReceivedTrackingData();
/// <summary>
/// Delegate for processing deserialized data.
/// </summary>
public delegate void ProcessReceivedData();
private PlaybackFunctions.ReaderFileStream _playbackFile = null;
private StartHeader _startHeader;
private EndHeader _endHeader;
private int _snapshotIndex = 0, _numSnapshotBytesReadSoFar = 0;
private bool _playedFirstFrame = false;
private int[] _snapshotToByteoffsetAfterHeader;
private float _networkTime = 0.0f;
private float _lastTimestamp = 0.0f;
/// <summary>
/// Restart all timestamps to the start timestamp.
/// </summary>
public void ResetTimestampsToStart()
{
_lastTimestamp = (float)StartNetworkTime;
_networkTime = (float)StartNetworkTime;
}
/// <summary>
/// Opens file for playback.
/// </summary>
/// <param name="handle">Native plugin handle.</param>
/// <param name="playbackPath">Optional playback path.</param>
public void OpenFileForPlayback(ulong handle, string playbackPath = null)
{
double expectedDataVersion;
MSDKUtility.GetVersion(handle, out expectedDataVersion);
_playbackFile = PlaybackFunctions.OpenFileForPlayback(
ref _startHeader,
ref _endHeader,
ref _snapshotToByteoffsetAfterHeader,
expectedDataVersion,
playbackPath);
_snapshotIndex = 0;
_numSnapshotBytesReadSoFar = 0;
_playedFirstFrame = false;
}
/// <summary>
/// Gets byte offset of snapshot (after header) as well as baseline
/// sync index for specified snapshot.
/// </summary>
/// <param name="snapshotIndex">Snapshot index to query.</param>
/// <returns>Byte offset (after) header for snapshot as well
/// as last baseline in terms of n snapshots in past. If 0, that means
/// that current snapshot is the baseline.</returns>
public (int, int) GetByteOffsetAndLastSyncForSnapshotIndex(int snapshotIndex)
{
int destinationSnapOffset = _snapshotToByteoffsetAfterHeader[snapshotIndex];
int snapshotsSinceLastSync = _playbackFile.GetSnapshotsSinceLastSync(
destinationSnapOffset);
return (destinationSnapOffset, snapshotsSinceLastSync);
}
/// <summary>
/// Reads next snapshot bytes. Modifies internal state by moving forward
/// in the plabyack file.
/// </summary>
/// <returns>Snapshot bytes read, if any.</returns>
public byte[] ReadNextSnapshotBytes()
{
if (_playbackFile == null)
{
Debug.LogError("Can't fetch snapshot bytes because no file has been " +
"opened yet.");
return null;
}
// if we haven't played first first, then assume snapshot index is 0
// otherwise, move to the next index
if (!_playedFirstFrame)
{
_snapshotIndex = 0;
}
else
{
_snapshotIndex++;
}
int snapshotsSinceBase = 0;
byte[] snapshotBytes = PlaybackFunctions.ReadSnapshotAtOffset(
_playbackFile,
ref _numSnapshotBytesReadSoFar,
_snapshotIndex,
ref snapshotsSinceBase,
_startHeader.NumSnapshots);
if (snapshotBytes != null)
{
_playedFirstFrame = true;
}
return snapshotBytes;
}
/// <summary>
/// Gets snapshot byte offset after header.
/// </summary>
/// <param name="snapshotIndex">Snapshot index.</param>
/// <returns>Offset of snapshot in terms of bytes after the start header
/// in the playback file.</returns>
public int GetSnapshotByteOffset(int snapshotIndex)
{
if (snapshotIndex >= _snapshotToByteoffsetAfterHeader.Length)
{
Debug.LogError($"Cannot return offset at index {snapshotIndex} because " +
$"the length of {_snapshotToByteoffsetAfterHeader.Length} is too small.");
return 0;
}
return _snapshotToByteoffsetAfterHeader[snapshotIndex];
}
/// <summary>
/// Gets snapshot at specific index.
/// </summary>
/// <param name="byteOffset">Byte offset to seek.</param>
/// <param name="snapshotIndex">Snapshot index.</param>
/// <param name="moveCurrentTrackedSnapshotToIndex">Whether or not to set currently
/// seeked snapshot to offset specified. If true, then functions like
/// <see cref="ReadNextSnapshotBytes"/> from the offset called into this function.</param>
/// <returns>Bytes deserialized, if any.</returns>
public byte[] GetBytesAtSpecificSnapshotIndex(int byteOffset, int snapshotIndex,
bool moveCurrentTrackedSnapshotToIndex = false)
{
var numSnapshotBytesReadSoFar = byteOffset;
int snapshotsSinceBase = 0;
var snapshotBytes = PlaybackFunctions.ReadSnapshotAtOffset(
_playbackFile,
ref numSnapshotBytesReadSoFar,
snapshotIndex,
ref snapshotsSinceBase,
_startHeader.NumSnapshots);
if (moveCurrentTrackedSnapshotToIndex)
{
_numSnapshotBytesReadSoFar = numSnapshotBytesReadSoFar;
_snapshotIndex = snapshotIndex;
}
return snapshotBytes;
}
/// <summary>
/// Processes snapshot bytes and returns network time.
/// </summary>
/// <param name="snapshotBytes">Snapshot bytes to process.</param>
/// <param name="isScrubbingData">If data is being scrubbed or not.</param>
/// <param name="deserializeDelegate">Deserialize delegate.</param>
/// <param name="lerpReceivedData">Lerp delegate.</param>
/// <param name="processReceivedData">Process received data delegate.</param>
/// <returns>Network time.</returns>
public float ProcessSnapshotBytesAndGetNetworkTime(
byte[] snapshotBytes,
bool isScrubbingData,
Deserialize deserializeDelegate,
LerpReceivedTrackingData lerpReceivedData,
ProcessReceivedData processReceivedData)
{
NativeArray<byte> nativeBytesArray = new NativeArray<byte>(
snapshotBytes.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < snapshotBytes.Length; i++)
{
nativeBytesArray[i] = snapshotBytes[i];
}
double readTimestamp = 0.0f;
DeserializeSnapshotTimestamp(nativeBytesArray, out readTimestamp);
if (deserializeDelegate(snapshotBytes))
{
if (!isScrubbingData)
{
lerpReceivedData();
}
processReceivedData();
}
return (float)readTimestamp;
}
/// <summary>
/// Public-version of the seek function.
/// </summary>
/// <param name="handle">Native handle.</param>
/// <param name="snapshotIndex">Snapshot index.</param>
/// <param name="processSnapshot">Delegate that processes the target snapshot and gets the timestamp.</param>
/// <param name="deserializeSnapshot">Network time that should be modified based on seeking.</param>
/// <returns>True if seek worked; false if not.</returns>
public bool Seek(
UInt64 handle,
int snapshotIndex,
ProcessSnapshotGetTimestamp processSnapshot,
DeserializeSnapshot deserializeSnapshot)
{
// Avoid seek if one can avoid it.
if (snapshotIndex == SnapshotIndex)
{
return false;
}
float newNetworkTime = NetworkTime;
bool seekSuccesful = Seek(
handle,
snapshotIndex,
processSnapshot,
deserializeSnapshot,
ref newNetworkTime);
if (!seekSuccesful)
{
return false;
}
// On next tick, the next frame will be seeked based on these
// time values.
NetworkTime = newNetworkTime;
LastTimeStamp = newNetworkTime;
return true;
}
/// <summary>
/// Convenient function used for seeking to a snapshot.
/// </summary>
/// <param name="handle">Native handle.</param>
/// <param name="snapshotIndex">Snapshot index.</param>
/// <param name="processSnapshot">Delegate that processes the target snapshot and gets the timestamp.</param>
/// <param name="deserializeSnapshot">Delegate that deserializes the snapshot.</param>
/// <param name="networkTime">Network time that should be modified based on seeking.</param>
/// <returns>True if seek worked; false if not.</returns>
private bool Seek(
UInt64 handle,
int snapshotIndex,
ProcessSnapshotGetTimestamp processSnapshot,
DeserializeSnapshot deserializeSnapshot,
ref float networkTime)
{
if (!HasActivePlaybackFile)
{
Debug.LogError("Cannot seek without playing back.");
return false;
}
if (snapshotIndex > NumSnapshots)
{
Debug.LogError($"Cannot seek to {snapshotIndex} because there are only " +
$"0-{NumSnapshots - 1} snapshots available.");
return false;
}
int snapshotsSinceLastSync, destinationSnapshotOffset;
(destinationSnapshotOffset, snapshotsSinceLastSync) =
GetByteOffsetAndLastSyncForSnapshotIndex(snapshotIndex);
byte[] snapshotBytes = null;
// If there have been x snapshots since the baseline, deserialize the snapshots
// before us first.
if (snapshotsSinceLastSync > 0)
{
int baselineSnapshot = snapshotIndex - snapshotsSinceLastSync;
Assert.IsTrue(baselineSnapshot >= 0);
// go from baseline to destination
for (int i = 0; i < snapshotsSinceLastSync; ++i)
{
int currentSnapshotIndex = baselineSnapshot + i;
int currentSnapshotByteOffset = GetSnapshotByteOffset(currentSnapshotIndex);
// get snapshot to ensure delta computation
snapshotBytes = GetBytesAtSpecificSnapshotIndex(
currentSnapshotByteOffset, currentSnapshotIndex, false);
if (snapshotBytes == null || !deserializeSnapshot(snapshotBytes))
{
Debug.LogError($"Could not get snapshot {currentSnapshotIndex} for " +
$"destination snapshot {snapshotIndex} during seeking, " +
$"looked for {i} snapshots back from destination. Baseline is " +
$"{snapshotsSinceLastSync} from destination.");
return false;
}
}
}
// Process final snapshot.
snapshotBytes = GetBytesAtSpecificSnapshotIndex(
destinationSnapshotOffset, snapshotIndex, true);
float targetSnapshotTimestamp = 0.0f;
if (snapshotBytes != null)
{
targetSnapshotTimestamp = processSnapshot(snapshotBytes);
}
else
{
Debug.LogError($"Could not seek to destination snapshot {snapshotIndex}.");
return false;
}
// go to final timestamp, offset by delta time so that render time is set to network time
networkTime = targetSnapshotTimestamp + Time.deltaTime;
_networkTime = networkTime;
// reset all interpolated data since we are seeking to a new point. We do
// not want to interpolate from data that comes before the destination snapshot.
MSDKUtility.ResetInterpolators(handle);
return true;
}
/// <summary>
/// Obtains trimmed data for a specified range of snapshots. This includes
/// the max snapshot (as opposed to just the bytes up to it).
/// </summary>
/// <param name="minTrimmedSnapshot">Min snapshot index.</param>
/// <param name="maxTrimmedSnapshot">Max snapshot index.</param>
/// <param name="newStartHeader">New start header for trimmed data.</param>
/// <param name="newEndHeader">New end header for trimmed data.</param>
/// <param name="trimmedBytes">Trimmed snapshot bytes.</param>
/// <returns>True if trim operation worked; false if not.</returns>
public bool AssembleTrimmedData(
int minTrimmedSnapshot,
int maxTrimmedSnapshot,
out StartHeader newStartHeader,
out EndHeader newEndHeader,
out byte[] trimmedBytes)
{
newStartHeader = new StartHeader();
newEndHeader = new EndHeader();
trimmedBytes = null;
// Do error checks first.
if (!HasActivePlaybackFile)
{
Debug.LogError("Can't get trimmed data because no playback file has been currently loaded.");
return false;
}
if (minTrimmedSnapshot < 0 || maxTrimmedSnapshot < 0)
{
Debug.LogError($"Trim range ({minTrimmedSnapshot}-{maxTrimmedSnapshot}) " +
$"has at least one negative value, which is invalid.");
return false;
}
if (maxTrimmedSnapshot < minTrimmedSnapshot ||
minTrimmedSnapshot >= NumSnapshots ||
maxTrimmedSnapshot >= NumSnapshots)
{
Debug.LogError("Please make sure your min and max trimmed snapshot " +
$"range is between ({0}-{NumSnapshots - 1}), and " +
$"that the max is greater than or equal to the min. Your specified range " +
$"is ({minTrimmedSnapshot}-{maxTrimmedSnapshot}).");
return false;
}
// make sure the min baseline is a full one
int snapshotsSinceLastSync, destinationSnapshotOffset;
(destinationSnapshotOffset, snapshotsSinceLastSync) =
GetByteOffsetAndLastSyncForSnapshotIndex(minTrimmedSnapshot);
if (snapshotsSinceLastSync > 0)
{
Debug.LogWarning($"Min trimmed snapshot index {minTrimmedSnapshot} is not a full one. "
+ $"The baseline is {snapshotsSinceLastSync} backwards in time, which would be "
+ $"{minTrimmedSnapshot - snapshotsSinceLastSync}. Will use that.");
minTrimmedSnapshot -= snapshotsSinceLastSync;
}
trimmedBytes = _playbackFile.GetSnapshotRangeBytes(
minTrimmedSnapshot,
maxTrimmedSnapshot,
_snapshotToByteoffsetAfterHeader);
// the header of the clone need to be updated too to indicate restricted range.
newStartHeader.NumSnapshots = (maxTrimmedSnapshot - minTrimmedSnapshot) + 1;
newStartHeader.NumTotalSnapshotBytes = trimmedBytes.Length;
newStartHeader.StartNetworkTime =
_playbackFile.GetNetworkTimeForSnapshot(_snapshotToByteoffsetAfterHeader[minTrimmedSnapshot]);
// re-use the old UTC timestamp to maintain some association with the original recording.
newEndHeader = new EndHeader(_endHeader.UTCTimestamp);
return true;
}
/// <summary>
/// Restarts snapshot reading to the beginning.
/// </summary>
public void RestartSnapshotReading()
{
_snapshotIndex = 0;
_numSnapshotBytesReadSoFar = 0;
_playedFirstFrame = false;
}
/// <summary>
/// Closest playbackfile if already open.
/// </summary>
public void ClosePlaybackFileIfOpen()
{
if (_playbackFile != null)
{
_playbackFile.Dispose();
_playbackFile = null;
}
}
}
}