Prowl.Graphite 2.8.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package Prowl.Graphite --version 2.8.2
                    
NuGet\Install-Package Prowl.Graphite -Version 2.8.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Prowl.Graphite" Version="2.8.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Prowl.Graphite" Version="2.8.2" />
                    
Directory.Packages.props
<PackageReference Include="Prowl.Graphite" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Prowl.Graphite --version 2.8.2
                    
#r "nuget: Prowl.Graphite, 2.8.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Prowl.Graphite@2.8.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Prowl.Graphite&version=2.8.2
                    
Install as a Cake Addin
#tool nuget:?package=Prowl.Graphite&version=2.8.2
                    
Install as a Cake Tool

Prowl.Graphite

A cross-platform, low-level graphics and compute abstraction for .NET, with a Vulkan backend. Graphite powers the rendering layer of the Prowl Game Engine and can be used to build high-performance 2D and 3D games, simulations, tools, and other graphical applications.

Graphite started life as a modified and butchered version of NeoVeldrid, and by extension Veldrid, but has diverged far enough in its setup and API surface that it is now considered a separate library rather than a fork. See API Differences for the systems that intentionally break from upstream Veldrid.

Features

  • A Vulkan backend, with macOS support via MoltenVK (Vulkan-over-Metal translation).
  • A monolithic ShaderProgram model that bundles shader and pipeline state, with per-backend shader compilation handled internally.
  • A string/id-driven PropertySet resource binding system that hides per-backend binding rules.
  • A declarative render graph (RenderPipeline, IPass, IPresentPass) that orders passes from their declared texture and buffer reads/writes and resolves shared, transient, and history resources automatically.
  • A frame-less ExecutionTask ring for CPU/GPU synchronization, with per-execution transient (bump-allocated) GPU memory.
  • Runtime-toggleable validation and profiling layers, controlled via GraphicsDeviceOptions.

Requirements

  • .NET 10 (net10.0).
  • A GPU and driver supporting Vulkan.
  • Silk.NET 2.23.0 (pulled in transitively; provides the native bindings).
  • Prowl.Vector 2.1.0 for vector and matrix math.

Quick Start

Rendering is built around a render graph: a RenderPipeline owns a list of IPasses plus one required IPresentPass, and a GraphicsDevice dispatches that pipeline against a list of views. The simplest possible pipeline has no offscreen passes at all, and draws straight into the swapchain from its present pass:

internal readonly struct SceneView : IRenderView
{
    public SceneView(uint width, uint height)
    {
        PixelWidth = width;
        PixelHeight = height;
    }

    public uint PixelWidth { get; }
    public uint PixelHeight { get; }
}

internal sealed class TrianglePresentPass : IPresentPass<SceneView>
{
    private readonly Mesh _triangle;
    private readonly GraphicsProgram _shader;

    public TrianglePresentPass(Mesh triangle, GraphicsProgram shader)
    {
        _triangle = triangle;
        _shader = shader;
    }

    public string Name => "Present";

    public void Setup(PresentContextBuilder builder) => builder.RequestSwapchain();

    public void Present(RenderContext<SceneView> context)
    {
        Framebuffer? target = context.SwapchainTarget;
        if (target == null)
            return;

        // The command buffer is already begun; submitting it ends and queues it.
        CommandBuffer cmd = context.GetCommandBuffer("Triangle");
        cmd.SetFramebuffer(target);
        cmd.ClearDepthStencil(1, 0);
        cmd.ClearColorTarget(0, new Color(0.10f, 0.12f, 0.16f, 1.0f));
        cmd.SetShader(_shader);
        cmd.SetVertexSource(_triangle);
        cmd.DrawIndexed();

        context.SubmitCommandBuffer(cmd);
        context.Present();
    }
}

internal sealed class TrianglePipeline : RenderPipeline<SceneView>
{
    private readonly IPresentPass<SceneView> _present;

    public TrianglePipeline(IPresentPass<SceneView> present) => _present = present;

    protected override void InitializePasses() => SetPresentPass(_present);
}

Creating a device and dispatching the pipeline each frame:

GraphicsDeviceOptions options = new()
{
    Debug = false,
    SwapchainDepthFormat = PixelFormat.D24_UNorm_S8_UInt,
    SyncToVerticalBlank = false,
    PreferStandardClipSpaceYDirection = true
};

