Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ProwlEngine/Prowl.Quill/llms.txt

Use this file to discover all available pages before exploring further.

The Silk.NET backend is structurally identical to the OpenTK backend but uses Silk.NET’s low-level OpenGL bindings instead of the OpenTK managed wrappers. Both backends share the same GLSL shaders from CanvasShaderSource and implement the same dual Kawase backdrop blur pipeline — the primary difference is in how GL calls are spelled (e.g. _gl.BufferData(...) instead of GL.BufferData(...)). If your project already depends on Silk.NET for windowing or input, this backend drops in without adding OpenTK as a dependency. It targets OpenGL 3.3 Core Profile and declares SupportsBackdropBlur = true.

Required Packages

dotnet add package Silk.NET.OpenGL
dotnet add package Silk.NET.Input
Prowl.Quill, Prowl.Scribe, and Prowl.Vector are also required.

Project Setup

The entry point creates a WindowOptions and delegates to a SilkWindow class that wraps Silk.NET’s IWindow:
var options = WindowOptions.Default with
{
    Size  = new Silk.NET.Maths.Vector2D<int>(1280, 720),
    Title = "Silk.NET Quill Example",
    VSync = true
};

using var window = new SilkWindow(options);
window.Run();

Implementing ICanvasRenderer

SilkNetRenderer takes a GL context in its constructor, following Silk.NET’s convention of passing the graphics context rather than relying on a thread-local current context:
public class SilkNetRenderer : ICanvasRenderer, IDisposable
{
    public static string FRAGMENT_SHADER_SOURCE => CanvasShaderSource.FragmentShader;
    private static string VERTEX_SHADER_SOURCE  => CanvasShaderSource.VertexShader;

    private readonly GL _gl;
    public bool SupportsBackdropBlur => true;

    public SilkNetRenderer(GL gl)
    {
        _gl = gl;
    }
}

Initialization

Call Initialize once after the OpenGL context has been created (typically inside the window’s OnLoad callback). It compiles shaders, creates the VAO/VBO/EBO, and sets the initial projection:
public unsafe void Initialize(int width, int height, TextureSilk defaultTexture)
{
    _defaultTexture = defaultTexture;
    CreateShaderProgram();
    CreateBuffers();
    UpdateProjection(width, height);
}
Shader compilation follows the standard pattern — compile vertex and fragment shaders, link the program, then cache all uniform locations at once:
private void CreateShaderProgram()
{
    _program = _gl.CreateProgram();

    uint vertShader = CompileShader(ShaderType.VertexShader,   VERTEX_SHADER_SOURCE);
    uint fragShader = CompileShader(ShaderType.FragmentShader, FRAGMENT_SHADER_SOURCE);

    _gl.AttachShader(_program, vertShader);
    _gl.AttachShader(_program, fragShader);
    _gl.LinkProgram(_program);
    CheckProgramLinking(_program);

    _gl.DetachShader(_program, vertShader);
    _gl.DetachShader(_program, fragShader);
    _gl.DeleteShader(vertShader);
    _gl.DeleteShader(fragShader);

    CacheUniformLocations();

    // Dual Kawase blur programs
    _blurDownProgram = BuildBlurProgram(CanvasShaderSource.BlurDownsampleShader);
    _blurUpProgram   = BuildBlurProgram(CanvasShaderSource.BlurUpsampleShader);
    _blurVao = _gl.GenVertexArray();
    _blurFbo = _gl.GenFramebuffer();
}

