Files
sbox-public/engine/Sandbox.Engine/Resources/Textures/Texture.Read.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

614 lines
22 KiB
C#

using NativeEngine;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace Sandbox;
public partial class Texture
{
/// <summary>
/// Reads pixel colors from the texture at the specified mip level
/// </summary>
[Pure]
public Color32[] GetPixels( int mip = 0 )
{
// BUG: Width/Height is on disk and not the currently streamed mip
// I don't know what would break if I made Width/Height represent that instead
var desc = g_pRenderDevice.GetTextureDesc( native );
mip = Math.Clamp( mip, 0, Mips - 1 );
var d = 1 << mip;
if ( Depth == 1 )
{
var rect = (X: 0, Y: 0, W: desc.m_nWidth / d, H: desc.m_nHeight / d);
var data = new Color32[rect.W * rect.H];
GetPixels( rect, 0, mip, data.AsSpan(), ImageFormat.RGBA8888 );
return data;
}
else
{
var box = (X: 0, Y: 0, Z: 0, Width: desc.m_nWidth / d, Height: desc.m_nHeight / d, Depth: Depth / d);
var data = new Color32[box.Width * box.Height * box.Depth];
GetPixels3D( box, mip, data.AsSpan(), ImageFormat.RGBA8888 );
return data;
}
}
public unsafe Bitmap GetBitmap( int mip )
{
mip = Math.Clamp( mip, 0, Mips - 1 );
var d = 1 << mip;
bool floatingPoint = ImageFormat == ImageFormat.RGBA16161616F;
var desc = g_pRenderDevice.GetTextureDesc( native );
var width = desc.m_nWidth / d;
var height = desc.m_nHeight / d;
var depth = desc.m_nDepth;
var outputFormat = floatingPoint ? ImageFormat.RGBA16161616F : ImageFormat.RGBA8888;
var targetMemoryRequired = NativeEngine.ImageLoader.GetMemRequired( width, height, 1, 1, outputFormat );
if ( targetMemoryRequired <= 0 )
{
//
// If desc.m_nWidth and height are 0, this is probably the rendersystemempty, which is obviously a disaster
//
throw new System.Exception( $"targetMemoryRequired <= 0 ({width}x{height} {outputFormat})" );
}
var bitmap = new Bitmap( width, height, floatingPoint );
var data = bitmap.GetBuffer();
if ( data.Length != targetMemoryRequired )
{
throw new System.Exception( $"Buffer isn't big enough {data.Length} != {targetMemoryRequired}" );
}
fixed ( byte* pData = data )
{
var rect = new NativeRect( 0, 0, width, height );
if ( !g_pRenderDevice.ReadTexturePixels( native, ref rect, 0, mip, ref rect, (IntPtr)pData, outputFormat, 0 ) )
return null;
}
return bitmap;
}
private static int GetImageFormatSize( ImageFormat format )
{
switch ( format )
{
case ImageFormat.RGBA8888:
case ImageFormat.BGRA8888:
case ImageFormat.ARGB8888:
case ImageFormat.ABGR8888:
case ImageFormat.R32F:
case ImageFormat.R32_UINT:
return 4;
case ImageFormat.RGB888:
case ImageFormat.BGR888:
return 3;
case ImageFormat.R16:
case ImageFormat.R16F:
return 2;
case ImageFormat.I8:
return 1;
default:
throw new NotImplementedException( $"Reading pixels with format {format} not yet implemented." );
}
}
private static int GetImageFormatSize( ImageFormat format, int width, int height )
{
return NativeEngine.ImageLoader.GetMemRequired( width, height, 1, 1, format );
}
/// <summary>
/// Reads a 2D range of pixel values from the texture at the specified mip level, writing to <paramref name="dstData"/>.
/// This reads one slice from a 2D texture array or 3D texture volume.
/// </summary>
/// <typeparam name="T">Pixel value type (e.g., <see cref="Color32"/>, <see cref="float"/>, <see cref="uint"/> or <see cref="byte"/>)</typeparam>
/// <param name="srcRect">Pixel region to read.</param>
/// <param name="slice">For 2D texture arrays or 3D texture volumes, which slice to read from.</param>
/// <param name="mip">Mip level to read from.</param>
/// <param name="dstData">Array to write to, starting at index 0 for the first read pixel.</param>
/// <param name="dstFormat">Pixel format to use when writing to <paramref name="dstData"/>. We only support some common formats for now.</param>
/// <param name="dstSize">Dimensions of destination pixel array. Matches <paramref name="srcRect"/> by default.</param>
[Pure]
public unsafe void GetPixels<T>( (int X, int Y, int Width, int Height) srcRect, int slice, int mip, Span<T> dstData, ImageFormat dstFormat, (int X, int Y) dstSize = default )
where T : unmanaged
{
var pixelSize = Unsafe.SizeOf<T>();
if ( dstSize.X < 0 || dstSize.Y < 0 )
throw new ArgumentException( $"{nameof( dstSize )} can't be negative" );
if ( dstSize.X == 0 )
dstSize.X = srcRect.Width;
if ( dstSize.Y == 0 )
dstSize.Y = srcRect.Height;
if ( srcRect.X < 0 || srcRect.Y < 0 ||
((long)srcRect.X + srcRect.Width) > Width ||
((long)srcRect.Y + srcRect.Height) > Height )
{
throw new ArgumentException( $"{nameof( srcRect )} out of range" );
}
var dstStride = (long)dstSize.X * pixelSize;
if ( dstStride > int.MaxValue || ((long)dstSize.Y * pixelSize) > int.MaxValue )
throw new ArgumentException( $"{nameof( dstSize )} too large" );
var sliceSize = GetImageFormatSize( dstFormat, dstSize.X, dstSize.Y );
if ( sliceSize <= 0 )
throw new ArgumentException( $"{nameof( dstSize )} invalid" );
if ( !SandboxedUnsafe.IsAcceptablePod<T>() )
throw new ArgumentException( $"{nameof( T )} must be a POD type" );
var dataSize = (long)dstData.Length * pixelSize;
if ( dataSize < sliceSize )
{
throw new ArgumentException( "Output array is too small to fit the given pixel range.",
nameof( dstData ) );
}
var nativeSrcRect = new NativeRect { x = srcRect.X, y = srcRect.Y, w = srcRect.Width, h = srcRect.Height };
var nativeDstRect = new NativeRect { x = 0, y = 0, w = dstSize.X, h = dstSize.Y };
fixed ( T* dataPtr = dstData )
{
if ( !g_pRenderDevice.ReadTexturePixels( native, ref nativeSrcRect, slice, mip, ref nativeDstRect, (IntPtr)dataPtr, dstFormat, (int)dstStride ) )
throw new Exception( "Unable to read texture pixels" );
}
}
/// <summary>
/// Reads a 2D range of pixel values from the texture at the specified mip level, writing to <paramref name="dstData"/>.
/// This reads one slice from a 2D texture array or 3D texture volume.
/// </summary>
/// <typeparam name="T">Pixel value type (e.g., <see cref="Color32"/>, <see cref="float"/>, <see cref="uint"/> or <see cref="byte"/>)</typeparam>
/// <param name="srcRect">Pixel region to read.</param>
/// <param name="slice">For 2D texture arrays or 3D texture volumes, which slice to read from.</param>
/// <param name="mip">Mip level to read from.</param>
/// <param name="dstData">Array to write to, starting at index 0 for the first read pixel.</param>
/// <param name="dstFormat">Pixel format to use when writing to <paramref name="dstData"/>. We only support some common formats for now.</param>
/// <param name="dstRect">Dimensions of destination pixel array. Matches <paramref name="srcRect"/> by default.</param>
/// <param name="dstStride">Stride of the destination array, this is likely your width in pixels.</param>
[Pure]
public unsafe void GetPixels<T>( (int X, int Y, int Width, int Height) srcRect, int slice, int mip, Span<T> dstData, ImageFormat dstFormat, (int X, int Y, int W, int H) dstRect, int dstStride )
where T : unmanaged
{
var pixelByteSize = Unsafe.SizeOf<T>();
if ( dstRect.W < 0 || dstRect.H < 0 )
throw new ArgumentException( $"{nameof( dstRect )} can't be negative" );
if ( dstRect.W == 0 )
dstRect.W = srcRect.Width;
if ( dstRect.H == 0 )
dstRect.H = srcRect.Height;
if ( srcRect.X < 0 || srcRect.Y < 0 ||
((long)srcRect.X + srcRect.Width) > Width ||
((long)srcRect.Y + srcRect.Height) > Height )
{
throw new ArgumentException( $"{nameof( srcRect )} out of range" );
}
var maxLength = (dstRect.Y + dstRect.H - 1) * dstStride + dstRect.X + dstRect.W;
if ( maxLength >= dstData.Length )
throw new ArgumentException( $"Output rect size ({maxLength}) exceeds destination array size {dstData.Length}" );
if ( maxLength <= 0 )
throw new ArgumentException( $"Output rect size ({maxLength}) below zero or equal to zero" );
if ( maxLength > int.MaxValue )
throw new ArgumentException( $"Output rect size ({maxLength}) too large" );
var sliceSize = GetImageFormatSize( dstFormat ) * maxLength;
if ( sliceSize <= 0 )
throw new ArgumentException( $"{nameof( dstRect )} invalid" );
if ( !SandboxedUnsafe.IsAcceptablePod<T>() )
throw new ArgumentException( $"{nameof( T )} must be a POD type" );
var dataSize = (long)dstData.Length * pixelByteSize;
if ( dataSize < sliceSize )
{
throw new ArgumentException( "Output array is too small to fit the given pixel range.",
nameof( dstData ) );
}
var nativeSrcRect = new NativeRect { x = srcRect.X, y = srcRect.Y, w = srcRect.Width, h = srcRect.Height };
var nativeDstRect = new NativeRect { x = dstRect.X, y = dstRect.Y, w = dstRect.W, h = dstRect.H };
fixed ( T* dataPtr = dstData )
{
if ( !g_pRenderDevice.ReadTexturePixels( native, ref nativeSrcRect, slice, mip, ref nativeDstRect, (IntPtr)dataPtr, dstFormat, (int)dstStride * pixelByteSize ) )
throw new Exception( "Unable to read texture pixels" );
}
}
/// <summary>
/// Reads a 3D range of pixel values from the texture at the specified mip level, writing to <paramref name="dstData"/>.
/// This can be used with a 2D texture array, or a 3D volume texture.
/// </summary>
/// <typeparam name="T">Pixel value type (e.g., <see cref="Color32"/>, <see cref="float"/>, <see cref="uint"/> or <see cref="byte"/>)</typeparam>
/// <param name="srcBox">Pixel region to read.</param>
/// <param name="mip">Mip level to read from.</param>
/// <param name="dstData">Array to write to, starting at index 0 for the first read pixel.</param>
/// <param name="dstFormat">Pixel format to use when writing to <paramref name="dstData"/>. We only support some common formats for now.</param>
/// <param name="dstSize">Dimensions of destination pixel array. Matches <paramref name="srcBox"/> by default.</param>
[Pure]
public void GetPixels3D<T>( (int X, int Y, int Z, int Width, int Height, int Depth) srcBox, int mip, Span<T> dstData, ImageFormat dstFormat, (int X, int Y, int Z) dstSize = default ) where T : unmanaged
{
if ( dstSize.X < 0 || dstSize.Y < 0 || dstSize.Z < 0 )
throw new ArgumentException( $"{nameof( dstSize )} can't be negative" );
if ( dstSize.X == 0 )
dstSize.X = srcBox.Width;
if ( dstSize.Y == 0 )
dstSize.Y = srcBox.Height;
if ( dstSize.Z == 0 )
dstSize.Z = srcBox.Depth;
if ( srcBox.X < 0 || srcBox.Y < 0 || srcBox.Z < 0 ||
((long)srcBox.X + srcBox.Width) > Width ||
((long)srcBox.Y + srcBox.Height) > Height ||
((long)srcBox.Z + srcBox.Depth) > Depth )
{
throw new ArgumentException( $"{nameof( srcBox )} out of range" );
}
if ( !SandboxedUnsafe.IsAcceptablePod<T>() )
throw new ArgumentException( $"{nameof( T )} must be a POD type" );
var sliceSize = GetImageFormatSize( dstFormat, dstSize.X, dstSize.Y );
if ( sliceSize <= 0 )
throw new ArgumentException( $"{nameof( dstSize )} invalid" );
var sliceStride = sliceSize / Unsafe.SizeOf<T>();
for ( var z = 0; z < srcBox.Depth; ++z )
{
GetPixels(
(srcBox.X, srcBox.Y, srcBox.Width, srcBox.Height),
srcBox.Z + z, mip,
dstData.Slice( z * sliceStride, sliceStride ),
dstFormat, (dstSize.X, dstSize.Y) );
}
}
/// <summary>
/// Reads a single pixel color.
/// </summary>
[Pure]
public unsafe Color32 GetPixel( float x, float y, int mip = 0 )
{
x = x.FloorToInt();
y = y.FloorToInt();
if ( x < 0 ) return Color.Green;
if ( y < 0 ) return Color.Green;
if ( x > Width - 1 ) return Color.Green;
if ( y > Height - 1 ) return Color.Green;
mip = Math.Clamp( mip, 0, Mips - 1 );
int d = (int)Math.Pow( 2, mip );
var data = new Color32[1];
var source = new NativeRect { x = (int)x, y = (int)y, w = 1, h = 1 };
var dest = new NativeRect { x = 0, y = 0, w = 1, h = 1 };
fixed ( Color32* dataPtr = data )
{
if ( !g_pRenderDevice.ReadTexturePixels( native, ref source, 0, mip, ref dest, (IntPtr)dataPtr, ImageFormat.RGBA8888, 0 ) )
return Color.Red;
}
return data[0];
}
/// <summary>
/// Reads a single pixel color from a volume or array texture.
/// </summary>
[Pure]
public unsafe Color32 GetPixel3D( float x, float y, float z, int mip = 0 )
{
x = x.FloorToInt();
y = y.FloorToInt();
z = z.FloorToInt();
if ( x < 0 ) return Color.Green;
if ( y < 0 ) return Color.Green;
if ( z < 0 ) return Color.Green;
if ( x > Width - 1 ) return Color.Green;
if ( y > Height - 1 ) return Color.Green;
if ( z > Depth - 1 ) return Color.Green;
mip = Math.Clamp( mip, 0, Mips - 1 );
var data = new Color32[1];
var source = new NativeRect { x = (int)x, y = (int)y, w = 1, h = 1 };
var dest = new NativeRect { x = 0, y = 0, w = 1, h = 1 };
fixed ( Color32* dataPtr = data )
{
if ( !g_pRenderDevice.ReadTexturePixels( native, ref source, (int)z, mip, ref dest, (IntPtr)dataPtr, ImageFormat.RGBA8888, 0 ) )
return Color.Red;
}
return data[0];
}
/// <summary>
/// Asynchronously reads all pixel colors from the texture at the specified mip level.
/// </summary>
/// <param name="callback">Callback function that receives the pixel data when ready.</param>
/// <param name="mip">Mip level to read from.</param>
/// <remarks>
/// This operation is asynchronous and won't block the calling thread while data is downloaded from the GPU.
/// The data provided to the callback is only valid for the duration of the callback execution.
/// Storing references to the Span beyond the callback's scope will result in undefined behavior.
/// </remarks>
public void GetPixelsAsync( Action<ReadOnlySpan<Color32>> callback, int mip = 0 )
{
mip = Math.Clamp( mip, 0, Mips - 1 );
var d = 1 << mip;
var desc = g_pRenderDevice.GetTextureDesc( native );
if ( Depth == 1 )
{
var width = desc.m_nWidth / d;
var height = desc.m_nHeight / d;
var rect = (X: 0, Y: 0, Width: width, Height: height);
GetPixelsAsync( callback, ImageFormat.RGBA8888, rect, 0, mip );
}
else
{
var box = (X: 0, Y: 0, Z: 0, Width: desc.m_nWidth / d, Height: desc.m_nHeight / d, Depth: Depth / d);
GetPixelsAsync3D( callback, ImageFormat.RGBA8888, box, mip );
}
}
/// <summary>
/// Asynchronously reads a 2D range of pixel values from the texture at the specified mip level.
/// </summary>
/// <typeparam name="T">Pixel value type (e.g., <see cref="Color32"/>, <see cref="float"/>, <see cref="uint"/> or <see cref="byte"/>)</typeparam>
/// <param name="callback">Callback function that receives the pixel data when ready.</param>
/// <param name="dstFormat">Pixel format to use when writing to the destination buffer.</param>
/// <param name="srcRect">Pixel region to read. If omitted full texture will be read.</param>
/// <param name="slice">For 2D texture arrays or 3D texture volumes, which slice to read from.</param>
/// <param name="mip">Mip level to read from.</param>
/// <remarks>
/// This operation is asynchronous and won't block the calling thread while data is downloaded from the GPU.
/// The data provided to the callback is only valid for the duration of the callback execution.
/// Storing references to the Span beyond the callback's scope will result in undefined behavior.
/// </remarks>
public void GetPixelsAsync<T>( Action<ReadOnlySpan<T>> callback, ImageFormat dstFormat = ImageFormat.Default, (int X, int Y, int Width, int Height) srcRect = default, int slice = 0, int mip = 0 ) where T : unmanaged
{
if ( srcRect.X < 0 || srcRect.Y < 0 ||
((long)srcRect.X + srcRect.Width) > Width ||
((long)srcRect.Y + srcRect.Height) > Height )
{
throw new ArgumentException( $"{nameof( srcRect )} out of range" );
}
if ( !SandboxedUnsafe.IsAcceptablePod<T>() )
throw new ArgumentException( $"{nameof( T )} must be a POD type" );
if ( dstFormat == ImageFormat.Default ) dstFormat = ImageFormat;
var context = g_pRenderDevice.CreateRenderContext( 0 );
context.ReadTextureAsync( this, ( readData, readFormat, readMip, readWidth, readHeight, doneWithData ) =>
{
if ( dstFormat != readFormat )
{
int dstSize = GetImageFormatSize( dstFormat, readWidth, readHeight );
byte[] dstBuffer = ArrayPool<byte>.Shared.Rent( dstSize );
try
{
ConvertImageDataTo( readData, readFormat, dstBuffer.AsSpan(), dstFormat, readWidth, readHeight );
doneWithData();
var typedResult = MemoryMarshal.Cast<byte, T>( dstBuffer );
callback( typedResult );
}
finally
{
ArrayPool<byte>.Shared.Return( dstBuffer );
}
}
else
{
var typedResult = MemoryMarshal.Cast<byte, T>( readData );
callback( typedResult );
}
}, slice, mip, srcRect );
context.Submit();
g_pRenderDevice.ReleaseRenderContext( context );
}
/// <summary>
/// Asynchronously reads a 3D range of pixel values from the texture at the specified mip level.
/// </summary>
/// <typeparam name="T">Pixel value type (e.g., <see cref="Color32"/>, <see cref="float"/>, <see cref="uint"/> or <see cref="byte"/>)</typeparam>
/// <param name="callback">Callback function that receives the pixel data when ready.</param>
/// <param name="dstFormat">Pixel format to use when writing to the destination buffer.</param>
/// <param name="srcBox">Pixel region to read. If omitted full texture will be read.</param>
/// <param name="mip">Mip level to read from.</param>
/// <remarks>
/// This operation is asynchronous and won't block the calling thread while data is downloaded from the GPU.
/// The data provided to the callback is only valid for the duration of the callback execution.
/// Storing references to the Span beyond the callback's scope will result in undefined behavior.
/// </remarks>
public void GetPixelsAsync3D<T>( Action<ReadOnlySpan<T>> callback, ImageFormat dstFormat = ImageFormat.Default, (int X, int Y, int Z, int Width, int Height, int Depth) srcBox = default, int mip = 0 ) where T : unmanaged
{
// Default to full texture if not specified
if ( srcBox.Depth == 0 )
srcBox.Depth = Depth;
if ( srcBox.X < 0 || srcBox.Y < 0 || srcBox.Z < 0 ||
((long)srcBox.X + srcBox.Width) > Width ||
((long)srcBox.Y + srcBox.Height) > Height ||
((long)srcBox.Z + srcBox.Depth) > Depth )
{
throw new ArgumentException( $"{nameof( srcBox )} out of range" );
}
if ( !SandboxedUnsafe.IsAcceptablePod<T>() )
throw new ArgumentException( $"{nameof( T )} must be a POD type" );
var sliceSize = GetImageFormatSize( dstFormat, srcBox.Width, srcBox.Height );
if ( sliceSize <= 0 )
throw new ArgumentException( $"{nameof( srcBox )} invalid size" );
if ( dstFormat == ImageFormat.Default ) dstFormat = ImageFormat;
var totalBytes = sliceSize * srcBox.Depth;
// Create final buffer to hold all slices
byte[] resultBuffer = ArrayPool<byte>.Shared.Rent( totalBytes );
var completedSlices = 0;
var context = g_pRenderDevice.CreateRenderContext( 0 );
// Process each slice asynchronously
for ( var z = 0; z < srcBox.Depth; z++ )
{
var currentZ = z;
var srcRect = (srcBox.X, srcBox.Y, srcBox.Width, srcBox.Height);
var sliceIndex = srcBox.Z + currentZ;
context.ReadTextureAsync( this, ( readData, readFormat, readMip, readWidth, readHeight, doneWithData ) =>
{
var sliceOffset = currentZ * sliceSize;
if ( dstFormat != readFormat )
{
ConvertImageDataTo( readData, readFormat, resultBuffer.AsSpan( sliceOffset ), dstFormat, readWidth, readHeight );
}
else
{
readData.CopyTo( resultBuffer.AsSpan( sliceOffset, sliceSize ) );
}
doneWithData();
// Check if this was the last slice
if ( Interlocked.Increment( ref completedSlices ) == srcBox.Depth )
{
try
{
var typedResult = MemoryMarshal.Cast<byte, T>( resultBuffer );
// All slices are ready, call the callback with the final buffer
callback( typedResult );
}
finally
{
ArrayPool<byte>.Shared.Return( resultBuffer );
}
}
}, sliceIndex, mip, srcRect );
}
context.Submit();
g_pRenderDevice.ReleaseRenderContext( context );
}
/// <summary>
/// Asynchronously reads the texture into a bitmap at the specified mip level.
/// </summary>
/// <param name="callback">Callback function that receives the bitmap when ready.</param>
/// <param name="mip">Mip level to read from.</param>
/// <remarks>
/// This operation is asynchronous and won't block the calling thread while data is downloaded from the GPU.
/// Unlike the other async methods, the Bitmap provided to the callback is valid beyond the callback's scope
/// as it owns its memory.
/// </remarks>
public void GetBitmapAsync( Action<Bitmap> callback, int mip = 0 )
{
mip = Math.Clamp( mip, 0, Mips - 1 );
var d = 1 << mip;
bool floatingPoint = ImageFormat == ImageFormat.RGBA16161616F;
var desc = g_pRenderDevice.GetTextureDesc( native );
var width = desc.m_nWidth / d;
var height = desc.m_nHeight / d;
var dstFormat = floatingPoint ? ImageFormat.RGBA16161616F : ImageFormat.RGBA8888;
var context = g_pRenderDevice.CreateRenderContext( 0 );
context.ReadTextureAsync( this, ( readData, readFormat, readMip, readWidth, readHeight, doneWithData ) =>
{
var bitmap = new Bitmap( width, height, floatingPoint );
var bitmapData = bitmap.GetBuffer();
if ( dstFormat != readFormat )
{
var byteSpan = MemoryMarshal.Cast<byte, byte>( bitmapData );
ConvertImageDataTo( readData, readFormat, byteSpan, dstFormat, width, height );
}
else
{
readData.CopyTo( bitmapData );
}
doneWithData();
callback( bitmap );
}, 0, mip, (0, 0, width, height) );
context.Submit();
g_pRenderDevice.ReleaseRenderContext( context );
}
/// <summary>
/// Converts image data from one format to another and writes to an existing buffer.
/// </summary>
private static unsafe void ConvertImageDataTo(
ReadOnlySpan<byte> srcData,
ImageFormat srcFormat,
Span<byte> dstBuffer,
ImageFormat dstFormat,
int width,
int height )
{
fixed ( byte* srcPtr = srcData )
fixed ( byte* dstPtr = dstBuffer )
{
ImageLoader.ConvertImageFormat(
(IntPtr)srcPtr, srcFormat,
(IntPtr)dstPtr, dstFormat,
width, height,
0, width * GetImageFormatSize( dstFormat ) );
}
}
}