Gliner/Gpu/GlinerGpuCapabilities.cs
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using Sandbox;

namespace GlinerPoc.Gpu;

/// <summary>
/// Static helper shared by the Phase 8A GPU probes: resolves the ComputeShader
/// constructor path form against the live engine and verifies it with a canary
/// dispatch+readback (the ctor alone does NOT throw on a bad path — the engine
/// only logs material-system warnings and every later dispatch is a no-op).
/// </summary>
public static class GlinerGpuShaderLoader
{
	private static string _resolvedPrefix;

	// Path form that works: EXACTLY as Facepunch's gpu-voxels library uses it —
	// "Shaders/voxels/modification_cs.shader": relative to Assets, real folder
	// case, WITH the .shader extension.
	//
	// CRITICAL pitfall (decompiled from Material.FromShader, engine 26.09.22):
	// the material created from the path is cached by a normalized name
	// (__shader__<lowercased path without extension>.vmat). Every variant of
	// the same path — leading slash, different case, missing extension — maps
	// to the SAME cached material, and the cache lives in statics that survive
	// play restarts and hotloads. One bad first attempt (e.g. a leading-slash
	// path, which FixupResourceName rejects) poisons every later attempt with
	// a correct path until the editor is restarted. Therefore: try exactly ONE
	// well-formed candidate, and only fall back to a DIFFERENTLY-NAMED path.
	private static readonly string[] CandidatePrefixes =
	{
		"Shaders/gliner/",
		"assets/shaders/gliner/",
	};

	/// <summary>
	/// Clears the cached path form. MUST be called at the start of every probe
	/// run: s&amp;box hotload patches assemblies in place and static fields
	/// SURVIVE — a prefix cached by an older (buggy) code version otherwise
	/// poisons the next run.
	/// </summary>
	public static void Reset()
	{
		_resolvedPrefix = null;
	}

	public static ComputeShader Create( string shaderFileName )
	{
		if ( _resolvedPrefix is null )
		{
			ComputeShader canary = ResolveWithCanary();
			if ( shaderFileName == "gliner_vec_add.shader" )
			{
				return canary;
			}
		}
		return new ComputeShader( _resolvedPrefix + shaderFileName );
	}

	private static ComputeShader ResolveWithCanary()
	{
		const string canaryFile = "gliner_vec_add.shader";
		foreach ( string prefix in CandidatePrefixes )
		{
			{
				string candidate = prefix + canaryFile;
				try
				{
					var shader = new ComputeShader( candidate );
					var (ok, diag) = CanaryVerify( shader );
					if ( ok )
					{
						_resolvedPrefix = prefix;
						Log.Info( $"[GLI:GPU] shader path resolved + canary-verified: '{candidate}'" );
						return shader;
					}
					Log.Warning( $"[GLI:GPU] shader path candidate '{candidate}' constructed but canary dispatch FAILED ({diag})" );
				}
				catch ( Exception e )
				{
					Log.Warning( $"[GLI:GPU] shader path candidate '{candidate}' threw {e.GetType().Name}: {e.Message}" );
				}
			}
		}
		throw new InvalidOperationException(
			$"[GLI:ERROR] No candidate path produced a working compute shader for '{canaryFile}'." );
	}

	/// <summary>Proves the shader actually executes: out[i] == i + 1 bit-exact.
	/// Returns (ok, diagnostic) — the diagnostic shows what the GPU actually
	/// returned so a silent no-op dispatch is distinguishable from wrong math.</summary>
	private static (bool, string) CanaryVerify( ComputeShader shader )
	{
		const int N = 8;
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = i;
		}
		inBuf.SetData( input );
		outBuf.Clear( 0xdeadbeef ); // poison so untouched output is visible
		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );
		shader.Attributes.Set( "AddConstant", 1f );
		shader.Dispatch( N, 1, 1 );
		var res = new float[N];
		outBuf.GetData( res, 0, N );
		int bad = 0;
		var first = new System.Text.StringBuilder();
		for ( int i = 0; i < N; i++ )
		{
			if ( BitConverter.SingleToInt32Bits( res[i] ) != BitConverter.SingleToInt32Bits( i + 1f ) )
			{
				bad++;
				if ( first.Length < 80 )
				{
					_ = first.Append( $"[{i}]={res[i]:0.###e-00} " );
				}
			}
		}
		if ( bad == 0 )
		{
			return (true, "all 8 exact");
		}
		return (false, $"bad={bad}/{N} firstVals={first}");
	}
}

