Gliner/Gpu/GlinerGpuExecutor.cs
using System;
using System.Collections.Concurrent;
using Sandbox;
namespace GlinerPoc.Gpu;
/// <summary>
/// Phase 8B.1 — the P8A.1-proven render-context dispatch, isolated into a
/// narrow reusable execution primitive.
///
/// Architecture (pinned by P8A.1; do not revisit without contrary evidence):
/// main thread : Submit jobs (buffer prep, uploads, CPU refs elsewhere)
/// render thread: SceneCustomObject.RenderSceneObject drains the queue
/// inside a valid Graphics.Context; job work performs
/// Graphics.Attributes bindings + ComputeShader.Dispatch
/// chains; optional diagnostic readback is issued here via
/// GpuBuffer.GetDataAsync (the only place it is legal).
///
/// Cancellation semantics (P8B.35, mirrors Phase 7 ownership):
/// queued-but-not-drained jobs may be cancelled (CancelQueued);
/// once a job's work begins it runs to completion — the CALLER discards
/// stale results via its own request/generation id. No GPU resource is
/// ever destroyed to "cancel".
///
/// This class owns NO tensors — buffers belong to GlinerGpuRuntime.
/// </summary>
public sealed class GlinerGpuExecutor : IDisposable
{
/// <summary>One unit of render-context work (+ optional diagnostic readback).</summary>
public sealed class Job
{
public string Name;
public Action Work;
public GpuBuffer<float> ReadbackBuffer;
public int ReadbackCount;
public Action<float[]> OnResult;
public long Seq;
}
private sealed class RenderHook : SceneCustomObject
{
public GlinerGpuExecutor Owner;
public RenderHook( SceneWorld sceneWorld, GlinerGpuExecutor owner )
: base( sceneWorld )
{
Owner = owner;
Bounds = new BBox( Vector3.One * -100000f, Vector3.One * 100000f );
RenderingEnabled = true;
}
public override void RenderSceneObject()
{
Owner.Drain();
}
}
private readonly ConcurrentQueue<Job> _queue = new();
private RenderHook _hook;
private long _seqCounter;
private long _cancelUpTo = -1;
public bool Initialized => _hook is not null;
public int JobsSubmitted { get; private set; }
public int JobsExecuted { get; private set; }
public int JobsCancelled { get; private set; }
public void Initialize( SceneWorld sceneWorld )
{
if ( Initialized )
{
return;
}
_hook = new RenderHook( sceneWorld, this );
}
public void Dispose()
{
_hook?.Delete();
_hook = null;
while ( _queue.TryDequeue( out _ ) )
{
// drop pending work; buffers are owned elsewhere
}
}
/// <summary>GPU-only job (production path: no readback).</summary>
public void Submit( string name, Action work )
{
SubmitReadback( name, work, null, 0, null );
}
/// <summary>
/// Job with a diagnostic readback. The readback is issued INSIDE the
/// render context (after the work) and completes asynchronously;
/// OnResult fires on the readback-completion thread — it must be cheap
/// and thread-safe (marshal to the main thread for UI/scene access).
/// </summary>
public void SubmitReadback( string name, Action work, GpuBuffer<float> readbackBuffer, int readbackCount, Action<float[]> onResult )
{
if ( !Initialized )
{
throw new InvalidOperationException( "[GLI:ERROR] GlinerGpuExecutor not initialized." );
}
_queue.Enqueue( new Job
{
Name = name,
Work = work,
ReadbackBuffer = readbackBuffer,
ReadbackCount = readbackCount,
OnResult = onResult,
Seq = System.Threading.Interlocked.Increment( ref _seqCounter ),
} );
JobsSubmitted++;
}
/// <summary>
/// Cancel every job currently queued but not yet drained. Jobs already
/// draining run to completion (their results are the caller's to discard).
/// Jobs submitted after this call run normally.
/// </summary>
public void CancelQueued()
{
_cancelUpTo = System.Threading.Interlocked.Read( ref _seqCounter );
}
private void Drain()
{
while ( _queue.TryDequeue( out var job ) )
{
if ( job.Seq <= _cancelUpTo )
{
JobsCancelled++;
continue;
}
try
{
job.Work?.Invoke();
}
catch ( Exception e )
{
Log.Error( $"[GLI:GPUX] job '{job.Name}' failed in render context: {e.GetType().Name}: {e.Message}" );
}
if ( job.ReadbackBuffer is not null && job.ReadbackCount > 0 )
{
var jb = job;
try
{
jb.ReadbackBuffer.GetDataAsync( ( ReadOnlySpan<float> data ) =>
{
var arr = new float[jb.ReadbackCount];
for ( int i = 0; i < jb.ReadbackCount; i++ )
{
arr[i] = data[i];
}
jb.OnResult?.Invoke( arr );
}, 0, jb.ReadbackCount );
}
catch ( Exception e )
{
Log.Error( $"[GLI:GPUX] job '{job.Name}' readback failed: {e.GetType().Name}: {e.Message}" );
}
}
JobsExecuted++;
}
}
}