sugarloaf 0.4.5

Sugarloaf is Rio rendering engine, designed to be multiplatform. It is based on WebGPU, Rust library for Desktops and WebAssembly for Web (JavaScript). This project is created and maintained for Rio terminal purposes but feel free to use it.
// Copyright (c) 2023-present, Raphael Amorim.
//
// This source code is licensed under the MIT license found in the
// LICENSE file in the root directory of this source tree.

// WGSL grid shader. Peer of `grid.metal`.
//
// Ported from `ghostty/src/renderer/shaders/shaders.metal`:
// - full_screen_vertex (line 191 in upstream)
// - cell_bg_fragment (line 451)
//
// Phase 1b scope: bg pass only. Same simplifications as the Metal
// port — no full Display P3 / linear-blending chain yet (the colors
// come in already sRGB-encoded from the CPU).
//
// Bindings:
// @group(0) @binding(0) Uniforms (140+4 = 144 bytes)
// @group(0) @binding(1) CellBg[] (cols * rows entries)
//
// Must match `WgpuGridRenderer`'s bind group layout in
// `sugarloaf/src/grid/webgpu.rs`.

struct Uniforms {
    projection:       mat4x4<f32>,   // offset   0
    grid_padding:     vec4<f32>,     //        64
    cursor_color:     vec4<f32>,     //        80
    cursor_bg_color:  vec4<f32>,     //        96
    cell_size:        vec2<f32>,     //       112
    grid_size:        vec2<u32>,     //       120
    cursor_pos:       vec2<u32>,     //       128
    _pad_cursor:      vec2<u32>,     //       136
    min_contrast:     f32,           //       144
    flags:            u32,           //       148
    padding_extend:   u32,           //       152
    input_colorspace: u32,           //       156
};

// Color space / transfer curve helpers. Matrices match the Metal
// peer (`grid.metal`) and `sugarloaf/src/renderer/renderer.metal`,
// so grid + quad pipelines produce byte-identical framebuffer values.
// WGSL has no `select` with a `bool3` mask returning vec3, so we use
// the scalar `select` per-component via a helper.
fn grid_srgb_to_linear(c: vec3<f32>) -> vec3<f32> {
    let lo = c / 12.92;
    let hi = pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
    return select(lo, hi, c > vec3<f32>(0.04045));
}

fn grid_linear_to_srgb(c: vec3<f32>) -> vec3<f32> {
    let lo = c * 12.92;
    let hi = pow(c, vec3<f32>(1.0 / 2.4)) * vec3<f32>(1.055) - vec3<f32>(0.055);
    return select(lo, hi, c > vec3<f32>(0.0031308));
}

fn grid_srgb_to_p3(linear_srgb: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        dot(linear_srgb, vec3<f32>(0.82246197, 0.17753803, 0.0)),
        dot(linear_srgb, vec3<f32>(0.03319420, 0.96680580, 0.0)),
        dot(linear_srgb, vec3<f32>(0.01708263, 0.07239744, 0.91051993))
    );
}

fn grid_rec2020_to_p3(linear_r2020: vec3<f32>) -> vec3<f32> {
    return vec3<f32>(
        dot(linear_r2020, vec3<f32>( 1.34357825, -0.28217967, -0.06139858)),
        dot(linear_r2020, vec3<f32>(-0.06529745,  1.08782226, -0.02252481)),
        dot(linear_r2020, vec3<f32>( 0.00282179, -0.02598807,  1.02316628))
    );
}

fn grid_prepare_output_rgb(srgb: vec3<f32>, input_colorspace: u32) -> vec3<f32> {
    var lin = grid_srgb_to_linear(srgb);
    if (input_colorspace == 0u) {
        lin = grid_srgb_to_p3(lin);
    } else if (input_colorspace == 2u) {
        lin = grid_rec2020_to_p3(lin);
    }
    return grid_linear_to_srgb(lin);
}

const PAD_EXTEND_LEFT:  u32 = 1u;
const PAD_EXTEND_RIGHT: u32 = 2u;
const PAD_EXTEND_UP:    u32 = 4u;
const PAD_EXTEND_DOWN:  u32 = 8u;

@group(0) @binding(0) var<uniform> uniforms: Uniforms;

// CellBg is `#[repr(C)] struct { rgba: [u8; 4] }` on the CPU (4 bytes
// each, one u32 when viewed as little-endian). WGSL has no u8, so we
// declare the buffer as `array<u32>` and unpack bytes manually.
// Array length = `cols * rows` (one u32 per cell).
@group(0) @binding(1) var<storage, read> cells: array<u32>;