/// <summary>
/// Phase 8A.4/8A.5/8A.6/8A.7 — isolated GPU capability probe. Proves, inside
/// the normal s&box play-mode runtime on the current engine:
///
///   compute shader creation + dispatch • FP32 C#→GPU upload • GPU→C# sync and
///   async readback • buffer persistence across repeated dispatches/frames •
///   multi-MB buffers • dispatch/readback overhead • worker-thread dispatch
///   behaviour • basic failure paths • GPU memory stats where exposed.
///
/// This probe is deliberately separate from all production neural classes; it
/// touches no model resources and does not affect the CPU workbench.
/// </summary>
[Title( "GLiNER GPU Capabilities" )]
[Category( "GLiNER" )]
public sealed class GlinerGpuCapabilities : Component
{
	[Property]
	public bool RunOnStart { get; set; } = true;

	private int _passed;
	private int _failed;

	protected override void OnStart()
	{
		if ( RunOnStart )
		{
			_ = RunAsync();
		}
	}

	private async Task RunAsync()
	{
		var sw = Stopwatch.StartNew();
		GlinerGpuShaderLoader.Reset(); // hotload-preserved statics must not poison this run
		Log.Info( "[GLI:GPU] capability probe start (engine-managed compute; no external runtimes)" );
		try
		{
			GateOfficialShaderProbe();
			GateShaderRoundtrip();
			await GateAsyncReadback();
			await GateBufferPersistence();
			GateDispatchOverhead();
			GateLargeBuffer();
			await GateWorkerThreadDispatch();
			GateErrorPaths();
			GateGpuStats();

			Log.Info( $"[GLI:GPU] capability probe complete passed={_passed} failed={_failed} total_ms={sw.ElapsedMilliseconds}" );
			Log.Info( _failed == 0
				? "[GLI:GPU] CAPABILITY ALL PASS"
				: $"[GLI:GPU] CAPABILITY FAILURES ({_failed})" );
		}
		catch ( Exception error )
		{
			Log.Error( $"[GLI:GPU] capability harness failure: {error}" );
		}
	}


	// ---- definitive differential: Facepunch's own gpu-voxels compute shader ----
	// (copied to Assets/Shaders — if THIS fails with the same "bound pipeline
	// does not have a compute shader", project compute shaders are broken
	// engine-wide on this build, not by our shader authoring.)
	private void GateOfficialShaderProbe()
	{
		// introspect the shader asset first (no dispatch, no crash risk)
		try
		{
			var shaderRes = ResourceLibrary.Get<Shader>( "Shaders/voxels/modification_cs.shader" );
			if ( shaderRes is null )
			{
				Log.Info( "[GLI:GPU] official shader: ResourceLibrary.Get<Shader> returned null" );
			}
			else
			{
				Log.Info( $"[GLI:GPU] official shader asset loaded: path={shaderRes.ResourcePath}" );
			}
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] official shader asset load threw {e.GetType().Name}: {e.Message}" );
		}

