Recursive AI

16-bit and 64-bit types: half, int16_t, double and uint64_t

All oneshader recipes

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:

XML
<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:

HLSL
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:

HLSL
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:

C#
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]);
Text
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

  • half is System.Half, int16_t short, uint16_t ushort, int64_t long, uint64_t ulong, double double.
  • Vectors of these have no System.Numerics type, so they’re one field per component: half3 Velocity becomes Velocity_X, Velocity_Y and Velocity_Z. A matrix of a 16-bit or 64-bit element is an error that says to split it.
  • RequiredFeatures is on every entry-point class and its Layout. Enable those device features when creating the Vulkan device (PipelineLayoutInfo.RequiredFeatures lists them too), and request shader-f16 on WebGPU.
  • Turn 16-bit types on per project with <OneShaderEnable16BitTypes>, per file with Enable16BitTypes="true" metadata, with -enable-16bit-types on the command line, or with CompileOptions.Enable16BitTypes at 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:

Text
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:

Text
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

TypeSPIR-VMetalWGSL
halfyesyes (native half)yes (f16, enable f16;)
int16_t, uint16_tyesyes (short, ushort)no
int64_t, uint64_tyesyes (long, ulong), not as vectors in buffersno
doubleyesnono
64-bit Interlocked*yes, on structured buffers and groupsharedonly unsigned InterlockedMax/Min on a buffer, result unusedno

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:

Text
/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:

Text
/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.