This repository has been archived on 2025-04-28. You can view files and clone it, but cannot push or open issues or pull requests.
ARPlusSystem/ARPlusSystem-250418/Library/PackageCache/com.unity.xr.arsubsystems@4.../Runtime/XRCpuImage/XRCpuImage.cs

577 lines
27 KiB
C#

using AOT;
using System;
using System.Runtime.InteropServices;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
namespace UnityEngine.XR.ARSubsystems
{
/// <summary>
/// Represents a single, raw image from a device camera. Provides access to the raw image plane data, as well as
/// conversion methods to convert to color and grayscale formats. See <see cref="Convert"/> and
/// <see cref="ConvertAsync(XRCpuImage.ConversionParams)"/>. Use
/// <see cref="XRCameraSubsystem.TryAcquireLatestCpuImage"/> to get a <see cref="XRCpuImage"/>.
/// </summary>
/// <remarks>
/// Each <see cref="XRCpuImage"/> must be explicitly disposed. Failing to do so will leak resources and could prevent
/// future camera image access.
/// </remarks>
public partial struct XRCpuImage : IDisposable, IEquatable<XRCpuImage>
{
int m_NativeHandle;
Api m_Api;
static Api.OnImageRequestCompleteDelegate s_OnAsyncConversionCompleteDelegate = OnAsyncConversionComplete;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle m_SafetyHandle;
#endif
/// <summary>
/// Container for native camera image construction metadata.
/// </summary>
public struct Cinfo : IEquatable<Cinfo>
{
/// <summary>
/// The handle representing the camera image on the native level.
/// </summary>
/// <value>
/// The handle representing the camera image on the native level.
/// </value>
public int nativeHandle => m_NativeHandle;
int m_NativeHandle;
/// <summary>
/// The dimensions of the camera image.
/// </summary>
/// <value>
/// The dimensions of the camera image.
/// </value>
public Vector2Int dimensions => m_Dimensions;
Vector2Int m_Dimensions;
/// <summary>
/// The number of video planes in the camera image.
/// </summary>
/// <value>
/// The number of video planes in the camera image.
/// </value>
public int planeCount => m_PlaneCount;
int m_PlaneCount;
/// <summary>
/// The timestamp for when the camera image was captured.
/// </summary>
/// <value>
/// The timestamp for when the camera image was captured.
/// </value>
public double timestamp => m_Timestamp;
double m_Timestamp;
/// <summary>
/// The format of the camera image.
/// </summary>
/// <value>
/// The format of the camera image.
/// </value>
public Format format => m_Format;
Format m_Format;
/// <summary>
/// Constructs the camera image `Cinfo`.
/// </summary>
/// <param name="nativeHandle">The handle representing the camera image on the native level.</param>
/// <param name="dimensions">The dimensions of the camera image.</param>
/// <param name="planeCount">The number of video planes in the camera image.</param>
/// <param name="timestamp">The timestamp for when the camera image was captured.</param>
/// <param name="format">The format of the camera image.</param>
public Cinfo(int nativeHandle, Vector2Int dimensions, int planeCount, double timestamp, Format format)
{
m_NativeHandle = nativeHandle;
m_Dimensions = dimensions;
m_PlaneCount = planeCount;
m_Timestamp = timestamp;
m_Format = format;
}
/// <summary>
/// Tests for equality.
/// </summary>
/// <param name="other">The other <see cref="Cinfo"/> to compare against.</param>
/// <returns>`True` if every field in <paramref name="other"/> is equal to this <see cref="Cinfo"/>, otherwise `false`.</returns>
public bool Equals(Cinfo other)
{
return (nativeHandle.Equals(other.nativeHandle) && dimensions.Equals(other.dimensions)
&& planeCount.Equals(other.planeCount) && timestamp.Equals(other.timestamp)
&& (format == other.format));
}
/// <summary>
/// Tests for equality.
/// </summary>
/// <param name="obj">The `object` to compare against.</param>
/// <returns>`True` if <paramref name="obj"/> is of type <see cref="Cinfo"/> and
/// <see cref="Equals(Cinfo)"/> also returns `true`; otherwise `false`.</returns>
public override bool Equals(System.Object obj) => obj is Cinfo other && Equals(other);
/// <summary>
/// Tests for equality. Same as <see cref="Equals(Cinfo)"/>.
/// </summary>
/// <param name="lhs">The <see cref="Cinfo"/> to compare with <paramref name="rhs"/>.</param>
/// <param name="rhs">The <see cref="Cinfo"/> to compare with <paramref name="lhs"/>.</param>
/// <returns>`True` if <paramref name="lhs"/> is equal to <paramref name="rhs"/>, otherwise `false`.</returns>
public static bool operator ==(Cinfo lhs, Cinfo rhs) => lhs.Equals(rhs);
/// <summary>
/// Tests for inequality. Same as `!`<see cref="Equals(Cinfo)"/>.
/// </summary>
/// <param name="lhs">The <see cref="Cinfo"/> to compare with <paramref name="rhs"/>.</param>
/// <param name="rhs">The <see cref="Cinfo"/> to compare with <paramref name="lhs"/>.</param>
/// <returns>`True` if <paramref name="lhs"/> is not equal to <paramref name="rhs"/>, otherwise `false`.</returns>
public static bool operator !=(Cinfo lhs, Cinfo rhs) => !(lhs == rhs);
/// <summary>
/// Generates a hash suitable for use with containers like `HashSet` and `Dictionary`.
/// </summary>
/// <returns>A hash code generated from this object's fields.</returns>
public override int GetHashCode() => HashCodeUtil.Combine(
nativeHandle.GetHashCode(),
dimensions.GetHashCode(),
planeCount.GetHashCode(),
timestamp.GetHashCode(),
((int)format).GetHashCode());
/// <summary>
/// Generates a string representation of this object suitable for debugging.
/// </summary>
/// <returns>Returns a string representation of this <see cref="Cinfo"/>.</returns>
public override string ToString() => $"nativeHandle: {nativeHandle} dimensions:{dimensions} planes:{planeCount} timestamp:{timestamp} format:{format}";
}
/// <summary>
/// The dimensions (width and height) of the image.
/// </summary>
/// <value>
/// The dimensions (width and height) of the image.
/// </value>
public Vector2Int dimensions { get; private set; }
/// <summary>
/// The image width.
/// </summary>
/// <value>
/// The image width.
/// </value>
public int width => dimensions.x;
/// <summary>
/// The image height.
/// </summary>
/// <value>
/// The image height.
/// </value>
public int height => dimensions.y;
/// <summary>
/// The number of image planes. A plane in this context refers to a channel in the raw video format,
/// not a physical surface.
/// </summary>
/// <value>
/// The number of image planes.
/// </value>
public int planeCount { get; private set; }
/// <summary>
/// The format used by the image planes. You will only need this if you plan to interpret the raw plane data.
/// </summary>
/// <value>
/// The format used by the image planes.
/// </value>
public Format format { get; private set; }
/// <summary>
/// The timestamp, in seconds, associated with this camera image
/// </summary>
/// <value>
/// The timestamp, in seconds, associated with this camera image
/// </value>
public double timestamp { get; private set; }
/// <summary>
/// Whether this <c>XRCpuImage</c> represents a valid image (that is, it has not been Disposed).
/// </summary>
/// <value>
/// <c>true</c> if this <c>XRCpuImage</c> represents a valid image. Otherwise, <c>false</c>.
/// </value>
public bool valid => (m_Api != null) && m_Api.NativeHandleValid(m_NativeHandle);
/// <summary>
/// Construct the <c>XRCpuImage</c> with the given native image information.
/// </summary>
/// <param name="api">The camera subsystem to use for interacting with the native image.</param>
/// <param name="cinfo">Construction information. See <see cref="Cinfo"/>.</param>
internal XRCpuImage(XRCpuImage.Api api, Cinfo cinfo)
{
m_Api = api;
m_NativeHandle = cinfo.nativeHandle;
this.dimensions = cinfo.dimensions;
this.planeCount = cinfo.planeCount;
this.timestamp = cinfo.timestamp;
this.format = cinfo.format;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
m_SafetyHandle = AtomicSafetyHandle.Create();
#endif
}
/// <summary>
/// Determines whether the given [TextureFormat](https://docs.unity3d.com/ScriptReference/TextureFormat.html)
/// is supported for conversion.
/// </summary>
/// <param name="format">A [TextureFormat](https://docs.unity3d.com/ScriptReference/TextureFormat.html) to
/// test.</param>
/// <returns>Returns `true` if <paramref name="format"/> is supported by the various conversion
/// methods.</returns>
public bool FormatSupported(TextureFormat format) => m_Api?.FormatSupported(this, format) == true;
/// <summary>
/// Get an image plane. A plane in this context refers to a channel in the raw video format, not a physical
/// surface.
/// </summary>
/// <param name="planeIndex">The index of the plane to get.</param>
/// <returns>A <see cref="Plane"/> describing the plane.</returns>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if <paramref name="planeIndex"/> is not within
/// the range [0, <see cref="planeCount"/>).</exception>
/// <exception cref="System.InvalidOperationException">Thrown if the requested plane is not valid.</exception>
public unsafe Plane GetPlane(int planeIndex)
{
ValidateNativeHandleAndThrow();
if (planeIndex < 0 || planeIndex >= planeCount)
{
throw new ArgumentOutOfRangeException(nameof(planeIndex), $"{nameof(planeIndex)} must be in the range 0 to {planeCount - 1}");
}
if (!m_Api.TryGetPlane(m_NativeHandle, planeIndex, out Plane.Cinfo imagePlaneCinfo))
{
throw new InvalidOperationException("The requested plane is not valid for this XRCpuImage.");
}
var data = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<byte>(
(void*)imagePlaneCinfo.dataPtr, imagePlaneCinfo.dataLength, Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref data, m_SafetyHandle);
#endif
return new Plane
{
rowStride = imagePlaneCinfo.rowStride,
pixelStride = imagePlaneCinfo.pixelStride,
data = data
};
}
/// <summary>
/// Get the number of bytes required to store a converted image with the given parameters.
/// </summary>
/// <param name="dimensions">The desired dimensions of the converted image.</param>
/// <param name="format">The desired <c>TextureFormat</c> for the converted image.</param>
/// <returns>The number of bytes required to store the converted image.</returns>
/// <exception cref="System.ArgumentException">Thrown if the desired <paramref name="format"/> is not
/// supported.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the desired <paramref name="dimensions"/>
/// exceed the native image dimensions.</exception>
/// <exception cref="System.InvalidOperationException">Thrown if the image is invalid.</exception>
/// <seealso cref="FormatSupported"/>
public int GetConvertedDataSize(Vector2Int dimensions, TextureFormat format)
{
ValidateNativeHandleAndThrow();
if (dimensions.x > this.dimensions.x)
{
throw new ArgumentOutOfRangeException(nameof(dimensions),
$"Converted image width must be less than or equal to native image width. {dimensions.x} > {this.dimensions.x}");
}
if (dimensions.y > this.dimensions.y)
{
throw new ArgumentOutOfRangeException(nameof(dimensions),
$"Converted image height must be less than or equal to native image height. {dimensions.y} > {this.dimensions.y}");
}
if (!FormatSupported(format))
{
throw new ArgumentException("Invalid texture format.", nameof(format));
}
if (!m_Api.TryGetConvertedDataSize(m_NativeHandle, dimensions, format, out int size))
{
throw new InvalidOperationException("XRCpuImage is not valid.");
}
return size;
}
/// <summary>
/// Get the number of bytes required to store a converted image with the given parameters.
/// </summary>
/// <param name="conversionParams">The desired conversion parameters.</param>
/// <returns>The number of bytes required to store the converted image.</returns>
/// <exception cref="System.ArgumentException">Thrown if the desired format is not supported.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the desired dimensions exceed the native
/// image dimensions.</exception>
/// <exception cref="System.InvalidOperationException">Thrown if the image is invalid.</exception>
/// <seealso cref="FormatSupported"/>
public int GetConvertedDataSize(ConversionParams conversionParams)
{
return GetConvertedDataSize(
conversionParams.outputDimensions,
conversionParams.outputFormat);
}
/// <summary>
/// Convert the `XRCpuImage` to one of the supported formats using the specified
/// <paramref name="conversionParams"/>.
/// </summary>
/// <param name="conversionParams">The parameters for the image conversion.</param>
/// <param name="destinationBuffer">A pointer to memory to which to write the converted image.</param>
/// <param name="bufferLength">The number of bytes pointed to by <paramref name="destinationBuffer"/>. Must be
/// greater than or equal to the value returned by
/// <see cref="GetConvertedDataSize(ConversionParams)"/>.</param>
/// <exception cref="System.ArgumentException">Thrown if the <paramref name="bufferLength"/> is smaller than
/// the data size required by the conversion.</exception>
/// <exception cref="System.InvalidOperationException">Thrown if the conversion failed.</exception>
/// <seealso cref="FormatSupported"/>
public void Convert(ConversionParams conversionParams, IntPtr destinationBuffer, int bufferLength)
{
ValidateNativeHandleAndThrow();
ValidateConversionParamsAndThrow(conversionParams);
int requiredDataSize = GetConvertedDataSize(conversionParams);
if (bufferLength < requiredDataSize)
{
throw new ArgumentException($"Conversion requires {requiredDataSize} bytes but only provided {bufferLength} bytes.", nameof(bufferLength));
}
if (!m_Api.TryConvert(m_NativeHandle, conversionParams, destinationBuffer, bufferLength))
{
throw new InvalidOperationException("Conversion failed.");
}
}
/// <summary>
/// Convert the `XRCpuImage` to one of the supported formats using the specified
/// <paramref name="conversionParams"/>.
/// </summary>
/// <param name="conversionParams">The parameters for the image conversion.</param>
/// <param name="destinationBuffer">The destination buffer for the converted data. The buffer must be
/// large enough to hold the converted data, that is, at least as large as the value returned by
/// <see cref="GetConvertedDataSize(ConversionParams)"/>.</param>
/// <exception cref="System.ArgumentException">Thrown if the <paramref name="destinationBuffer"/>
/// has insufficient space for the converted data.</exception>
/// <exception cref="System.InvalidOperationException">Thrown if the conversion failed.</exception>
/// <seealso cref="FormatSupported"/>
public unsafe void Convert(ConversionParams conversionParams, NativeSlice<byte> destinationBuffer)
{
Convert(conversionParams, new IntPtr(destinationBuffer.GetUnsafePtr()), destinationBuffer.Length);
}
/// <summary>
/// Convert the <c>XRCpuImage</c> to one of the supported formats using the specified
/// <paramref name="conversionParams"/>. The conversion is performed asynchronously. Use the returned
/// <see cref="AsyncConversion"/> to check for the status of the conversion, and retrieve the data
/// when complete.
/// </summary>
/// <remarks>
/// It is safe to <c>Dispose</c> the <c>XRCpuImage</c> before the asynchronous operation has completed.
/// </remarks>
/// <param name="conversionParams">The parameters to use for the conversion.</param>
/// <returns>A <see cref="AsyncConversion"/> which can be used to check the status of the
/// conversion operation and get the resulting data.</returns>
/// <seealso cref="FormatSupported"/>
public AsyncConversion ConvertAsync(ConversionParams conversionParams)
{
ValidateNativeHandleAndThrow();
ValidateConversionParamsAndThrow(conversionParams);
return new AsyncConversion(m_Api, m_NativeHandle, conversionParams);
}
/// <summary>
/// <para>Convert the <c>XRCpuImage</c> to one of the supported formats using the specified
/// <paramref name="conversionParams"/>. The conversion is performed asynchronously, and
/// <paramref name="onComplete"/> is invoked when the conversion is complete, whether successful or not.</para>
/// <para>The <c>NativeArray</c> provided in the <paramref name="onComplete"/> delegate is only valid during
/// the invocation and is disposed immediately upon return.</para>
/// </summary>
/// <param name="conversionParams">The parameters to use for the conversion.</param>
/// <param name="onComplete">A delegate to invoke when the conversion operation completes. The delegate is
/// always invoked, regardless of whether the conversion succeeded.</param>
/// <seealso cref="FormatSupported"/>
public void ConvertAsync(
ConversionParams conversionParams,
Action<AsyncConversionStatus, ConversionParams, NativeArray<byte>> onComplete)
{
ValidateNativeHandleAndThrow();
ValidateConversionParamsAndThrow(conversionParams);
var handle = GCHandle.Alloc(onComplete);
var context = GCHandle.ToIntPtr(handle);
m_Api.ConvertAsync(m_NativeHandle, conversionParams, s_OnAsyncConversionCompleteDelegate, context);
}
/// <summary>
/// Callback from native code for when the asynchronous conversion is complete.
/// </summary>
/// <param name="status">The status of the conversion operation.</param>
/// <param name="conversionParams">The parameters for the conversion.</param>
/// <param name="dataPtr">The native pointer to the converted data.</param>
/// <param name="dataLength">The memory size of the converted data.</param>
/// <param name="context">The native context for the conversion operation.</param>
[MonoPInvokeCallback(typeof(Api.OnImageRequestCompleteDelegate))]
static unsafe void OnAsyncConversionComplete(
AsyncConversionStatus status, ConversionParams conversionParams, IntPtr dataPtr,
int dataLength, IntPtr context)
{
var handle = GCHandle.FromIntPtr(context);
var onComplete = (Action<AsyncConversionStatus, ConversionParams, NativeArray<byte>>)handle.Target;
if (onComplete != null)
{
var data = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<byte>(
(void*)dataPtr, dataLength, Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
var safetyHandle = AtomicSafetyHandle.Create();
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref data, safetyHandle);
#endif
onComplete(status, conversionParams, data);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.Release(safetyHandle);
#endif
}
handle.Free();
}
/// <summary>
/// Ensure the image is valid.
/// </summary>
/// <exception cref="System.InvalidOperationException">Thrown if the image is invalid.</exception>
void ValidateNativeHandleAndThrow()
{
if (!valid)
{
throw new InvalidOperationException("XRCpuImage is not valid.");
}
}
/// <summary>
/// Ensure the conversion parameters are valid.
/// </summary>
/// <param name="conversionParams">The conversion parameters to validate.</param>
/// <exception cref="System.ArgumentOutOfRangeException">Thrown if the input image rect exceeds the actual
/// image dimensions or if the output dimensions exceed the input dimensions.</exception>
/// <exception cref="System.ArgumentException">Thrown if the texture format is not suppported.</exception>
/// <seealso cref="FormatSupported"/>
void ValidateConversionParamsAndThrow(ConversionParams conversionParams)
{
if ((conversionParams.inputRect.x + conversionParams.inputRect.width > width) ||
(conversionParams.inputRect.y + conversionParams.inputRect.height > height))
{
throw new ArgumentOutOfRangeException(
nameof(conversionParams),
"Input rect must be completely within the original image.");
}
if ((conversionParams.outputDimensions.x > conversionParams.inputRect.width) ||
(conversionParams.outputDimensions.y > conversionParams.inputRect.height))
{
throw new ArgumentOutOfRangeException($"Output dimensions must be less than or equal to the inputRect's dimensions: ({conversionParams.outputDimensions.x}x{conversionParams.outputDimensions.y} > {conversionParams.inputRect.width}x{conversionParams.inputRect.height}).");
}
if (!FormatSupported(conversionParams.outputFormat))
{
throw new ArgumentException("TextureFormat not supported.", nameof(conversionParams));
}
}
/// <summary>
/// Dispose native resources associated with this request, including the raw image data. Any
/// <see cref="Plane"/>s returned by <see cref="GetPlane"/> are invalidated immediately after
/// calling <c>Dispose</c>.
/// </summary>
public void Dispose()
{
if (m_Api == null || m_NativeHandle == 0)
{
return;
}
m_Api.DisposeImage(m_NativeHandle);
m_NativeHandle = 0;
m_Api = null;
#if ENABLE_UNITY_COLLECTIONS_CHECKS
AtomicSafetyHandle.Release(m_SafetyHandle);
#endif
}
/// <summary>
/// Generates a hash suitable for use with containers like `HashSet` and `Dictionary`.
/// </summary>
/// <returns>A hash code generated from this object's fields.</returns>
public override int GetHashCode() => HashCodeUtil.Combine(
dimensions.GetHashCode(),
planeCount.GetHashCode(),
m_NativeHandle.GetHashCode(),
((int)format).GetHashCode(),
HashCodeUtil.ReferenceHash(m_Api));
/// <summary>
/// Tests for equality.
/// </summary>
/// <param name="obj">The `object` to compare against.</param>
/// <returns>`True` if <paramref name="obj"/> is of type <see cref="XRCpuImage"/> and
/// <see cref="Equals(XRCpuImage)"/> also returns `true`; otherwise `false`.</returns>
public override bool Equals(object obj) => obj is XRCpuImage other && Equals(other);
/// <summary>
/// Tests for equality.
/// </summary>
/// <param name="other">The other <see cref="XRCpuImage"/> to compare against.</param>
/// <returns>`True` if every field in <paramref name="other"/> is equal to this <see cref="XRCpuImage"/>, otherwise false.</returns>
public bool Equals(XRCpuImage other) =>
dimensions.Equals(other.dimensions) &&
(planeCount == other.planeCount) &&
(format == other.format) &&
(m_NativeHandle == other.m_NativeHandle) &&
(m_Api == other.m_Api);
/// <summary>
/// Tests for equality. Same as <see cref="Equals(XRCpuImage)"/>.
/// </summary>
/// <param name="lhs">The <see cref="XRCpuImage"/> to compare with <paramref name="rhs"/>.</param>
/// <param name="rhs">The <see cref="XRCpuImage"/> to compare with <paramref name="lhs"/>.</param>
/// <returns>`True` if <paramref name="lhs"/> is equal to <paramref name="rhs"/>, otherwise `false`.</returns>
public static bool operator ==(XRCpuImage lhs, XRCpuImage rhs) => lhs.Equals(rhs);
/// <summary>
/// Tests for inequality. Same as `!`<see cref="Equals(XRCpuImage)"/>.
/// </summary>
/// <param name="lhs">The <see cref="XRCpuImage"/> to compare with <paramref name="rhs"/>.</param>
/// <param name="rhs">The <see cref="XRCpuImage"/> to compare with <paramref name="lhs"/>.</param>
/// <returns>`True` if <paramref name="lhs"/> is not equal to <paramref name="rhs"/>, otherwise `false`.</returns>
public static bool operator !=(XRCpuImage lhs, XRCpuImage rhs) => !lhs.Equals(rhs);
/// <summary>
/// Generates a string representation of this <see cref="XRCpuImage"/>.
/// </summary>
/// <returns>A string representation of this <see cref="XRCpuImage"/>.</returns>
public override string ToString() => $"(Width: {width}, Height: {height}, PlaneCount: {planeCount}, Format: {format})";
}
}