struct VsOut {
    @builtin(position) position: vec4<f32>,
};

@vertex
fn grid_bg_vertex(@builtin(vertex_index) vid: u32) -> VsOut {
 // Fullscreen triangle (same trick as the Metal port).
 // vid 0: (-1, -3)
 // vid 1: (-1, 1)
 // vid 2: ( 3, 1)
    var x = -1.0;
    var y =  1.0;
    if (vid == 2u) { x =  3.0; }
    if (vid == 0u) { y = -3.0; }

    var out: VsOut;
    out.position = vec4<f32>(x, y, 1.0, 1.0);
    return out;
}

fn load_cell_bg(idx: u32) -> vec4<f32> {
 // One u32 per cell; unpack RGBA little-endian bytes.
    let word = cells[idx];
    let r = f32((word >>  0u) & 0xFFu) / 255.0;
    let g = f32((word >>  8u) & 0xFFu) / 255.0;
    let b = f32((word >> 16u) & 0xFFu) / 255.0;
    let a = f32((word >> 24u) & 0xFFu) / 255.0;
 // Premultiply.
    return vec4<f32>(r * a, g * a, b * a, a);
}

@fragment
fn grid_bg_fragment(in: VsOut) -> @location(0) vec4<f32> {
 // `grid_padding` is (top, right, bottom, left).
 // Use .w (left) + .x (top) to find the grid origin, same as Metal port.
    let cell_fx = (in.position.xy - vec2<f32>(uniforms.grid_padding.w, uniforms.grid_padding.x))
                    / uniforms.cell_size;
    let orig_grid_pos = vec2<i32>(floor(cell_fx));
    var grid_pos = orig_grid_pos;

 // Horizontal padding.
    let cols = i32(uniforms.grid_size.x);
    if (grid_pos.x < 0) {
        if ((uniforms.padding_extend & PAD_EXTEND_LEFT) != 0u) {
            grid_pos.x = 0;
        } else {
            return vec4<f32>(0.0);
        }
    } else if (grid_pos.x > cols - 1) {
        if ((uniforms.padding_extend & PAD_EXTEND_RIGHT) != 0u) {
            grid_pos.x = cols - 1;
        } else {
            return vec4<f32>(0.0);
        }
    }

 // Vertical padding.
    let rows = i32(uniforms.grid_size.y);
    if (grid_pos.y < 0) {
        if ((uniforms.padding_extend & PAD_EXTEND_UP) != 0u) {
            grid_pos.y = 0;
        } else {
            return vec4<f32>(0.0);
        }
    } else if (grid_pos.y > rows - 1) {
        if ((uniforms.padding_extend & PAD_EXTEND_DOWN) != 0u) {
            grid_pos.y = rows - 1;
        } else {
            return vec4<f32>(0.0);
        }
    }

 // Cursor overlay at in-bounds cursor cell only (skip
 // padding-extended fragments so an edge cursor doesn't bleed
 // into the window margin).
    if (uniforms.cursor_bg_color.a > 0.0
        && orig_grid_pos.x == i32(uniforms.cursor_pos.x)
        && orig_grid_pos.y == i32(uniforms.cursor_pos.y)) {
        let rgb = grid_prepare_output_rgb(
            uniforms.cursor_bg_color.rgb,
            uniforms.input_colorspace,
        );
        let a = uniforms.cursor_bg_color.a;
        return vec4<f32>(rgb * a, a);
    }

 // Load cell, convert to output color space, then premultiply.
 // Same pipeline as the quad fill in `sugarloaf/src/renderer/renderer.metal`
 // so the grid and window-fill paths produce identical framebuffer
 // values.
    let idx = u32(grid_pos.y) * uniforms.grid_size.x + u32(grid_pos.x);
    let word = cells[idx];
    let r = f32((word >>  0u) & 0xFFu) / 255.0;
    let g = f32((word >>  8u) & 0xFFu) / 255.0;
    let b = f32((word >> 16u) & 0xFFu) / 255.0;
    let a = f32((word >> 24u) & 0xFFu) / 255.0;
    let rgb = grid_prepare_output_rgb(vec3<f32>(r, g, b), uniforms.input_colorspace);
    return vec4<f32>(rgb * a, a);
}

// -------------------------------------------------------------------
// Cell Text Shader
//
// WGSL twin of `grid_text_vertex` / `grid_text_fragment` in grid.metal.
// Same simplifications: no full Display P3 / linear-blending chain,
// no min-contrast, single-cell cursor only.
// -------------------------------------------------------------------

const ATLAS_GRAYSCALE: u32 = 0u;
const ATLAS_COLOR:     u32 = 1u;

