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