gg Architecture

July 15, 2026 · View on GitHub

This document describes the architecture of the gg 2D graphics library.

Overview

gg is a 2D graphics library for Go, inspired by HTML5 Canvas API and modern Rust 2D graphics libraries (vello, tiny-skia).

Core principle (v0.26.0): CPU raster is the foundation, GPU is an optional accelerator.

                        ┌───────────────────┐
                        │ User Application  │
                        └─────────┬─────────┘

                           ┌──────▼──────┐
                           │     gg      │
                           │  2D Canvas  │
                           └──────┬──────┘

                    ┌─────────────┼─────────────┐
                    │                           │
             ┌──────▼──────┐             ┌──────▼──────┐
             │  CPU Raster │             │    GPU      │
             │  (default)  │             │ Accelerator │
             └──────┬──────┘             └──────┬──────┘
                    │                           │ (optional, 6 tiers)
             ┌──────▼──────┐             ┌──────▼──────┐
             │  internal/  │             │  internal/  │
             │   raster    │             │    gpu      │
             └─────────────┘             └──────┬──────┘

                                         ┌──────▼──────┐
                                         │    wgpu     │
                                         └──────┬──────┘

                                  ┌──────┬──────┼──────┬──────┐
                                  │      │      │      │      │
                               ┌──▼──┐┌──▼──┐┌──▼──┐┌──▼──┐┌──▼──┐
                               │ Vk  ││DX12 ││Metal││GLES ││Soft │
                               └─────┘└─────┘└─────┘└─────┘└─────┘
                                            wgpu/hal

GPU Accelerator (v0.26.0+, Compute v0.30.0)

GPU acceleration is opt-in via the GPUAccelerator interface:

// GPUAccelerator is an optional GPU acceleration provider.
type GPUAccelerator interface {
    Name() string
    Init() error
    Close()
    CanAccelerate(op AcceleratedOp) bool
    FillPath(target GPURenderTarget, path *Path, paint *Paint) error
    StrokePath(target GPURenderTarget, path *Path, paint *Paint) error
    FillShape(target GPURenderTarget, shape DetectedShape, paint *Paint) error
    StrokeShape(target GPURenderTarget, shape DetectedShape, paint *Paint) error
    Flush(target GPURenderTarget) error
}

// Register via blank import pattern
import _ "github.com/gogpu/gg/gpu"  // enables GPU acceleration

Optional extension interfaces for gogpu integration:

  • DeviceProviderAware -- share GPU device with an external provider (e.g., gogpu window)
  • Per-pass render target -- GPURenderTarget.View (gpucontext.TextureView) enables GPU-direct rendering to any texture view (surface or offscreen). Follows WebGPU spec per-render-pass target pattern. SurfaceTargetAware (deprecated, use GPURenderTarget.View)

Seven-Tier GPU Rendering

The GPU accelerator in internal/gpu/ uses a unified render session (GPURenderSession) that dispatches shapes and text to six rendering tiers:

TierNameContentTechnique
1SDFCircles, ellipses, rounded rectsSDF shader evaluation per-pixel
2aConvexConvex polygonsFan tessellation (no stencil needed)
2bStencil+CoverArbitrary pathsStencil buffer for winding, then cover pass
3Textured QuadDrawImage, GPU texturesGPU textured quad pipeline
4MSDF TextText glyphs (dynamic/animated)Multi-channel SDF with median+smoothstep shader
5ComputeFull scenes (many paths)Vello-style 9-stage compute pipeline (GPU or CPU fallback)
6Glyph MaskText glyphs (static/UI, ≤48px)CPU-rasterized R8 alpha atlas, GPU textured quads, ClearType LCD subpixel, font hinting

Tiers 1–4, 6 use a render-pass pipeline (one render pass, multiple pipeline switches). Tier 5 uses a compute-only pipeline (9 dispatch stages, no render pass). Auto-selection routes horizontal text ≤48px to Tier 6 (pixel-perfect with font hinting and optional ClearType LCD subpixel rendering), else Tier 4 (scalable MSDF).

This mirrors enterprise engines (Skia Ganesh/Graphite, Flutter Impeller, Gio).

All tiers compute ortho projection at flush time from actual render target dimensions (ADR-025, Skia sk_RTAdjust pattern). This enables correct rendering to offscreen textures of any size (RepaintBoundary compositing).

Key design:

  • RegisterAccelerator() for opt-in GPU
  • ErrFallbackToCPU sentinel error for graceful degradation
  • AcceleratedOp bitfield for capability checking
  • Shape detection (DetectShape) auto-identifies circles, rects, rounded rects from path data
  • Flush() dispatches batched GPU operations in a single pass
  • Two render modes: offscreen (readback to CPU) and surface (zero-copy to window)
  • PipelineMode (Auto/RenderPass/Compute) selects the appropriate pipeline
  • CPU raster always available as fallback

Software Backend Strategy (v0.50.4+, rasterAtlas)

On CPU-only adapters (llvmpipe, SwiftShader, WARP, no-GPU), gg uses the rasterAtlas strategy (Skia Graphite kRasterAtlas pattern):

Fill()/Stroke()

    ├── GPU shape pipelines (SDF, convex, stencil) → SKIPPED (hang on SPIR-V interpreter)

    └── All shapes → pendingDraws → CPU SoftwareRenderer dispatch

         ├── Main surface (View=nil):
         │    flushCPUToPixmap → shapes rendered to c.pixmap
         │    GPU texture commands (DrawGPUTexture, DrawImage) → retained in pendingDraws
         │    → session render → SPIR-V textured quad → readback → composite into pixmap
         │    → WriteSurfacePixels (zero-copy to window)

         └── Offscreen (View=offscreen texture):
              flushCPUToView → shapes rendered to tmpPixmap → RGBA→BGRA → WriteTexture

