Skip to main content

Documentation Index

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

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

Paper is intentionally decoupled from any specific graphics API. All rendering goes through ICanvasRenderer, a small interface that receives batched draw-call data from Prowl.Quill and submits it to whatever GPU backend you choose. This page explains the interface, shows how to wire it up, and points you to the three ready-to-run reference implementations in the repository.

When Do You Need a Custom Renderer?

If you are using the provided OpenTK or Raylib samples you do not need to implement anything — those renderers are complete and production-quality. You only need to write your own ICanvasRenderer when:
  • You are embedding Paper in an engine that manages its own render pipeline (Unity, Godot, a custom engine).
  • You want to target a graphics API not yet covered (DirectX, Vulkan, Metal, WebGPU).
  • You need to integrate Paper’s draw calls into an existing render graph or command buffer model.

Reference Implementations

Study these three samples before writing your own. They all implement the same interface against very different backends and between them cover the major patterns you will encounter.

OpenTK (OpenGL 4)

Samples/OpenTK/PaperRenderer.cs
Full-featured OpenGL 4.3 renderer with a VAO/VBO pipeline, projection matrix, scissor, gradient brushes, and a dual-Kawase backdrop blur implementation.

Raylib

Samples/RaylibSample/RaylibCanvasRenderer.cs
Under 500 lines. Uses Raylib’s Rlgl low-level API to submit triangles directly. Demonstrates that the interface is simple enough for a minimal integration.

WebGL / WASM

Samples/WasmExample/WebGLCanvasRenderer.cs
Bridges the .NET/WASM runtime to a JavaScript WebGL2 context through JS interop. Shows how to handle texture IDs as opaque tokens and batch vertex data across the boundary.

The ICanvasRenderer Interface

Paper only calls three methods on your renderer, plus a texture lifecycle pair:
public interface ICanvasRenderer
{
    // Create a new empty RGBA texture of the given dimensions.
    // Return an opaque handle — Paper treats it as object and passes it back.
    object CreateTexture(uint width, uint height);

    // Return the dimensions of a previously created texture.
    Int2 GetTextureSize(object texture);

    // Upload a region of pixel data (RGBA 8-bit) into a texture.
    void SetTextureData(object texture, IntRect bounds, byte[] data);

    // Submit a completed frame's worth of draw calls to the GPU.
    // canvas.Vertices and canvas.Indices hold the vertex/index buffers;
    // drawCalls carries per-draw-call metadata (brush, scissor, texture, shader).
    void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls);

    // Optional: return true if the backend can composite a blurred backdrop.
    bool SupportsBackdropBlur { get; }
}
RenderCalls is called once per paper.EndFrame(). All vertex and index data for the entire frame lives in canvas.Vertices and canvas.Indices. Each DrawCall in the list has an ElementCount telling you how many indices it consumes, starting from the offset left by the previous draw call.

Step-by-Step: Implementing a Renderer

1
Set Up the Shader Pipeline
2
Paper produces anti-aliased geometry using a single fragment shader that handles flat fills, gradients, texture sampling, scissoring, and optional backdrop blur. The vertex format is 20 bytes per vertex:
3
offset  0: float x        (position)
offset  4: float y
offset  8: float u        (UV / AA params)
offset 12: float v
offset 16: ubyte r, g, b, a   (pre-multiplied colour)
4
Copy the GLSL source from Samples/Common/CanvasShaderSource.cs for OpenGL-based backends or from the inline string constants in RaylibCanvasRenderer.cs. The fragment shader expects these uniforms:
5
uniform mat4  projection;       // orthographic: (0,w) x (0,h)
uniform sampler2D texture0;     // font atlas / image texture
uniform mat4  scissorMat;       // scissor transform
uniform vec2  scissorExt;       // scissor half-extents
uniform mat4  brushMat;         // gradient transform
uniform int   brushType;        // 0=none, 1=linear, 2=radial, 3=box
uniform vec4  brushColor1;
uniform vec4  brushColor2;
uniform vec4  brushParams;      // gradient geometry
uniform vec2  brushParams2;     // corner radius + feather
uniform mat4  brushTextureMat;  // texture image transform
uniform float dpiScale;         // logical-to-pixel ratio
6
Implement the Interface Methods
7
public class MyRenderer : ICanvasRenderer
{
    public bool SupportsBackdropBlur => false; // enable once blur passes are ready

