Skip to content

romeoahmed/heavy-slug

Repository files navigation

heavy-slug

Zig Vulkan Metal Slang License

heavy-slug is a Zig 0.16 library for analytic, resolution-independent GPU text rendering. It shapes Unicode with HarfBuzz, captures native font outlines with FreeType and HarfBuzz draw callbacks, encodes compact cubic coverage blobs, and renders text with mesh and fragment shaders on Vulkan 1.4 and Metal 4.

It follows the central idea of Slug: keep glyph coverage analytic until the fragment shader instead of baking text into a raster atlas. The public boundary is deliberately small: applications own windows, devices, queues, swapchains or layers, command buffers, frame completion, and presentation; heavy-slug owns shaping, glyph encoding, cache state, and backend draw data.

UTF-8 text -> HarfBuzz shaping -> cubic coverage blob
           -> CPU h-band meshlets -> analytic GPU coverage

Contents

Use The Library

Enable the module you need, then let the host application keep ownership of the graphics objects and call the backend renderer from its frame loop.

Module Enable With Purpose
heavy_slug default Backend-neutral value types and core renderer options.
heavy_slug_vulkan -Dvulkan=true or Vulkan demo builds Vulkan 1.4 backend.
heavy_slug_metal -Dmetal=true or Metal demo builds Metal 4 backend.

Primary heavy_slug exports are FontSource, FontOptions, FontHandle, TextRun, ScreenTextRun, Color, Transform, View, PrecisionPolicy, PrecisionSelection, FillRule, RendererOptions, DrawTextResult, SubmitResult, FrameDiagnostics, FrameWarning, FrameToken, and ShaderStats. Backend modules expose the application-facing flow through Context, Renderer, Frame, Target, RendererOptions, FontHandle, DrawTextResult, SubmitResult, FrameToken, Stats, and shader_stats_enabled. Vulkan also exposes requirement helpers such as required_api_version and required_device_extensions, plus Renderer.setFixedRenderState(target) which the host calls once per command buffer recording before the first Frame.submit(). Metal exposes Host, the borrowed native object bundle used to create a backend context.

Typical frame shape:

const heavy_slug = @import("heavy_slug");

const font = try renderer.loadFont(.{ .path = "assets/NotoSansJP-Regular.otf" }, .{
    .size_px = 32,
});

const view = heavy_slug.View.identity(width_px, height_px);
var frame = try renderer.beginFrame(view);
_ = try frame.drawText(.{
    .font = font,
    .text = "Heavy Slug",
    .transform = heavy_slug.Transform.translation(80, 140),
    .color = heavy_slug.Color.white,
});

_ = try frame.drawScreenText(.{
    .font = font,
    .text = "FPS 60.0  Zoom 1.00x",
    .screen_from_text = heavy_slug.Transform.translation(24, height_px - 28),
    .color = heavy_slug.Color.white,
});

const submit = try frame.submit(target);
const token = switch (submit) {
    .submitted_text => |token| token,
    .submitted_clear_only => |token| token,
    .empty_noop => |empty| empty.previous_token,
};

Color is linear, premultiplied-alpha RGBA. Use Color.straight(r, g, b, a) for conventional non-premultiplied input (the constructor multiplies the RGB channels by a for you) and Color.premultiplied(r, g, b, a) only when the caller already owns premultiplied data. The Vulkan and Metal backends blend with src = ONE, dst = ONE_MINUS_SRC_ALPHA, so straight RGBA passed through the wrong constructor would render as a too-bright haloed glyph against any non-opaque background.

Useful commands:

Goal Command
Build the core library zig build
Run core and build-tool tests zig build test
Test Vulkan or Metal zig build test -Dvulkan=true / zig build test -Dmetal=true
Compile shaders zig build spirv / zig build msl
Run a demo zig build run -Ddemo=true -Ddemo-backend=vulkan or -Ddemo-backend=metal

-Ddemo-backend=auto chooses Vulkan on Windows/Linux and Metal on macOS. -Dshader-stats=true adds GPU-side counters for shader diagnostics.

Architecture

Application
  owns window, graphics device, queues, swapchain/layer, frame completion
      |
      v
Backend: heavy_slug_vulkan or heavy_slug_metal
  owns GPU buffers, shader state, draw recording/submission glue
      |
      v
