Add support for passing features to WESL compilation

This commit is contained in:
PJB3005
2025-10-09 17:10:20 +02:00
parent 3d6fda1aca
commit 9a00bd89ef
10 changed files with 263 additions and 83 deletions

View File

@@ -1,6 +1,7 @@
global using Robust.Client.Interop.RobustNative.Webgpu;
global using static Robust.Client.Interop.RobustNative.Webgpu.Wgpu;
global using static Robust.Shared.Utility.FfiHelper;
global using unsafe WGPUTexture = Robust.Client.Interop.RobustNative.Webgpu.WGPUTextureImpl*;
global using unsafe WGPUDevice = Robust.Client.Interop.RobustNative.Webgpu.WGPUDeviceImpl*;

View File

@@ -1,9 +1,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Robust.Shared.Maths;
namespace Robust.Client.Graphics.Rhi.WebGpu;
@@ -180,64 +178,6 @@ internal sealed unsafe partial class RhiWebGpu
return Encoding.UTF8.GetBytes(label);
}
/// <param name="buf">Must be pinned memory or I WILL COME TO YOUR HOUSE!!</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void* BumpAllocate(ref Span<byte> buf, int size)
{
// Round up to 8 to make sure everything stays aligned inside.
var alignedSize = MathHelper.CeilingPowerOfTwo(size, 8);
if (buf.Length < alignedSize)
ThrowBumpAllocOutOfSpace();
var ptr = Unsafe.AsPointer(ref MemoryMarshal.AsRef<byte>(buf));
buf = buf[alignedSize..];
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T* BumpAllocate<T>(ref Span<byte> buf) where T : unmanaged
{
var ptr = (T*)BumpAllocate(ref buf, sizeof(T));
// Yeah I don't trust myself.
*ptr = default;
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T* BumpAllocate<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T*)ptr;
}
// Workaround for C# not having pointers in generics.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static T** BumpAllocatePtr<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T*) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T**)ptr;
}
private static byte* BumpAllocateUtf8(ref Span<byte> buf, string? str)
{
if (str == null)
return null;
var byteCount = Encoding.UTF8.GetByteCount(str) + 1;
var ptr = BumpAllocate(ref buf, byteCount);
var dstSpan = new Span<byte>(ptr, byteCount);
Encoding.UTF8.GetBytes(str, dstSpan);
dstSpan[^1] = 0;
return (byte*) ptr;
}
private static WGPUStringView BumpAllocateStringView(ref Span<byte> buf, string? str)
{
if (str == null)
@@ -255,12 +195,6 @@ internal sealed unsafe partial class RhiWebGpu
};
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowBumpAllocOutOfSpace()
{
throw new InvalidOperationException("Out of bump allocator space!");
}
private sealed class WgpuPromise<TResult> : IDisposable
{

View File

@@ -31,6 +31,9 @@ internal static unsafe partial class Wesl
[DllImport("robust-native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void wesl_free_exec_result(WeslExecResult* result);
[DllImport("robust-native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void wesl_free_parse_result(WeslParseResult* result);
[DllImport("robust-native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
public static extern void wesl_free_translation_unit(WeslTranslationUnit* unit);

View File

@@ -1,4 +1,5 @@
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text;
using Robust.Client.Graphics;
using Robust.Shared.Console;
@@ -21,7 +22,32 @@ internal sealed class CompileShaderCommand : IConsoleCommand
{
var path = args[0];
var x = _shaderCompiler.CompileToWgsl(new ResPath(path), ImmutableDictionary<string, bool>.Empty);
var features = new Dictionary<string, bool>();
for (var i = 1; i < args.Length; i++)
{
var split = args[i].Split('=', 2);
var value = split[1].Trim();
if (!bool.TryParse(value, out var boolValue))
{
if (value == "0")
{
boolValue = false;
}
else if (value == "1")
{
boolValue = true;
}
else
{
shell.WriteError($"Invalid feature value: '{value}'");
return;
}
}
features.Add(split[0].Trim(), boolValue);
}
var x = _shaderCompiler.CompileToWgsl(new ResPath(path), features);
if (!x.Success)
{
shell.WriteError("Compilation failed");
@@ -41,6 +67,7 @@ internal sealed class CompileShaderCommand : IConsoleCommand
"<path>");
}
return CompletionResult.Empty;
// Features.
return CompletionResult.FromHint("<feature>=<1|0>");
}
}

View File

@@ -5,6 +5,7 @@ namespace Robust.Client.Graphics;
public interface IShaderCompiler
{
ShaderCompileResultWgsl CompileToWgsl(ResPath path);
ShaderCompileResultWgsl CompileToWgsl(ResPath path, IReadOnlyDictionary<string, bool> features);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -34,6 +35,11 @@ internal sealed class ShaderCompiler : IShaderCompiler, IDisposable
RegisterPackage(new ResPath("/Shaders"), "Content");
}
public ShaderCompileResultWgsl CompileToWgsl(ResPath path)
{
return CompileToWgsl(path, ImmutableDictionary<string, bool>.Empty);
}
public unsafe ShaderCompileResultWgsl CompileToWgsl(ResPath path, IReadOnlyDictionary<string, bool> features)
{
using var _ = _rwLock.ReadGuard();
@@ -53,10 +59,22 @@ internal sealed class ShaderCompiler : IShaderCompiler, IDisposable
byte[] modulePathNullTerminated = [.. Encoding.UTF8.GetBytes(modulePath), 0];
void* freeBoolMap = null;
WeslBoolMap* boolMap = null;
WeslResult result;
if (features.Count > 0)
{
if (!TryWriteBoolMap(stackalloc byte[1024], features, out boolMap))
{
var heapBuffer = FfiHelper.CreateHeapBumpAllocateBuffer(16384, out freeBoolMap);
if (!TryWriteBoolMap(heapBuffer, features, out boolMap))
throw new ArgumentException("Too many features specified!");
}
}
fixed (byte* pPath = modulePathNullTerminated)
{
result = Wesl.wesl_compile(null, (sbyte*)pPath, &compileOptions, null, null);
result = Wesl.wesl_compile(null, (sbyte*)pPath, &compileOptions, null, boolMap);
}
try
@@ -74,9 +92,46 @@ internal sealed class ShaderCompiler : IShaderCompiler, IDisposable
finally
{
Wesl.wesl_free_result(&result);
if (freeBoolMap != null)
NativeMemory.Free(freeBoolMap);
}
}
private static unsafe bool TryWriteBoolMap(
Span<byte> buffer,
IReadOnlyDictionary<string, bool> map,
out WeslBoolMap* ptr)
{
if (!FfiHelper.TryBumpAllocate(ref buffer, out ptr))
return false;
var count = map.Count;
if (!FfiHelper.TryBumpAllocate(ref buffer, count, out Ptr<byte>* strings, out var stringsSpan))
return false;
if (!FfiHelper.TryBumpAllocate(ref buffer, count, out bool* bools, out var boolSpan))
return false;
var i = 0;
foreach (var (key, value) in map)
{
boolSpan[i] = value;
if (!FfiHelper.TryBumpAllocateUtf8(ref buffer, key, out var stringPtr))
return false;
stringsSpan[i] = stringPtr;
i += 1;
}
ptr->values = bools;
ptr->keys = (sbyte**)strings;
ptr->len = (UIntPtr)count;
return true;
}
private unsafe WeslResolverOptions MakeResolverOptions()
{
return new WeslResolverOptions

View File

@@ -0,0 +1 @@
global using Robust.Shared.Analyzers;

View File

@@ -12,6 +12,10 @@
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="Serilog" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Robust.Shared.Maths\Robust.Shared.Maths.csproj" />
</ItemGroup>
<Import Project="..\MSBuild\Robust.Properties.targets" />
</Project>

View File

@@ -0,0 +1,167 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using Robust.Shared.Maths;
namespace Robust.Shared.Utility;
internal static unsafe class FfiHelper
{
internal static Span<byte> CreateHeapBumpAllocateBuffer(int size, out void* ptr)
{
ptr = NativeMemory.Alloc(checked((nuint)size));
return new Span<byte>(ptr, size);
}
internal static bool TryBumpAllocate(ref Span<byte> buf, int size, out void* ptr)
{
// Round up to 8 to make sure everything stays aligned inside.
var alignedSize = MathHelper.CeilingPowerOfTwo(size, 8);
if (buf.Length < alignedSize)
{
ptr = null;
return false;
}
ptr = Unsafe.AsPointer(ref MemoryMarshal.AsRef<byte>(buf));
buf = buf[alignedSize..];
return true;
}
/// <param name="buf">Must be pinned memory or I WILL COME TO YOUR HOUSE!!</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void* BumpAllocate(ref Span<byte> buf, int size)
{
if (!TryBumpAllocate(ref buf, size, out var ptr))
ThrowBumpAllocOutOfSpace();
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryBumpAllocate<T>(ref Span<byte> buf, out T* ptr) where T : unmanaged
{
if (TryBumpAllocate(ref buf, sizeof(T), out var voidPtr))
{
ptr = (T*)voidPtr;
// Yeah I don't trust myself.
*ptr = default;
return true;
}
ptr = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static T* BumpAllocate<T>(ref Span<byte> buf) where T : unmanaged
{
var ptr = (T*)BumpAllocate(ref buf, sizeof(T));
// Yeah I don't trust myself.
*ptr = default;
return ptr;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryBumpAllocate<T>(ref Span<byte> buf, int count, out T* ptr) where T : unmanaged
{
var size = checked(sizeof(T) * count);
if (TryBumpAllocate(ref buf, size, out var voidPtr))
{
ptr = (T*)voidPtr;
new Span<byte>(ptr, size).Clear();
return true;
}
ptr = null;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool TryBumpAllocate<T>(
ref Span<byte> buf,
int count,
out T* ptr,
out Span<T> span)
where T : unmanaged
{
var size = checked(sizeof(T) * count);
if (TryBumpAllocate(ref buf, size, out var voidPtr))
{
ptr = (T*)voidPtr;
span = new Span<T>(ptr, size);
// Yeah I don't trust myself.
span.Clear();
return true;
}
ptr = null;
span = default;
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static T* BumpAllocate<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T*)ptr;
}
// Workaround for C# not having pointers in generics.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static T** BumpAllocatePtr<T>(ref Span<byte> buf, int count) where T : unmanaged
{
var size = checked(sizeof(T*) * count);
var ptr = BumpAllocate(ref buf, size);
// Yeah I don't trust myself.
new Span<byte>(ptr, size).Clear();
return (T**)ptr;
}
internal static bool TryBumpAllocateUtf8(ref Span<byte> buf, string? str, out byte* ptr)
{
if (str == null)
{
ptr = null;
return true;
}
ptr = (byte*) Unsafe.AsPointer(ref MemoryMarshal.AsRef<byte>(buf));
if (!Encoding.UTF8.TryGetBytes(str, buf, out var written) || written == buf.Length)
{
ptr = null;
return false;
}
buf[written] = 0; // Nul terminator
buf = buf[(written + 1)..];
return true;
}
internal static byte* BumpAllocateUtf8(ref Span<byte> buf, string? str)
{
if (str == null)
return null;
var byteCount = Encoding.UTF8.GetByteCount(str) + 1;
var ptr = BumpAllocate(ref buf, byteCount);
var dstSpan = new Span<byte>(ptr, byteCount);
Encoding.UTF8.GetBytes(str, dstSpan);
dstSpan[^1] = 0;
return (byte*) ptr;
}
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowBumpAllocOutOfSpace()
{
throw new InvalidOperationException("Out of bump allocator space!");
}
}

View File

@@ -1,13 +0,0 @@
namespace Robust.Shared.Utility;
/// <summary>
/// Pointer-wrapper struct so pointers can be sanely stored in generics and records.
/// </summary>
/// <typeparam name="T">The actual type pointed to</typeparam>
internal unsafe struct Ptr<T> where T : unmanaged
{
public T* P;
public static implicit operator T*(Ptr<T> t) => t.P;
public static implicit operator Ptr<T>(T* ptr) => new() { P = ptr };
}