GraphicsDevice device = GraphicsDevice.CreateVulkan(options, swapchainDescription, vulkanOptions);

GraphicsProgram shader = /* load + create a ShaderProgram */;
Mesh triangle = /* create vertex/index buffers */;
TrianglePipeline pipeline = new(new TrianglePresentPass(triangle, shader));
SceneView[] views = { new SceneView(600, 600) };

// Per-frame render loop: builds an ExecutionTask internally, runs the pipeline for every view, and
// swaps buffers if any view's present pass requested it.
device.DispatchGraph(pipeline, views);

The Samples/ directory contains complete, runnable versions of this and larger graphs (window creation, shader loading, and mesh setup included).

Backends

Backend Windows Linux macOS
Vulkan Yes Yes Yes (via MoltenVK)

A device is created through the backend-specific factory methods on GraphicsDevice (CreateVulkan). The GraphicsBackend enum enumerates the available backends.

Building

The solution targets net10.0. Build everything with:

dotnet build Prowl.Graphite.slnx

Build configuration flags

One MSBuild property controls backend trimming. It can be set on the command line (-p:ExcludeVulkan=true) or in Directory.Build.props.

Property Default Effect
ExcludeVulkan false Excludes the Vulkan backend (and its Silk.NET packages) from the build, defining EXCLUDE_VULKAN_BACKEND.

Validation and Profiling Layers

Graphite ships two optional layers that mirror the core source tree, toggled at runtime through GraphicsDeviceOptions rather than at compile time:

  • Validation (GraphicsDeviceOptions.EnableValidation, defaults to enabled when null): extra argument and state checks that throw descriptive exceptions on misuse. Validation lives under Graphite/ValidationLayers, mirroring the structure of Graphite/Core and Graphite/Platform, and every check is gated behind GraphicsDevice.ValidationEnabled.
  • Profiling (GraphicsDeviceOptions.EnableProfiling, defaults to disabled when null): allocation and command counters collected by the GraphicsDevice and readable through GraphicsDevice.GetProfile(). Profiling lives under Graphite/Profiling, mirroring the same structure, and every counter is gated behind GraphicsDevice.ProfilingEnabled.

Both settings are read once at device creation and apply for the device's lifetime. Leave EnableValidation on during development; disable it for release builds where the extra checks aren't needed. EnableProfiling stays off unless you're actively reading GetProfile().

API Differences

These are the systems that intentionally diverge from upstream Veldrid/NeoVeldrid.

Pipeline API

The previous Pipeline API has been gutted in favor of a monolithic ShaderProgram object, which encapsulates pipeline data slightly differently. The concrete types are GraphicsProgram and ComputeProgram, both deriving from the abstract ShaderProgram.

Conceptually, ShaderProgram and Pipeline are very similar in behavior, but there are a few key differences:

ShaderProgram compiles per-platform shaders itself. This tradeoff was chosen because of how the library is used in Prowl: Shader objects cannot be compiled separately from Pipeline objects, or reused. Prowl's shader markdown syntax directly couples pipeline state with shader state, and Prowl's only extra axis is differing compiled Variants, which need to be compiled regardless. Decoupled shaders and pipeline states did not benefit Prowl in any way, so they were removed.

PrimitiveTopology and OutputDescription have been divorced from pipelines/shader programs in favor of simplicity. In most renderers, pipelines are already cached and indexed by their output description. The Vulkan backend is the only one that benefits from bundling the output description with the pipeline; there, the OutputDescription is saved on the command buffer and used to index an internal Vulkan pipeline cache that keys cached pipelines on the combination of ShaderProgram, PrimitiveTopology, and OutputDescription. When a ShaderProgram is disposed, its internally cached pipelines are disposed alongside it.

Command Buffers

CommandList has been renamed to CommandBuffer, shamelessly mirroring Unity's API to reduce friction when porting over.

CommandBuffer.SetPipeline has been replaced with CommandBuffer.SetShader - conceptually the same call.

IVertexSource

CommandBuffer.SetVertexBuffer/SetIndexBuffer has been replaced with CommandBuffer.SetVertexSource. A new IVertexSource interface provides a resolver architecture where a bound shader program requests buffers at a given location.

The API is designed to strike a balance between Unity's mesh-style binding system and a flexible binding API for lower-level users:

