Table of Contents

Prerequisites

This guide covers the environment setup required before working with Zenith.NET.

System Requirements

Hardware

Zenith.NET supports multiple graphics backends across platforms:

Platform DirectX 12 Metal 4 Vulkan 1.4
Windows Yes No Yes
Linux No No Yes
Android No No Yes
macOS No Yes Yes
iOS No Yes Yes
Note

These tutorials are designed for desktop platforms (Windows, Linux, and macOS).

Software

  • .NET SDK: 10.0 or later
  • IDE: Visual Studio 2026, VS Code, or JetBrains Rider

Building the Tutorials

The example code in these tutorials is designed to be extensible. We'll create a base project structure that all tutorials will share.

Creating the Project

dotnet new console -n ZenithTutorials
cd ZenithTutorials

Required Packages

Install the following NuGet 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.Slang
dotnet add package Silk.NET.Windowing
dotnet add package Silk.NET.Input

Project Configuration

Update your .csproj file:

<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="Zenith.NET.DirectX12" Version="*" />
    <PackageReference Include="Zenith.NET.Metal" Version="*" />
    <PackageReference Include="Zenith.NET.Vulkan" Version="*" />
    <PackageReference Include="Zenith.NET.Extensions.Slang" Version="*" />
    <PackageReference Include="Silk.NET.Windowing" Version="*" />
    <PackageReference Include="Silk.NET.Input" Version="*" />
  </ItemGroup>

</Project>
Note

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

Project Structure

Organize your project with the following directory structure:

ZenithTutorials/
├── Program.cs         # Application entry point
├── App.cs             # Application framework
├── IRenderer.cs       # Renderer interface
├── BindingHelper.cs   # Cross-platform resource binding helper
├── CocoaHelper.cs     # macOS CAMetalLayer helper
├── Usings.cs          # Global using statements
└── Renderers/         # All tutorial renderers

Global Usings

Create Usings.cs for shared using statements across all files:

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

This eliminates repetitive using statements in each renderer file.

Renderer Interface

All tutorial renderers implement a common interface. Create IRenderer.cs:

namespace ZenithTutorials;

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

    void Render();

    void Resize(uint width, uint height);
}

This interface ensures all renderers follow a consistent pattern:

  • Update - Called each frame for logic updates (animations, input handling)
  • Render - Called each frame to record and submit draw commands
  • Resize - Called when the window size changes
  • Dispose - Cleanup GPU resources

Binding Helper

Different graphics backends use different indexing schemes for resource bindings:

Backend Index Scheme
DirectX 12 Per-type: CBV, SRV, UAV, Sampler each start at 0
Vulkan Global: All resources share index space (0, 1, 2, ...)
Metal Per-category: Buffer, Texture, Sampler each start at 0

Create BindingHelper.cs to handle these differences 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.Vulkan:
                {
                    for (int i = 0; i < bindings.Length; i++)
                    {
                        ref ResourceBinding binding = ref bindings[i];

                        binding = binding with { Index = (uint)i };
                    }
                }
                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 => bufferIndex++,

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

                                ResourceType.Sampler => samplerIndex++,

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

        return bindings;
    }
}

Usage example:

resourceLayout = App.Context.CreateResourceLayout(new()
{
    Bindings = BindingHelper.Bindings
    (
        new() { Type = ResourceType.Texture, Count = 1, StageFlags = ShaderStageFlags.Pixel },
        new() { Type = ResourceType.Sampler, Count = 1, StageFlags = ShaderStageFlags.Pixel }
    )
});

The helper automatically assigns the correct Index values based on the current backend, so you don't need to specify them manually.

Cocoa Helper

On macOS, creating a rendering surface requires a CAMetalLayer. Silk.NET.Windowing doesn't expose this directly, so we need a helper to create it using Objective-C runtime interop.

Create CocoaHelper.cs:

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;
    }
}

The CAMetalLayer can be used with both Metal and Vulkan backends on macOS.

Application Framework

All tutorials share a common application framework that handles window creation, graphics context initialization, and the main loop.

App.cs

Create App.cs as the reusable application framework:

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;

    static App()
    {
        // Ensure platform is supported
        if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux() && !OperatingSystem.IsMacOS())
        {
            throw new PlatformNotSupportedException("This tutorial only supports Windows, Linux, and macOS.");
        }

        // Create window with no graphics API (we manage rendering ourselves)
        window = Window.Create(WindowOptions.Default with
        {
            API = GraphicsAPI.None,
            Title = "Zenith.NET Tutorial",
            Size = new(1280, 720)
        });

        window.Initialize();

        // Create graphics context and surface based on platform
        Surface surface;
        if (OperatingSystem.IsWindows())
        {
            Context = GraphicsContext.CreateDirectX12(useValidationLayer: true);

            surface = Surface.Win32(window.Native!.Win32!.Value.Hwnd, Width, Height);
        }
        else if (OperatingSystem.IsLinux())
        {
            Context = GraphicsContext.CreateVulkan(useValidationLayer: true);

            surface = Surface.Xlib(window.Native!.X11!.Value.Display, (nint)window.Native.X11.Value.Window, Width, Height);
        }
        else
        {
            Context = GraphicsContext.CreateMetal(useValidationLayer: true);

            surface = Surface.Apple(CocoaHelper.CreateLayer(window.Native!.Cocoa!.Value), Width, Height);
        }

        // Log validation messages for debugging
        Context.ValidationMessage += (sender, args) =>
        {
            Console.WriteLine($"[{args.Source} - {args.Severity}] {args.Message}");
        };

        // Create swap chain for double-buffered rendering
        SwapChain = Context.CreateSwapChain(new()
        {
            Surface = surface,
            ColorTargetFormat = PixelFormat.B8G8R8A8UNorm,
            DepthStencilTargetFormat = PixelFormat.D32FloatS8UInt
        });
    }

    public static GraphicsContext Context { get; }

    public static SwapChain SwapChain { get; }

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

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

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

        window.Update += renderer.Update;

        window.Render += delta =>
        {
            // Skip rendering when window is minimized
            if (Width <= 0 || Height <= 0)
            {
                return;
            }

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

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

            // Notify renderer first, then resize swap chain
            renderer.Resize(Width, Height);
            SwapChain.Resize(Width, Height);
        };

        window.Run();
    }

    public static void Cleanup()
    {
        SwapChain.Dispose();
        Context.Dispose();
        window.Dispose();
    }
}

Program.cs

Create Program.cs as the simple entry point:

using ZenithTutorials;
using ZenithTutorials.Renderers;

App.Run<HelloTriangleRenderer>();

App.Cleanup();
Note

HelloTriangleRenderer will be created in the next tutorial.

This framework provides:

  • Platform validation - Ensures only supported platforms (Windows, Linux, macOS) are used
  • Window creation with Silk.NET (1280×720 default size)
  • Cross-platform backend selection (DirectX 12 on Windows, Vulkan on Linux, Metal on macOS)
  • SwapChain management for presenting frames
  • Resize handling for responsive rendering
  • Generic renderer pattern using App.Run<TRenderer>() for easy tutorial switching
  • Static access to App.Context and App.SwapChain from renderers

Verify Installation

Before continuing, verify your setup compiles correctly:

dotnet build

If the build succeeds, you're ready to start Hello Triangle!

Source Code

Tip

The complete source code for all tutorials is available on GitHub: ZenithTutorials