Key invariants:

  • deviceReady=true, gpuReady=false — GPU device alive for texture operations, shape pipelines disabled (Skia TextureProxy::Make() pattern)
  • earlyBlitOnly optimization skips ensurePipelines() for texture-only frames but ensurePipelineWithStencil() must be ensured for readback path
  • uploadPixmapToView must NOT run after flushCPUToView (would overwrite offscreen content with c.pixmap background data)
  • BGRA textures require R/B swap in SPIR-V readTexel and writeRasterToTarget
  • CopyTextureToBuffer must respect BytesPerRow alignment (256-byte boundary)

Vello Compute Pipeline (Tier 5, v0.30.0)

The compute pipeline is a port of vello's 9-stage GPU compute architecture to Go. It processes entire scenes (many paths) in parallel on the GPU using compute shaders.

9-Stage Pipeline

Scene → [1] pathtag_reduce → [2] pathtag_scan → [3] draw_reduce → [4] draw_leaf

Output ← [9] fine ← [8] path_tiling ← [7] coarse ← [6] backdrop ← [5] path_count
StageShaderPurpose
1pathtag_reduce.wgslParallel reduction of path tag monoids
2pathtag_scan.wgslPrefix scan producing cumulative offsets
3draw_reduce.wgslParallel reduction of draw monoids
4draw_leaf.wgslPrefix scan + draw info extraction
5path_count.wgslPer-tile segment counting + line flattening
6backdrop.wgslLeft-to-right backdrop prefix sum per row
7coarse.wgslPer-tile command list (PTCL) generation
8path_tiling.wgslSegment clipping and tile assignment
9`fine.wgsl$\text{Per}-\text{pixel} \text{rasterization} (16 \times 16 \text{tiles}, 64 \text{threads} \times 4 \text{pixels} \text{each})

\text{PipelineMode} \text{Selection}

$PipelineMode` controls which GPU pipeline is used:

ModeBehavior
PipelineModeAutoAuto-select based on scene complexity heuristics
PipelineModeRenderPassForce render-pass pipeline (Tiers 1–4)
PipelineModeComputeForce compute pipeline (Tier 5)

In Auto mode, the compute pipeline is preferred for scenes with many paths, while the render-pass pipeline is preferred for simple scenes with few shapes.

CPU Reference Implementation

The tilecompute/ package contains a complete CPU reference implementation of the 9-stage pipeline. RasterizeScenePTCL() runs the full pipeline on CPU, producing identical output to the GPU compute shaders. This enables:

  • Golden tests (GPU vs CPU pixel comparison)
  • Debugging shader correctness
  • CPU-only fallback when no GPU is available

Smart Rasterizer Selection (v0.32.0)

gg uses multi-factor auto-selection to choose the optimal rasterization algorithm per-path. Five algorithms are available, each optimal for different scenarios:

AlgorithmTypeTilesOriginOptimal ForLocation
AnalyticFillerCPU— (scanline)Skia AAA (pixel-perfect port of Chrome/Android rasterizer)General paths, small shapesinternal/raster/
AnalyticFiller ConvexCPU— (scanline)Skia AAA convex fast path (1.6x faster)Convex shapes (rect, circle, triangle)`internal/raster/$
\text{SparseStrips}\text{CPU}4 \times 4\text{Vello} \text{sparse_strips}\text{Complex} \text{paths}, \text{CPU}/\text{SIMD} \text{workloads}internal/gpu/sparsestrips.go,fine.go,coarse.gointernal/gpu/sparse_strips*.go`, `fine.go`, `coarse.go
\text{TileCompute}\text{CPU}16 \times 16\text{Vello} 9-\text{stage} \text{compute} (\text{CPU} \text{port})\text{Extreme} \text{complexity} (10\text{K}+ \text{segments})$internal/gpu/tilecompute/`
SDFAcceleratorCPU+GPU— (per-pixel)Original (gg)Geometric shapes (circles, rrects)sdf_accelerator.go, `internal/gpu/sdf_gpu.go$
\text{Vello} \text{PTCL}\text{GPU}16 \times 16\text{Vello} 9-\text{stage} \text{compute}\text{Full} \text{scenes} (\text{many} \text{paths}, \text{GPU} \text{compute})$internal/gpu/vello_*.go`

Auto-Selection Flow

Context.Fill()

    ├── RasterizerMode == SDF? → force SDF on accelerator → try GPU
    ├── RasterizerMode == Auto? → try GPU accelerator (SDF/Stencil/Compute)

    └── SoftwareRenderer.Fill()

         ├── RasterizerMode == Analytic? → AnalyticFiller (scanline)
         ├── RasterizerMode == SparseStrips? → forced SparseStrips (4×4)
         ├── RasterizerMode == TileCompute? → forced TileCompute (16×16)

         └── RasterizerMode == Auto?

              ├── bbox < 32px? → AnalyticFiller (tile overhead exceeds benefit)
              ├── elements > adaptiveThreshold(bboxArea)? → CoverageFiller
              │    └── AdaptiveFiller auto-selects:
              │         ├── segments > 10K AND area > 2MP → TileCompute (16×16)
              │         └── otherwise → SparseStrips (4×4)
              └── otherwise → AnalyticFiller (scanline)

Adaptive Threshold Formula

threshold = clamp(2048 / sqrt(bboxArea), 32, 256)
``$

| \text{Bounding} \text{Box} | \text{Threshold} | \text{Rationale} |
|-------------|-----------|-----------|
| 50 \times 50 \text{px} | 29 \text{elements} | \text{Small} \text{area} — \text{scanline} \text{is} \text{cheap} |
| 100 \times 100 \text{px} | 20 \text{elements} | \text{Medium} — \text{tile} \text{rasterizer} \text{starts} \text{winning} |
| 200 \times 200 \text{px} | 10 \text{elements} | \text{Large} — \text{tile} \text{overhead} \text{amortized} |
| 500 \times 500 \text{px} | 4 \text{elements} | \text{Very} \text{large} — \text{almost} \text{always} \text{tile} |