private void CacheUniformLocations()
{
    _projectionLocation       = _gl.GetUniformLocation(_program, "projection");
    _textureSamplerLocation   = _gl.GetUniformLocation(_program, "texture0");
    _scissorMatLocation       = _gl.GetUniformLocation(_program, "scissorMat");
    _scissorExtLocation       = _gl.GetUniformLocation(_program, "scissorExt");
    _brushMatLocation         = _gl.GetUniformLocation(_program, "brushMat");
    _brushTypeLocation        = _gl.GetUniformLocation(_program, "brushType");
    _brushColor1Location      = _gl.GetUniformLocation(_program, "brushColor1");
    _brushColor2Location      = _gl.GetUniformLocation(_program, "brushColor2");
    _brushParamsLocation      = _gl.GetUniformLocation(_program, "brushParams");
    _brushParams2Location     = _gl.GetUniformLocation(_program, "brushParams2");
    _brushTextureMatLocation  = _gl.GetUniformLocation(_program, "brushTextureMat");
    _dpiScaleLocation         = _gl.GetUniformLocation(_program, "dpiScale");
    _backdropTexLocation      = _gl.GetUniformLocation(_program, "backdropTexture");
    _viewportSizeLocation     = _gl.GetUniformLocation(_program, "viewportSize");
    _backdropBlurAmountLocation = _gl.GetUniformLocation(_program, "backdropBlurAmount");
}
Buffer layout mirrors the OpenTK backend — a 20-byte stride with position at offset 0, texcoord at offset 8, and a 4-byte packed RGBA color at offset 16:
private unsafe void CreateBuffers()
{
    _vao = _gl.GenVertexArray();
    _gl.BindVertexArray(_vao);

    _vbo = _gl.GenBuffer();
    _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);

    uint stride = (uint)Vertex.SizeInBytes;

    _gl.EnableVertexAttribArray(0); // Position: vec2 at offset 0
    _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, stride, (void*)0);

    _gl.EnableVertexAttribArray(1); // TexCoord: vec2 at offset 8
    _gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, stride, (void*)8);

    _gl.EnableVertexAttribArray(2); // Color: vec4 ubyte normalized at offset 16
    _gl.VertexAttribPointer(2, 4, VertexAttribPointerType.UnsignedByte, true, stride, (void*)16);

    _ebo = _gl.GenBuffer();
    _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ebo);

    _gl.BindVertexArray(0);
}

Per-Frame Loop

// Inside SilkWindow's OnRender callback:
float dpiScale = (float)window.FramebufferSize.X / window.Size.X;
_canvas.BeginFrame(window.Size.X, window.Size.Y, dpiScale);

_demos[_currentDemoIndex].RenderFrame(deltaTime, _offset, _zoom, _rotation);

_gl.Clear(ClearBufferMask.ColorBufferBit);
_canvas.Render();

RenderCalls — Geometry Submission

RenderCalls sets up blend state, uploads geometry once per frame, then iterates draw calls through ProcessDrawCall:
public unsafe void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls)
{
    if (drawCalls.Count == 0 || canvas.Vertices.Count == 0) return;

    SetupRenderState();
    UploadGeometryData(canvas);

    int indexOffset = 0;
    foreach (var drawCall in drawCalls)
    {
        ProcessDrawCall(drawCall, indexOffset, (float)canvas.FramebufferScale);
        indexOffset += drawCall.ElementCount;
    }

    _gl.BindVertexArray(0);
    _gl.UseProgram(0);
}

private void SetupRenderState()
{
    _gl.Disable(EnableCap.DepthTest);
    _gl.Enable(EnableCap.Blend);
    _gl.BlendFunc(BlendingFactor.One, BlendingFactor.OneMinusSrcAlpha);
    _gl.UseProgram(_program);
    SetProjectionMatrix();
    _gl.BindVertexArray(_vao);
    _gl.Uniform1(_textureSamplerLocation, 0);
}
Inside ProcessDrawCall, each draw call can optionally trigger a backdrop blur capture before the main geometry is drawn:
private unsafe void ProcessDrawCall(DrawCall drawCall, int indexOffset, float dpiScale)
{
    if (drawCall.Brush.BackdropBlur > 0f)
        RenderBackdropBlur((float)drawCall.Brush.BackdropBlur);

    TextureSilk texture = (drawCall.Texture as TextureSilk) ?? _defaultTexture;
    texture.Use(TextureUnit.Texture0);

    if (drawCall.Shader is uint customProgram)
    {
        _gl.UseProgram(customProgram);
        int projLoc = _gl.GetUniformLocation(customProgram, "projection");
        if (projLoc >= 0) SetMatrix4Uniform(projLoc, _projection);
        if (drawCall.ShaderUniforms != null)
            SetCustomUniforms(customProgram, drawCall.ShaderUniforms);
    }
    else
    {
        _gl.UseProgram(_program);
        SetProjectionMatrix();
        _gl.Uniform1(_dpiScaleLocation, dpiScale);
        drawCall.GetScissor(out var scissorMat, out var scissorExt);
        SetScissorUniforms(scissorMat, scissorExt);
        SetBrushUniforms(drawCall.Brush);
        _gl.Uniform2(_viewportSizeLocation, (float)_fbWidth, (float)_fbHeight);
        _gl.Uniform1(_backdropTexLocation, 3);
        _gl.Uniform1(_backdropBlurAmountLocation, (float)drawCall.Brush.BackdropBlur);
    }

    _gl.DrawElements(
        PrimitiveType.Triangles,
        (uint)drawCall.ElementCount,
        DrawElementsType.UnsignedInt,
        (void*)(indexOffset * sizeof(uint))
    );
}

