pub const QUAD_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
// Instance data — must match QuadInstance in pipelines/quad.rs (56 bytes).
struct QuadInstance {
@location(0) pos: vec2<f32>,
@location(1) size: vec2<f32>,
@location(2) color_packed: u32,
@location(3) border_packed: u32,
@location(4) corner_radius: f32,
@location(5) border_width: f32,
@location(6) _pad0: vec2<f32>, // .x = rotation (radians, Wave 4 §5.3); .y reserved
@location(7) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) frag_pos: vec2<f32>,
@location(1) size: vec2<f32>,
@location(2) color: vec4<f32>,
@location(3) corner_radius: f32,
@location(4) border_width: f32,
@location(5) border_color: vec4<f32>,
@location(6) clip_rect: vec4<f32>,
};
fn quad_vert_pos(vertex_index: u32) -> vec2<f32> {
// tris: TL,TR,BL TR,BR,BL
let xs = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
let ys = array<f32, 6>(0.0, 0.0, 1.0, 0.0, 1.0, 1.0);
return vec2<f32>(xs[vertex_index], ys[vertex_index]);
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
instance: QuadInstance,
) -> VertexOut {
// The centered border straddles the rect edge, extending outward
// by border_width * 0.5 — the enclosing quad must be padded enough
// to cover that extension (plus a 1px AA fringe), not just a fixed
// 1px like the legacy inset-only shader.
let aa_pad = max(1.0, instance.border_width * 0.5 + 1.0);
let padded_size = instance.size + vec2<f32>(aa_pad * 2.0, aa_pad * 2.0);
let half = instance.size * 0.5;
let center = instance.pos + half;
// Centered, PRE-rotation local offset — an AFFINE function of `uv`
// (see this shader's own doc comment for the full "why `local`, not
// `rotated`" derivation, including the design-doc bug this corrects).
let uv = quad_vert_pos(vertex_index);
let local = uv * padded_size - padded_size * 0.5;
let rotation = instance._pad0.x;
let cs = cos(rotation);
let sn = sin(rotation);
let rotated = vec2<f32>(local.x * cs - local.y * sn, local.x * sn + local.y * cs);
let px = center + rotated; // actual device-space vertex position — the rotated geometry
let frag_pos = local + half; // rect-LOCAL (un-rotated) SDF coordinate — reduces EXACTLY to the pre-rotation formula at rotation == 0.0
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.clip_pos = vec4<f32>(ndc, 0.0, 1.0);
out.frag_pos = frag_pos;
out.size = instance.size;
out.color = unpack4x8unorm(instance.color_packed);
out.corner_radius = instance.corner_radius;
out.border_width = instance.border_width;
out.border_color = unpack4x8unorm(instance.border_packed);
out.clip_rect = instance.clip_rect;
return out;
}
fn sdf_rounded_rect(p: vec2<f32>, half: vec2<f32>, r: f32) -> f32 {
let q = abs(p) - half + vec2<f32>(r, r);
return length(max(q, vec2<f32>(0.0))) + min(max(q.x, q.y), 0.0) - r;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.clip_pos.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
let half = in.size * 0.5;
let p = in.frag_pos - half;
let r = clamp(in.corner_radius, 0.0, min(half.x, half.y));
let dist = sdf_rounded_rect(p, half, r);
let aa = fwidth(dist);
let fill_cov = 1.0 - smoothstep(-aa, aa, dist);
// Build the fragment's PREMULTIPLIED contribution directly (never
// round-trip through a "straight" intermediate that then gets
// premultiplied again — mixing two straight colors weighted by a
// coverage fraction and THEN multiplying the mix by that same
// fraction double-counts it, silently halving the effective
// coverage at exactly the fill-alpha-0 StrokeRect case this
// pipeline relies on).
var out_rgb = in.color.rgb * (in.color.a * fill_cov);
var out_a = in.color.a * fill_cov;
if in.border_width > 0.0 {
// CSS-centered stroke band straddling the SDF=0 boundary
// (design §3) — matches `uzor-urx-cpu::stroke_rect_aa`'s
// outer/inner split, replacing legacy's inset-only border.
let half_w = in.border_width * 0.5;
let band = abs(dist) - half_w;
let border_cov = 1.0 - smoothstep(-aa, aa, band);
let border_a = in.border_color.a * border_cov;
// Composite the border OVER the fill — standard premultiplied
// src-over, both operands already premultiplied.
out_rgb = in.border_color.rgb * border_a + out_rgb * (1.0 - border_a);
out_a = border_a + out_a * (1.0 - border_a);
}
if out_a <= 0.0 { discard; }
return vec4<f32>(out_rgb, out_a);
}
"#;
pub const LINE_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
// Instance data — must match LineInstance in pipelines/line.rs (44 bytes).
struct LineInstance {
@location(0) start: vec2<f32>,
@location(1) end: vec2<f32>,
@location(2) color_packed: u32,
@location(3) width: f32,
@location(4) cap_flags: f32,
@location(5) clip_rect: vec4<f32>,
};
struct LineVsOut {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) seg_start: vec2<f32>,
@location(2) seg_end: vec2<f32>,
@location(3) width: f32,
@location(4) cap_flags: f32,
@location(5) frag_pos: vec2<f32>,
@location(6) clip_rect: vec4<f32>,
};
fn quad_vert_pos(vertex_index: u32) -> vec2<f32> {
let xs = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
let ys = array<f32, 6>(0.0, 0.0, 1.0, 0.0, 1.0, 1.0);
return vec2<f32>(xs[vertex_index], ys[vertex_index]);
}
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
inst: LineInstance,
) -> LineVsOut {
let dir = inst.end - inst.start;
let len = length(dir);
let tangent = select(vec2<f32>(1.0, 0.0), dir / len, len > 0.0001);
let normal = vec2<f32>(-tangent.y, tangent.x);
let half_w = inst.width * 0.5 + 1.0; // +1 px AA fringe
let along = quad_vert_pos(vi).x;
let across = quad_vert_pos(vi).y;
// Pad start/end by half_w along tangent so caps are inside the quad.
let s = inst.start - tangent * half_w;
let e = inst.end + tangent * half_w;
let base = mix(s, e, along);
let px = base + normal * ((across - 0.5) * (inst.width + 2.0));
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: LineVsOut;
out.position = vec4<f32>(ndc, 0.0, 1.0);
out.color = unpack4x8unorm(inst.color_packed);
out.seg_start = inst.start;
out.seg_end = inst.end;
out.width = inst.width;
out.cap_flags = inst.cap_flags;
out.frag_pos = px;
out.clip_rect = inst.clip_rect;
return out;
}
fn sdf_capsule(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>, r: f32) -> f32 {
let pa = p - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);
return length(pa - ba * h) - r;
}
@fragment
fn fs_main(in: LineVsOut) -> @location(0) vec4<f32> {
let cr = in.clip_rect;
if in.frag_pos.x < cr.x || in.frag_pos.y < cr.y
|| in.frag_pos.x > cr.x + cr.z || in.frag_pos.y > cr.y + cr.w {
discard;
}
let r = in.width * 0.5;
let dist = sdf_capsule(in.frag_pos, in.seg_start, in.seg_end, r);
let aa = fwidth(dist);
let cov = 1.0 - smoothstep(-aa, aa, dist);
if cov <= 0.0 { discard; }
// Butt caps via cap_flags (matches the legacy shader's behaviour).
// 0 = round-round, 1 = butt-start, 2 = butt-end, 3 = butt-both
let flags = u32(in.cap_flags + 0.5);
let dir = in.seg_end - in.seg_start;
let len_sq = max(dot(dir, dir), 1e-6);
let h = dot(in.frag_pos - in.seg_start, dir) / len_sq;
if (flags & 1u) != 0u && h < 0.0 { discard; }
if (flags & 2u) != 0u && h > 1.0 { discard; }
// Premultiplied output built directly (design §7) — the pipeline's
// blend state expects this, unlike legacy's straight-alpha output.
let a = in.color.a * cov;
if a <= 0.0 { discard; }
return vec4<f32>(in.color.rgb * a, a);
}
"#;
pub const PATH_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
// Instance data — must match TriInstance in pipelines/path.rs (56 bytes).
struct TriInstance {
@location(0) v0: vec2<f32>,
@location(1) v1: vec2<f32>,
@location(2) v2: vec2<f32>,
@location(3) color0_packed: u32,
@location(4) color1_packed: u32,
@location(5) color2_packed: u32,
@location(6) _pad0: f32,
@location(7) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
@location(1) clip_rect: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
inst: TriInstance,
) -> VertexOut {
var px: vec2<f32>;
var color: vec4<f32>;
switch vi {
case 0u: { px = inst.v0; color = unpack4x8unorm(inst.color0_packed); }
case 1u: { px = inst.v1; color = unpack4x8unorm(inst.color1_packed); }
case 2u: { px = inst.v2; color = unpack4x8unorm(inst.color2_packed); }
default: { px = inst.v0; color = unpack4x8unorm(inst.color0_packed); }
}
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.position = vec4<f32>(ndc, 0.0, 1.0);
// Interpolated across the triangle for free via this vertex→fragment
// varying — no explicit lerp code needed (design §4).
out.color = color;
out.clip_rect = inst.clip_rect;
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.position.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
// Premultiplied output built directly (design §7), same discipline
// as Quad/Line.
let a = in.color.a;
if a <= 0.0 { discard; }
return vec4<f32>(in.color.rgb * a, a);
}
"#;
pub const GLYPH_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var atlas_tex: texture_2d<f32>;
@group(1) @binding(1) var atlas_sampler: sampler;
// Text-gamma LUT (URX text-gamma design, 2026-07-26, §2.5b) — exact
// integer-coordinate fetch ONLY (`textureLoad`, never `textureSample`),
// same non-filterable contract as GRADIENT_SHADER_NATIVE's `lut_tex`.
// Static: built once at NativeGlyphAtlas construction from the SAME
// `uzor_urx_core::text_gamma::TEXT_GAMMA_CURVE` table the CPU backend
// reads — never re-uploaded per frame, never per-gradient row churn.
@group(1) @binding(2) var text_gamma_lut: texture_2d<f32>;
// Instance data — must match GlyphInstance in pipelines/glyph.rs (56 bytes).
struct GlyphInstance {
@location(0) pos: vec2<f32>,
@location(1) size: vec2<f32>,
@location(2) uv_pos: vec2<f32>,
@location(3) uv_size: vec2<f32>,
@location(4) color_packed: u32,
@location(5) _pad0: f32,
@location(6) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) color: vec4<f32>,
@location(2) clip_rect: vec4<f32>,
// Flat integer varying (WGSL forbids interpolating integers) — same
// requirement GRADIENT_SHADER_NATIVE's `kind_extend`/`lut_row`
// already document. Set from `GlyphInstance._pad0` in vs_main.
@location(3) @interpolate(flat) gamma_bin: u32,
};
fn quad_vert_pos(vertex_index: u32) -> vec2<f32> {
let xs = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
let ys = array<f32, 6>(0.0, 0.0, 1.0, 0.0, 1.0, 1.0);
return vec2<f32>(xs[vertex_index], ys[vertex_index]);
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
instance: GlyphInstance,
) -> VertexOut {
let uv_local = quad_vert_pos(vertex_index);
let px = instance.pos + uv_local * instance.size;
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.clip_pos = vec4<f32>(ndc, 0.0, 1.0);
out.uv = instance.uv_pos + uv_local * instance.uv_size;
out.color = unpack4x8unorm(instance.color_packed);
out.clip_rect = instance.clip_rect;
out.gamma_bin = u32(instance._pad0 + 0.5);
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.clip_pos.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
let coverage = textureSample(atlas_tex, atlas_sampler, in.uv).r;
if coverage < 0.0039 { discard; } // < 1/255 — same early-out legacy uses
// Text-gamma coverage adjustment (URX text-gamma design §2.5b) —
// exact-fetch LUT read, NEVER `pow()` in-shader (byte-parity vs
// CPU's own table read is structural, not just measured small).
// CPU's own LUT read is a rounded-clamped index with zero
// interpolation between entries; `textureLoad` reproduces that
// exactly. When `text_gamma_enabled` is false, `encode.rs` always
// writes gamma_bin=0 and row 0 is the identity curve (gamma=1.0
// built into the LUT itself) — so `adjusted == coverage` exactly,
// no shader-side branch needed at all.
let cov_i = i32(clamp(round(coverage * 255.0), 0.0, 255.0));
let adjusted = textureLoad(text_gamma_lut, vec2<i32>(cov_i, i32(in.gamma_bin)), 0).r;
// Premultiply BOTH rgb and alpha here — `in.color` arrives STRAIGHT
// (non-premultiplied brush colour, `GlyphInstance.color`); the
// pipeline's blend state expects a premultiplied fragment output
// (design §5, same discipline as Quad/Line/Path).
let premul_rgb = in.color.rgb * in.color.a;
let out_rgb = premul_rgb * adjusted;
let out_a = in.color.a * adjusted;
if out_a <= 0.0 { discard; }
return vec4<f32>(out_rgb, out_a);
}
"#;
pub const STENCIL_MASK_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
// Instance data — must match TriInstance in pipelines/path.rs (56
// bytes). color0/1/2 + _pad0 are declared for byte-layout parity with
// TriInstance's vertex buffer layout but never read.
struct TriInstance {
@location(0) v0: vec2<f32>,
@location(1) v1: vec2<f32>,
@location(2) v2: vec2<f32>,
@location(3) color0_packed: u32,
@location(4) color1_packed: u32,
@location(5) color2_packed: u32,
@location(6) _pad0: f32,
@location(7) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) position: vec4<f32>,
@location(0) clip_rect: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
inst: TriInstance,
) -> VertexOut {
var px: vec2<f32>;
switch vi {
case 0u: { px = inst.v0; }
case 1u: { px = inst.v1; }
case 2u: { px = inst.v2; }
default: { px = inst.v0; }
}
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.position = vec4<f32>(ndc, 0.0, 1.0);
out.clip_rect = inst.clip_rect;
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.position.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
return vec4<f32>(1.0, 1.0, 1.0, 1.0); // dummy — ColorWrites::empty() blocks this from ever landing
}
"#;
pub const BLEND_COMPOSITE_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var layer_tex: texture_2d<f32>;
@group(1) @binding(1) var layer_sampler: sampler;
// Instance data — must match BlendCompositeInstance in
// pipelines/blend_composite.rs (32 bytes).
struct BlendCompositeInstance {
@location(0) alpha: f32,
@location(1) _pad: vec3<f32>,
@location(2) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) alpha: f32,
@location(2) clip_rect: vec4<f32>,
};
fn quad_vert_pos(vertex_index: u32) -> vec2<f32> {
let xs = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
let ys = array<f32, 6>(0.0, 0.0, 1.0, 0.0, 1.0, 1.0);
return vec2<f32>(xs[vertex_index], ys[vertex_index]);
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
instance: BlendCompositeInstance,
) -> VertexOut {
// Full-viewport quad (design §0.2 — no position/size fields on the
// instance at all; the layer's resolve texture IS viewport-sized,
// so `uv_local` maps 1:1 onto it).
let uv_local = quad_vert_pos(vertex_index);
let px = uv_local * uniforms.screen_size;
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.clip_pos = vec4<f32>(ndc, 0.0, 1.0);
out.uv = uv_local;
out.alpha = instance.alpha;
out.clip_rect = instance.clip_rect;
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.clip_pos.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
// `texel` is ALREADY premultiplied (resolved from content rendered
// through the premultiplied blend state every native pipeline
// shares) — scaling an already-premultiplied value by a scalar is a
// uniform multiply across ALL 4 channels, same reasoning
// `uzor-urx-cpu::blend::composite_layer_srcover` uses. NOT
// `texel.rgb * texel.a` re-premultiplied (design §3.6's sketch —
// wrong for this data flow, see this file's module doc).
let texel = textureSample(layer_tex, layer_sampler, in.uv);
let out_rgba = texel * in.alpha;
if out_rgba.a <= 0.0 { discard; }
return out_rgba;
}
"#;
pub const GRADIENT_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var lut_tex: texture_2d<f32>;
// Instance data — must match GradientInstance in
// pipelines/gradient.rs (64 bytes).
struct GradientInstance {
@location(0) v0: vec2<f32>,
@location(1) v1: vec2<f32>,
@location(2) v2: vec2<f32>,
@location(3) p0: vec2<f32>,
@location(4) p1: f32,
@location(5) p2: f32,
@location(6) kind_extend: u32,
@location(7) lut_row: u32,
@location(8) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) position: vec4<f32>,
@location(0) p0: vec2<f32>,
@location(1) p1: f32,
@location(2) p2: f32,
@location(3) @interpolate(flat) kind_extend: u32,
@location(4) @interpolate(flat) lut_row: u32,
@location(5) clip_rect: vec4<f32>,
};
@vertex
fn vs_main(
@builtin(vertex_index) vi: u32,
inst: GradientInstance,
) -> VertexOut {
var px: vec2<f32>;
switch vi {
case 0u: { px = inst.v0; }
case 1u: { px = inst.v1; }
case 2u: { px = inst.v2; }
default: { px = inst.v0; }
}
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.position = vec4<f32>(ndc, 0.0, 1.0);
// p0/p1/p2/kind_extend/lut_row are IDENTICAL across v0/v1/v2 (they
// describe the whole gradient, not this one vertex) — passed
// through unchanged; `@interpolate(flat)` is REQUIRED for the two
// `u32` fields (WGSL forbids perspective/linear interpolation of
// integer varyings outright), and is a semantic no-op for the f32
// ones since every vertex already carries the same value.
out.p0 = inst.p0;
out.p1 = inst.p1;
out.p2 = inst.p2;
out.kind_extend = inst.kind_extend;
out.lut_row = inst.lut_row;
out.clip_rect = inst.clip_rect;
return out;
}
fn apply_spread(t: f32, extend: u32) -> f32 {
switch extend {
case 1u: { let f = t - floor(t); return select(f, f + 1.0, f < 0.0); } // Repeat
case 2u: { let m = abs(((t % 2.0) + 2.0) % 2.0 - 1.0); return 1.0 - m; } // Reflect
default: { return clamp(t, 0.0, 1.0); } // Pad
}
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.position.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
let kind = in.kind_extend & 3u;
let extend = (in.kind_extend >> 2u) & 3u;
var t: f32;
if kind == 0u {
// Radial — CONCENTRIC ONLY, matching CPU's own focal-degrade
// policy (design §2.6): uses end_center/end_radius
// unconditionally, never start_center/start_radius.
let d = distance(px_abs, in.p0);
t = apply_spread(d / max(in.p1, 1e-3), extend);
} else if kind == 1u {
let ang = atan2(px_abs.y - in.p0.y, px_abs.x - in.p0.x);
let span = select(in.p2 - in.p1, 6.283185307179586, abs(in.p2 - in.p1) < 1e-6);
t = apply_spread((ang - in.p1) / span, extend);
} else {
// Linear (kind == 2u) — `p0` = device-space start, `(p1, p2)`
// = device-space axis (end - start); `t = dot(p - start, axis)
// / dot(axis, axis)`, the same affine projection CPU's
// `fill_rect_gradient_aa` and this crate's OLD per-vertex path
// both used, now evaluated per FRAGMENT so every stop — not
// just whichever two colours happened to land at a mesh
// vertex — is sampled correctly (see this shader's own doc
// comment / `encode.rs::transform_gradient_params`).
let axis = vec2<f32>(in.p1, in.p2);
let axis_len_sq = dot(axis, axis);
var t_raw: f32 = 0.0;
if axis_len_sq > 1e-9 {
t_raw = dot(px_abs - in.p0, axis) / axis_len_sq;
}
t = apply_spread(t_raw, extend);
}
let col = i32(clamp(round(t * 255.0), 0.0, 255.0));
// Exact integer-coordinate fetch — NO linear filtering (design
// §2.2/§2.3): CPU's own LUT read is a rounded-clamped index with
// zero interpolation between entries, and textureLoad reproduces
// that exactly.
let premul = textureLoad(lut_tex, vec2<i32>(col, i32(in.lut_row)), 0);
if premul.a <= 0.0 { discard; }
// Already premultiplied (design §2.2's LUT construction) — no
// further premultiply step here, same "don't double-premultiply"
// discipline as BLEND_COMPOSITE_SHADER_NATIVE above.
return premul;
}
"#;
pub const IMAGE_SHADER_NATIVE: &str = r#"
struct Uniforms {
screen_size: vec2<f32>,
};
@group(0) @binding(0)
var<uniform> uniforms: Uniforms;
@group(1) @binding(0) var image_tex: texture_2d<f32>;
@group(1) @binding(1) var image_sampler: sampler;
// Instance data — must match ImageInstance in pipelines/image.rs
// (56 bytes).
struct ImageInstance {
@location(0) pos: vec2<f32>,
@location(1) size: vec2<f32>,
@location(2) uv_pos: vec2<f32>,
@location(3) uv_size: vec2<f32>,
@location(4) rotation: f32,
@location(5) tint_packed: u32,
@location(6) clip_rect: vec4<f32>,
};
struct VertexOut {
@builtin(position) clip_pos: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) tint: vec4<f32>,
@location(2) clip_rect: vec4<f32>,
};
fn quad_vert_pos(vertex_index: u32) -> vec2<f32> {
let xs = array<f32, 6>(0.0, 1.0, 0.0, 1.0, 1.0, 0.0);
let ys = array<f32, 6>(0.0, 0.0, 1.0, 0.0, 1.0, 1.0);
return vec2<f32>(xs[vertex_index], ys[vertex_index]);
}
@vertex
fn vs_main(
@builtin(vertex_index) vertex_index: u32,
instance: ImageInstance,
) -> VertexOut {
// Same rotation technique as Quad SDF's own extension (design
// §5.3, a DIFFERENT pipeline/instance type — see this file's
// module doc): rotate the LOCAL centered half-extent around the
// quad's own center, computed from `pos`/`size` BEFORE rotation is
// applied.
let center = instance.pos + instance.size * 0.5;
let uv_local = quad_vert_pos(vertex_index);
let local_offset = uv_local * instance.size - instance.size * 0.5;
let cs = cos(instance.rotation);
let sn = sin(instance.rotation);
let rotated = vec2<f32>(
local_offset.x * cs - local_offset.y * sn,
local_offset.x * sn + local_offset.y * cs,
);
let px = center + rotated;
let ndc = vec2<f32>(
px.x / uniforms.screen_size.x * 2.0 - 1.0,
px.y / uniforms.screen_size.y * -2.0 + 1.0,
);
var out: VertexOut;
out.clip_pos = vec4<f32>(ndc, 0.0, 1.0);
out.uv = instance.uv_pos + uv_local * instance.uv_size;
let tint_straight = unpack4x8unorm(instance.tint_packed);
// Premultiply the STRAIGHT tint by its own alpha here — the
// fragment stage multiplies this against the ALREADY-premultiplied
// sampled texel (see this file's module doc's premultiply
// resolution).
out.tint = vec4<f32>(tint_straight.rgb * tint_straight.a, tint_straight.a);
out.clip_rect = instance.clip_rect;
return out;
}
@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
let px_abs = in.clip_pos.xy;
let cr = in.clip_rect;
if px_abs.x < cr.x || px_abs.y < cr.y
|| px_abs.x > cr.x + cr.z || px_abs.y > cr.y + cr.w {
discard;
}
// Already premultiplied (uzor_urx_image::ImageData's own storage
// contract) — do NOT re-premultiply, see this file's module doc.
let texel = textureSample(image_tex, image_sampler, in.uv);
let out_rgba = texel * in.tint;
if out_rgba.a <= 0.0 { discard; }
return out_rgba;
}
"#;