### \text{CoverageFiller} \text{Interface}

$``go
// CoverageFiller is a tile-based coverage rasterizer for complex paths.
type CoverageFiller interface {
    FillCoverage(path *Path, width, height int, fillRule FillRule,
        callback func(x, y int, coverage uint8))
}

// ForceableFiller allows forced algorithm selection.
type ForceableFiller interface {
    CoverageFiller
    SparseFiller() CoverageFiller   // 4×4 tiles
    ComputeFiller() CoverageFiller  // 16×16 tiles
}

Registration follows the same pattern as GPUAccelerator:

  • RegisterCoverageFiller() / GetCoverageFiller() — registration pair
  • gg/gpu/ registers `AdaptiveFiller$ (\text{auto} 4 \times 4 \text{vs} 16 \times 16)
  • $gg/raster/registersAdaptiveFiller` independently (CPU-only, no GPU deps)

RasterizerMode API

Per-context force override for debugging, benchmarking, and known workloads:

dc := gg.NewContext(800, 600)

dc.SetRasterizerMode(gg.RasterizerSparseStrips) // force 4×4 tiles
dc.SetRasterizerMode(gg.RasterizerSDF)          // force SDF for shapes
dc.SetRasterizerMode(gg.RasterizerAuto)          // restore auto-selection
ModeBehavior
RasterizerAutoMulti-factor auto-selection (default)
RasterizerAnalyticForce scanline, bypass tile rasterizer
`RasterizerSparseStrips$\text{Force} 4 \times 4 \text{tiles} \text{via} $ForceableFiller`
`RasterizerTileCompute$\text{Force} 16 \times 16 \text{tiles} \text{via} $ForceableFiller`
RasterizerSDFForce SDF for shapes, bypass min-size check

Text Rendering Pipeline (v0.29.0+, CPU Transform v0.32.1, Hinting+ClearType v0.36.0)

Text rendering uses a multi-tier strategy. GPU MSDF handles text when available; the CPU pipeline uses a hybrid decision tree for transform-aware rendering. Glyph Mask (Tier 6) provides pixel-perfect rendering with auto-hinting and optional ClearType LCD subpixel rendering for small axis-aligned text.

Pipeline Flow

dc.DrawString(s, x, y)

    ├── [1] GPU MSDF Text (if GPUTextAccelerator registered)
    │       CTM passed to vertex shader → correct scale/rotation/skew

    └── [2] CPU Pipeline (drawStringCPU — 3-tier decision tree)

             ├── Tier 0: Translation-only? → bitmap fast path (text.Draw)
             │            No quality loss, position transformed by CTM

             ├── Tier 1: Uniform positive scale ≤256px?
             │            → bitmap at device size (Strategy A, Skia pattern)
             │            FontSource.Face(fontSize * scale) at transformed position

             └── Tier 2: Everything else (rotation, shear, non-uniform scale,
                          negative scale, scale >256px)
                          → glyph outlines as vector paths (Strategy B, Vello pattern)
                          OutlineExtractor → Path (all glyphs, one fill)
                          → path.Transform(CTM) → SoftwareRenderer.Fill()

Design Decisions (Enterprise References)

DecisionReferenceRationale
256px atlas thresholdSkia kSkSideTooBigForAtlasAbove this, bitmap quality degrades; outlines scale perfectly
Translation-only fast pathCairo _cairo_gstate_get_font_ctmMost common case, zero overhead vs identity
Glyph outlines as PathVello resolve_glyph_runExact rendering at any transform, no quality loss
Y-flip (y - outlineY)TrueType/PostScript (Y-up) → screen (Y-down)Industry standard for font outline conversion
All glyphs in one Pathscene/text.go:ToCompositePathSingle fill call, more efficient than per-glyph
FillRuleNonZeroFont outline conventionStandard winding rule for TrueType/OpenType contours
MultiFace fallbackSource() == nil → bitmapGraceful degradation for composite faces
Lazy OutlineExtractorGC-managed lifecycleNo changes to NewContext() or Close()

Key Files

FileContent
text.godrawStringCPU decision tree, drawStringBitmap/Scaled/AsOutlines
context.gooutlineExtractor field (lazy init)
text/glyph_outline.goOutlineExtractor, GlyphOutline, OutlineSegment
text/face.goFace.Glyphs(), Face.Source(), Face.Size()

HiDPI/Retina Device Scale

gg uses the Cairo-pattern device_scale for DPI-transparent drawing. User code operates in logical coordinates (points/DIP), while the internal pixmap is allocated at physical pixel resolution. A permanent base scale transform maps logical to physical automatically.

API

// Create HiDPI-aware context (800x600 logical, 1600x1200 physical on Retina 2x)
dc := gg.NewContextWithScale(800, 600, 2.0)

// Or via functional option
dc := gg.NewContext(800, 600, gg.WithDeviceScale(2.0))

// Or set dynamically
dc.SetDeviceScale(2.0)

dc.Width()       // 800 (logical)
dc.Height()      // 600 (logical)
dc.PixelWidth()  // 1600 (physical)
dc.PixelHeight() // 1200 (physical)
dc.DeviceScale() // 2.0

// Drawing uses logical coordinates — no DPI awareness needed in user code
dc.DrawCircle(400, 300, 100) // centered in logical space
dc.Fill()                     // rasterized at physical resolution

Implementation

  • NewContextWithScale(w, h, scale) allocates pixmap at w*scale × h*scale
  • A permanent Scale(scale, scale) base matrix is applied to the transform stack
  • Identity() resets to the base matrix (not the identity matrix)
  • All drawing operations flow through the transform stack and are automatically scaled
  • SoftwareRenderer receives device scale for DPI-aware tolerances

DPI-Aware Rasterization (femtovg pattern)

On HiDPI displays, rasterization tolerances are adjusted for sharper output:

ParameterFormulaEffect on Retina 2x
Curve flatten tolerancebaseTol / deviceScale0.05 instead of 0.1 — finer subdivision
Stroke expansion tolerancebaseTol / deviceScaleTighter stroke edges

gogpu Integration (ggcanvas)

// ggcanvas automatically uses platform scale factor
canvas := ggcanvas.MustNewWithScale(provider, logicalW, logicalH, scaleFactor)

ggcanvas.NewWithScale() creates the GPU texture at physical pixel dimensions while the gg Context operates in logical coordinates. The scale factor typically comes from gogpu.App.ScaleFactor().

Package Structure

gg/
├── context.go              # Canvas-like drawing context
├── software.go             # SoftwareRenderer (adaptive threshold + force mode)
├── accelerator.go          # GPUAccelerator interface + registration
├── coverage_filler.go      # CoverageFiller + ForceableFiller interfaces
├── rasterizer_mode.go      # RasterizerMode type (Auto/Analytic/SparseStrips/TileCompute/SDF)
├── sdf.go                  # CPU SDF coverage functions (circles, ellipses, rects)
├── sdf_accelerator.go      # SDFAccelerator (CPU-based SDF, ForceSDFAware)
├── shape_detect.go         # DetectShape: auto-detect circles/rects/rrects from paths
├── pipeline_mode.go        # PipelineMode (Auto/RenderPass/Compute)
├── options.go              # Configuration options
├── path.go                 # Vector path operations (SetPath, DrawPath, FillPath)
├── path_svg.go             # SVG path data parser (ParseSVGPath)
├── paint.go                # Fill and stroke styles
├── lcd_layout.go           # LCD ClearType layout types (LCDLayoutRGB/BGR/None)
├── pixmap.go               # Pixel buffer operations
├── text.go                 # Text rendering

├── gpu/                    # PUBLIC opt-in: GPU accelerator + tile rasterizer
│   └── gpu.go              # init() registers SDFAccelerator + AdaptiveFiller

├── raster/                 # PUBLIC opt-in: tile rasterizer only (no GPU)
│   └── raster.go           # init() registers AdaptiveFiller (CPU-only)

├── svg/                    # SVG renderer (Parse + Render SVG XML → image.RGBA)
│   ├── svg.go              # Public API: Parse, Render, RenderWithColor
│   ├── parser.go           # SVG XML parser → Document tree
│   ├── renderer.go         # Element rendering via gg.Context
│   ├── colors.go           # SVG color parsing (#hex, rgb(), named)
│   ├── transform.go        # SVG transform parsing (translate, rotate, scale)
│   └── document.go         # Document, Element types

├── integration/
│   └── ggcanvas/           # gogpu integration (Canvas for windowed rendering)
│       ├── canvas.go       # Canvas with Draw(), Flush(), Resize()
│       └── render.go       # RenderTo, RenderDirect (zero-copy)

├── render/                 # Cross-package rendering
│   ├── scene.go            # Scene with damage tracking
│   ├── software.go         # Software renderer
│   ├── layers.go           # LayeredTarget for z-ordering
│   ├── device.go           # DeviceHandle (gpucontext.DeviceProvider)
│   ├── target.go           # Render target abstraction
│   ├── renderer.go         # Renderer interface
│   └── gpu_renderer.go     # GPU-accelerated renderer

