Files
zterm/src/statusline_shader.wgsl
T
2025-12-18 23:43:31 +01:00

324 lines
14 KiB
WebGPU Shading Language

// Statusline shader - optimized for single-row text rendering
// Simpler than the full terminal cell shader, focused on text with colors
// ═══════════════════════════════════════════════════════════════════════════════
// GAMMA CONVERSION FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════
// Luminance weights for perceived brightness (ITU-R BT.709)
const Y: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
// Convert sRGB to linear RGB
fn srgb2linear(x: f32) -> f32 {
if x <= 0.04045 {
return x / 12.92;
} else {
return pow((x + 0.055) / 1.055, 2.4);
}
}
// Convert linear RGB to sRGB
fn linear2srgb(x: f32) -> f32 {
if x <= 0.0031308 {
return 12.92 * x;
} else {
return 1.055 * pow(x, 1.0 / 2.4) - 0.055;
}
}
// Kitty's legacy gamma-incorrect text blending for crisp rendering
fn foreground_contrast_legacy(over_srgb: vec3<f32>, over_alpha: f32, under_srgb: vec3<f32>) -> f32 {
let over_linear = vec3<f32>(srgb2linear(over_srgb.r), srgb2linear(over_srgb.g), srgb2linear(over_srgb.b));
let under_linear = vec3<f32>(srgb2linear(under_srgb.r), srgb2linear(under_srgb.g), srgb2linear(under_srgb.b));
let under_luminance = dot(under_linear, Y);
let over_luminance = dot(over_linear, Y);
let luminance_diff = over_luminance - under_luminance;
if abs(luminance_diff) < 0.001 {
return over_alpha;
}
let blended_srgb = linear2srgb(over_luminance) * over_alpha + linear2srgb(under_luminance) * (1.0 - over_alpha);
let blended_linear = srgb2linear(blended_srgb);
let new_alpha = (blended_linear - under_luminance) / luminance_diff;
return clamp(new_alpha, 0.0, 1.0);
}
// ═══════════════════════════════════════════════════════════════════════════════
// STATUSLINE DATA STRUCTURES
// ═══════════════════════════════════════════════════════════════════════════════
// Per-cell data for statusline rendering
// Matches GPUCell struct in renderer.rs exactly for buffer compatibility
struct StatuslineCell {
// Foreground color (packed: type in low byte, color data in upper bytes)
fg: u32,
// Background color (packed same way)
bg: u32,
// Decoration foreground color (unused in statusline, but needed for struct alignment)
decoration_fg: u32,
// Sprite index in atlas (0 = no glyph/space). High bit = colored glyph.
sprite_idx: u32,
// Cell attributes (unused in statusline, but needed for struct alignment)
attrs: u32,
}
// Sprite info for glyph positioning
struct SpriteInfo {
// UV coordinates in atlas (x, y, width, height) - normalized 0-1
uv: vec4<f32>,
// Padding
_padding: vec2<f32>,
// Size in pixels (width, height)
size: vec2<f32>,
}
// Statusline parameters uniform
struct StatuslineParams {
// Number of characters in statusline
char_count: u32,
// Cell dimensions in pixels
cell_width: f32,
cell_height: f32,
// Screen dimensions in pixels
screen_width: f32,
screen_height: f32,
// Y position of statusline (in pixels from top)
y_offset: f32,
// Padding for alignment
_padding: vec2<f32>,
}
// Color table for indexed colors
struct ColorTable {
colors: array<vec4<f32>, 258>,
}
// ═══════════════════════════════════════════════════════════════════════════════
// BINDINGS
// ═══════════════════════════════════════════════════════════════════════════════
@group(0) @binding(0)
var atlas_texture: texture_2d<f32>;
@group(0) @binding(1)
var atlas_sampler: sampler;
@group(1) @binding(0)
var<uniform> color_table: ColorTable;
@group(1) @binding(1)
var<uniform> params: StatuslineParams;
@group(1) @binding(2)
var<storage, read> cells: array<StatuslineCell>;
@group(1) @binding(3)
var<storage, read> sprites: array<SpriteInfo>;
// ═══════════════════════════════════════════════════════════════════════════════
// CONSTANTS
// ═══════════════════════════════════════════════════════════════════════════════
const COLOR_TYPE_DEFAULT: u32 = 0u;
const COLOR_TYPE_INDEXED: u32 = 1u;
const COLOR_TYPE_RGB: u32 = 2u;
const COLORED_GLYPH_FLAG: u32 = 0x80000000u;
// ═══════════════════════════════════════════════════════════════════════════════
// VERTEX OUTPUT
// ═══════════════════════════════════════════════════════════════════════════════
struct VertexOutput {
@builtin(position) clip_position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) fg_color: vec4<f32>,
@location(2) bg_color: vec4<f32>,
@location(3) @interpolate(flat) is_background: u32,
@location(4) @interpolate(flat) is_colored_glyph: u32,
}
// ═══════════════════════════════════════════════════════════════════════════════
// HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════
// Statusline default background color (0x1a1a1a in linear RGB)
const STATUSLINE_DEFAULT_BG: vec3<f32> = vec3<f32>(0.00972, 0.00972, 0.00972);
// Resolve a packed color to RGBA
fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> {
let color_type = packed & 0xFFu;
if color_type == COLOR_TYPE_DEFAULT {
if is_foreground {
return color_table.colors[256];
} else {
// Statusline uses a solid default background, not the terminal's transparent one
return vec4<f32>(STATUSLINE_DEFAULT_BG, 1.0);
}
} else if color_type == COLOR_TYPE_INDEXED {
let index = (packed >> 8u) & 0xFFu;
return color_table.colors[index];
} else {
// RGB color
let r = f32((packed >> 8u) & 0xFFu) / 255.0;
let g = f32((packed >> 16u) & 0xFFu) / 255.0;
let b = f32((packed >> 24u) & 0xFFu) / 255.0;
return vec4<f32>(srgb2linear(r), srgb2linear(g), srgb2linear(b), 1.0);
}
}
// Convert pixel coordinate to NDC
fn pixel_to_ndc(pixel: vec2<f32>, screen: vec2<f32>) -> vec2<f32> {
return vec2<f32>(
(pixel.x / screen.x) * 2.0 - 1.0,
1.0 - (pixel.y / screen.y) * 2.0
);
}
// ═══════════════════════════════════════════════════════════════════════════════
// BACKGROUND VERTEX SHADER
// ═══════════════════════════════════════════════════════════════════════════════
@vertex
fn vs_statusline_bg(
@builtin(vertex_index) vertex_index: u32,
@builtin(instance_index) instance_index: u32
) -> VertexOutput {
// Skip if out of bounds
if instance_index >= params.char_count {
var out: VertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0);
return out;
}
let cell = cells[instance_index];
// Calculate cell position (single row, left to right)
let cell_x = f32(instance_index) * params.cell_width;
let cell_y = params.y_offset;
// Quad vertex positions for TriangleStrip
var positions: array<vec2<f32>, 4>;
positions[0] = vec2<f32>(cell_x, cell_y);
positions[1] = vec2<f32>(cell_x + params.cell_width, cell_y);
positions[2] = vec2<f32>(cell_x, cell_y + params.cell_height);
positions[3] = vec2<f32>(cell_x + params.cell_width, cell_y + params.cell_height);
let screen_size = vec2<f32>(params.screen_width, params.screen_height);
let ndc_pos = pixel_to_ndc(positions[vertex_index], screen_size);
let fg = resolve_color(cell.fg, true);
let bg = resolve_color(cell.bg, false);
// Statusline always has solid background (no transparency for default bg)
var out: VertexOutput;
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
out.uv = vec2<f32>(0.0, 0.0);
out.fg_color = fg;
out.bg_color = bg;
out.is_background = 1u;
out.is_colored_glyph = 0u;
return out;
}
// ═══════════════════════════════════════════════════════════════════════════════
// GLYPH VERTEX SHADER
// ═══════════════════════════════════════════════════════════════════════════════
@vertex
fn vs_statusline_glyph(
@builtin(vertex_index) vertex_index: u32,
@builtin(instance_index) instance_index: u32
) -> VertexOutput {
// Skip if out of bounds
if instance_index >= params.char_count {
var out: VertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0);
return out;
}
let cell = cells[instance_index];
let sprite_idx = cell.sprite_idx & ~COLORED_GLYPH_FLAG;
let is_colored = (cell.sprite_idx & COLORED_GLYPH_FLAG) != 0u;
// Skip if no glyph
if sprite_idx == 0u {
var out: VertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0);
return out;
}
let sprite = sprites[sprite_idx];
// Skip if sprite has no size
if sprite.size.x <= 0.0 || sprite.size.y <= 0.0 {
var out: VertexOutput;
out.clip_position = vec4<f32>(0.0, 0.0, 0.0, 0.0);
return out;
}
// Calculate glyph position
let glyph_x = f32(instance_index) * params.cell_width;
let glyph_y = params.y_offset;
// Quad vertex positions
var positions: array<vec2<f32>, 4>;
positions[0] = vec2<f32>(glyph_x, glyph_y);
positions[1] = vec2<f32>(glyph_x + sprite.size.x, glyph_y);
positions[2] = vec2<f32>(glyph_x, glyph_y + sprite.size.y);
positions[3] = vec2<f32>(glyph_x + sprite.size.x, glyph_y + sprite.size.y);
// UV coordinates
var uvs: array<vec2<f32>, 4>;
uvs[0] = vec2<f32>(sprite.uv.x, sprite.uv.y);
uvs[1] = vec2<f32>(sprite.uv.x + sprite.uv.z, sprite.uv.y);
uvs[2] = vec2<f32>(sprite.uv.x, sprite.uv.y + sprite.uv.w);
uvs[3] = vec2<f32>(sprite.uv.x + sprite.uv.z, sprite.uv.y + sprite.uv.w);
let screen_size = vec2<f32>(params.screen_width, params.screen_height);
let ndc_pos = pixel_to_ndc(positions[vertex_index], screen_size);
let fg = resolve_color(cell.fg, true);
let bg = resolve_color(cell.bg, false);
var out: VertexOutput;
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
out.uv = uvs[vertex_index];
out.fg_color = fg;
out.bg_color = bg;
out.is_background = 0u;
out.is_colored_glyph = select(0u, 1u, is_colored);
return out;
}
// ═══════════════════════════════════════════════════════════════════════════════
// FRAGMENT SHADER
// ═══════════════════════════════════════════════════════════════════════════════
@fragment
fn fs_statusline(in: VertexOutput) -> @location(0) vec4<f32> {
if in.is_background == 1u {
return in.bg_color;
}
// Sample glyph from atlas
let glyph_sample = textureSample(atlas_texture, atlas_sampler, in.uv);
if in.is_colored_glyph == 1u {
// Colored glyph (emoji) - use atlas color directly
return glyph_sample;
}
// Regular glyph - apply foreground color with legacy gamma blending
let glyph_alpha = glyph_sample.a;
let adjusted_alpha = foreground_contrast_legacy(in.fg_color.rgb, glyph_alpha, in.bg_color.rgb);
return vec4<f32>(in.fg_color.rgb, in.fg_color.a * adjusted_alpha);
}