GpuSkin.cs
using System;
using Sandbox.Rendering;
using SquishyTerry.Sim;

namespace SquishyTerry;

/// <summary>
/// Runs the <see cref="SkinLayer"/> wrinkle cloth on the GPU, like the original game did. The CPU
/// SkinLayer still builds the topology and rest lengths; every frame this uploads the simulation
/// mesh and records the same passes (attach, constrain, 15 x neighbour/opposite relax, normals) from
/// <c>shaders/squishy_skin_cs.shader</c> into a command list that the camera runs while rendering.
///
/// The skin is drawn from separate "render" buffers that the command list refreshes after the opaque
/// pass, so the depth pre-pass and the colour pass of a frame always see the same surface.
/// </summary>
public sealed class GpuSkin : IDisposable
{
	const int Iterations = 15;

	readonly SkinLayer _layer;
	readonly ComputeShader _shader = new( "shaders/squishy_skin_cs.shader" );
	readonly CommandList _commands = new( "Squishy skin cloth" );
	readonly int _count;

	readonly GpuBuffer<Vector4> _basePositions, _baseNormals, _bary, _attach, _cloth, _scratch, _normals;
	readonly GpuBuffer<Vector4> _renderPositions, _renderNormals;
	readonly GpuBuffer<int> _baseIndices, _skinTriangle, _fixed, _neighbours, _opposites;
	readonly GpuBuffer<float> _neighbourRest, _oppositeRest;

	readonly Vector4[] _baseUpload;
	readonly int[] _fixedUpload;

	CameraComponent _camera;
	bool _initialized;
	int _pinnedTriangle = -1;

	/// <summary>Cloth positions to draw (simulation space), one float4 per skin vertex.</summary>
	public GpuBuffer<Vector4> Positions => _renderPositions;

	/// <summary>Cloth normals to draw (simulation space).</summary>
	public GpuBuffer<Vector4> Normals => _renderNormals;

	public GpuSkin( Face face, SkinLayer layer )
	{
		_layer = layer;
		_count = layer.VertexCount;
		layer.InitializeRest( face );

		int n = _count;
		int baseCount = face.VertexCount;

		_basePositions = new GpuBuffer<Vector4>( baseCount );
		_baseNormals = new GpuBuffer<Vector4>( baseCount );
		_baseIndices = new GpuBuffer<int>( face.Indices.Length );
		_baseIndices.SetData( face.Indices, 0 );
		_baseUpload = new Vector4[baseCount];

		_skinTriangle = new GpuBuffer<int>( n );
		_skinTriangle.SetData( layer.BaseTriangle, 0 );

		var bary = new Vector4[n];
		for ( int i = 0; i < n; i++ )
			bary[i] = new Vector4( layer.BaryW[i], layer.BaryU[i], layer.BaryV[i], layer.Strength[i] );
		_bary = new GpuBuffer<Vector4>( n );
		_bary.SetData( bary, 0 );

		_fixedUpload = new int[n];
		_fixed = new GpuBuffer<int>( n );
		_fixed.SetData( _fixedUpload, 0 );

		_neighbours = new GpuBuffer<int>( n * 6 );
		_neighbours.SetData( layer.Neighbours, 0 );
		_opposites = new GpuBuffer<int>( n * 6 );
		_opposites.SetData( layer.Opposites, 0 );
		_neighbourRest = new GpuBuffer<float>( n * 6 );
		_neighbourRest.SetData( layer.NeighbourRest, 0 );
		_oppositeRest = new GpuBuffer<float>( n * 6 );
		_oppositeRest.SetData( layer.OppositeRest, 0 );

		_attach = new GpuBuffer<Vector4>( n );
		_cloth = new GpuBuffer<Vector4>( n );
		_scratch = new GpuBuffer<Vector4>( n );
		_normals = new GpuBuffer<Vector4>( n );

		// Seed the render buffers with the CPU rest state so the first frames draw a proper surface.
		var seed = new Vector4[n];
		for ( int i = 0; i < n; i++ ) seed[i] = new Vector4( layer.Pos[i].X, layer.Pos[i].Y, layer.Pos[i].Z, 0 );
		_renderPositions = new GpuBuffer<Vector4>( n );
		_renderPositions.SetData( seed, 0 );
		for ( int i = 0; i < n; i++ ) seed[i] = new Vector4( layer.Normals[i].X, layer.Normals[i].Y, layer.Normals[i].Z, 0 );
		_renderNormals = new GpuBuffer<Vector4>( n );
		_renderNormals.SetData( seed, 0 );
	}

