Table of Contents

Prerequisites

Before starting the tutorials, you need to set up the project and create the shared framework code that all tutorials will use.

Development Environment

  • .NET 10.0 SDK or later
  • A GPU with DirectX 12, Metal 4, or Vulkan 1.4 support
  • Visual Studio 2026, VS Code, or JetBrains Rider
Note

These tutorials target desktop platforms: Windows, macOS, and Linux.

Creating the Project

dotnet new console -n ZenithTutorials
cd ZenithTutorials

Required Packages

dotnet add package Zenith.NET.DirectX12
dotnet add package Zenith.NET.Metal
dotnet add package Zenith.NET.Vulkan
dotnet add package Zenith.NET.Extensions.ImageSharp
dotnet add package Zenith.NET.Extensions.Slang
dotnet add package Silk.NET.Windowing
dotnet add package Silk.NET.Input

Project Configuration

Your .csproj should look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
        <PackageReference Include="Silk.NET.Input" Version="*" />
        <PackageReference Include="Silk.NET.Windowing" Version="*" />
        <PackageReference Include="Zenith.NET.DirectX12" Version="*" />
        <PackageReference Include="Zenith.NET.Extensions.ImageSharp" Version="*" />
        <PackageReference Include="Zenith.NET.Extensions.Slang" Version="*" />
        <PackageReference Include="Zenith.NET.Metal" Version="*" />
        <PackageReference Include="Zenith.NET.Vulkan" Version="*" />
  </ItemGroup>

  <ItemGroup>
    <None Update="Assets\**\*">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>
Note

AllowUnsafeBlocks is required because the tutorials use sizeof with custom structs for GPU buffer sizing.

Project Structure

ZenithTutorials/
├── Program.cs
├── App.cs
├── IRenderer.cs
├── BindingHelper.cs
├── CocoaHelper.cs
├── Usings.cs
├── Assets/
│   └── shoko.png
└── Renderers/
    ├── HelloTriangleRenderer.cs
    ├── TexturedQuadRenderer.cs
    ├── SpinningCubeRenderer.cs
    ├── ComputeShaderRenderer.cs
    ├── IndirectDrawingRenderer.cs
    ├── RayTracingRenderer.cs
    └── MeshShadingRenderer.cs

Asset File

Save the following image as Assets/shoko.png in your project (right-click → Save As):

shoko.png

Framework Code

The following files provide the shared infrastructure for all tutorials. Copy each file into your project.

Usings.cs

global using System.Numerics;
global using System.Runtime.CompilerServices;
global using System.Runtime.InteropServices;
global using Zenith.NET;
global using Zenith.NET.Extensions.ImageSharp;
global using Zenith.NET.Extensions.Slang;
global using Buffer = Zenith.NET.Buffer;

IRenderer.cs

All tutorial renderers implement this interface:

namespace ZenithTutorials;

internal interface IRenderer : IDisposable
{
    void Update(double deltaTime);

    void Render();

    void Resize(uint width, uint height);
}
Method Called Purpose
Update Every frame Update logic (animations, transforms)
Render Every frame Issue GPU commands
Resize On window resize Recreate size-dependent resources
Dispose On exit Clean up GPU resources

App.cs

The application framework manages window creation, graphics context initialization, and the render loop:

using Silk.NET.Windowing;
using Zenith.NET.DirectX12;
using Zenith.NET.Metal;
using Zenith.NET.Vulkan;

namespace ZenithTutorials;

internal static class App
{
    private static readonly IWindow window;
    private static readonly SwapChain swapChain;

    static App()
    {
        if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS() && !OperatingSystem.IsLinux())
        {
            throw new PlatformNotSupportedException("This application only supports Windows, macOS, and Linux.");
        }

        if (OperatingSystem.IsWindows())
        {
            Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);
        }
        else if (OperatingSystem.IsMacOS())
        {
            Context = GraphicsContext.CreateMetal(useValidationLayer: true);
        }
        else
        {
            Context = GraphicsContext.CreateVulkan(useValidationLayer: true);
        }

        Context.ValidationMessage += static (sender, args) => Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}");

        window = Window.Create(WindowOptions.Default with
        {
            API = GraphicsAPI.None,
            Title = "Zenith Tutorials",
            Size = new(1280, 720)
        });
        window.Initialize();
        window.Center();

        Surface surface;
        if (OperatingSystem.IsWindows())
        {
            surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
        }
        else if (OperatingSystem.IsMacOS())
        {
            surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
        }
        else
        {
            surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
        }

        swapChain = Context.CreateSwapChain(new() { Surface = surface, ColorTargetFormat = PixelFormat.B8G8R8A8UNorm, DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt });
    }

    public static GraphicsContext Context { get; }

    public static uint Width => (uint)window.FramebufferSize.X;

    public static uint Height => (uint)window.FramebufferSize.Y;

    public static FrameBuffer FrameBuffer => swapChain.FrameBuffer;

    public static void Run<TRenderer>() where TRenderer : IRenderer, new()
    {
        try
        {
            using TRenderer renderer = new();

            window.Update += delta =>
            {
                if (Width is 0 || Height is 0)
                {
                    return;
                }

                renderer.Update(delta);
            };

            window.Render += delta =>
            {
                if (Width is 0 || Height is 0)
                {
                    return;
                }

                renderer.Render();
                swapChain.Present();
            };

            window.Resize += size =>
            {
                if (Width is 0 || Height is 0)
                {
                    return;
                }

                renderer.Resize(Width, Height);
                swapChain.Resize(Width, Height);
            };

            window.Run();
        }
        finally
        {
            swapChain.Dispose();
            window.Dispose();

            Context.Dispose();
        }
    }
}