Core: heavy_slug
  owns fonts, shaping, outline normalization, blob encoding, cache metadata
      |
      v
Slang mesh and fragment shaders
  clip meshlets and integrate analytic coverage

The hot path uses one byte-addressed glyph blob pool plus compact per-frame glyph and meshlet streams. Cached glyphs are keyed by font, glyph id, precision tier, and variation key; cache entries retire only after the frame token that may reference them has completed.

Algorithm

heavy-slug is useful when text is transformed, zoomed, panned, or reused without wanting a new raster atlas for every scale. A glyph is cached as a small analytic coverage program: fixed-point cubic Bezier spans plus a conservative horizontal-band candidate index. The mesh shader bounds where work can happen; the fragment shader computes the curve coverage for the current pixel.

Cubic Bernstein blobs

All outline segments are raised to cubic Bernstein spans:

$$\begin{aligned} B(t) &= \sum_{i=0}^{3} p_i B_i^3(t),\\\ B_i^3(t) &= \binom{3}{i}(1-t)^{3-i}t^i,\qquad 0 \le t \le 1 . \end{aligned}$$

The CPU splits cubics at roots of $x'(t)$, $y'(t)$, and $\det(B'(t),B''(t))$, then bisects any span whose quantized control polygon is still not monotone on both axes. By the Bernstein convex-hull property, this makes simple outward-rounded bounds useful for fast rejection.

Fill direction comes from the exact Green-form area integral, not a control-polygon approximation. For one cubic span, with $[a,b]=a_x b_y-a_y b_x$:

$$\begin{aligned} A_c=\frac{1}{20}(&6[p_0,p_1]+3[p_0,p_2]+[p_0,p_3]\\\ &+3[p_1,p_2]+3[p_1,p_3]+6[p_2,p_3]). \end{aligned}$$

Coverage integral

For a local pixel cell

$$P=[-\tfrac12,\tfrac12]\times[-\tfrac12,\tfrac12],$$

Fubini turns coverage area into horizontal slice length, and Green turns each oriented boundary contribution into a line integral. After clipping a y-monotone span to the pixel y-range, the shader accumulates:

$$I(\gamma)= \int_a^b \mathrm{clamp}(x(t)+\tfrac12,\,0,\,1)\,y'(t)\,dt .$$

Spans wholly left of the cell contribute 0; spans wholly right contribute $y(b)-y(a)$; partial spans use the integral above. For valid font glyph outlines with simple local winding, the signed sum resolves the fast non-zero or even-odd fill path. General overlapping or self-intersecting vector paths are outside the exact-fill contract of this renderer path.

Stable quintic evaluation

For a partial span,

$$x(t)-x_L=\sum_{i=0}^{3}\xi_i B_i^3(t),\qquad y'(t)=3\sum_{j=0}^{2}\Delta y_j B_j^2(t),$$

so the integrand is a degree-five Bernstein polynomial:

$$(x(t)-x_L)y'(t)=\sum_{k=0}^{5} q_k B_k^5(t), \qquad q_k=3\sum_{i+j=k}\xi_i\Delta y_j\, \frac{\binom{3}{i}\binom{2}{j}}{\binom{5}{k}} .$$

The sum for $q_k$ ranges over valid pairs $0\le i\le3$, $0\le j\le2$ with $i+j=k$. Three-point Gauss-Legendre integration is exact for degree-five polynomials, and the shader evaluates the cubic and first-difference derivative in factorized Bernstein/de Casteljau form rather than expanding to the power basis. That keeps the stable cases explicit and avoids avoidable cancellation.

Precision and acceleration

The public PrecisionPolicy chooses even fixed-point tiers from a screen-space error budget. If $A$ is the local-to-screen linear map and $n$ fraction bits are used, the policy bounds rounding error by:

$$\|A\|_\infty\,2^{-(n+1)} .$$

It requires that value to stay below target_error_px, rejects non-finite or ill-conditioned transforms, and encodes glyph bounds with outward rounding. Precision selection returns structured supported/unsupported data so frame diagnostics can report the largest observed screen-space scale and required fraction bits without turning a single degraded glyph into a whole-frame error.

The h-band table is a conservative CSR index from y bands to curve ids:

$$\{i:\mathrm{bbox}_y(C_i)\cap H_k\ne\varnothing\} \subseteq \mathrm{candidates}(H_k).$$

Fragments merge a bounded number of adjacent band lists and deduplicate curve ids before integration; if the window is invalid or too wide, the shader falls back to a full curve scan.

CPU meshlet planning emits pixel-center support domains, not just glyph bbox strips: curve bounds are inflated by the local half-pixel footprint on all sides, and y slices scan neighboring h-bands through that influence domain. Fixed-point geometry enters f32 only through explicit relative exact checks within the 2^24 integer radius of the current chart anchor; the shader does not clamp Bezier control points to make them representable.

Requirements

Core builds need Zig 0.16.0. FreeType 2.14.3 and HarfBuzz 14.2.0 are pinned source dependencies; Zig compiles and links them statically, so normal core builds do not require a separate C/C++ compiler or system FreeType/HarfBuzz packages.

Backend and demo commands add native toolchains:

Path Additional Requirements
Shaders slangc with Slang 2026, SPIR-V 1.6, and metallib_4_0 support.
Vulkan Vulkan loader, Vulkan 1.4 driver, push descriptors, dynamic rendering, VK_EXT_mesh_shader, and VK_EXT_shader_object.
Metal macOS 26.0+, Swift 6.3, Apple SDK with Metal 4, and Metal/QuartzCore/Foundation/AppKit/SwiftUI.
Wayland demo wayland-scanner, wayland-client, xkbcommon, and pinned Wayland protocol XML dependencies.

Core-only zig build and zig build test do not require slangc, Vulkan, Metal, Wayland, Cocoa, or a window toolkit.

Backends And Demos

The demos are native host examples, not a portability layer. Vulkan uses Win32 or Wayland; Metal uses SwiftUI/AppKit with a CAMetalLayer. All demos use B to switch between explicit light and dark appearance. The shared scene uses NotoSansJP-Regular.otf for a dense vertically grouped multilingual and symbol sample corpus, plus a screen-space metrics overlay for FPS, relative view zoom, native display scale, and renderer warnings. Vulkan demo window titles are validated UTF-8; Win32 presents them as UTF-16 native titles, and Wayland publishes the exact title through xdg-shell.

Vulkan hosts report completed GPU work with Renderer.markFrameComplete(token). The Metal backend tracks completion through bridge-managed frame slots and waits for submitted work during teardown. Frame.submit() returns an explicit SubmitResult: Vulkan reports empty text batches as .empty_noop because the host still owns swapchain presentation, while Metal submits .submitted_clear_only for empty text batches so the drawable is cleared and presented. Renderer.stats() exposes CPU/backend counters for shaping, cache, uploads, retirements, pool state, submitted glyphs, and meshlets; Frame.diagnostics() exposes frame-local run/glyph reject counters and stable FrameWarning values for application UI. The demo snapshots those warnings before drawing its screen-space overlay, then displays every public warning vertically with Frame.drawScreenText(). -Dshader-stats=true adds GPU counters for meshlet culling, h-band candidate use, full-scan fallback, bbox rejection, and curve integration.

Development

Run the narrowest verification that covers the change, then broaden when touching public APIs, shader ABI, backend resource binding, or demo platforms.

Path Purpose
src/core/ Font, outline, blob, cache, and renderer-core internals.
src/gpu/ Backend-neutral resource model, mesh budgets, and shader stats.
src/backends/ Vulkan and Metal backend modules.
shaders/ Slang core modules, backend shims, and shader entries.
demo/ Native Win32, Wayland, and SwiftUI/AppKit demo hosts.
build/, tools/ Zig build helpers and Slang reflection layout generation.

Use zig fmt build.zig build/ demo/ src/ tools/ for Zig formatting. CI is verification-only: formatting and script/YAML checks, core tests on Ubuntu/macOS/Windows, shader compilation, Vulkan backend/demo build tests, Swift format lint, and Metal backend/demo build tests on macOS.

Credit

heavy-slug builds on Slug's practical GPU-side analytic glyph coverage and recasts it around cubic blobs, h-band meshlets, and modern Vulkan/Metal APIs.

License

MIT. See LICENSE.

About

Zig GPU text rendering library for analytic, resolution-independent text rendering

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages