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