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 XRCameraImage. /// /// /// Each XRCameraImage must be explicitly disposed. Failing to do so will leak resources and could prevent /// future camera image access. /// public struct XRCameraImage : IDisposable, IEquatable { int m_NativeHandle; XRCameraSubsystem m_CameraSubsystem; static XRCameraSubsystem.OnImageRequestCompleteDelegate s_OnAsyncConversionComplete; #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle m_SafetyHandle; #endif /// /// 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 { get { return dimensions.x; } } /// /// The image height. /// /// /// The image height. /// public int height { get { return 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 CameraImageFormat 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 XRCameraImage represents a valid image (i.e., not Disposed). /// /// /// true if this XRCameraImage represents a valid image. Otherwise, false. /// public bool valid { get { return (m_CameraSubsystem != null) && m_CameraSubsystem.NativeHandleValid(m_NativeHandle); } } /// /// Initialize the static callback for when the image request completes. /// static XRCameraImage() { s_OnAsyncConversionComplete = new XRCameraSubsystem.OnImageRequestCompleteDelegate(OnAsyncConversionComplete); } /// /// Construct the XRCameraImage with the given native image information. /// /// The camera subsystem to use for interacting with the native image. /// The native image handle. /// The dimensions of the native image. /// The number of video planes in the native image. /// The timestamp for when the native image was captured. /// The camera image format of the native image. internal XRCameraImage( XRCameraSubsystem cameraSubsystem, int nativeHandle, Vector2Int dimensions, int planeCount, double timestamp, CameraImageFormat format) { m_CameraSubsystem = cameraSubsystem; m_NativeHandle = nativeHandle; this.dimensions = dimensions; this.planeCount = planeCount; this.timestamp = timestamp; this.format = format; #if ENABLE_UNITY_COLLECTIONS_CHECKS m_SafetyHandle = AtomicSafetyHandle.Create(); #endif } /// /// Determines whether the given TextureFormat is supported for conversion. /// /// /// These texture formats are supported: /// /// TextureFormat.R8 /// TextureFormat.Alpha8 /// TextureFormat.RGB24 /// TextureFormat.RGBA32 /// TextureFormat.ARGBA32 /// TextureFormat.BGRA32 /// /// /// A TextureFormat to test. /// true if the format is supported by the various conversion methods. public static bool FormatSupported(TextureFormat format) { switch (format) { case TextureFormat.Alpha8: case TextureFormat.R8: case TextureFormat.RGB24: case TextureFormat.RGBA32: case TextureFormat.ARGB32: case TextureFormat.BGRA32: return true; default: return false; } } /// /// 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 XRCameraImagePlane GetPlane(int planeIndex) { ValidateNativeHandleAndThrow(); if (planeIndex < 0 || planeIndex >= planeCount) { throw new ArgumentOutOfRangeException("planeIndex", string.Format("planeIndex must be in the range 0 to {0}", planeCount - 1)); } XRCameraSubsystem.CameraImagePlaneCinfo imagePlaneCinfo; if (!m_CameraSubsystem.TryGetPlane(m_NativeHandle, planeIndex, out imagePlaneCinfo)) { throw new InvalidOperationException("The requested plane is not valid for this XRCameraImage."); } 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 XRCameraImagePlane { 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("width", string.Format("Converted image width must be less than or equal to native image width. {0} > {1}", dimensions.x, this.dimensions.x)); } if (dimensions.y > this.dimensions.y) { throw new ArgumentOutOfRangeException("height", string.Format("Converted image height must be less than or equal to native image height. {0} > {1}", dimensions.y, this.dimensions.y)); } if (!FormatSupported(format)) { throw new ArgumentException("Invalid texture format.", "format"); } int size; if (!m_CameraSubsystem.TryGetConvertedDataSize(m_NativeHandle, dimensions, format, out size)) { throw new InvalidOperationException("XRCameraImage 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(XRCameraImageConversionParams conversionParams) { return GetConvertedDataSize( conversionParams.outputDimensions, conversionParams.outputFormat); } /// /// Convert the XRCameraImage 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(XRCameraImageConversionParams conversionParams, IntPtr destinationBuffer, int bufferLength) { ValidateNativeHandleAndThrow(); ValidateConversionParamsAndThrow(conversionParams); int requiredDataSize = GetConvertedDataSize(conversionParams); if (bufferLength < requiredDataSize) { throw new ArgumentException(string.Format( "Conversion requires {0} bytes but only provided {1} bytes.", requiredDataSize, bufferLength), "bufferLength"); } if (!m_CameraSubsystem.TryConvert(m_NativeHandle, conversionParams, destinationBuffer, bufferLength)) { throw new InvalidOperationException("Conversion failed."); } } /// /// Convert the XRCameraImage 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 XRCameraImage 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 XRAsyncCameraImageConversion ConvertAsync(XRCameraImageConversionParams conversionParams) { ValidateNativeHandleAndThrow(); ValidateConversionParamsAndThrow(conversionParams); return new XRAsyncCameraImageConversion(m_CameraSubsystem, m_NativeHandle, conversionParams); } /// /// Convert the XRCameraImage 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. /// public void ConvertAsync( XRCameraImageConversionParams conversionParams, Action> onComplete) { ValidateNativeHandleAndThrow(); ValidateConversionParamsAndThrow(conversionParams); var handle = GCHandle.Alloc(onComplete); var context = GCHandle.ToIntPtr(handle); m_CameraSubsystem.ConvertAsync(m_NativeHandle, conversionParams, s_OnAsyncConversionComplete, context); } /// /// Callback from native code for when the asychronous 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(XRCameraSubsystem.OnImageRequestCompleteDelegate))] static unsafe void OnAsyncConversionComplete( AsyncCameraImageConversionStatus status, XRCameraImageConversionParams 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("XRCameraImage 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(XRCameraImageConversionParams conversionParams) { if ((conversionParams.inputRect.x + conversionParams.inputRect.width > width) || (conversionParams.inputRect.y + conversionParams.inputRect.height > height)) { throw new ArgumentOutOfRangeException( "conversionParams.inputRect", "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(string.Format( "Output dimensions must be less than or equal to the inputRect's dimensions: ({0}x{1} > {2}x{3}).", conversionParams.outputDimensions.x, conversionParams.outputDimensions.y, conversionParams.inputRect.width, conversionParams.inputRect.height)); } if (!FormatSupported(conversionParams.outputFormat)) { throw new ArgumentException("TextureFormat not supported.", "conversionParams.format"); } } /// /// 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_CameraSubsystem == null || m_NativeHandle == 0) { return; } m_CameraSubsystem.DisposeImage(m_NativeHandle); m_NativeHandle = 0; m_CameraSubsystem = null; #if ENABLE_UNITY_COLLECTIONS_CHECKS AtomicSafetyHandle.Release(m_SafetyHandle); #endif } public override int GetHashCode() { unchecked { var hash = width.GetHashCode(); hash = hash * 486187739 + height.GetHashCode(); hash = hash * 486187739 + planeCount.GetHashCode(); hash = hash * 486187739 + m_NativeHandle.GetHashCode(); hash = hash * 486187739 + ((int)format).GetHashCode(); if (m_CameraSubsystem != null) { hash = hash * 486187739 + m_CameraSubsystem.GetHashCode(); } return hash; } } public override bool Equals(object obj) { return ((obj is XRCameraImage) && Equals((XRCameraImage)obj)); } public bool Equals(XRCameraImage other) { return (width == other.width) && (height == other.height) && (planeCount == other.planeCount) && (format == other.format) && (m_NativeHandle == other.m_NativeHandle) && (m_CameraSubsystem == other.m_CameraSubsystem); } public static bool operator ==(XRCameraImage lhs, XRCameraImage rhs) { return lhs.Equals(rhs); } public static bool operator !=(XRCameraImage lhs, XRCameraImage rhs) { return !lhs.Equals(rhs); } public override string ToString() { return string.Format( "(Width: {0}, Height: {1}, PlaneCount: {2}, Format: {3})", width, height, planeCount, format); } } }