    public object CreateTexture(uint width, uint height)
    {
        // Allocate a GPU texture and return an opaque handle.
        // Example for a hypothetical API:
        var tex = MyGfxApi.CreateTexture2D(width, height, Format.RGBA8);
        return tex;
    }

    public Int2 GetTextureSize(object texture)
    {
        var tex = (MyTexture)texture;
        return new Int2((int)tex.Width, (int)tex.Height);
    }

    public void SetTextureData(object texture, IntRect bounds, byte[] data)
    {
        var tex = (MyTexture)texture;
        tex.UpdateRegion(bounds.Min.X, bounds.Min.Y,
                         bounds.Size.X, bounds.Size.Y, data);
    }

    public void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls)
    {
        if (drawCalls.Count == 0) return;

        // 1. Upload vertex + index buffers to the GPU.
        UploadGeometry(canvas.Vertices, canvas.Indices);

        // 2. Configure shared render state (blend, depth-test, etc.).
        SetBlendState(premultipliedAlpha: true);

        // 3. Iterate draw calls.
        int indexOffset = 0;
        foreach (var dc in drawCalls)
        {
            BindTexture(dc.Texture ?? _whiteTexture);

            if (dc.Shader != null)
            {
                // Custom shader path
                UseShader((MyShader)dc.Shader);
                ApplyCustomUniforms(dc.ShaderUniforms);
            }
            else
            {
                // Default Paper shader
                UseShader(_paperShader);

                // Scissor
                dc.GetScissor(out var scissorMat, out var scissorExt);
                SetUniform("scissorMat", scissorMat);
                SetUniform("scissorExt", scissorExt);

                // Gradient brush
                SetUniform("brushType",   (int)dc.Brush.Type);
                SetUniform("brushMat",    dc.Brush.BrushMatrix);
                SetUniform("brushColor1", dc.Brush.Color1);
                SetUniform("brushColor2", dc.Brush.Color2);
                SetUniform("brushParams", new Float4(
                    (float)dc.Brush.Point1.X, (float)dc.Brush.Point1.Y,
                    (float)dc.Brush.Point2.X, (float)dc.Brush.Point2.Y));
                SetUniform("brushParams2", new Float2(
                    (float)dc.Brush.CornerRadii, (float)dc.Brush.Feather));
                SetUniform("brushTextureMat", dc.Brush.TextureMatrix);
                SetUniform("dpiScale",    (float)canvas.FramebufferScale);
            }

            DrawIndexed(indexOffset, dc.ElementCount);
            indexOffset += dc.ElementCount;
        }
    }
}
8
Wire Up the Renderer with Paper
9
Construct a Paper instance, passing your renderer, the initial viewport size, and a FontAtlasSettings describing how fonts should be rasterized.
10
var renderer = new MyRenderer();
renderer.Initialize(windowWidth, windowHeight);

var fontAtlas = new Prowl.Quill.FontAtlasSettings(); // default settings
var paper = new Paper(renderer, windowWidth, windowHeight, fontAtlas);
11
Handle Resizes
12
When the window dimensions change, notify both the renderer (so it rebuilds its projection matrix) and Paper (so it re-flows the layout).
13
void OnWindowResize(int newWidth, int newHeight)
{
    renderer.UpdateProjection(newWidth, newHeight);   // your renderer method
    paper.SetResolution(newWidth, newHeight);
}
14
Handle HiDPI / Retina Displays
15
On high-DPI displays there is a gap between the logical window size (what Paper uses for layout) and the physical framebuffer size (what the GPU draws into). Set DisplayFramebufferScale so Paper rasterizes fonts at the right density.
16
// On Retina macOS or with a 200% Windows DPI setting:
paper.DisplayFramebufferScale = new Float2(2f, 2f);

