using NativeEngine;
using System.Collections.Concurrent;
using System.Threading;
namespace Sandbox;
///
/// Provides methods for reading GPU data asynchronously without blocking the render thread.
///
///
/// Handles the management of callbacks and memory for reading textures and buffers from GPU memory.
/// Data retrieved through these methods is only valid during the callback execution.
///
internal static class AsyncGPUReadback
{
internal delegate void TextureReadDelegate( Span readData, ImageFormat readFormat, int readMipLevel, int readWidth, int readHeight, Action doneWithData );
private static ConcurrentDictionary _activeReadTextureCallbacks = new();
private static int _nextReadTextureCallbackId = 1;
///
/// Reads texture data from GPU memory, data is kept valid until after the callback task is finished.
/// If srcRect is not specified, the entire texture will be read.
///
internal static void ReadTextureAsync( this IRenderContext context, Texture texture, TextureReadDelegate callback, int slice = 0, int mipLevel = 0, (int X, int Y, int Width, int Height) srcRect = default )
{
if ( texture is null )
return;
context.ReadTextureAsync( texture.native, callback, slice, mipLevel, srcRect );
}
internal static void ReadTextureAsync( this IRenderContext context, ITexture texture, TextureReadDelegate callback, int slice = 0, int mipLevel = 0, (int X, int Y, int Width, int Height) srcRect = default )
{
if ( texture.IsNull )
return;
// Generate a unique ID for this callback
int callbackId = Interlocked.Increment( ref _nextReadTextureCallbackId );
// Destroyed after Done is called
var nativeCallbackObject = CReadTexturePixelsManagedCallback.Create();
nativeCallbackObject.SetManagedId( callbackId );
_activeReadTextureCallbacks.TryAdd( callbackId, callback );
var nativeRect = new NativeRect( srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height );
context.ReadTexturePixels( texture, nativeCallbackObject, nativeRect, slice, mipLevel, true );
}
///
/// Called by native to dispatch the managed callback for texture read operations.
///
internal static void DispatchManagedReadTextureCallback( CReadTexturePixelsManagedCallback caller, IntPtr pData, ImageFormat format, int nMipLevel, int nWidth, int nHeight, int nPitchInBytes )
{
var callerId = caller.GetManagedId();
if ( _activeReadTextureCallbacks.TryRemove( callerId, out var callback ) )
{
// We move this to another thread, to unblock the render thread as soon as possible
Task.Run( () =>
{
var doneEarly = false;
try
{
unsafe
{
var doneWithData = () =>
{
doneEarly = true;
caller.Done();
};
var bytes = new Span( pData.ToPointer(), nHeight * nPitchInBytes );
callback( bytes, format, nMipLevel, nWidth, nHeight, doneWithData );
}
}
finally
{
if ( !doneEarly )
{
caller.Done();
}
}
} );
}
}
internal delegate void BufferReadDelegate( Span pData );
private static ConcurrentDictionary _activeReadBufferCallbacks = new();
private static int _nextReadBufferCallbackId = 1;
internal static void ReadBufferAsync( this IRenderContext context, GpuBuffer buffer, BufferReadDelegate callback, int offset, int bytesToRead )
{
// Generate a unique ID for this callback
int callbackId = Interlocked.Increment( ref _nextReadBufferCallbackId );
// Destroyed after Done is called
var nativeCallbackObject = CReadBufferManagedCallback.Create();
nativeCallbackObject.SetManagedId( callbackId );
_activeReadBufferCallbacks.TryAdd( callbackId, callback );
context.ReadBuffer( buffer.native, nativeCallbackObject, offset, bytesToRead, true );
}
///
/// Called by native to dispatch the managed callback for buffer read operations.
///
internal static void DispatchManagedReadBufferCallback( CReadBufferManagedCallback caller, IntPtr pData, int nBytes )
{
var callerId = caller.GetManagedId();
if ( _activeReadBufferCallbacks.TryRemove( callerId, out var callback ) )
{
Task.Run( () =>
{
try
{
unsafe
{
var bytes = new Span( pData.ToPointer(), nBytes );
callback( bytes );
}
}
finally
{
caller.Done();
}
} );
}
}
}