.NET Bindings

Goldy provides first-class C# bindings via P/Invoke interop over the native Rust FFI layer.

Installation

NuGet Package

dotnet add package Goldy

Or add to your .csproj directly:

<PackageReference Include="Goldy" Version="0.1.*" />

The NuGet package bundles native Goldy + Slang libraries for all supported platforms — no separate native installation is needed.

Building from Source

cargo build --package goldy-ffi --release
dotnet add reference path/to/goldy/dotnet/Goldy/Goldy.csproj

Requirements

  • .NET 8.0 or later
  • Windows x64, Linux x64, or macOS (x64 / arm64)
  • A GPU with Vulkan 1.4+, DX12, or Metal Tier 2+ support

Quick Start

Headless Rendering

using Goldy;

using var instance = new Instance();
using var device = instance.CreateDevice(DeviceType.DiscreteGpu);
using var target = new RenderTarget(device, 800, 600, TextureFormat.Rgba8Unorm);

var encoder = new CommandEncoder();
encoder.Clear(new Color(0.2f, 0.3f, 0.8f, 1.0f));

target.Render(encoder);

byte[] pixels = target.ReadToCpu();
Console.WriteLine($"Rendered {pixels.Length} bytes ({target.Width}x{target.Height})");

Windowed Rendering

For interactive applications, use Surface with a window handle:

using Goldy;

using var surface = new Surface(device, windowHandle);

while (running)
{
    using var frame = surface.Acquire();

    var encoder = new CommandEncoder();
    encoder.Clear(Color.CornflowerBlue);
    // ... draw calls ...

    frame.Render(encoder);
    surface.Present(frame);
}

Shaders (Slang)

Goldy uses Slang as its shader language across all backends:

var source = """
    [shader("vertex")]
    float4 vs_main(float2 pos : POSITION) : SV_Position {
        return float4(pos, 0.0, 1.0);
    }

    [shader("fragment")]
    float4 fs_main() : SV_Target {
        return float4(1.0, 0.5, 0.0, 1.0);
    }
    """;

using var shader = new ShaderModule(device, source);
using var pipeline = new RenderPipeline(device, shader, new RenderPipelineDesc
{
    TargetFormat = TextureFormat.Rgba8Unorm,
    Topology = PrimitiveTopology.TriangleList,
});

Resource Management

All Goldy objects implement IDisposable. Use using declarations or using blocks to ensure GPU resources are released promptly:

// Preferred: using declaration (C# 8+)
using var device = instance.CreateDevice(DeviceType.DiscreteGpu);

// Also valid: explicit using block
using (var target = new RenderTarget(device, 512, 512, TextureFormat.Rgba8Unorm))
{
    // target is released when the block exits
}

Key Differences from Rust

AspectRustC#
Instance creationInstance::new()?new Instance()
Error handlingResult<T, GoldyError>Exceptions
Device lifetimeArc<Device>IDisposable / using
Buffer creationBuffer::with_data(&device, &[T], access)Buffer.WithData<T>(device, data, access)
Pixel readbackVec<u8>byte[]
EnumsDeviceType::DiscreteGpuDeviceType.DiscreteGpu

API Reference

Instance

public sealed class Instance : IDisposable
{
    public Instance();
    public IEnumerable<AdapterInfo> EnumerateAdapters();
    public Device CreateDevice(DeviceType deviceType);
    public Device CreateDeviceById(uint adapterId);
}

Device

public sealed class Device : IDisposable
{
    public uint AdapterId { get; }
    public bool IsValid { get; }
    public ulong GpuProgress { get; }
    public void WaitUntil(ulong value);
    public bool WaitUntilTimeout(ulong value, uint timeoutMs);
    public bool HasLibrary(string name);
}

Buffer

public sealed class Buffer : IDisposable
{
    public static Buffer New(Device device, ulong size, DataAccess access);
    public static Buffer WithData<T>(Device device, T[] data, DataAccess access)
        where T : unmanaged;
    public void Write<T>(T[] data) where T : unmanaged;
    public void Write<T>(ulong offset, T[] data) where T : unmanaged;
    public ulong Size { get; }
}

ShaderModule

public sealed class ShaderModule : IDisposable
{
    public ShaderModule(Device device, string slangSource);
}