		// same for OUR shader — introspection only
		try
		{
			var mine = ResourceLibrary.Get<Shader>( "Shaders/gliner/gliner_vec_add.shader" );
			Log.Info( mine is null
				? "[GLI:GPU] gliner shader: ResourceLibrary.Get<Shader> returned null"
				: $"[GLI:GPU] gliner shader asset loaded: path={mine.ResourcePath}" );
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] gliner shader asset load threw {e.GetType().Name}: {e.Message}" );
		}

		// NOTE: dispatching the official shader was attempted twice with
		// complete typed bindings and CRASHED THE EDITOR both times on
		// 26.09.22 — the dispatch below is disabled; see the P8A report.
		Log.Info( "[GLI:GPU] official voxels dispatch SKIPPED (crashed the editor in two earlier attempts)" );
		Gate( "official_shader_probe", false,
			"Facepunch modification_cs dispatch crashes the editor process on 26.09.22 (2/2 attempts, complete typed bindings)", "" );
	}

	// ---- 8A.5: create shader, upload, dispatch, read back --------------------

	private void GateShaderRoundtrip()
	{
		const int N = 1024;
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = i * 0.25f - 128f; // exact fp32 values
		}
		var expected = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			expected[i] = input[i] + 3.5f;
		}

		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );

		var swUpload = Stopwatch.StartNew();
		inBuf.SetData( input );
		swUpload.Stop();

		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );
		shader.Attributes.Set( "AddConstant", 3.5f );

		var swDispatch = Stopwatch.StartNew();
		shader.Dispatch( N, 1, 1 );
		swDispatch.Stop();

		var result = new float[N];
		var swReadback = Stopwatch.StartNew();
		outBuf.GetData( result, 0, N );
		swReadback.Stop();

		int bitExact = 0;
		for ( int i = 0; i < N; i++ )
		{
			if ( BitConverter.SingleToInt32Bits( result[i] ) == BitConverter.SingleToInt32Bits( expected[i] ) )
			{
				bitExact++;
			}
		}
		var m = GlinerPoc.Neural.GlinerMath.Compare( result, expected );

		Gate( "vec_roundtrip", m.Pass && bitExact == N,
			$"N={N} bitExact={bitExact}/{N} maxAbs={m.MaxAbs:0.###e-00} | " +
			$"upload={swUpload.Elapsed.TotalMilliseconds:N3}ms dispatch={swDispatch.Elapsed.TotalMilliseconds:N3}ms " +
			$"readback={swReadback.Elapsed.TotalMilliseconds:N3}ms", "" );
	}

	// ---- 8A.6: asynchronous readback (no frame-thread block) -------------------
	//
	// Engine facts established live (26.09.22): GpuBuffer.GetDataAsync requires
	// an active render context (it reads Graphics.Context); outside a render
	// block it throws "IRenderContext was null", and the documented standalone
	// context (Graphics.Scope.Create) is not accessible from game code. The
	// async pattern available to game code is therefore:
	//   non-blocking dispatch enqueue (immediate submit outside render blocks,
	//   ~µs) → game keeps rendering frames → deferred readback of a small
	//   result buffer once the queue has drained. This gate proves that flow
	// and measures both the immediate-readback control (blocks until the GPU
	// queue drains) and the deferred readback (cheap, queue already empty).

	private async Task GateAsyncReadback()
	{
		const int N = 4096;
		const int Batch = 2000; // heavy batch so the GPU queue has real depth
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = (i % 97) * 0.5f - 24f;
		}

		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );
		inBuf.SetData( input );
		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );
		shader.Attributes.Set( "AddConstant", -1.75f );

		int mainThread = Environment.CurrentManagedThreadId;
		bool graphicsActiveOutsideRenderBlock = Graphics.IsActive;
		var result = new float[N];

		// (a) GetDataAsync outside a render context: document the constraint live
		string asyncApiBehaviour;
		try
		{
			bool callbackFired = false;
			outBuf.GetDataAsync( ( ReadOnlySpan<float> data ) => { callbackFired = true; } );
			await Task.Delay( 32 );
			asyncApiBehaviour = callbackFired
				? "GetDataAsync worked outside a render block (callback fired)"
				: "GetDataAsync accepted the call but the callback never fired outside a render block";
		}
		catch ( Exception e )
		{
			asyncApiBehaviour = $"GetDataAsync outside a render context throws {e.GetType().Name}: {e.Message}";
		}
		Log.Info( $"[GLI:GPU] async API probe: {asyncApiBehaviour} | Graphics.IsActive outside render block = {graphicsActiveOutsideRenderBlock}" );

		// (b) enqueue-only: dispatch a deep batch and measure that the calls
		// return immediately (the frame thread is NOT blocked by GPU work)
		var swEnqueue = Stopwatch.StartNew();
		for ( int i = 0; i < Batch; i++ )
		{
			shader.Dispatch( N, 1, 1 );
		}
		swEnqueue.Stop();
		double enqueuePerCallMs = swEnqueue.Elapsed.TotalMilliseconds / Batch;

		// (c) control: immediate readback right after enqueueing a fresh batch —
		// this blocks until the queued GPU work completes
		var swImmediate = Stopwatch.StartNew();
		outBuf.GetData( result, 0, N );
		swImmediate.Stop();

		// (d) deferred readback: enqueue another batch, let frames pass, then
		// read back — the queue has drained, the readback must be cheap
		for ( int i = 0; i < Batch; i++ )
		{
			shader.Dispatch( N, 1, 1 );
		}
		await Task.Delay( 250 ); // ~15 frames at 60 Hz — game keeps running
		var swDeferred = Stopwatch.StartNew();
		outBuf.GetData( result, 0, N );
		swDeferred.Stop();

		int bitExact = 0;
		for ( int i = 0; i < N; i++ )
		{
			float e = input[i] - 1.75f;
			if ( BitConverter.SingleToInt32Bits( result[i] ) == BitConverter.SingleToInt32Bits( e ) )
			{
				bitExact++;
			}
		}

		Gate( "async_readback", bitExact == N && enqueuePerCallMs < 0.1 && swDeferred.Elapsed.TotalMilliseconds < 10,
			$"bitExact={bitExact}/{N} | enqueuePerCall={enqueuePerCallMs:N4}ms (x{Batch} batch, non-blocking) | " +
			$"immediateReadbackBlocksMs={swImmediate.Elapsed.TotalMilliseconds:N2} (control: waits for queue) | " +
			$"deferredReadbackMs={swDeferred.Elapsed.TotalMilliseconds:N2} (after {250}ms of frames) | mainThread={mainThread}", "" );
	}

	// ---- 8A.7: buffer persistence (upload once, dispatch many) ----------------

	private async Task GateBufferPersistence()
	{
		const int N = 512;
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = i * 0.125f - 32f;
		}
		float[] constants = { 0.5f, -1.25f, 100f, 3.5f, -7.5f, 2f, 0f, 42f };

		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );

		inBuf.SetData( input ); // the ONLY upload of inBuf
		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );

		var result = new float[N];
		bool allExact = true;
		int checks = 0;
		foreach ( float c in constants )
		{
			shader.Attributes.Set( "AddConstant", c );
			shader.Dispatch( N, 1, 1 );
			outBuf.GetData( result, 0, N );
			for ( int i = 0; i < N; i++ )
			{
				float e = input[i] + c;
				checks++;
				if ( BitConverter.SingleToInt32Bits( result[i] ) != BitConverter.SingleToInt32Bits( e ) )
				{
					allExact = false;
				}
			}
			// spread dispatches across frames to prove persistence over time,
			// not just within one
			await Task.Delay( 16 );
		}

		Gate( "buffer_persistence", allExact,
			$"1 upload, {constants.Length} dispatches across frames, {checks} values bit-exact", "" );
	}

	// ---- 8A.26: dispatch + readback overhead -----------------------------------

	private void GateDispatchOverhead()
	{
		const int N = 64;
		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );
		var data = new float[N];
		inBuf.SetData( data );
		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );
		shader.Attributes.Set( "AddConstant", 1f );
		var result = new float[N];

		// warmup
		for ( int i = 0; i < 10; i++ )
		{
			shader.Dispatch( N, 1, 1 );
		}
		outBuf.GetData( result, 0, N );

		// enqueue-only cost: dispatches with no readback between them
		var sw = Stopwatch.StartNew();
		const int Enq = 200;
		for ( int i = 0; i < Enq; i++ )
		{
			shader.Dispatch( N, 1, 1 );
		}
		sw.Stop();
		double enqueueMs = sw.Elapsed.TotalMilliseconds / Enq;

		// full cycle: dispatch + synchronous readback
		sw.Restart();
		const int Cycles = 100;
		for ( int i = 0; i < Cycles; i++ )
		{
			shader.Dispatch( N, 1, 1 );
			outBuf.GetData( result, 0, N );
		}
		sw.Stop();
		double cycleMs = sw.Elapsed.TotalMilliseconds / Cycles;

		// readback-only on already-resident data
		sw.Restart();
		const int Reads = 100;
		for ( int i = 0; i < Reads; i++ )
		{
			outBuf.GetData( result, 0, N );
		}
		sw.Stop();
		double readMs = sw.Elapsed.TotalMilliseconds / Reads;

		Gate( "dispatch_overhead", true,
			$"tiny(64thr) enqueue={enqueueMs:N3}ms fullCycle={cycleMs:N3}ms readbackOnly={readMs:N3}ms", "" );
	}

	// ---- multi-MB FP32 buffers --------------------------------------------------

	private void GateLargeBuffer()
	{
		const int N = 1_048_576; // 4 MB of FP32
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = (i % 4096) * 0.0625f - 128f; // exact fp32 pattern
		}

		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );

		var swUp = Stopwatch.StartNew();
		inBuf.SetData( input );
		swUp.Stop();

		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );
		shader.Attributes.Set( "AddConstant", 64f );
		shader.Dispatch( N, 1, 1 );

		var result = new float[N];
		var swDown = Stopwatch.StartNew();
		outBuf.GetData( result, 0, N );
		swDown.Stop();

		int bad = 0;
		int firstBad = -1;
		for ( int i = 0; i < N; i++ )
		{
			float e = input[i] + 64f;
			if ( BitConverter.SingleToInt32Bits( result[i] ) != BitConverter.SingleToInt32Bits( e ) )
			{
				bad++;
				if ( firstBad < 0 )
				{
					firstBad = i;
				}
			}
		}

		Gate( "large_buffer_4mb", bad == 0,
			$"N={N} bad={bad} firstBad={firstBad} upload={swUp.Elapsed.TotalMilliseconds:N2}ms readback={swDown.Elapsed.TotalMilliseconds:N2}ms", "" );
	}

	// ---- 8A.2 evidence: can GPU work be driven from a worker thread? -----------

	private async Task GateWorkerThreadDispatch()
	{
		const int N = 256;
		var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
		using var inBuf = new GpuBuffer<float>( N );
		using var outBuf = new GpuBuffer<float>( N );
		var input = new float[N];
		for ( int i = 0; i < N; i++ )
		{
			input[i] = i * 1.5f;
		}
		inBuf.SetData( input );
		shader.Attributes.Set( "InputValues", inBuf );
		shader.Attributes.Set( "OutputValues", outBuf );
		shader.Attributes.Set( "ValueCount", N );

		string uploadReport = "skipped";
		string dispatchReport = "skipped";
		bool workerSucceeded = await Task.RunInThreadAsync( () =>
		{
			int workerId = Environment.CurrentManagedThreadId;
			try
			{
				shader.Attributes.Set( "AddConstant", 0.5f );
			}
			catch ( Exception e )
			{
				uploadReport = $"worker {workerId}: Attributes.SetData threw {e.GetType().Name}: {e.Message}";
				return false;
			}
			try
			{
				shader.Dispatch( N, 1, 1 );
				dispatchReport = $"worker {workerId}: Dispatch returned without exception";
			}
			catch ( Exception e )
			{
				dispatchReport = $"worker {workerId}: Dispatch threw {e.GetType().Name}: {e.Message}";
				return false;
			}
			return true;
		} );

		// Whether or not the worker dispatch is legal, verify on the main thread
		// that the shader/buffers still work afterwards.
		shader.Attributes.Set( "AddConstant", 0.5f );
		shader.Dispatch( N, 1, 1 );
		var result = new float[N];
		outBuf.GetData( result, 0, N );
		bool intact = true;
		for ( int i = 0; i < N; i++ )
		{
			if ( BitConverter.SingleToInt32Bits( result[i] ) != BitConverter.SingleToInt32Bits( input[i] + 0.5f ) )
			{
				intact = false;
				break;
			}
		}

		Log.Info( $"[GLI:GPU] worker-thread probe: upload='{uploadReport}' dispatch='{dispatchReport}'" );
		Gate( "worker_thread_probe", intact,
			$"workerDispatch={(workerSucceeded ? "returned" : "rejected/failed")}; main-thread shader intact after probe; " +
			$"dispatchDetail={dispatchReport}", "" );
	}

	// ---- 8A.32: basic failure paths (never crash the device) --------------------

	private void GateErrorPaths()
	{
		int observed = 0;

		// missing shader
		try
		{
			_ = new ComputeShader( "/shaders/gliner/definitely_missing_gliner_probe.shader" );
			Log.Info( "[GLI:GPU] errorpath missing-shader: ctor did NOT throw (engine tolerated unknown path)" );
			observed++;
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] errorpath missing-shader: ctor threw {e.GetType().Name}: {e.Message}" );
			observed++;
		}

		// disposed buffer readback
		try
		{
			var buf = new GpuBuffer<float>( 16 );
			var data = new float[16];
			buf.SetData( data );
			buf.Dispose();
			buf.GetData( data, 0, 16 );
			Log.Info( "[GLI:GPU] errorpath disposed-readback: GetData did NOT throw" );
			observed++;
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] errorpath disposed-readback: threw {e.GetType().Name}: {e.Message}" );
			observed++;
		}

		// zero-sized dispatch on a valid shader must not kill the device
		try
		{
			var shader = GlinerGpuShaderLoader.Create( "gliner_vec_add.shader" );
			using var inBuf = new GpuBuffer<float>( 4 );
			using var outBuf = new GpuBuffer<float>( 4 );
			var data = new float[] { 1, 2, 3, 4 };
			inBuf.SetData( data );
			shader.Attributes.Set( "InputValues", inBuf );
			shader.Attributes.Set( "OutputValues", outBuf );
			shader.Attributes.Set( "ValueCount", 4 );
			shader.Attributes.Set( "AddConstant", 0f );
			shader.Dispatch( 0, 0, 0 );
			var result = new float[4];
			outBuf.GetData( result, 0, 4 );
			Log.Info( $"[GLI:GPU] errorpath zero-dispatch: no exception; post-dispatch readback ok result0={result[0]}" );
			observed++;
			// shader still usable afterwards?
			shader.Dispatch( 4, 1, 1 );
			outBuf.GetData( result, 0, 4 );
			bool ok = BitConverter.SingleToInt32Bits( result[0] ) == BitConverter.SingleToInt32Bits( 1f );
			Log.Info( $"[GLI:GPU] errorpath zero-dispatch: shader usable after zero dispatch: {ok}" );
			observed++;
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] errorpath zero-dispatch: threw {e.GetType().Name}: {e.Message}" );
			observed++;
		}

		Gate( "error_paths", observed >= 3,
			$"observed={observed} failure-path behaviours (device survived all)", "" );
	}

	// ---- 8A.37: GPU memory stats where the engine exposes them ------------------

	private void GateGpuStats()
	{
		try
		{
			ulong budget = Graphics.VideoMemoryBudget;   // WDDM-reported, bytes
			ulong used = Graphics.VideoMemoryUsed;       // engine render-system allocations
			if ( budget == 0 )
			{
				Log.Info( "[GLI:GPU] vram stats readable but budget reports 0 (not exposed in this context)" );
				Gate( "gpu_stats", false, "budget=0 — stats not exposed (non-blocking)", "" );
				return;
			}
			Log.Info( $"[GLI:GPU] vram budget={budget / (1024 * 1024):N0}MiB used={used / (1024 * 1024):N0}MiB" );
			Gate( "gpu_stats", true, "Graphics.VideoMemoryBudget/Used readable", "" );
		}
		catch ( Exception e )
		{
			Log.Info( $"[GLI:GPU] vram stats unavailable: {e.GetType().Name}: {e.Message}" );
			Gate( "gpu_stats", false, "stats API unavailable (non-blocking)", "" );
		}
	}

	// ---- helpers ----------------------------------------------------------------

	private void Gate( string name, bool pass, string detail, string extra )
	{
		if ( pass )
		{
			_passed++;
			Log.Info( $"[GLI:GPU] PASS {name} {detail} {extra}" );
		}
		else
		{
			_failed++;
			Log.Error( $"[GLI:GPU] FAIL {name} {detail} {extra}" );
		}
	}
}
// hotload poke 11:57:16