// Or pass dpiScale to BeginFrame each frame (sets X and Y simultaneously):
float dpi = (float)framebufferWidth / logicalWidth;
paper.BeginFrame(deltaTime, dpiScale: dpi);
17
Scale default style values (padding, border widths, etc.) proportionally with ScaleAllSizes. Call this once at startup, not every frame.
18
paper.ScaleAllSizes(dpiScale);

FontAtlasSettings

FontAtlasSettings controls how Prowl.Quill allocates and updates the GPU font texture atlas. The defaults work for most applications, but you may want to adjust them when:
  • You are loading a large number of distinct fonts or sizes (increase atlas dimensions).
  • You need subpixel rendering or specific SDF parameters.
  • You are targeting memory-constrained hardware (reduce atlas size, limit glyph range).
var fontAtlas = new Prowl.Quill.FontAtlasSettings
{
    // Atlas texture dimensions — must be power-of-two
    Width  = 2048,
    Height = 2048,
    // … other settings exposed by FontAtlasSettings
};

var paper = new Paper(renderer, w, h, fontAtlas);

Loading Fonts

Paper uses FontFile objects from Prowl.Scribe. Load font bytes from disk or an embedded resource and construct a FontFile:
// From disk
byte[] bytes = File.ReadAllBytes("path/to/Roboto-Regular.ttf");
FontFile regularFont = new FontFile(bytes);

// From an embedded resource stream
using Stream stream = Assembly.GetExecutingAssembly()
    .GetManifestResourceStream("MyApp.Resources.Roboto-Regular.ttf")!;
FontFile regularFont = new FontFile(stream);

Fallback Fonts

Register fallback fonts so that characters absent from the primary font (e.g. emoji, CJK, icon fonts) are automatically sourced from a secondary glyph table:
FontFile icons = new FontFile(File.ReadAllBytes("fa-solid-900.ttf"));
paper.AddFallbackFont(icons);

// Enumerate fonts registered on the host OS
foreach (FontFile systemFont in paper.EnumerateSystemFonts())
    paper.AddFallbackFont(systemFont);

Skeleton Implementation Reference

The following is a minimal compilable skeleton that you can use as a starting point. Fill in the GPU API calls for your target backend.
using Prowl.Quill;
using Prowl.Vector;
using Prowl.Vector.Geometry;

public class SkeletonRenderer : ICanvasRenderer
{
    private object _whiteTexture;

    public bool SupportsBackdropBlur => false;

    public void Initialize(int width, int height)
    {
        // Compile shader, create VAO/VBO, and create a default white 1×1 texture.
        _whiteTexture = CreateTexture(1, 1);
        SetTextureData(_whiteTexture, new IntRect(0, 0, 1, 1),
                       new byte[] { 255, 255, 255, 255 });
        UpdateProjection(width, height);
    }

    public void UpdateProjection(int width, int height)
    {
        // Rebuild the orthographic projection matrix: (0,w)×(0,h).
    }

    // ── ICanvasRenderer ────────────────────────────────────────────────────

    public object CreateTexture(uint width, uint height)
    {
        // Allocate a GPU texture and return an opaque handle.
        throw new NotImplementedException();
    }

    public Int2 GetTextureSize(object texture)
    {
        // Return (width, height) of the texture.
        throw new NotImplementedException();
    }

    public void SetTextureData(object texture, IntRect bounds, byte[] data)
    {
        // Upload RGBA pixel data into the specified texture region.
        throw new NotImplementedException();
    }

    public void RenderCalls(Canvas canvas, IReadOnlyList<DrawCall> drawCalls)
    {
        if (drawCalls.Count == 0) return;

        // 1. Upload canvas.Vertices and canvas.Indices to GPU buffers.
        // 2. Set render state: premultiplied alpha blend, no depth test.
        // 3. Bind Paper shader, set projection uniform.
        // 4. For each DrawCall:
        //    a. Bind dc.Texture (or _whiteTexture).
        //    b. Set scissor uniforms via dc.GetScissor().
        //    c. Set brush uniforms from dc.Brush.
        //    d. Draw dc.ElementCount triangles from the running index offset.
        throw new NotImplementedException();
    }

    public void Dispose()
    {
        // Release GPU resources (buffers, textures, shaders).
    }
}

Build docs developers (and LLMs) love