public interface IVertexSource
{
    // Provides the draw topology this source wants. Reasoning: topology is coupled with vertex data,
    // as it directly influences index counts.
    PrimitiveTopology Topology { get; }

    // Resolves a device buffer slot. layoutSlot is the index in the created shader's vertex inputs.
    // layout is the source layout description used by the shader, for binding vertex data by name.
    // VertexBinding is a union of the resolved DeviceBuffer and the offset in the buffer to use.
    void ResolveSlot(uint layoutSlot, in VertexLayoutDescription layout, out VertexBinding binding);

    // Resolves an index buffer slot. Returns false if no index buffer is available.
    // Provides format and offset data.
    bool TryGetIndexBuffer(out DeviceBuffer buffer, out IndexFormat format, out uint offset);
}

New resource binding API

To replace the resource binder, a new PropertySet API has been created. It acts as a merged property builder that maps user-facing strings/ids to their cross-platform binding equivalent. Creating a shader requires more reflection information up front, but the tradeoff is that user-facing code never has to reason about complicated binding rules across platforms, such as the differences between D3D registers and Vulkan sets/bindings.

// PropertyID is a lightweight wrapper over an interned string->int for fast dictionary indexing.
PropertyID internedId = "MainTexture";

PropertySet propertySet = new();

// SetTexture accepts a paired sampler for platforms with combined texture/sampler binding.
propertySet.SetTexture(internedId, MainTextureObject, MainTextureSampler);

// SetSampler binds a sampler independently of any texture.
propertySet.SetSampler("SecondaryTexture_SamplerObject", SecondarySampler);

// Transient uniform properties. Transient uniforms are owned by the execution ring's transient
// allocator, and are automatically allocated and disposed.
propertySet.SetFloat("FloatProperty", 10.3f);
propertySet.SetMatrix("MatrixProperty", ObjectMatrix);

// Set an SSBO buffer.
propertySet.SetBuffer("SSBOBuffer", MySSBOBuffer);

// Set a static, read-only UBO buffer with fixed uniforms. Any SetX() call that would write into
// this UBO is ignored while 'readOnly' is true.
propertySet.SetBuffer("UBOBuffer", MyUBOBuffer, readOnly: true);

// Set a writable UBO buffer. When 'readOnly' is false, SetX() calls use this buffer as their
// backing storage, letting users control the backing UBO lifetime manually.
propertySet.SetBuffer("UBOBuffer", MyUBOBuffer, readOnly: false);

Execution ring

Veldrid leaves CPU/GPU synchronization entirely to the caller: GraphicsDevice.SubmitCommands takes an optional Fence you own, and any double/triple buffering (which command list or resource generation is safe to reuse) is the caller's responsibility to track by hand.

Graphite builds a frames-in-flight ring into the device instead, addressed through ExecutionTask. GraphicsDevice.BeginExecution() grabs a free ring slot (blocking on the oldest in-flight task if all slots are busy) and hands back an ExecutionTask; CompleteExecution closes it out non-blockingly, and the ring's own fence tells you when it's safe to reuse that slot again. In practice you rarely call these directly - GraphicsDevice.DispatchGraph does it for you around a RenderPipeline run:

ExecutionTask task = device.BeginExecution();  // Blocks if the oldest ring slot is still in flight.
// ... rent command buffers via a RenderContext, submit them ...
device.CompleteExecution(task);                // Signals the task's completion fence; does not block.
device.SwapBuffers();

Key pieces:

  • GraphicsDevice.MaxExecutingTasks - the ring depth. Configured via GraphicsDeviceOptions.MaxFramesInFlight (defaults to 3 when left 0).
  • BeginExecution / CompleteExecution - open and close an execution. BeginExecution blocks only when the ring slot it is about to reuse has not yet completed on the GPU.
  • ExecutionTask.Id / ExecutionTask.RingSlot - a monotonic id (starting at 1; 0 is the "none" sentinel) and the [0, MaxExecutingTasks) slot it occupies.
  • ExecutionTask.CompletionFence - owned and recycled by the ring. Do not reset it or hold the reference past the next BeginExecution for the same ring slot.
  • IsExecutionComplete / WaitForExecution / LastCompletedExecutionId / ExecutingTasks / ActiveExecutions - poll, block on, or query execution completion. These also opportunistically advance the device's notion of the last completed execution.