├── internal/
│   ├── raster/             # CPU rasterization core
│   │   ├── edge.go         # Line edge types
│   │   ├── edge_builder.go # Path to typed edges conversion
│   │   ├── alpha_runs.go   # RLE-encoded coverage buffer
│   │   ├── curve_edge.go   # QuadraticEdge, CubicEdge (forward diff)
│   │   ├── curve_aet.go    # CurveAwareAET (Active Edge Table)
│   │   ├── analytic_filler.go  # Trapezoidal integration filler
│   │   ├── fixed.go        # FDot6, FDot16 fixed-point types
│   │   ├── path_geometry.go    # Y-monotonic curve chopping
│   │   └── scene_adapter.go   # Scene to raster bridge
│   │
│   ├── gpu/                # GPU rendering pipeline (six-tier) + tile rasterizers
│   │   ├── backend.go      # GPU backend implementation
│   │   ├── gpu_shared.go    # GPUShared (global: device, pipelines, engines, TexturePool)
│   │   ├── gpu_render_context.go # GPURenderContext (per gg.Context: pending ops, clip, frame)
│   │   ├── gpu_types.go    # Shared types (scissorSegment, extractConvexPolygon)
│   │   ├── texture_pool.go # TexturePool (Flutter RenderTargetCache pattern)
│   │   ├── sdf_gpu.go      # SDFAccelerator wrapper (delegates to GPUShared + GPURenderContext)
│   │   ├── sdf_render.go   # SDF render pipeline (Tier 1)
│   │   ├── adaptive_filler.go    # AdaptiveFiller (auto 4×4 vs 16×16 tiles)
│   │   ├── sparse_strips_filler.go  # SparseStripsFiller (4×4 tiles, CoverageFiller)
│   │   ├── tilecompute_filler.go    # TileComputeFiller (16×16 tiles, CoverageFiller)
│   │   ├── convex_renderer.go  # Convex polygon renderer (Tier 2a)
│   │   ├── convexity.go    # Convexity detection algorithm
│   │   ├── stencil_renderer.go # Stencil+Cover renderer (Tier 2b)
│   │   ├── stencil_pipeline.go # Stencil render pipeline setup
│   │   ├── image_pipeline.go  # TexturedQuadPipeline (Tier 3: GPU DrawImage)
│   │   ├── image_cache.go     # ImageCache (LRU 64-entry, identity-keyed)
│   │   ├── render_session.go   # GPURenderSession (unified render pass)
│   │   ├── gpu_textures.go # MSAA + stencil + resolve texture management
│   │   ├── tessellate.go   # Fan tessellation for paths
│   │   ├── adapter.go      # Analytic AA adapter
│   │   ├── analytic_filler.go  # GPU-side analytic filler
│   │   ├── analytic_filler_vello.go  # Vello tile rasterizer
│   │   ├── vello_tiles.go  # 16x16 tile binning + DDA
│   │   ├── coarse.go       # Coarse rasterization pass
│   │   ├── fine.go         # Fine rasterization pass
│   │   ├── pipeline.go     # Render pipeline management
│   │   ├── pipeline_cache_core.go  # PipelineCache (FNV-1a)
│   │   ├── command_encoder.go  # CommandEncoder state machine
│   │   ├── texture.go      # Texture with lazy default view
│   │   ├── buffer.go       # Buffer with async mapping
│   │   ├── text_pipeline.go    # MSDF text rendering pipeline (Tier 4)
│   │   ├── glyph_mask_engine.go   # Glyph mask engine (Tier 6, shaping → atlas → quads)
│   │   ├── glyph_mask_pipeline.go # Glyph mask GPU pipeline (Tier 6, R8 alpha atlas)
│   │   ├── vello_accelerator.go  # VelloAccelerator (Tier 5 compute integration)
│   │   ├── vello_compute.go     # VelloComputeDispatcher (hal-based GPU dispatch)
│   │   ├── scene_bridge.go # Scene to native bridge
│   │   ├── golden_test.go  # GPU vs CPU golden comparison tests
│   │   │
│   │   ├── tilecompute/    # Vello compute pipeline CPU reference
│   │   │   ├── types.go         # PathDef, SceneElement, LineSoup, Tile, PathSegment
│   │   │   ├── scene_encode.go  # EncodeScene/EncodeSceneDef, PackScene
│   │   │   ├── flatten.go       # Euler spiral curve flattening
│   │   │   ├── pathtag.go       # Path tag monoid reduce/scan
│   │   │   ├── draw_leaf.go     # Draw monoid reduce/scan + ClipInp generation
│   │   │   ├── clip_leaf.go     # Clip matching (sequential stack, Vello parity)
│   │   │   ├── path_count.go    # Per-tile segment counting
│   │   │   ├── rasterizer.go    # RasterizeScenePTCL/SceneDefPTCL (full pipeline)
│   │   │   ├── coarse.go        # Coarse rasterization + PTCL + clip state
│   │   │   ├── fine.go          # Fine per-pixel rasterization + packed blend stack
│   │   │   └── shaders/         # WGSL compute shaders (9 stages)
│   │   │       ├── pathtag_reduce.wgsl
│   │   │       ├── pathtag_scan.wgsl
│   │   │       ├── draw_reduce.wgsl
│   │   │       ├── draw_leaf.wgsl
│   │   │       ├── path_count.wgsl
│   │   │       ├── backdrop.wgsl
│   │   │       ├── coarse.wgsl
│   │   │       ├── path_tiling.wgsl
│   │   │       └── fine.wgsl
│   │   │
│   │   └── shaders/        # WGSL render-pass shaders
│   │       ├── sdf_render.wgsl    # SDF shape rendering (Tier 1)
│   │       ├── convex.wgsl        # Convex polygon fill (Tier 2a)
│   │       ├── stencil_fill.wgsl  # Stencil fill pass (Tier 2b)
│   │       ├── cover.wgsl         # Cover pass (Tier 2b)
│   │       ├── textured_quad.wgsl # Textured quad (Tier 3: DrawImage)
│   │       ├── fine.wgsl          # Fine rasterization
│   │       ├── coarse.wgsl        # Coarse rasterization
│   │       ├── flatten.wgsl       # Path flattening
│   │       ├── blend.wgsl         # Blending operations
│   │       ├── blit.wgsl          # Blit / copy
│   │       ├── composite.wgsl     # Compositing
│   │       ├── strip.wgsl         # Strip rendering
│   │       ├── msdf_text.wgsl     # MSDF text rendering (Tier 4)
│   │       ├── glyph_mask.wgsl    # Glyph mask rendering (Tier 6)
│   │       └── glyph_mask_lcd.wgsl # LCD ClearType subpixel (Tier 6)
│   │
│   ├── cache/              # LRU caching infrastructure
│   │   ├── cache.go        # Generic cache
│   │   ├── lru.go          # LRU eviction
│   │   └── sharded.go      # Sharded cache for concurrency
│   │
│   ├── gpucore/            # GPU core types and shaders
│   │   ├── adapter.go      # Core GPU adapter
│   │   ├── pipeline.go     # Core pipeline
│   │   ├── types.go        # Core GPU types
│   │   └── shaders/        # WGSL compute shaders
│   │
│   ├── blend/              # Color blending (29 modes)
│   ├── parallel/           # Parallel tile rendering
│   ├── wide/               # SIMD operations
│   ├── stroke/             # Stroke expansion (kurbo/tiny-skia)
│   └── filter/             # Blur, shadow, color matrix

├── scene/                 # Retained-mode scene graph
│   ├── scene.go           # Scene encoding + font registry for TagText
│   ├── tag.go             # Tag constants (incl. TagText 0x60)
│   ├── encoding.go        # Dual-stream encoding (path/draw/text data)
│   ├── decoder.go         # Sequential decoder for all tag types
│   ├── text.go            # TagText: DrawText/DrawGlyphs (glyph references, ADR-022)
│   ├── gpu_renderer.go    # GPU scene renderer (resolveText → DrawShapedGlyphs)
│   ├── renderer.go        # CPU tile-parallel renderer (incl. TagText fallback)
│   ├── builder.go         # Scene builder API
│   ├── path.go            # Scene path type (float32)
│   └── tile.go            # Tile grid and dirty region tracking

├── recording/              # Drawing recording for vector export
│   ├── recorder.go         # Command-based drawing recorder
│   ├── command.go          # Typed command definitions
│   ├── backend.go          # Backend interface (Writer, File)
│   ├── registry.go         # Backend registration
│   └── backends/raster/    # Built-in raster backend

