Use native 16-bit half and int16_t and 64-bit double and uint64_t in HLSL with OneShader 1.2.0, get the matching C# structs, and see which device features each target needs and what Metal and WebGPU refuse.
OneShader 1.2.0 compiles the sized types the way DXC does. With 16-bit types enabled (DXC’s -enable-16bit-types), half, min16float, min16int, min16uint and the float16_t, int16_t and uint16_t families are native 16-bit types, in arithmetic and in buffers with DXC’s byte layout. double, int64_t and uint64_t work on SPIR-V in arithmetic, in buffers and in 64-bit Interlocked* operations.
The project
16-bit types are switched on for the project. Totals.hlsl uses 64-bit types, which only SPIR-V has, so that file is built for SPIR-V only:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <OneShaderEnable16BitTypes>true</OneShaderEnable16BitTypes> <OneShaderTargets>SpirV;Msl;Wgsl</OneShaderTargets> </PropertyGroup> <ItemGroup> <PackageReference Include="RecursiveAI.OneShader.Build" Version="1.2.0" /> <!-- double and 64-bit integers exist only on SPIR-V. --> <OneShader Include="Shaders/Totals.hlsl" Targets="SpirV" /> </ItemGroup> </Project>
Shaders/Particles.hlsl keeps particle state in half-precision floats:
struct Particle { half3 Velocity; // 16-bit floats: half the bytes of a float3 half Life; uint Flags; }; cbuffer Step : register(b0) { half DeltaTime; uint Count; }; RWStructuredBuffer<Particle> Particles : register(u1); [shader("compute")] [numthreads(64, 1, 1)] void CSMain(uint3 id : SV_DispatchThreadID) { if (id.x >= Count) return; Particle p = Particles[id.x]; p.Life -= DeltaTime; p.Velocity *= (half)0.98; if (p.Life <= 0) p.Flags |= 1; Particles[id.x] = p; }
Shaders/Totals.hlsl keeps a 64-bit byte counter and a double-precision sum:
struct Sums { uint64_t Bytes; // a 64-bit counter that does not wrap at 4 GB double Energy; // 64-bit float accumulation }; StructuredBuffer<uint> Sizes : register(t0); StructuredBuffer<float> Samples : register(t1); RWStructuredBuffer<Sums> Result : register(u2); [shader("compute")] [numthreads(64, 1, 1)] void CSMain(uint3 id : SV_DispatchThreadID) { InterlockedAdd(Result[0].Bytes, (uint64_t)Sizes[id.x]); double e = (double)Samples[id.x] * (double)Samples[id.x]; if (id.x == 0) Result[0].Energy += e; }
Program.cs:
using System.Runtime.InteropServices; using OneShader.Layouts.WebGpu; using Sized.Shaders; // 16-bit members are System.Half, one field per component. var p = new Particles.Particle { Velocity_X = (Half)1.5f, Velocity_Y = (Half)0, Velocity_Z = (Half)(-2), Life = (Half)3 }; Console.WriteLine($"Particle: {Particles.Particle.Size} bytes, Life at {Marshal.OffsetOf<Particles.Particle>("Life")}, Flags at {Marshal.OffsetOf<Particles.Particle>("Flags")}"); Console.WriteLine($"Step: {Particles.Step.Size} bytes, Count at {Marshal.OffsetOf<Particles.Step>("Count")}"); // 64-bit members are ulong and double. Console.WriteLine($"Sums: {Totals.Sums.Size} bytes, Energy at {Marshal.OffsetOf<Totals.Sums>("Energy")}"); // What the device must enable. Console.WriteLine($"Particles.CSMain.RequiredFeatures: {Particles.CSMain.RequiredFeatures}"); Console.WriteLine($"Totals.CSMain.RequiredFeatures: {Totals.CSMain.RequiredFeatures}"); if (Particles.Pipeline.TryToWebGpu(out WebGpuPipelineLayout? webgpu, out var error)) Console.WriteLine($"WebGPU requiredFeatures: [{string.Join(", ", webgpu!.RequiredFeatures ?? [])}]"); else Console.WriteLine(error); // half in WGSL is f16. string wgsl = System.Text.Encoding.UTF8.GetString(Particles.CSMain.Wgsl); Console.WriteLine(wgsl.Split('\n')[0]);
Particle: 16 bytes, Life at 6, Flags at 8 Step: 16 bytes, Count at 4 Sums: 16 bytes, Energy at 8 Particles.CSMain.RequiredFeatures: Float16, StorageBuffer16, UniformBuffer16 Totals.CSMain.RequiredFeatures: Float64, Int64, Int64Atomics WebGPU requiredFeatures: [shader-f16] enable f16;
The layouts are DXC’s. dxc -enable-16bit-types -spirv on the same Particles.hlsl gives member offsets 0, 6 and 8 and an array stride of 16, and dxc -spirv on Totals.hlsl gives offsets 0 and 8 and a stride of 16, as OneShader does. Both SPIR-V files pass spirv-val, and the WGSL passes naga.
What the generated C# looks like
halfisSystem.Half,int16_tshort,uint16_tushort,int64_tlong,uint64_tulong,doubledouble.- Vectors of these have no
System.Numericstype, so they’re one field per component:half3 VelocitybecomesVelocity_X,Velocity_YandVelocity_Z. A matrix of a 16-bit or 64-bit element is an error that says to split it. RequiredFeaturesis on every entry-point class and itsLayout. Enable those device features when creating the Vulkan device (PipelineLayoutInfo.RequiredFeatureslists them too), and requestshader-f16on WebGPU.- Turn 16-bit types on per project with
<OneShaderEnable16BitTypes>, per file withEnable16BitTypes="true"metadata, with-enable-16bit-typeson the command line, or withCompileOptions.Enable16BitTypesat run time.
Without 16-bit types
As in DXC, half and the min16* types are then 32-bit values (marked RelaxedPrecision on SPIR-V), and the explicitly sized names are an error:
i16.hlsl(1,20): error OSH0360: The type 'uint16_t' needs 16-bit types enabled. Compile with -enable-16bit-types (MSBuild: OneShaderEnable16BitTypes=true), or use half, min16float, int or uint.A double passed to a float-only intrinsic is narrowed to float, as DXC does, with a warning:
dn.hlsl(2,64): warning OSH0361: 'sqrt' has no double version: the argument is converted to float, so the result has float precision. Use +, -, *, /, min, max, abs, clamp or fma to keep double precision.What each target has
| Type | SPIR-V | Metal | WGSL |
|---|---|---|---|
half | yes | yes (native half) | yes (f16, enable f16;) |
int16_t, uint16_t | yes | yes (short, ushort) | no |
int64_t, uint64_t | yes | yes (long, ulong), not as vectors in buffers | no |
double | yes | no | no |
64-bit Interlocked* | yes, on structured buffers and groupshared | only unsigned InterlockedMax/Min on a buffer, result unused | no |
A refusal is an error at the first use, only when that target is selected, with what to change. Totals.hlsl compiled for Metal and WGSL:
/tmp/oneshader-docs/cookbook/Sized/Shaders/Totals.hlsl(15,5): error OSH5102: MSL has no 64-bit floating-point type. Use float, or compile this shader for SPIR-V only. /tmp/oneshader-docs/cookbook/Sized/Shaders/Totals.hlsl(15,5): error OSH5102: Metal 2.4 supports 64-bit atomics only as unsigned InterlockedMax / InterlockedMin on a buffer without using the returned value. Use that form, 32-bit atomics, or compile this shader for SPIR-V only. /tmp/oneshader-docs/cookbook/Sized/Shaders/Totals.hlsl(15,5): error OSH5202: WGSL has no 64-bit floating-point type. Use float, or compile this shader for SPIR-V only. /tmp/oneshader-docs/cookbook/Sized/Shaders/Totals.hlsl(15,5): error OSH5202: WGSL has no 64-bit integer types. Use int or uint (or a uint2 pair), or compile this shader for SPIR-V or MSL only.
And a uint16_t buffer on WGSL:
/tmp/i16.hlsl(2,57): error OSH5202: WGSL has no 16-bit integer types (only f16). Use int or uint, or compile this shader for SPIR-V or MSL only.The per-target rules are in SPIR-V, MSL and WGSL, and the generated members under 16-bit and 64-bit members in Generated bindings.