Netcode Bootstrap

This commit is contained in:
Luis Gonzalez
2026-05-31 14:27:52 -07:00
parent 99d8d2d2a9
commit 7fa77ce821
1813 changed files with 2921554 additions and 84 deletions
@@ -0,0 +1,169 @@
using Unity.Assertions;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using UnityEngine;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Toolbox
{
public static class ComputeBufferTools
{
static ComputeShader copyCS;
static ComputeKernel copyKernel;
static ComputeKernel clearKernel;
static readonly string COMPUTE_SHADER_NAME = "RukhankaGPUBufferManipulation";
static readonly string COPY_KERNEL_NAME = "CopyBuffer";
static readonly string CLEAR_KERNEL_NAME = "ClearBuffer";
static readonly int ShaderID_copyBufferElementsCount = Shader.PropertyToID("copyBufferElementsCount");
static readonly int ShaderID_srcBuf = Shader.PropertyToID("srcBuf");
static readonly int ShaderID_dstBuf = Shader.PropertyToID("dstBuf");
static readonly int ShaderID_srcOffset = Shader.PropertyToID("srcOffset");
static readonly int ShaderID_dstOffset = Shader.PropertyToID("dstOffset");
static readonly int ShaderID_clearValue = Shader.PropertyToID("clearValue");
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static GraphicsBuffer GrowNoCopy(GraphicsBuffer gb, int newElementCount)
{
Assert.IsNotNull(gb);
if (gb == null)
return null;
if (newElementCount <= gb.count)
return gb;
var newBuf = new GraphicsBuffer(gb.target, gb.usageFlags, newElementCount, gb.stride);
gb.Dispose();
return newBuf;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static void Copy<T>(GraphicsBuffer src, GraphicsBuffer dst, uint srcOffsetInElements, uint dstOffsetInElements, uint copyCount) where T: unmanaged
{
var elementSizeInBytes = UnsafeUtility.SizeOf<T>();
Copy(src, dst, (uint)(srcOffsetInElements * elementSizeInBytes), (uint)(dstOffsetInElements * elementSizeInBytes), (uint)(copyCount * elementSizeInBytes));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static void Copy(GraphicsBuffer src, GraphicsBuffer dst, uint srcOffsetInBytes, uint dstOffsetInBytes, uint numBytesToCopy)
{
Assert.IsTrue(numBytesToCopy % 4 == 0);
Assert.IsTrue(srcOffsetInBytes % 4 == 0);
Assert.IsTrue(dstOffsetInBytes % 4 == 0);
var numIntsToCopy = numBytesToCopy / 4;
if (numIntsToCopy > 0)
{
copyCS ??= Resources.Load<ComputeShader>(COMPUTE_SHADER_NAME);
copyKernel ??= new ComputeKernel(copyCS, COPY_KERNEL_NAME);
const uint maxDispatchCount = 0xffff;
uint maxCopyOpsForSingleDispatch = maxDispatchCount * copyKernel.numThreadGroups.x;
uint copyByteCounter = 0;
while (numIntsToCopy > 0)
{
var iterationNumCopyOps = math.min(maxCopyOpsForSingleDispatch, numIntsToCopy);
numIntsToCopy -= iterationNumCopyOps;
copyCS.SetBuffer(copyKernel, ShaderID_srcBuf, src);
copyCS.SetBuffer(copyKernel, ShaderID_dstBuf, dst);
copyCS.SetInt(ShaderID_copyBufferElementsCount, (int)iterationNumCopyOps);
copyCS.SetInt(ShaderID_dstOffset, (int)(dstOffsetInBytes + copyByteCounter));
copyCS.SetInt(ShaderID_srcOffset, (int)(srcOffsetInBytes + copyByteCounter));
copyKernel.Dispatch((int)iterationNumCopyOps, 1, 1);
copyByteCounter += iterationNumCopyOps * 4;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// We cannot resize compute buffer. The only way is to create new, copy data, destroy old and return created one.
public static GraphicsBuffer Resize(GraphicsBuffer gb, int newElementCount)
{
Assert.IsNotNull(gb);
if (gb == null)
return null;
// In case of new and old sizes match simply return
if (newElementCount == gb.count)
return gb;
Assert.IsTrue((gb.target & GraphicsBuffer.Target.Raw) != 0, "Graphics buffer must be created with 'GraphicsBuffer.Target.Raw' flag");
var newBuf = new GraphicsBuffer(gb.target, gb.usageFlags, newElementCount, gb.stride);
var elementsToCopy = math.min(newBuf.count, gb.count);
Copy(gb, newBuf, 0, 0, (uint)(elementsToCopy * gb.stride));
gb.Dispose();
return newBuf;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static void Clear(GraphicsBuffer gb, uint startByteOffset, uint clearBytesCount, uint clearValue = 0)
{
Assert.IsNotNull(gb);
Assert.IsTrue(clearBytesCount % 4 == 0);
if (gb == null)
return;
Assert.IsTrue((gb.target & GraphicsBuffer.Target.Raw) != 0, "Partial clear only for raw buffers");
if (clearBytesCount > 0)
{
copyCS ??= Resources.Load<ComputeShader>(COMPUTE_SHADER_NAME);
clearKernel ??= new ComputeKernel(copyCS, CLEAR_KERNEL_NAME);
const uint maxDispatchCount = 0xffff;
uint maxClearOpsForSingleDispatch = maxDispatchCount * clearKernel.numThreadGroups.x;
var numIntsToClear = clearBytesCount / 4;
uint clearByteCounter = 0;
while (numIntsToClear > 0)
{
var iterationNumClearOps = math.min(maxClearOpsForSingleDispatch, numIntsToClear);
numIntsToClear -= iterationNumClearOps;
copyCS.SetBuffer(clearKernel, ShaderID_dstBuf, gb);
copyCS.SetInt(ShaderID_copyBufferElementsCount, (int)iterationNumClearOps);
copyCS.SetInt(ShaderID_dstOffset, (int)(startByteOffset + clearByteCounter));
copyCS.SetInt(ShaderID_clearValue, (int)clearValue);
clearKernel.Dispatch((int)iterationNumClearOps, 1, 1);
clearByteCounter += iterationNumClearOps * 4;
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static GraphicsBuffer CreateOrGrowGraphicsBuffer<T>
(
GraphicsBuffer gb,
GraphicsBuffer.Target target,
GraphicsBuffer.UsageFlags usage,
int elementCount,
bool preserveContents,
int extraCountAfterResize = 0x1000
) where T: unmanaged
{
if (gb == null)
{
// In case of zero input size, increase it to make a buffer in any case
elementCount = math.max(0xff, elementCount);
gb = new GraphicsBuffer(target, usage, elementCount, UnsafeUtility.SizeOf<T>());
return gb;
}
if (elementCount <= gb.count)
return gb;
// To prevent frequent buffer recreations, resize buffer with some additional capacity
elementCount += extraCountAfterResize;
gb = preserveContents ? Resize(gb, elementCount) : GrowNoCopy(gb, elementCount);
return gb;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: ed8ef19ce487f1945a429a413ea24b6c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 298480
packageName: Rukhanka Animation System 2
packageVersion: 2.9.0
assetPath: Packages/com.rukhanka.animation/Rukhanka.Toolbox/GPU/ComputeBufferTools.cs
uploadId: 897522
@@ -0,0 +1,76 @@
using Unity.Assertions;
using Unity.Mathematics;
using UnityEngine;
using SystemInfo = UnityEngine.Device.SystemInfo;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Toolbox
{
public class ComputeKernel
{
public readonly uint3 numThreadGroups;
public readonly int kernelIndex;
public readonly string kernelName;
public readonly ComputeShader computeShader;
readonly uint MAX_WORKGROUP_COUNT = 0xffff;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public ComputeKernel(ComputeShader cs, string kernelName)
{
kernelIndex = cs.FindKernel(kernelName);
this.kernelName = kernelName;
cs.GetKernelThreadGroupSizes(kernelIndex, out numThreadGroups.x, out numThreadGroups.y, out numThreadGroups.z);
Assert.IsTrue(numThreadGroups.x <= SystemInfo.maxComputeWorkGroupSizeX, $"Kernel '{kernelName}({kernelIndex})' of shader '{cs.name}' work group size X '{numThreadGroups.x}' exceeds hardware limit of '{SystemInfo.maxComputeWorkGroupSizeX}'");
Assert.IsTrue(numThreadGroups.y <= SystemInfo.maxComputeWorkGroupSizeY, $"Kernel '{kernelName}({kernelIndex})' of shader '{cs.name}' work group size Y '{numThreadGroups.y}' exceeds hardware limit of '{SystemInfo.maxComputeWorkGroupSizeY}'");
Assert.IsTrue(numThreadGroups.z <= SystemInfo.maxComputeWorkGroupSizeZ, $"Kernel '{kernelName}({kernelIndex})' of shader '{cs.name}' work group size Z '{numThreadGroups.z}' exceeds hardware limit of '{SystemInfo.maxComputeWorkGroupSizeZ}'");
computeShader = cs;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void Dispatch(int workGroupSizeX, int workgroupSizeY, int workgroupSizeZ)
{
var workGroupSize = new int3(workGroupSizeX, workgroupSizeY, workgroupSizeZ);
var numDispatches = (int3)math.ceil(workGroupSize / (float3)numThreadGroups);
#if UNITY_ASSERTIONS
if (ValidateDispatch(workGroupSizeX, workgroupSizeY, workgroupSizeZ))
#endif
computeShader.Dispatch(kernelIndex, numDispatches.x, numDispatches.y, numDispatches.z);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void Dispatch(uint workGroupSizeX, uint workgroupSizeY, uint workgroupSizeZ)
{
Dispatch((int)workGroupSizeX, (int)workgroupSizeY, (int)workgroupSizeZ);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public uint3 GetMaxWorkGroupSize()
{
return MAX_WORKGROUP_COUNT * numThreadGroups;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public bool ValidateDispatch(int workGroupSizeX, int workgroupSizeY, int workgroupSizeZ)
{
var workGroupSize = new int3(workGroupSizeX, workgroupSizeY, workgroupSizeZ);
var numDispatches = (int3)math.ceil(workGroupSize / (float3)numThreadGroups);
if (numDispatches.x > MAX_WORKGROUP_COUNT)
Debug.LogError($"Kernel '{kernelName}({kernelIndex})' of shader '{computeShader.name}' dispatch thread group count X '{numDispatches.x}' exceeds hardware limit of '{MAX_WORKGROUP_COUNT}'. Try to increase kernel work group size.");
if (numDispatches.y > MAX_WORKGROUP_COUNT)
Debug.LogError($"Kernel '{kernelName}({kernelIndex})' of shader '{computeShader.name}' dispatch thread group count Y '{numDispatches.y}' exceeds hardware limit of '{MAX_WORKGROUP_COUNT}'. Try to increase kernel work group size.");
if (numDispatches.z > MAX_WORKGROUP_COUNT)
Debug.LogError($"Kernel '{kernelName}({kernelIndex})' of shader '{computeShader.name}' dispatch thread group count Z '{numDispatches.z}' exceeds hardware limit of '{MAX_WORKGROUP_COUNT}'. Try to increase kernel work group size.");
return math.all(numDispatches <= (int)MAX_WORKGROUP_COUNT);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static implicit operator int(ComputeKernel c) => c.kernelIndex;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: e59641556c7d860438f1848a81d5e0ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 298480
packageName: Rukhanka Animation System 2
packageVersion: 2.9.0
assetPath: Packages/com.rukhanka.animation/Rukhanka.Toolbox/GPU/ComputeKernel.cs
uploadId: 897522
@@ -0,0 +1,133 @@
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Rendering;
using Unity.Assertions;
using UnityEngine;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Toolbox
{
// This class purpose to wrap compute buffer which updated every frame
public class FrameFencedGPUBufferPool<T>: IDisposable where T: unmanaged
{
BufferPool bufferPool;
NativeQueue<int> busyBuffers;
int currentFrameBufferIndex = -1;
int elementCount;
readonly GraphicsBuffer.Target bufferTargetFlags;
readonly GraphicsBuffer.UsageFlags bufferUsageFlags;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public FrameFencedGPUBufferPool(int count, GraphicsBuffer.Target target, GraphicsBuffer.UsageFlags usageFlags)
{
elementCount = count;
bufferTargetFlags = target;
bufferUsageFlags = usageFlags;
bufferPool = new BufferPool(count, UnsafeUtility.SizeOf<T>(), target, usageFlags);
busyBuffers = new (Allocator.Persistent);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void Dispose()
{
busyBuffers.Dispose();
bufferPool?.Dispose();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void BeginFrame()
{
Assert.IsTrue(currentFrameBufferIndex == -1);
RecoverBuffers();
currentFrameBufferIndex = bufferPool.GetBufferId();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void EndFrame()
{
Assert.IsFalse(currentFrameBufferIndex == -1);
busyBuffers.Enqueue(currentFrameBufferIndex);
currentFrameBufferIndex = -1;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void RecoverBuffers()
{
var totalLiveFrames = SparseUploader.NumFramesInFlight + 1;
for (var i = totalLiveFrames; i < busyBuffers.Count; ++i)
{
var bufID = busyBuffers.Dequeue();
bufferPool.PutBufferId(bufID);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public bool Grow(int requiredElementsCount)
{
var chunkSizeInBytes = 0x1000 * 16;
var elementSizeInBytes = UnsafeUtility.SizeOf<T>();
var chunkSizeInElements = chunkSizeInBytes / elementSizeInBytes;
var newSizeInElements = requiredElementsCount + chunkSizeInElements;
if (elementCount < newSizeInElements)
{
var newSizeInBytes = newSizeInElements * elementSizeInBytes;
if (newSizeInBytes > SystemInfo.maxGraphicsBufferSize)
{
var formattedRequiredBytes = CommonTools.FormatMemory(newSizeInBytes);
var formattedMaxComputeBufferBytes = CommonTools.FormatMemory(SystemInfo.maxGraphicsBufferSize);
throw new InvalidOperationException($"Requested buffer size ({requiredElementsCount} elements, {formattedRequiredBytes}) exceeded maximum compute buffer capacity of '{formattedMaxComputeBufferBytes}'");
}
bufferPool?.Dispose();
busyBuffers.Clear();
bufferPool = new BufferPool(newSizeInElements, UnsafeUtility.SizeOf<T>(), bufferTargetFlags, bufferUsageFlags);
elementCount = newSizeInElements;
currentFrameBufferIndex = -1;
return true;
}
return false;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public NativeArray<T> LockBufferForWrite(int startIndex, int count)
{
var b = bufferPool.GetBufferFromId(currentFrameBufferIndex);
var rv = b.LockBufferForWrite<T>(startIndex, count);
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void UnlockBufferAfterWrite(int count)
{
var b = bufferPool.GetBufferFromId(currentFrameBufferIndex);
b.UnlockBufferAfterWrite<T>(count);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public GraphicsBuffer GetBuffer()
{
if (currentFrameBufferIndex < 0)
return null;
var b = bufferPool.GetBufferFromId(currentFrameBufferIndex);
return b;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public static implicit operator GraphicsBuffer(FrameFencedGPUBufferPool<T> b) => b.GetBuffer();
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 348ddec2996af1d44bc38079d7dfa13e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 298480
packageName: Rukhanka Animation System 2
packageVersion: 2.9.0
assetPath: Packages/com.rukhanka.animation/Rukhanka.Toolbox/GPU/FrameFencedGPUBufferPool.cs
uploadId: 897522
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f33d6ad918265a147aaf4ae0b3655ab8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,36 @@
#pragma kernel CopyBuffer
#pragma kernel ClearBuffer
/////////////////////////////////////////////////////////////////////////////////
ByteAddressBuffer srcBuf;
RWByteAddressBuffer dstBuf;
int copyBufferElementsCount;
int srcOffset, dstOffset;
int clearValue;
/////////////////////////////////////////////////////////////////////////////////
[numthreads(128, 1, 1)]
void CopyBuffer(uint tid: SV_DispatchThreadID)
{
if (tid >= (uint)copyBufferElementsCount)
return;
uint inDataOffset = tid * 4 + srcOffset;
int v = srcBuf.Load(inDataOffset);
uint outDataOffset = tid * 4 + dstOffset;
dstBuf.Store(outDataOffset, v);
}
/////////////////////////////////////////////////////////////////////////////////
[numthreads(128, 1, 1)]
void ClearBuffer(uint tid: SV_DispatchThreadID)
{
if (tid >= (uint)copyBufferElementsCount)
return;
uint outDataOffset = tid * 4 + dstOffset;
dstBuf.Store(outDataOffset, clearValue);
}
@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: a2f4338f751d23e4db39523b314244b9
ComputeShaderImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 298480
packageName: Rukhanka Animation System 2
packageVersion: 2.9.0
assetPath: Packages/com.rukhanka.animation/Rukhanka.Toolbox/GPU/Resources/RukhankaGPUBufferManipulation.compute
uploadId: 897522
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Unity.Rendering;
using UnityEngine;
using UnityEngine.Assertions;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
namespace Rukhanka.Toolbox
{
public class SparseUploaderPool: IDisposable
{
Stack<SparseUploader> freeUploaders;
List<SparseUploader> allUploaders;
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public SparseUploaderPool()
{
freeUploaders = new ();
allUploaders = new ();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void Dispose()
{
foreach (var u in allUploaders)
{
u.Dispose();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void FrameCleanup()
{
foreach (var u in allUploaders)
{
u.FrameCleanup();
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public SparseUploader GetUploader(GraphicsBuffer gb)
{
SparseUploader rv;
if (freeUploaders.Count > 0)
{
rv = freeUploaders.Pop();
}
else
{
rv = new SparseUploader(gb);
allUploaders.Add(rv);
Assert.IsTrue(allUploaders.Count < 0xff, "Looks like 'PutUploader' call is forgotten somewhere! There are too much of created uploaders.");
}
rv.ReplaceBuffer(gb);
return rv;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
public void PutUploader(SparseUploader su)
{
freeUploaders.Push(su);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 18f595187c7f2c141b045a24d8360f70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 298480
packageName: Rukhanka Animation System 2
packageVersion: 2.9.0
assetPath: Packages/com.rukhanka.animation/Rukhanka.Toolbox/GPU/SparseUploaderPool.cs
uploadId: 897522