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