Transient (per-execution) memory

Each ring slot owns a bump-allocated transient buffer. RenderContext.AllocateTransient(sizeInBytes) hands back a DeviceBufferRange that is valid for GPU use until the execution's completion fence signals, after which the memory is recycled. This is what backs transient PropertySet uniforms. The allocator is governed by:

Option Default Behavior
TransientBufferInitialSize 4 MB Initial size of each per-slot transient buffer.
TransientBufferSoftCapBytes 64 MB Per-execution soft cap; exceeding it logs a one-shot warning.
TransientBufferHardCapBytes 256 MB Per-execution hard cap; exceeding it throws a RenderException.

Render graph and pass lifetime

Rendering is no longer "record commands into a CommandBuffer yourself each frame" - it's a declarative graph of passes over a RenderPipeline<TView>:

  • IPass<TView> - a single offscreen pass. Setup(RenderContextBuilder) runs once (lazily, on first use) and declares the resources the pass reads and writes: GetInputTexture(id) / GetInputBuffer(id) reference a resource by ID only (the producer owns the description), while GetOutputTexture(id, desc) / GetOutputBuffer(id, desc) declare a resource this pass produces. Render(RenderContext<TView>) runs every dispatch and records the pass's actual work.
  • RasterPass<TView> - a convenience base for the common raster pass. Declare the render target in Setup with SetTarget(builder, id, desc) (or SetTargets for a multi-format MRT target), then in Render rent a command buffer, call BindTarget(context, cmd) to bind the target and apply its declared load/clear ops, record draws, and submit. Raw IPass remains the low-level escape hatch.
  • IPresentPass<TView> - the one required, terminal pass. Setup(PresentContextBuilder) declares the resources it reads from the graph and whether it needs the window's swapchain this run (RequestSwapchain()). Present(...) runs after every other pass; grab context.SwapchainTarget, draw into it, and call context.Present() to arm the present. Do nothing to stay offscreen.
  • RenderPipeline<TView> - subclass and override InitializePasses() to call AddPass for each IPass and SetPresentPass once. It may also declare shared resources centrally with DeclareTexture(id, desc) / DeclareBuffer(id, desc) so many passes can reference them by ID without one owning the description. The pipeline lazily solves the declared passes into a RenderGraph the first time it runs: passes are topologically sorted so readers run after their writers. Every input ID must be produced by some pass output or a central declaration, otherwise build throws; a dependency cycle throws too.
  • Command buffers - obtained only from the context (context.GetCommandBuffer(name)), which begins the buffer, and submitted only through the same context (context.SubmitCommandBuffer(cmd)), which ends and queues it. Passes never call Begin/End. A buffer rented but never submitted is released and logs a warning. Submitted buffers retire through the execution ring's fence; passes never block.
  • GraphTextureDesc - describes a graph texture: view-relative (GraphTextureDesc.ViewSized, scaled off IRenderView.PixelWidth/PixelHeight) or fixed-size (GraphTextureDesc.Sized), plus color formats and whether it has a depth attachment.
  • GraphBufferDesc - describes a graph buffer by size and usage (GraphBufferDesc.Structured, .Uniform, or .Of); a structured buffer is distinct from a uniform or vertex buffer. Buffer resources are transient (rented per execution) like textures.
  • Load/store ops - an output declaration carries TargetLoadStoreOps (per-attachment LoadAction Clear/Load/DontCare and StoreAction Store/DontCare). The default follows lifetime: transient targets Clear, persistent/history/imported targets Load. RasterPass.BindTarget applies them; clear values are supplied at record time.
  • History and imported resources - GetOutputTexture(id, desc, history: N) (and the buffer form) makes a persistent versioned resource: the graph keeps a ring of N+1 physical copies and rotates the current one each execution. Resolve by age with context.GetRenderTexture(handle, framesAgo: k) (framesAgo: 0 is the current write target) - the basis for TAA history and temporal reprojection. builder.ImportTexture(id, existing) brings an externally-owned render target into the graph; the caller keeps ownership.
  • TextureHandle / BufferHandle - the opaque handles a pass gets back from the builder during setup; resolve them to a real RenderTexture / DeviceBuffer during rendering via context.GetRenderTexture(handle) / context.GetRenderBuffer(handle).
  • IRenderView - the minimal size contract (PixelWidth/PixelHeight) a pipeline's view type must implement so view-relative textures can be sized; add richer per-view data (matrices, frustum) on top in your own type. How a pass obtains and issues its draws is entirely up to the pass body; the graph does not model draw commands, culling, or sorting.
  • IPassProfiler - optional per-dispatch hooks (BeginSample/EndSample, and a RequestCapture/Capture pair the pipeline calls between passes to copy pass outputs to an intermediate texture for inspection).