RenderPipeline / RenderPipelineDesc

public sealed class RenderPipeline : IDisposable
{
    public RenderPipeline(Device device, ShaderModule shader, RenderPipelineDesc desc);
}

public sealed class RenderPipelineDesc
{
    public TextureFormat TargetFormat { get; set; }
    public PrimitiveTopology Topology { get; set; }
    // ... vertex layout, depth state
}

CommandEncoder / RenderPass

public sealed class CommandEncoder
{
    public CommandEncoder();
    public void Clear(Color color);
    public RenderPass BeginRenderPass();
}

public sealed class RenderPass : IDisposable
{
    public void SetPipeline(RenderPipeline pipeline);
    public void SetVertexBuffer(uint slot, Buffer buffer);
    public void Draw(uint vertexStart, uint vertexCount,
                     uint instanceStart = 0, uint instanceCount = 1);
    public void DrawIndexed(uint indexCount, uint instanceCount = 1);
}

RenderTarget

public sealed class RenderTarget : IDisposable
{
    public RenderTarget(Device device, uint width, uint height, TextureFormat format);
    public void Render(CommandEncoder encoder);
    public byte[] ReadToCpu();
    public void ReadToBuffer(byte[] output);
    public uint Width { get; }
    public uint Height { get; }
    public TextureFormat Format { get; }
    public int BufferSize { get; }
}

Surface / SurfaceFrame

public sealed class Surface : IDisposable
{
    public Surface(Device device, nint windowHandle);
    public SurfaceFrame Acquire();
    public void Present(SurfaceFrame frame);
    public void Resize(uint width, uint height);
    public uint Width { get; }
    public uint Height { get; }
}

public sealed class SurfaceFrame : IDisposable
{
    public void Render(CommandEncoder encoder);
}

Compute

public sealed class ComputePipeline : IDisposable
{
    public ComputePipeline(Device device, ShaderModule computeShader);
}

public sealed class ComputeEncoder
{
    public ComputeEncoder();
    public void SetPipeline(ComputePipeline pipeline);
    public void BindResources(params Buffer[] buffers);
    public void BindResourcesRaw(uint[] indices);
    public void Dispatch(uint x, uint y, uint z);
    public void DispatchIndirect(Buffer buffer, ulong offset);
    public void ClearBuffer(Buffer buffer, ulong offset, ulong size);
    public void Dispatch(Device device);     // dispatch and block
    public ulong Submit(Device device);      // submit, return timeline value
}

Texture / Sampler

public sealed class Texture : IDisposable
{
    public Texture(Device device, uint width, uint height, TextureFormat format,
                   SpatialAccess access, TextureFlags flags = TextureFlags.None);
    public void Write(byte[] data);
    public uint Width { get; }
    public uint Height { get; }
    public TextureFormat Format { get; }
}

public sealed class Sampler : IDisposable
{
    public Sampler(Device device, SamplerDesc desc);
}

public struct SamplerDesc
{
    public FilterMode MagFilter { get; set; }
    public FilterMode MinFilter { get; set; }
    public AddressMode AddressModeU { get; set; }
    public AddressMode AddressModeV { get; set; }
}

Enums

public enum DeviceType   { DiscreteGpu, IntegratedGpu, Cpu, Other }
public enum BackendType  { Vulkan, Metal, Dx12 }
public enum DataAccess   { Scattered, Broadcast }
public enum SpatialAccess { Interpolated, Direct }
public enum FilterMode   { Nearest, Linear }
public enum AddressMode  { Repeat, MirrorRepeat, ClampToEdge, ClampToBorder }

public enum TextureFormat
{
    Rgba8Unorm, Rgba8Srgb, Bgra8Unorm,
    Rgba16Float, Rgba32Float, Depth32Float,
}

public struct Color
{
    public float R, G, B, A;
    public Color(float r, float g, float b, float a);
    public static Color CornflowerBlue { get; }
    public static Color Black { get; }
    public static Color White { get; }
}

Non-Blocking Submissions

ComputeEncoder.Submit returns a ulong device timeline value. Poll or wait on it via Device.GpuProgress and Device.WaitUntil:

ulong ticket = computeEncoder.Submit(device);

// ... do other work ...

device.WaitUntil(ticket);   // block until the GPU catches up