const BOOL_NO_MIN_CONTRAST: u32 = 1u;
const BOOL_IS_CURSOR_GLYPH: u32 = 2u;

struct CellTextVertexIn {
 // Per-instance attributes (attribute locations match the wgpu
 // vertex buffer layout in grid/webgpu.rs).
    @location(0) glyph_pos:  vec2<u32>,
    @location(1) glyph_size: vec2<u32>,
    @location(2) bearings:   vec2<i32>,
    @location(3) grid_pos:   vec2<u32>,
    @location(4) color:      vec4<f32>,   // UNorm8x4 input, 0..1
    @location(5) atlas:      u32,
    @location(6) bools:      u32,
};

struct TextVsOut {
    @builtin(position) position: vec4<f32>,
    @location(0) @interpolate(flat) atlas: u32,
    @location(1) @interpolate(flat) color: vec4<f32>,
    @location(2) tex_coord: vec2<f32>,
};

// Atlases. Group(1) keeps them separate from the bg bind group so
// the bg pipeline (which doesn't need atlases) uses a smaller bind
// group layout. We use `textureLoad` (no sampler) to match Metal's
// `coord::pixel + filter::nearest` — integer pixel fetch.
@group(1) @binding(0) var atlas_grayscale: texture_2d<f32>;
@group(1) @binding(1) var atlas_color:     texture_2d<f32>;

@vertex
fn grid_text_vertex(
    @builtin(vertex_index) vid: u32,
    in: CellTextVertexIn,
) -> TextVsOut {
 // Cell origin in pixel space.
    let cell_pos = uniforms.cell_size * vec2<f32>(in.grid_pos);

 // Quad corner (0..1) from vertex id — 4-vertex triangle strip.
 // 0 --> 1
 // | .'|
 // | / |
 // | L |
 // 2 --> 3
    var corner: vec2<f32>;
    corner.x = select(0.0, 1.0, vid == 1u || vid == 3u);
    corner.y = select(0.0, 1.0, vid == 2u || vid == 3u);

 // Glyph bbox inside cell: bearings.x from left, bearings.y from
 // bottom (font convention).
    let size = vec2<f32>(in.glyph_size);
    var offset = vec2<f32>(in.bearings);
    offset.y = uniforms.cell_size.y - offset.y;

    var quad = cell_pos + size * corner + offset;

 // Shift by grid_padding (top/left).
    quad.x += uniforms.grid_padding.w;
    quad.y += uniforms.grid_padding.x;

    var out: TextVsOut;
    out.position = uniforms.projection * vec4<f32>(quad, 0.0, 1.0);

 // Atlas tex coords in PIXEL space — sampler is set to nearest,
 // unnormalized coords equivalent via textureLoad below.
    out.tex_coord = vec2<f32>(in.glyph_pos) + vec2<f32>(in.glyph_size) * corner;
    out.atlas = in.atlas;

 // Foreground color — `in.color` arrives normalized via UNorm8x4.
 // Convert to output color space first, then premultiply. Same
 // pipeline as `grid_bg_fragment` and the quad fill so glyph/cell
 // bg/window bg agree.
    var color = in.color;
    color = vec4<f32>(
        grid_prepare_output_rgb(color.rgb, uniforms.input_colorspace) * color.a,
        color.a,
    );

 // Cursor-pos fg swap. Skip when cursor_color.a == 0 — that's the
 // hollow / unfocused path where text colour stays untouched.
    let is_cursor_pos = in.grid_pos.x == uniforms.cursor_pos.x
                     && in.grid_pos.y == uniforms.cursor_pos.y;
    if ((in.bools & BOOL_IS_CURSOR_GLYPH) == 0u
        && is_cursor_pos
        && uniforms.cursor_color.a > 0.0) {
        let c = uniforms.cursor_color;
        color = vec4<f32>(
            grid_prepare_output_rgb(c.rgb, uniforms.input_colorspace) * c.a,
            c.a,
        );
    }

    out.color = color;
    return out;
}

@fragment
fn grid_text_fragment(in: TextVsOut) -> @location(0) vec4<f32> {
 // Pixel-space tex_coord → integer sample via textureLoad (no
 // sampler filter; matches Metal's `coord::pixel` + `filter::nearest`).
    let ic = vec2<i32>(in.tex_coord);
    if (in.atlas == ATLAS_GRAYSCALE) {
        let a = textureLoad(atlas_grayscale, ic, 0).r;
        return in.color * a;
    } else {
        return textureLoad(atlas_color, ic, 0);
    }
}