Dispatch a pipeline against a list of views with GraphicsDevice.DispatchGraph:

device.DispatchGraph(pipeline, views, profiler: null);

This opens one ExecutionTask, runs RenderPipeline.ExecuteView (ordered passes, then the present pass) for every view, completes the execution, and calls SwapBuffers() if any view's present pass requested a present.

Samples

Runnable samples live under Samples/ and share common setup (windowing, shader and model loading) through the Shared project:

  • HelloTriangle - the minimal render loop: one present pass, no offscreen passes.
  • TexturedQuad - texture and sampler binding.
  • Cube / CubeGrid - 3D transforms and instancing-style draws.
  • PBRRenderer - a multi-pass render graph: an offscreen "Scene" pass, a two-step bloom (downsample/upsample), and a present pass that composites Scene + bloom to the swapchain. The graph orders the four passes from their declared texture reads/writes.

Run one with, for example:

dotnet run --project Samples/HelloTriangle

Testing

Tests live under Tests/ and are split into CPU tests (pure value-type tests, run in parallel) and GPU tests (which share one device per backend and run serialized). GPU shaders are authored in Slang (.slang) under Tests/Shaders and compiled to SPIR-V for Vulkan at runtime; there are no checked-in compiled shaders. See Tests/README.md for the current suite layout and the in-progress migration of older suites onto the GraphicsProgram / PropertySet / ExecutionTask API.

dotnet test Tests/Prowl.Graphite.Tests.csproj

Credits

Thank you to mellinoe and ciberman, the creators of Veldrid and NeoVeldrid, for being unaware of what I did to your libraries. Having a base, known-stable library has massively boosted development and shaved hours of boilerplate off development time.

Prowl.Graphite has had radical filesystem and API changes relative to upstream Veldrid/NeoVeldrid. As such, changes and fixes from NeoVeldrid cannot be easily merged, and will land in the commit history with the prefix (NeoVeldrid) and the same commit name, but with altered file paths, locations, and logic. If any of the original contributors would like more or different credit for their work, or would like me to stop sourcing from their commits, please reach out.

License

This project is part of the Prowl Game Engine and is licensed under the MIT License. See the LICENSE file in the project root for full details. Portions are derived from Veldrid (Copyright (c) 2017 Eric Mellino and Veldrid contributors) and NeoVeldrid (Copyright (c) 2026 Javier Mora and NeoVeldrid contributors), both MIT licensed. </content> </invoke>

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on Prowl.Graphite:

Package Downloads
Prowl.Graphite.ShaderDef

The ShaderDef language spec and variant runtime for Prowl.Graphite.

Prowl.Graphite.Compiler

The Slang-based shader compiler for Prowl.Graphite.

Prowl.Graphite.Shaders

The shared keyword and shader-variant system for Prowl.Graphite.

Prowl.Graphite.Variants

The shared keyword and shader-variant system for Prowl.Graphite.

Prowl.Graphite.ShaderDef.Compiler

The Slang-based shader parser and compiler for Prowl.Graphite.ShaderDef.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.8.5 0 7/24/2026
2.8.3 0 7/24/2026
2.8.2 33 7/24/2026
2.7.0 121 7/16/2026
2.6.3 115 7/13/2026
2.6.0 116 7/12/2026
2.5.5 115 7/11/2026
2.5.4 113 7/9/2026
2.5.1 111 7/9/2026
2.4.0 133 7/7/2026
2.3.4 137 7/3/2026
2.3.1 137 7/3/2026
0.8.0 142 7/1/2026
0.7.1 141 6/29/2026
0.7.0 143 6/27/2026
0.6.2 134 6/29/2026
0.6.1 131 6/23/2026
0.6.0 146 6/23/2026
0.5.0 144 6/22/2026
0.4.2 143 6/21/2026
Loading failed