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})"; } }