App provides:

Member Description
Context The GraphicsContext for the current platform
Width / Height Current framebuffer dimensions
FrameBuffer The swap chain's current frame buffer
Run<T>() Creates a renderer, runs the window loop, and cleans up on exit

BindingHelper.cs

Each graphics backend (DirectX 12, Metal, Vulkan) uses different resource binding index conventions. BindingHelper assigns the correct indices automatically:

namespace ZenithTutorials;

internal static class BindingHelper
{
    public static ResourceBinding[] Bindings(params ResourceBinding[] bindings)
    {
        switch (App.Context.Backend)
        {
            case Backend.DirectX12:
                {
                    uint cbvIndex = 0;
                    uint srvIndex = 0;
                    uint uavIndex = 0;
                    uint samplerIndex = 0;

                    for (int i = 0; i < bindings.Length; i++)
                    {
                        ref ResourceBinding binding = ref bindings[i];

                        binding = binding with
                        {
                            Index = binding.Type switch
                            {
                                ResourceType.ConstantBuffer => cbvIndex++,

                                ResourceType.StructuredBuffer or
                                ResourceType.Texture or
                                ResourceType.AccelerationStructure => srvIndex++,

                                ResourceType.StructuredBufferReadWrite or
                                ResourceType.TextureReadWrite => uavIndex++,

                                ResourceType.Sampler => samplerIndex++,

                                _ => binding.Index
                            }
                        };
                    }
                }
                break;

            case Backend.Metal:
                {
                    uint bufferIndex = 0;
                    uint textureIndex = 0;
                    uint samplerIndex = 0;

                    for (int i = 0; i < bindings.Length; i++)
                    {
                        ref ResourceBinding binding = ref bindings[i];

                        binding = binding with
                        {
                            Index = binding.Type switch
                            {
                                ResourceType.ConstantBuffer or
                                ResourceType.StructuredBuffer or
                                ResourceType.StructuredBufferReadWrite or
                                ResourceType.AccelerationStructure => bufferIndex++,

                                ResourceType.Texture or
                                ResourceType.TextureReadWrite => textureIndex++,

                                ResourceType.Sampler => samplerIndex++,

                                _ => binding.Index
                            }
                        };
                    }
                }
                break;

            case Backend.Vulkan:
                {
                    for (int i = 0; i < bindings.Length; i++)
                    {
                        ref ResourceBinding binding = ref bindings[i];

                        binding = binding with { Index = (uint)i };
                    }
                }
                break;
        }

        return bindings;
    }
}
Backend Index Strategy
DirectX 12 Separate counters per register type (CBV, SRV, UAV, Sampler)
Metal Separate counters per resource category (Buffer, Texture, Sampler)
Vulkan Sequential binding indices

CocoaHelper.cs

Required for macOS to create a CAMetalLayer for the window surface:

namespace ZenithTutorials;

internal static partial class CocoaHelper
{
    private const string LibObjC = "/usr/lib/libobjc.A.dylib";

    [LibraryImport(LibObjC, EntryPoint = "objc_getClass")]
    private static partial nint GetClass([MarshalAs(UnmanagedType.LPUTF8Str)] string name);

    [LibraryImport(LibObjC, EntryPoint = "sel_registerName")]
    private static partial nint Selector([MarshalAs(UnmanagedType.LPUTF8Str)] string name);

    [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
    private static partial nint Send(nint receiver, nint selector);

    [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
    private static partial nint Send(nint receiver, nint selector, [MarshalAs(UnmanagedType.I1)] bool arg);

    [LibraryImport(LibObjC, EntryPoint = "objc_msgSend")]
    private static partial nint Send(nint receiver, nint selector, nint arg);

    public static nint CreateLayer(nint cocoa)
    {
        nint layer = Send(GetClass("CAMetalLayer"), Selector("layer"));
        Send(layer, Selector("retain"));

        nint view = Send(cocoa, Selector("contentView"));
        Send(view, Selector("setWantsLayer:"), true);
        Send(view, Selector("setLayer:"), layer);

        return layer;
    }
}
Note

On Windows and Linux, this file is not used but must be present to compile.

Program.cs

The entry point provides an interactive tutorial selector:

using ZenithTutorials;
using ZenithTutorials.Renderers;

(string Name, Action Run)[] tutorials =
[
    ("Hello Triangle",   App.Run<HelloTriangleRenderer>),
    ("Textured Quad",    App.Run<TexturedQuadRenderer>),
    ("Spinning Cube",    App.Run<SpinningCubeRenderer>),
    ("Compute Shader",   App.Run<ComputeShaderRenderer>),
    ("Indirect Drawing", App.Run<IndirectDrawingRenderer>),
    ("Ray Tracing",      App.Run<RayTracingRenderer>),
    ("Mesh Shading",     App.Run<MeshShadingRenderer>)
];

for (int i = 0; i < tutorials.Length; i++)
{
    Console.WriteLine($"{i + 1}. {tutorials[i].Name}");
}

Console.Write("Select a tutorial to run: ");

if (int.TryParse(Console.ReadKey().KeyChar.ToString(), out int choice) && choice >= 1 && choice <= tutorials.Length)
{
    Console.WriteLine($"\nRunning '{tutorials[choice - 1].Name}' tutorial...");

    tutorials[choice - 1].Run();
}
Tip

If you are following the tutorials sequentially, comment out renderers you haven't implemented yet to avoid build errors.

Next Steps

With the framework in place, you're ready to start the first tutorial:

  • Hello Triangle - Render your first triangle with a graphics pipeline

Source Code

Tip

View the complete tutorial project on GitHub: ZenithTutorials