Gliner/Gpu/GlinerBackends.cs
using System;
using System.Threading.Tasks;
using Sandbox;

namespace GlinerPoc.Gpu;

/// <summary>
/// Phase 8E.1 — the narrow inference-backend contract. Exactly two
/// implementations exist (ScalarCpuBackend, GpuFp32Backend). Both consume
/// the SAME immutable GlinerEncodedRequest (produced once by the shared
/// GlinerProcessor — never tokenized twice) and return the SAME
/// GlinerDecisionResult contract; there is no semantic difference between
/// backends, only execution. The service/UI never see neural details.
/// A null result means the request was cancelled.
/// </summary>
public interface IGlinerInferenceBackend
{
	string Name { get; }
	bool IsReady { get; }
	Task<GlinerPoc.Neural.GlinerDecisionResult> ClassifyEncodedAsync(
		GlinerPoc.Preprocessing.GlinerEncodedRequest encoded, Func<bool> cancelled );
}

/// <summary>
/// Phase 8E.3 — the permanent numerical oracle behind the backend contract.
/// Wraps the validated scalar engine WITHOUT changing its math; execution
/// stays on the component worker thread (Task.RunInThreadAsync) because
/// scalar inference takes seconds and observes the cancel delegate between
/// encoder layers.
/// </summary>
public sealed class ScalarCpuBackend : IGlinerInferenceBackend
{
	private readonly GlinerPoc.Neural.GlinerClassificationEngine _engine;
	public string Name => "Native Scalar CPU";
	public bool IsReady => _engine is not null;

	public ScalarCpuBackend( GlinerPoc.Neural.GlinerClassificationEngine engine ) => _engine = engine;

	public async Task<GlinerPoc.Neural.GlinerDecisionResult> ClassifyEncodedAsync(
		GlinerPoc.Preprocessing.GlinerEncodedRequest encoded, Func<bool> cancelled )
	{
		var result = await GameTask.RunInThreadAsync( () =>
		{
			bool ok = _engine.TryClassifyEncoded( encoded, out var r, null, cancelled );
			return ok ? r : null;
		} );
		return result;
	}
}

/// <summary>
/// Phase 8E.4 — the validated GPU model behind the same contract. Owns the
/// GPU runtime stack (executor/render hook, runtime/shaders, model weights,
/// engine); weights initialize ONCE and stay resident — never re-uploaded per
/// request. Execution happens through the render-context executor; the
/// cancel delegate drops queued jobs / discards dispatched completions
/// (GPU kernels are never aborted mid-flight).
/// </summary>
public sealed class GpuFp32Backend : IGlinerInferenceBackend
{
	private readonly GlinerGpuRuntime _runtime;
	private readonly GlinerGpuModelWeights _model;
	private readonly GlinerGpuClassificationEngine _engine;

	public string Name => GlinerGpuClassificationEngine.BackendName;
	public bool IsReady => _engine is not null;
	public GlinerGpuRuntime Runtime => _runtime;
	public GlinerGpuModelWeights Model => _model;
	public GlinerGpuClassificationEngine Engine => _engine;
	/// <summary>Persistent GPU weight bytes (diagnostics).</summary>
	public long WeightBytes => _model?.WeightBytes ?? 0;

	public GpuFp32Backend( GlinerGpuRuntime runtime, GlinerGpuModelWeights model,
		GlinerGpuClassificationEngine engine )
	{
		_runtime = runtime;
		_model = model;
		_engine = engine;
	}

	public Task<GlinerPoc.Neural.GlinerDecisionResult> ClassifyEncodedAsync(
		GlinerPoc.Preprocessing.GlinerEncodedRequest encoded, Func<bool> cancelled )
		=> _engine.ClassifyEncodedAsync( encoded, diagnostic: false, cancelled );

	public void Dispose()
	{
		_model?.Dispose();
		_runtime?.Dispose();
	}
}
// p8e1
// p8e2
// p8e3
// p8e4
// p8e5