├── surface/                # Render surfaces
│   ├── image_surface.go    # Image-based surface
│   └── path.go             # Surface path utilities

├── text/                   # Text rendering
│   ├── shaper.go           # Pluggable shaper interface
│   ├── shaper_builtin.go   # Default shaper (basic LTR)
│   ├── shaper_gotext.go    # HarfBuzz shaper (go-text)
│   ├── layout.go           # Multi-line layout engine
│   ├── glyf_parser.go      # TrueType glyf table parser (composite hardening: cycle detection + work budget)
│   ├── tt_glyph.go         # TT bytecode glyph loader (composite hardening mirrors glyf_parser)
│   ├── glyph_cache.go      # LRU glyph cache (16-shard)
│   ├── glyph_run.go        # GlyphRunBuilder for batching
│   ├── glyph_outline.go    # Outline extraction + grid-fit hinting + gvar composite guard
│   ├── glyph_mask_rasterizer.go # CPU rasterization (grayscale + LCD/ClearType)
│   ├── glyph_mask_atlas.go # R8 alpha atlas (shelf packing, LRU, LCD 3x, size buckets, frame-based compact)
│   ├── lcd_filter.go       # ClearType 5-tap FIR filter, LCDLayout (RGB/BGR)
│   ├── msdf/               # MSDF text rendering
│   └── emoji/              # Color emoji support

└── internal/image/          # Image I/O (PNG, JPEG, WebP)

Scene Renderer (scene/)

Retained-mode scene graph with tile-based parallel rendering. The scene.Renderer handles orchestration (tile grid, worker pool, dirty regions, layer cache) while delegating pixel rendering to gg.SoftwareRenderer.

Scene Text (ADR-022, v0.46.0)

Scene text uses TagText glyph references instead of vector paths. Text is shaped once at recording time (Scene.DrawText), stored as compact GlyphRunData headers (30 bytes) + GlyphEntry arrays (10 bytes/glyph), and resolved at render time:

Recording:                          Rendering:

text.Shape(str, face)                GPU Scene Renderer (resolveText)
    ↓                                  → dc.DrawShapedGlyphs
GlyphRunData + GlyphEntry[]              → Tier 6/4 auto-selection
    ↓                                    → hinted, atlas-batched
scene.EncodeText(TagText)
    ↓                                CPU Tile Renderer (renderTextOnTile)
textData stream (compact)               → outline extraction from stored glyphs
                                        → per-glyph Fill (fallback)

Font registry (Scene.fontRegistry) maps FontSourceID → *text.FontSource for cross-context font sharing. Merged in Scene.Append/AppendWithTranslation.

Architecture

scene.Scene (encoded draw commands + font registry)

       ├──── GPU path (GPUSceneRenderer)
       │        ↓
       │     resolveText → dc.DrawShapedGlyphs → Tier 6/4

       └──── CPU path (Renderer, tile-parallel)

             renderTextOnTile → outline extraction → SoftwareRenderer.Fill

Delegation Pattern (v0.29.4)

Following the universal pattern confirmed by Qt Quick, Skia, Vello, and Flutter/Impeller: scene graph orchestrates, immediate-mode backend rasterizes.

Per-tile rendering:

  1. Acquire SoftwareRenderer + Pixmap from sync.Pool
  2. Decode scene commands (fill, stroke, transform, etc.)
  3. Convert scene.Path (float32) → gg.Path (float64) with tile offset subtraction
  4. Convert scene.Brushgg.Paint (fill rule, stroke params)
  5. Delegate: sr.Fill(pm, path, paint) / sr.Stroke(pm, path, paint)
  6. Composite tile onto target with premultiplied source-over alpha blending
  7. Return resources to pool

Key Components

ComponentPurpose
scene.SceneEncodes draw commands into byte stream
scene.RendererOrchestrates tile-parallel rendering
TileGrid64x64 tile partitioning
WorkerPoolGoroutine pool for parallel tile rendering
DirtyRegionTracks changed areas to minimize re-rendering
LayerCacheCaches rendered layers between frames
tilePoolsync.Pool for per-tile SoftwareRenderer/Pixmap reuse
RoundRectShapeSDF-based rounded rect with per-pixel tile rendering (~5x faster)
BeginClip/EndClipAlpha mask compositing for clip regions (Cairo/Skia pattern)

CPU Tile Rasterizers (v0.25.0+)

Two tile-based CPU rasterizers ported from Vello, each optimized for different scenarios:

SparseStrips (4×4 tiles)

Port of Vello's sparse_strips rasterizer. Default CoverageFiller for complex paths.

$ \text{Path} → \text{FlattenContext} → \text{Line} \text{Segments} → \text{CoarseRasterizer} (\text{bin} \text{to} 4 \times 4 \text{tiles}) → \text{FineRasterizer} (\text{per}-\text{tile} \text{coverage}) → \text{StripRenderer} → \text{Blend} $

  • Location: internal/gpu/sparse_strips.go, fine.go, coarse.go, segment.go
  • Tile size: 4×4 (16 pixels) — optimized for CPU/SIMD (4px = f32x4 lane width)
  • Winding: int32 backdrop prefix sum between tiles, float accumulation within tiles
  • Best for: Complex paths with many elements, general-purpose CPU workloads

TileCompute (16×16 tiles)

Port of Vello's 9-stage compute pipeline running on CPU. Produces bit-exact results with the GPU compute path (Tier 5).

$ \text{Path} → \text{FlattenContext} → \text{Line} \text{Segments} → \text{Coarse} (\text{bin} \text{to} 16 \times 16 \text{tiles}) → \text{fillPath}() (\text{per}-\text{tile}, \text{Vello} \text{area} \text{formula}) → \text{float32} \text{alpha} → \text{Blend} $

  • Location: `internal/gpu/tilecompute/$
  • \text{Tile} \text{size}: 16 \times 16 (256 \text{pixels}) — \text{matches} \text{GPU} \text{compute} \text{workgroup} \text{dimensions}
  • \text{Area} \text{accumulation}: \text{Per}-\text{tile} $area[256]`, full reset per tile (zero drift by design)
  • Best for: Extreme complexity (10K+ segments), GPU compute validation/fallback