	/// <summary>Hooks the cloth passes into a camera's rendering (after the opaque pass).</summary>
	public void AttachTo( CameraComponent camera )
	{
		if ( camera == _camera ) return;
		_camera?.RemoveCommandList( _commands );
		_camera = camera;
		_camera?.AddCommandList( _commands, Stage.AfterOpaque, 0 );
	}

	/// <summary>Uploads this frame's simulation mesh and records the cloth passes (g.prototype.update in the original).</summary>
	public void Update( Face face, bool dragging, int dragTriangle )
	{
		var pos = face.Pos;
		for ( int i = 0; i < pos.Length; i++ ) _baseUpload[i] = new Vector4( pos[i].X, pos[i].Y, pos[i].Z, 0 );
		_basePositions.SetData( _baseUpload, 0 );
		var nrm = face.Normals;
		for ( int i = 0; i < nrm.Length; i++ ) _baseUpload[i] = new Vector4( nrm[i].X, nrm[i].Y, nrm[i].Z, 0 );
		_baseNormals.SetData( _baseUpload, 0 );

		// Like the original, the pinned set is only rewritten while dragging.
		if ( dragging && dragTriangle != _pinnedTriangle )
		{
			Array.Clear( _fixedUpload );
			foreach ( int v in _layer.VerticesOfBaseTriangle( dragTriangle ) ) _fixedUpload[v] = 1;
			_fixed.SetData( _fixedUpload, 0 );
			_pinnedTriangle = dragTriangle;
		}

		Record();
	}

	void Record()
	{
		var c = _commands;
		c.Reset();

		// Shared inputs.
		c.Set( "Count", _count );
		c.Set( "BasePositions", _basePositions );
		c.Set( "BaseNormals", _baseNormals );
		c.Set( "BaseIndices", _baseIndices );
		c.Set( "SkinTriangle", _skinTriangle );
		c.Set( "SkinBary", _bary );
		c.Set( "Fixed", _fixed );
		c.Set( "Attach", _attach );

		Dispatch( 0 );
		c.UavBarrier( _attach );

		if ( !_initialized )
		{
			c.Set( "Output", _cloth );
			Dispatch( 1 );
			c.UavBarrier( _cloth );
			_initialized = true;
		}

		c.Set( "Output", _cloth );
		Dispatch( 2 );
		c.UavBarrier( _cloth );

		// Ping-pong: neighbours cloth -> scratch, opposites scratch -> cloth; each iteration ends in _cloth.
		for ( int it = 0; it < Iterations; it++ )
		{
			c.Set( "RespectFixed", it < Iterations - 1 ? 1 : 0 );

			c.Set( "Links", _neighbours );
			c.Set( "Rest", _neighbourRest );
			c.Set( "Input", _cloth );
			c.Set( "Output", _scratch );
			Dispatch( 3 );
			c.UavBarrier( _scratch );

			c.Set( "Links", _opposites );
			c.Set( "Rest", _oppositeRest );
			c.Set( "Input", _scratch );
			c.Set( "Output", _cloth );
			Dispatch( 3 );
			c.UavBarrier( _cloth );
		}

		c.Set( "Links", _neighbours );
		c.Set( "Input", _cloth );
		c.Set( "Output", _normals );
		Dispatch( 4 );
		c.UavBarrier( _normals );

		// Publish for the next frame's draw.
		c.Set( "Input", _cloth );
		c.Set( "Output", _renderPositions );
		Dispatch( 5 );
		c.Set( "Input", _normals );
		c.Set( "Output", _renderNormals );
		Dispatch( 5 );
		c.UavBarrier( _renderPositions );
		c.UavBarrier( _renderNormals );
	}

	void Dispatch( int pass )
	{
		_commands.SetCombo( "D_PASS", pass );
		_commands.DispatchCompute( _shader, _count, 1, 1 );
	}

	public void Dispose()
	{
		_camera?.RemoveCommandList( _commands );
		_camera = null;

		_basePositions.Dispose();
		_baseNormals.Dispose();
		_bary.Dispose();
		_attach.Dispose();
		_cloth.Dispose();
		_scratch.Dispose();
		_normals.Dispose();
		_renderPositions.Dispose();
		_renderNormals.Dispose();
		_baseIndices.Dispose();
		_skinTriangle.Dispose();
		_fixed.Dispose();
		_neighbours.Dispose();
		_opposites.Dispose();
		_neighbourRest.Dispose();
		_oppositeRest.Dispose();
	}
}