Texture Methods

public object CreateTexture(uint width, uint height)
{
    return TextureSilk.CreateNew(_gl, width, height);
}

public Int2 GetTextureSize(object texture)
{
    if (texture is not TextureSilk silkTexture)
        throw new ArgumentException("Invalid texture type");
    return new Int2((int)silkTexture.Width, (int)silkTexture.Height);
}

public void SetTextureData(object texture, IntRect bounds, byte[] data)
{
    if (texture is not TextureSilk silkTexture)
        throw new ArgumentException("Invalid texture type");
    silkTexture.SetData(bounds, data);
}

Backdrop Blur

The blur implementation is functionally identical to the OpenTK backend — a six-level mip pyramid, dual Kawase downsample then upsample, result bound on texture unit 3:
private void RenderBackdropBlur(float radius)
{
    EnsureBlurTargets(_fbWidth, _fbHeight);
    ComputeBlurParams(radius, out int iterations, out float offset);

    _gl.Disable(EnableCap.Blend);
    _gl.BindVertexArray(_blurVao);
    _gl.ActiveTexture(TextureUnit.Texture0);

    // Blit default framebuffer into level 0 (half-res)
    _gl.BindFramebuffer(FramebufferTarget.DrawFramebuffer, _blurFbo);
    _gl.FramebufferTexture2D(FramebufferTarget.DrawFramebuffer,
        FramebufferAttachment.ColorAttachment0,
        TextureTarget.Texture2D, _blurTex[0], 0);
    _gl.BindFramebuffer(FramebufferTarget.ReadFramebuffer, 0);
    _gl.BlitFramebuffer(0, 0, _fbWidth, _fbHeight,
        0, 0, _blurSize[0].X, _blurSize[0].Y,
        ClearBufferMask.ColorBufferBit, BlitFramebufferFilter.Linear);

    _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _blurFbo);

    _gl.UseProgram(_blurDownProgram);
    for (int i = 0; i < iterations; i++)
        BlurPass(_blurTex[i], _blurTex[i + 1], _blurSize[i + 1], _downHalfpixelLoc, _blurSize[i]);

    _gl.UseProgram(_blurUpProgram);
    for (int i = iterations; i > 0; i--)
        BlurPass(_blurTex[i], _blurTex[i - 1], _blurSize[i - 1], _upHalfpixelLoc, _blurSize[i - 1]);

    // Restore canvas draw state
    _gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
    _gl.Viewport(0, 0, (uint)_fbWidth, (uint)_fbHeight);
    _gl.Enable(EnableCap.Blend);
    _gl.BindVertexArray(_vao);

    // Bind blurred result on unit 3 for the canvas shader
    _gl.ActiveTexture(TextureUnit.Texture3);
    _gl.BindTexture(TextureTarget.Texture2D, _blurTex[0]);
    _gl.ActiveTexture(TextureUnit.Texture0);
}

Cleanup

public void Dispose()
{
    _gl.DeleteBuffer(_vbo);
    _gl.DeleteBuffer(_ebo);
    _gl.DeleteVertexArray(_vao);
    _gl.DeleteProgram(_program);
    if (_blurDownProgram != 0) _gl.DeleteProgram(_blurDownProgram);
    if (_blurUpProgram   != 0) _gl.DeleteProgram(_blurUpProgram);
    if (_blurVao != 0) _gl.DeleteVertexArray(_blurVao);
    if (_blurFbo != 0) _gl.DeleteFramebuffer(_blurFbo);
    for (int i = 0; i < MaxBlurLevels; i++)
        if (_blurTex[i] != 0) _gl.DeleteTexture(_blurTex[i]);
}

Build docs developers (and LLMs) love