Shared Components

  • FlattenContext (internal/gpu/flatten.go): Euler spiral curve flattening (Vello)
  • CoarseRasterizer (internal/gpu/coarse.go): Segment-to-tile binning with DDA
  • Backdrop (internal/gpu/coarse.go): int32 prefix sum for winding propagation
  • Analytic coverage: Exact per-pixel trapezoidal area calculation (no supersampling)
  • Fill rules: NonZero (winding != 0) and EvenOdd (winding is odd)

AnalyticFiller (CPU Scanline AA)

Hybrid architecture combining Vello's coverage formula with Skia/tiny-skia's scanline infrastructure.

From Vello fine.rs (internal/raster/analytic_filler.go):

  • Trapezoidal area integration per pixel (area*sign + acc)
  • Left-to-right accumulator propagation (acc += pixelH * sign)

From tiny-skia / Skia (internal/raster/):

  • EdgeBuilder: path → Y-monotonic edges (edge_builder.go)
  • CurveEdge: forward differencing for quadratic/cubic curves (curve_edge.go)
  • CurveAwareAET: Active Edge Table for scanline intersections (curve_aet.go)
  • AlphaRuns: RLE-encoded coverage buffer (alpha_runs.go)
  • FDot6/FDot16: fixed-point arithmetic (fixed.go)

Enterprise-grade curve rendering using forward differencing and trapezoidal coverage calculation.

Forward Differencing Edges

// O(1) per step - only additions, no multiplications
type QuadraticEdge struct {
    fFirstX, fFirstY FDot16  // Current position (fixed-point)
    fDx, fDy         FDot16  // First derivative
    fDDx, fDDy       FDot16  // Second derivative (constant)
    fLastY           int     // End scanline
}

Fixed-Point Arithmetic

TypeFormatPrecisionUse Case
FDot626.61/64 pxY coordinates
FDot1616.161/65536 pxX coordinates, derivatives
FDot824.81/256Coverage values

Stroke Expansion (internal/stroke)

Converts stroked paths to filled outlines using the kurbo/tiny-skia algorithm:

  • Line Caps: Butt, Round, Square
  • Line Joins: Miter (with limit), Round, Bevel
  • Curves: Quadratic and cubic Bezier flattening with tolerance

gogpu Integration (integration/ggcanvas)

The integration/ggcanvas/ package bridges gg with gogpu for windowed rendering:

import "github.com/gogpu/gg/integration/ggcanvas"

canvas := ggcanvas.New(provider, width, height)
// Auto-configures: device scale, LCD ClearType (ADR-024), resource tracking

// Draw() marks canvas dirty atomically — recommended pattern:
canvas.Draw(func(dc *gg.Context) {
    dc.DrawCircle(400, 300, 100)
    dc.Fill()
})

// Zero-copy surface rendering (gg draws directly to window surface):
canvas.RenderDirect(surfaceView, surfaceWidth, surfaceHeight)

// Or readback-based rendering (GPU -> CPU -> texture):
canvas.RenderTo(drawContext)

Key implementation details:

  • Draw() helper — draws with gg.Context and marks dirty atomically, skipping GPU upload when content is unchanged
  • Deferred texture destructionResize() sets a sizeChanged flag instead of destroying the texture immediately, preventing DX12 descriptor heap issues
  • Porter-Duff compositing — GPU readback uses "over" compositing (compositeBGRAOverRGBA) for correct multi-flush blending
  • Auto-registration — Canvas detects if the provider implements TrackResource(io.Closer) (duck-typed interface) and auto-registers. On shutdown, gogpu closes all tracked resources in LIFO order — no manual defer canvas.Close() or OnClose wiring needed.

When used with gogpu, the accelerator shares the gogpu GPU device via DeviceProviderAware, and can render directly to any texture view (surface or offscreen) via GPURenderTarget.View (gpucontext.TextureView), eliminating the GPU->CPU->GPU round-trip. Target is per-pass (WebGPU spec pattern), enabling multi-context rendering (RepaintBoundary, offscreen export).

Four-Level Damage Pipeline (ADR-021, v0.45.0)

Enterprise dirty region tracking — only changed screen areas are redrawn and presented:

Level 1: OBJECT DIFF (DamageTracker)
    Per-object bounding box comparison between frames → []image.Rectangle

Level 2: TILE DIRTY (DirtyRegion)
    Damage rects → mark intersecting 64×64 tiles (atomic bitmap, lock-free)

Level 3: GPU SCISSOR (FlushGPUWithViewDamage)
    LoadOpLoad (preserve previous frame) + scissor clip to dirty region

Level 4: OS PRESENT (PresentWithDamage)
    Per-rect OS blit: BitBlt (Windows), XPutImage (Linux), VK_KHR_incremental_present

Key components:

  • scene/damage.goDamageTracker for retained-mode scenes (frame-to-frame object diff)
  • Path.Bounds() — incremental bounding box during path construction (Skia SkPathRef::fBounds pattern, O(1) per verb)
  • Context.FrameDamage()[]image.Rectangle list of per-operation damage rects for immediate-mode
  • Threshold merge — >16 rects merged to bounding box (Swiss cheese prevention, Wayland compositor pattern)
  • DamageRectSetter — ggcanvas passes per-rect damage to gogpu.SetDamageRects()wgpu.PresentWithDamage()
  • GOGPU_DEBUG_DAMAGE=1 — green overlay on damage regions (Android SurfaceFlinger pattern, full recompose per debug frame)

Render Mode (ADR-020)

GOGPU_RENDER_MODE env var controls 2D rendering path:

ModeBehavior
auto (default)CPU rasterizer on software adapter, GPU on real hardware
cpuForce CPU rasterizer (benchmarking)
gpuForce GPU path even on software (shader testing via SPIR-V interpreter)

Implemented via AdapterAware interface on GPU accelerator + AcceleratorCanRenderDirect() check. References: Qt6 QT_QUICK_BACKEND, SDL3 SDL_RENDER_DRIVER.

Recording System (v0.23.0)

Command-based drawing recording for vector export (Cairo/Skia-inspired).

User Code → Recorder → Commands → Recording → Backend → Output

                    ResourcePool
                   (paths, brushes)

Available Backends

BackendPackageFormat
Rasterrecording/rasterBuilt-in PNG/image
PDFgithub.com/gogpu/gg-pdfPDF documents
SVGgithub.com/gogpu/gg-svgSVG vector graphics

Relationship to gogpu Ecosystem

                    gpucontext (shared interfaces)

naga (shader compiler)     │
  │                        │
  └──► wgpu ◄──────────────┤
         │                 │
         ├──► gogpu ───────┤ (implements DeviceProvider)
         │                 │
         └──► gg ──────────┘ (consumes DeviceProvider)

          this project

         ┌──────┴──────┐
         │             │
      gg-pdf        gg-svg
    (PDF export)  (SVG export)

gg and gogpu are independent libraries that can interoperate via gpucontext:

Aspectgggogpu
Purpose2D graphics libraryGPU framework
CPU renderingBuilt-in (core)No
GPU renderingOptional acceleratorPrimary
Dependencieswgpu, naga, gpucontextwgpu, gpucontext
gpucontext roleConsumerProvider

Key Design Patterns

PatternSourceImplementation
Scene DelegationQt/Skia/Vello/FlutterScene orchestrates tiles, SoftwareRenderer rasterizes
GPU Acceleratorgg v0.26.0Opt-in GPU via import _ "github.com/gogpu/gg/gpu"
Six-Tier RenderingSkia Ganesh/Impeller/VelloSDF, convex, stencil+cover, MSDF text, glyph mask (render pass) + compute pipeline
9-Stage Computevellopathtag→draw→path_count→backdrop→coarse→path_tiling→fine GPU compute
SDF Shape RenderingShadertoy/GPU GemsPer-pixel signed distance field for circles/rrects
Stencil-Then-CoverGPU Gems 3, NV_path_renderingWinding via stencil buffer, then cover fill
Fan TessellationSkia GaneshConvex path to triangle fan for GPU
Shape DetectionggAuto-detect circle/rect/rrect from path elements
Lazy Default Viewwgpu-rs`sync.Once$ \text{for} \text{thread}-\text{safe} \text{texture} \text{view}
\text{State} \text{Machine}\text{wgpu}\text{Command} \text{encoder} \text{lifecycle}
\text{FNV}-1\text{a} \text{Hashing}\text{wgpu}-\text{core}\text{Pipeline} \text{cache} \text{key} \text{generation}
\text{Serial}-\text{Based} \text{LRU}\text{vello}\text{Glyph} \text{cache} \text{eviction}
\text{Stroke} \text{Expansion}\text{kurbo}/\text{tiny}-\text{skia}\text{Forward}/\text{backward} \text{offset} \text{paths}
\text{Forward} \text{Differencing}\text{Skia}\text{O}(1) \text{curve} \text{edge} \text{stepping}
\text{Fixed}-\text{Point} \text{Math}\text{Skia}\text{FDot6}/\text{FDot16} \text{sub}-\text{pixel} \text{precision}
\text{Trapezoidal} \text{Coverage}\text{vello}\text{Exact} \text{per}-\text{pixel} \text{AA} \text{calculation}
\text{Tile} \text{Rasterization}\text{vello}16\text{x16} \text{tile} \text{binning} + \text{DDA}
\text{Multi}-\text{Engine} \text{Rasterizer}\text{coregex}/\text{gg}\text{Adaptive} \text{algorithm} \text{selection} \text{per}-\text{path} (\text{analytic}/4 \times 4/16 \times 16/\text{SDF})
\text{Adaptive} \text{Threshold}\text{gg}$2048/sqrt(bboxArea)` — scales threshold with shape size
CoverageFiller Registrationaccelerator.go patternTile rasterizer registration via RegisterCoverageFiller()
Hybrid Text TransformSkia/Cairo/Vello3-tier decision tree: bitmap → scaled bitmap → outline paths
Scene TagTextSkia TextBlob/Vello GlyphRunShape once at recording, resolve at render (glyph references, not vector paths)
DrawShapedGlyphsSkia drawTextBlobPre-shaped glyph rendering without re-shaping (ADR-022)
Atlas Size BucketsSkia SubRunControl4 discrete sizes absorb zoom without atlas overflow (16/24/32/48px)
Atlas CompactSkia GrDrawOpAtlasFrame-based page eviction (32-frame stale threshold)
Pressure HysteresisSkia/ChromeEnter bucketed mode at 50%, exit at 25% — prevents oscillation
Font HintingFreeType auto-hinterGrid-fit outline Y/X coordinates for crisp stems at small sizes
ClearType LCDFreeType/Microsoft3× horizontal oversampling + 5-tap FIR filter for per-channel RGB alpha
Command PatternCairo/SkiaRecording system for vector export
Driver Patterndatabase/sqlBackend registration via blank import
Device SharingSkia GraphiteDeviceProviderAware for gogpu integration
Per-Pass Render TargetWebGPU spec, Skia GrContextGPURenderTarget.View for per-pass target (surface or offscreen)
LCD Auto-DetectionSkia/Chrome, Qt6Platform subpixel detection (ADR-024): Windows SPI+registry, macOS None, Linux Xft/Wayland. Auto-enabled via PlatformProvider.
Deferred Ortho ProjectionSkia sk_RTAdjust, VelloOrtho computed at flush time from render target dimensions, not draw time (ADR-025). Enables correct offscreen rendering.

See Also