Manually clip primitive geometry

This commit is contained in:
John Tur 2026-08-11 23:04:10 -04:00
parent 7bddd16a09
commit bb2095a51d
4 changed files with 264 additions and 145 deletions

View file

@ -16,10 +16,13 @@ float4 to_device_position_transformed(float2 unit_vertex, Bounds_ScaledPixels bo
float2 to_tile_position(float2 unit_vertex, AtlasTile tile,
constant Size_DevicePixels *atlas_size);
float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds);
float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds, TransformationMatrix transformation);
Bounds_ScaledPixels clip_to_mask(Bounds_ScaledPixels bounds,
Bounds_ScaledPixels mask);
bool transform_is_axis_aligned(TransformationMatrix transformation);
Bounds_ScaledPixels mask_in_transform_space(Bounds_ScaledPixels mask,
TransformationMatrix transformation);
float corner_dash_velocity(float dv1, float dv2);
float dash_alpha(float t, float period, float length, float dash_velocity,
float antialias_threshold);
@ -51,7 +54,6 @@ struct QuadVertexOutput {
float4 background_solid [[flat]];
float4 background_color0 [[flat]];
float4 background_color1 [[flat]];
float clip_distance [[clip_distance]][4];
};
struct QuadFragmentInput {
@ -73,10 +75,9 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
[[buffer(QuadInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Quad quad = quads[quad_id];
float4 device_position =
to_device_position(unit_vertex, quad.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds,
quad.content_mask.bounds);
float4 device_position = to_device_position(
unit_vertex, clip_to_mask(quad.bounds, quad.content_mask.bounds),
viewport_size);
float4 border_color = hsla_to_rgba(quad.border_color);
GradientColor gradient = prepare_fill_color(
@ -93,8 +94,7 @@ vertex QuadVertexOutput quad_vertex(uint unit_vertex_id [[vertex_id]],
border_color,
gradient.solid,
gradient.color0,
gradient.color1,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
gradient.color1};
}
fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
@ -450,7 +450,6 @@ struct ShadowVertexOutput {
float4 position [[position]];
float4 color [[flat]];
uint shadow_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct ShadowFragmentInput {
@ -481,17 +480,15 @@ vertex ShadowVertexOutput shadow_vertex(
bounds.size.height += 2. * margin;
}
float4 device_position =
to_device_position(unit_vertex, bounds, viewport_size);
float4 clip_distance =
distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask.bounds);
float4 device_position = to_device_position(
unit_vertex, clip_to_mask(bounds, shadow.content_mask.bounds),
viewport_size);
float4 color = hsla_to_rgba(shadow.color);
return ShadowVertexOutput{
device_position,
color,
shadow_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
shadow_id};
}
fragment float4 shadow_fragment(ShadowFragmentInput input [[stage_in]],
@ -558,7 +555,6 @@ struct UnderlineVertexOutput {
float4 position [[position]];
float4 color [[flat]];
uint underline_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct UnderlineFragmentInput {
@ -575,16 +571,14 @@ vertex UnderlineVertexOutput underline_vertex(
[[buffer(ShadowInputIndex_ViewportSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
Underline underline = underlines[underline_id];
float4 device_position =
to_device_position(unit_vertex, underline.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds,
underline.content_mask.bounds);
float4 device_position = to_device_position(
unit_vertex, clip_to_mask(underline.bounds, underline.content_mask.bounds),
viewport_size);
float4 color = hsla_to_rgba(underline.color);
return UnderlineVertexOutput{
device_position,
color,
underline_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
underline_id};
}
fragment float4 underline_fragment(UnderlineFragmentInput input [[stage_in]],
@ -642,11 +636,32 @@ vertex MonochromeSpriteVertexOutput monochrome_sprite_vertex(
[[buffer(SpriteInputIndex_AtlasTextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
MonochromeSprite sprite = sprites[sprite_id];
float4 device_position =
to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation, viewport_size);
float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds,
sprite.content_mask.bounds, sprite.transformation);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
float4 device_position;
float2 tile_position;
float4 clip_distance;
if (transform_is_axis_aligned(sprite.transformation)) {
Bounds_ScaledPixels mask =
mask_in_transform_space(sprite.content_mask.bounds, sprite.transformation);
Bounds_ScaledPixels clipped = clip_to_mask(sprite.bounds, mask);
device_position = to_device_position_transformed(
unit_vertex, clipped, sprite.transformation, viewport_size);
float2 local_position =
unit_vertex * float2(clipped.size.width, clipped.size.height) +
float2(clipped.origin.x, clipped.origin.y);
float2 fraction =
(local_position - float2(sprite.bounds.origin.x, sprite.bounds.origin.y)) /
float2(sprite.bounds.size.width, sprite.bounds.size.height);
tile_position = to_tile_position(fraction, sprite.tile, atlas_size);
clip_distance = float4(1.0);
} else {
// A rotated sprite intersected with the axis-aligned mask isn't
// representable as a quad, so fall back to per-fragment clipping.
device_position = to_device_position_transformed(
unit_vertex, sprite.bounds, sprite.transformation, viewport_size);
tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
clip_distance = distance_from_clip_rect_transformed(
unit_vertex, sprite.bounds, sprite.content_mask.bounds, sprite.transformation);
}
float4 color = hsla_to_rgba(sprite.color);
return MonochromeSpriteVertexOutput{
device_position,
@ -659,6 +674,8 @@ fragment float4 monochrome_sprite_fragment(
MonochromeSpriteFragmentInput input [[stage_in]],
constant MonochromeSprite *sprites [[buffer(SpriteInputIndex_Sprites)]],
texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) {
// Only rotated sprites need per-fragment clipping; axis-aligned sprites are
// clipped geometrically in the vertex shader.
if (any(input.clip_distance < float4(0.0))) {
return float4(0.0);
}
@ -676,7 +693,6 @@ struct PolychromeSpriteVertexOutput {
float4 position [[position]];
float2 tile_position;
uint sprite_id [[flat]];
float clip_distance [[clip_distance]][4];
};
struct PolychromeSpriteFragmentInput {
@ -696,16 +712,21 @@ vertex PolychromeSpriteVertexOutput polychrome_sprite_vertex(
float2 unit_vertex = unit_vertices[unit_vertex_id];
PolychromeSprite sprite = sprites[sprite_id];
Bounds_ScaledPixels clipped =
clip_to_mask(sprite.bounds, sprite.content_mask.bounds);
float4 device_position =
to_device_position(unit_vertex, sprite.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask.bounds);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size);
to_device_position(unit_vertex, clipped, viewport_size);
float2 position =
unit_vertex * float2(clipped.size.width, clipped.size.height) +
float2(clipped.origin.x, clipped.origin.y);
float2 fraction =
(position - float2(sprite.bounds.origin.x, sprite.bounds.origin.y)) /
float2(sprite.bounds.size.width, sprite.bounds.size.height);
float2 tile_position = to_tile_position(fraction, sprite.tile, atlas_size);
return PolychromeSpriteVertexOutput{
device_position,
tile_position,
sprite_id,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
sprite_id};
}
fragment float4 polychrome_sprite_fragment(
@ -850,7 +871,6 @@ fragment float4 path_sprite_fragment(
struct SurfaceVertexOutput {
float4 position [[position]];
float2 texture_position;
float clip_distance [[clip_distance]][4];
};
struct SurfaceFragmentInput {
@ -868,17 +888,21 @@ vertex SurfaceVertexOutput surface_vertex(
[[buffer(SurfaceInputIndex_TextureSize)]]) {
float2 unit_vertex = unit_vertices[unit_vertex_id];
SurfaceBounds surface = surfaces[surface_id];
Bounds_ScaledPixels clipped =
clip_to_mask(surface.bounds, surface.content_mask.bounds);
float4 device_position =
to_device_position(unit_vertex, surface.bounds, viewport_size);
float4 clip_distance = distance_from_clip_rect(unit_vertex, surface.bounds,
surface.content_mask.bounds);
// We are going to copy the whole texture, so the texture position corresponds
// to the current vertex of the unit triangle.
float2 texture_position = unit_vertex;
to_device_position(unit_vertex, clipped, viewport_size);
// We are going to copy the whole texture, so the texture position
// corresponds to the vertex's fraction within the surface bounds.
float2 position =
unit_vertex * float2(clipped.size.width, clipped.size.height) +
float2(clipped.origin.x, clipped.origin.y);
float2 texture_position =
(position - float2(surface.bounds.origin.x, surface.bounds.origin.y)) /
float2(surface.bounds.size.width, surface.bounds.size.height);
return SurfaceVertexOutput{
device_position,
texture_position,
{clip_distance.x, clip_distance.y, clip_distance.z, clip_distance.w}};
texture_position};
}
fragment float4 surface_fragment(SurfaceFragmentInput input [[stage_in]],
@ -1113,15 +1137,61 @@ float blur_along_x(float x, float y, float sigma, float corner,
return integral.y - integral.x;
}
float4 distance_from_clip_rect(float2 unit_vertex, Bounds_ScaledPixels bounds,
Bounds_ScaledPixels clip_bounds) {
float2 position =
unit_vertex * float2(bounds.size.width, bounds.size.height) +
float2(bounds.origin.x, bounds.origin.y);
return float4(position.x - clip_bounds.origin.x,
clip_bounds.origin.x + clip_bounds.size.width - position.x,
position.y - clip_bounds.origin.y,
clip_bounds.origin.y + clip_bounds.size.height - position.y);
// Intersects `bounds` with `mask` so the emitted geometry never covers pixels
// outside the content mask, making per-fragment clipping unnecessary. An empty
// intersection collapses to zero size, which rasterizes to nothing. Fragment
// shaders reload the original bounds by instance id, so their math is
// unaffected by the shrunken geometry.
Bounds_ScaledPixels clip_to_mask(Bounds_ScaledPixels bounds,
Bounds_ScaledPixels mask) {
float2 origin = max(float2(bounds.origin.x, bounds.origin.y),
float2(mask.origin.x, mask.origin.y));
float2 extent =
min(float2(bounds.origin.x + bounds.size.width,
bounds.origin.y + bounds.size.height),
float2(mask.origin.x + mask.size.width,
mask.origin.y + mask.size.height));
float2 size = max(extent - origin, float2(0.));
Bounds_ScaledPixels result = bounds;
result.origin.x = origin.x;
result.origin.y = origin.y;
result.size.width = size.x;
result.size.height = size.y;
return result;
}
// Whether the transformation only scales and translates, keeping rectangles
// axis-aligned in screen space. Zero scale is excluded so callers can safely
// invert the transformation.
bool transform_is_axis_aligned(TransformationMatrix transformation) {
return transformation.rotation_scale[0][1] == 0. &&
transformation.rotation_scale[1][0] == 0. &&
transformation.rotation_scale[0][0] != 0. &&
transformation.rotation_scale[1][1] != 0.;
}
// Maps the screen-space mask into pre-transform space. Only valid for
// axis-aligned transformations; min/max normalization handles negative scale
// (e.g. rotation by 180 degrees).
Bounds_ScaledPixels mask_in_transform_space(Bounds_ScaledPixels mask,
TransformationMatrix transformation) {
float2 scale = float2(transformation.rotation_scale[0][0],
transformation.rotation_scale[1][1]);
float2 translation = float2(transformation.translation[0],
transformation.translation[1]);
float2 p0 = (float2(mask.origin.x, mask.origin.y) - translation) / scale;
float2 p1 = (float2(mask.origin.x + mask.size.width,
mask.origin.y + mask.size.height) -
translation) /
scale;
float2 origin = min(p0, p1);
float2 size = max(p0, p1) - origin;
Bounds_ScaledPixels result = mask;
result.origin.x = origin.x;
result.origin.y = origin.y;
result.size.width = size.x;
result.size.height = size.y;
return result;
}
float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds_ScaledPixels bounds,

View file

@ -195,17 +195,42 @@ fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec
return vec4<f32>(tl.x, br.x, tl.y, br.y);
}
fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
return distance_from_clip_rect_impl(position, clip_bounds);
}
fn distance_from_clip_rect_transformed(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
let transformed = transpose(transform.rotation_scale) * position + transform.translation;
return distance_from_clip_rect_impl(transformed, clip_bounds);
}
// Intersects `bounds` with `mask` so the emitted geometry never covers pixels
// outside the content mask, making per-fragment clipping unnecessary. An empty
// intersection collapses to zero size, which rasterizes to nothing. Fragment
// shaders reload the original bounds by instance id, so their math is
// unaffected by the shrunken geometry.
fn clip_to_mask(bounds: Bounds, mask: Bounds) -> Bounds {
let origin = max(bounds.origin, mask.origin);
let extent = min(bounds.origin + bounds.size, mask.origin + mask.size);
return Bounds(origin, max(extent - origin, vec2<f32>(0.0)));
}
// Whether the transformation only scales and translates, keeping rectangles
// axis-aligned in screen space. Zero scale is excluded so callers can safely
// invert the transformation.
fn transform_is_axis_aligned(transform: TransformationMatrix) -> bool {
let m = transform.rotation_scale;
return m[0][1] == 0.0 && m[1][0] == 0.0 && m[0][0] != 0.0 && m[1][1] != 0.0;
}
// Maps the screen-space mask into pre-transform space. Only valid for
// axis-aligned transformations; min/max normalization handles negative scale
// (e.g. rotation by 180 degrees).
fn mask_in_transform_space(mask: Bounds, transform: TransformationMatrix) -> Bounds {
let scale = vec2<f32>(transform.rotation_scale[0][0], transform.rotation_scale[1][1]);
let p0 = (mask.origin - transform.translation) / scale;
let p1 = (mask.origin + mask.size - transform.translation) / scale;
let origin = min(p0, p1);
return Bounds(origin, max(p0, p1) - origin);
}
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
let cutoff = srgb < vec3<f32>(0.04045);
@ -531,11 +556,9 @@ struct QuadVarying {
@builtin(position) position: vec4<f32>,
@location(0) @interpolate(flat) border_color: vec4<f32>,
@location(1) @interpolate(flat) quad_id: u32,
// TODO: use `clip_distance` once Naga supports it
@location(2) clip_distances: vec4<f32>,
@location(3) @interpolate(flat) background_solid: vec4<f32>,
@location(4) @interpolate(flat) background_color0: vec4<f32>,
@location(5) @interpolate(flat) background_color1: vec4<f32>,
@location(2) @interpolate(flat) background_solid: vec4<f32>,
@location(3) @interpolate(flat) background_color0: vec4<f32>,
@location(4) @interpolate(flat) background_color1: vec4<f32>,
}
@vertex
@ -544,7 +567,7 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
let quad = load_quad(instance_id);
var out = QuadVarying();
out.position = to_device_position(unit_vertex, quad.bounds);
out.position = to_device_position(unit_vertex, clip_to_mask(quad.bounds, quad.content_mask));
let gradient = prepare_gradient_color(
quad.background.tag,
@ -557,17 +580,11 @@ fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta
out.background_color1 = gradient.color1;
out.border_color = hsla_to_rgba(quad.border_color);
out.quad_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
return out;
}
@fragment
fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
// Alpha clip first, since we don't have `clip_distance`.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
let quad = load_quad(input.quad_id);
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
@ -968,8 +985,6 @@ struct ShadowVarying {
@builtin(position) position: vec4<f32>,
@location(0) @interpolate(flat) color: vec4<f32>,
@location(1) @interpolate(flat) shadow_id: u32,
//TODO: use `clip_distance` once Naga supports it
@location(3) clip_distances: vec4<f32>,
}
@vertex
@ -989,20 +1004,14 @@ fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) ins
}
var out = ShadowVarying();
out.position = to_device_position(unit_vertex, geometry);
out.position = to_device_position(unit_vertex, clip_to_mask(geometry, shadow.content_mask));
out.color = hsla_to_rgba(shadow.color);
out.shadow_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, geometry, shadow.content_mask);
return out;
}
@fragment
fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
// Alpha clip first, since we don't have `clip_distance`.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
let shadow = load_shadow(input.shadow_id);
let half_size = shadow.bounds.size / 2.0;
let center = shadow.bounds.origin + half_size;
@ -1161,8 +1170,6 @@ struct UnderlineVarying {
@builtin(position) position: vec4<f32>,
@location(0) @interpolate(flat) color: vec4<f32>,
@location(1) @interpolate(flat) underline_id: u32,
//TODO: use `clip_distance` once Naga supports it
@location(3) clip_distances: vec4<f32>,
}
@vertex
@ -1171,10 +1178,9 @@ fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index)
let underline = load_underline(instance_id);
var out = UnderlineVarying();
out.position = to_device_position(unit_vertex, underline.bounds);
out.position = to_device_position(unit_vertex, clip_to_mask(underline.bounds, underline.content_mask));
out.color = hsla_to_rgba(underline.color);
out.underline_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
return out;
}
@ -1183,11 +1189,6 @@ fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
const WAVE_FREQUENCY: f32 = 2.0;
const WAVE_HEIGHT_RATIO: f32 = 0.8;
// Alpha clip first, since we don't have `clip_distance`.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
let underline = load_underline(input.underline_id);
if (underline.wavy == 0u)
{
@ -1227,7 +1228,7 @@ struct MonoSpriteVarying {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
@location(3) clip_distances: vec4<f32>,
@location(2) clip_distances: vec4<f32>,
}
@vertex
@ -1236,11 +1237,21 @@ fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index
let sprite = load_mono_sprite(instance_id);
var out = MonoSpriteVarying();
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
if (transform_is_axis_aligned(sprite.transformation)) {
let mask = mask_in_transform_space(sprite.content_mask, sprite.transformation);
let clipped = clip_to_mask(sprite.bounds, mask);
out.position = to_device_position_transformed(unit_vertex, clipped, sprite.transformation);
let local_position = unit_vertex * clipped.size + clipped.origin;
out.tile_position = to_tile_position((local_position - sprite.bounds.origin) / sprite.bounds.size, sprite.tile);
out.clip_distances = vec4<f32>(1.0);
} else {
// A rotated sprite intersected with the axis-aligned mask isn't
// representable as a quad, so fall back to per-fragment clipping.
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
}
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
return out;
}
@ -1249,7 +1260,9 @@ fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
let alpha_corrected = apply_contrast_and_gamma_correction(sample, input.color.rgb, gamma_params.grayscale_enhanced_contrast, gamma_params.gamma_ratios);
// Alpha clip after using the derivatives.
// Only rotated sprites need per-fragment clipping; axis-aligned sprites
// are clipped geometrically in the vertex shader. Alpha clip after using
// the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
@ -1275,30 +1288,25 @@ struct PolySpriteVarying {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) sprite_id: u32,
@location(3) clip_distances: vec4<f32>,
}
@vertex
fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let sprite = load_poly_sprite(instance_id);
let clipped = clip_to_mask(sprite.bounds, sprite.content_mask);
var out = PolySpriteVarying();
out.position = to_device_position(unit_vertex, sprite.bounds);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.position = to_device_position(unit_vertex, clipped);
let position = unit_vertex * clipped.size + clipped.origin;
out.tile_position = to_tile_position((position - sprite.bounds.origin) / sprite.bounds.size, sprite.tile);
out.sprite_id = instance_id;
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
return out;
}
@fragment
fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
let sample = textureSample(t_sprite, s_sprite, input.tile_position);
// Alpha clip after using the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
let sprite = load_poly_sprite(input.sprite_id);
let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
@ -1332,27 +1340,22 @@ const ycbcr_to_RGB = mat4x4<f32>(
struct SurfaceVarying {
@builtin(position) position: vec4<f32>,
@location(0) texture_position: vec2<f32>,
@location(3) clip_distances: vec4<f32>,
}
@vertex
fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
let clipped = clip_to_mask(surface_locals.bounds, surface_locals.content_mask);
var out = SurfaceVarying();
out.position = to_device_position(unit_vertex, surface_locals.bounds);
out.texture_position = unit_vertex;
out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
out.position = to_device_position(unit_vertex, clipped);
let position = unit_vertex * clipped.size + clipped.origin;
out.texture_position = (position - surface_locals.bounds.origin) / surface_locals.bounds.size;
return out;
}
@fragment
fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
// Alpha clip after using the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return vec4<f32>(0.0);
}
let y_cb_cr = vec4<f32>(
textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,

View file

@ -15,7 +15,7 @@ struct SubpixelSpriteOutput {
@builtin(position) position: vec4<f32>,
@location(0) tile_position: vec2<f32>,
@location(1) @interpolate(flat) color: vec4<f32>,
@location(3) clip_distances: vec4<f32>,
@location(2) clip_distances: vec4<f32>,
}
struct SubpixelSpriteFragmentOutput {
@ -29,10 +29,21 @@ fn vs_subpixel_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_i
let sprite = b_subpixel_sprites[instance_id];
var out = SubpixelSpriteOutput();
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
if (transform_is_axis_aligned(sprite.transformation)) {
let mask = mask_in_transform_space(sprite.content_mask, sprite.transformation);
let clipped = clip_to_mask(sprite.bounds, mask);
out.position = to_device_position_transformed(unit_vertex, clipped, sprite.transformation);
let local_position = unit_vertex * clipped.size + clipped.origin;
out.tile_position = to_tile_position((local_position - sprite.bounds.origin) / sprite.bounds.size, sprite.tile);
out.clip_distances = vec4<f32>(1.0);
} else {
// A rotated sprite intersected with the axis-aligned mask isn't
// representable as a quad, so fall back to per-fragment clipping.
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
}
out.color = hsla_to_rgba(sprite.color);
out.clip_distances = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
return out;
}
@ -44,7 +55,9 @@ fn fs_subpixel_sprite(input: SubpixelSpriteOutput) -> SubpixelSpriteFragmentOutp
}
let alpha_corrected = apply_contrast_and_gamma_correction3(sample, input.color.rgb, gamma_params.subpixel_enhanced_contrast, gamma_params.gamma_ratios);
// Alpha clip after using the derivatives.
// Only rotated sprites need per-fragment clipping; axis-aligned sprites
// are clipped geometrically in the vertex shader. Alpha clip after using
// the derivatives.
if (any(input.clip_distances < vec4<f32>(0.0))) {
return SubpixelSpriteFragmentOutput(vec4<f32>(0.0), vec4<f32>(0.0));
}

View file

@ -114,17 +114,48 @@ float4 distance_from_clip_rect_impl(float2 position, Bounds clip_bounds) {
return float4(tl.x, br.x, tl.y, br.y);
}
float4 distance_from_clip_rect(float2 unit_vertex, Bounds bounds, Bounds clip_bounds) {
float2 position = unit_vertex * bounds.size + bounds.origin;
return distance_from_clip_rect_impl(position, clip_bounds);
}
float4 distance_from_clip_rect_transformed(float2 unit_vertex, Bounds bounds, Bounds clip_bounds, TransformationMatrix transformation) {
float2 position = unit_vertex * bounds.size + bounds.origin;
float2 transformed = mul(position, transformation.rotation_scale) + transformation.translation;
return distance_from_clip_rect_impl(transformed, clip_bounds);
}
// Intersects `bounds` with `mask` so the emitted geometry never covers pixels
// outside the content mask, making per-fragment clipping unnecessary. An empty
// intersection collapses to zero size, which rasterizes to nothing. Fragment
// shaders reload the original bounds by instance id, so their math is
// unaffected by the shrunken geometry.
Bounds clip_to_mask(Bounds bounds, Bounds mask) {
Bounds result;
result.origin = max(bounds.origin, mask.origin);
float2 extent = min(bounds.origin + bounds.size, mask.origin + mask.size);
result.size = max(extent - result.origin, float2(0.0, 0.0));
return result;
}
// Whether the transformation only scales and translates, keeping rectangles
// axis-aligned in screen space. Zero scale is excluded so callers can safely
// invert the transformation.
bool transform_is_axis_aligned(TransformationMatrix transformation) {
return transformation.rotation_scale[0][1] == 0.0 &&
transformation.rotation_scale[1][0] == 0.0 &&
transformation.rotation_scale[0][0] != 0.0 &&
transformation.rotation_scale[1][1] != 0.0;
}
// Maps the screen-space mask into pre-transform space. Only valid for
// axis-aligned transformations; min/max normalization handles negative scale
// (e.g. rotation by 180 degrees).
Bounds mask_in_transform_space(Bounds mask, TransformationMatrix transformation) {
float2 scale = float2(transformation.rotation_scale[0][0], transformation.rotation_scale[1][1]);
float2 p0 = (mask.origin - transformation.translation) / scale;
float2 p1 = (mask.origin + mask.size - transformation.translation) / scale;
Bounds result;
result.origin = min(p0, p1);
result.size = max(p0, p1) - result.origin;
return result;
}
// Convert linear RGB to sRGB
float3 linear_to_srgb(float3 color) {
return pow(color, float3(2.2, 2.2, 2.2));
@ -516,7 +547,6 @@ struct QuadVertexOutput {
nointerpolation float4 background_solid: COLOR1;
nointerpolation float4 background_color0: COLOR2;
nointerpolation float4 background_color1: COLOR3;
float4 clip_distance: SV_ClipDistance;
};
struct QuadFragmentInput {
@ -534,7 +564,7 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_I
float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
uint quad_id = batch_start_index + instance_id;
Quad quad = quads[quad_id];
float4 device_position = to_device_position(unit_vertex, quad.bounds);
float4 device_position = to_device_position(unit_vertex, clip_to_mask(quad.bounds, quad.content_mask));
GradientColor gradient = prepare_gradient_color(
quad.background.tag,
@ -542,7 +572,6 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_I
quad.background.solid,
quad.background.colors
);
float4 clip_distance = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
float4 border_color = hsla_to_rgba(quad.border_color);
QuadVertexOutput output;
@ -552,7 +581,6 @@ QuadVertexOutput quad_vertex(uint vertex_id: SV_VertexID, uint instance_id: SV_I
output.background_solid = gradient.solid;
output.background_color0 = gradient.color0;
output.background_color1 = gradient.color1;
output.clip_distance = clip_distance;
return output;
}
@ -870,7 +898,6 @@ struct ShadowVertexOutput {
nointerpolation uint shadow_id: TEXCOORD0;
float4 position: SV_Position;
nointerpolation float4 color: COLOR;
float4 clip_distance: SV_ClipDistance;
};
struct ShadowFragmentInput {
@ -897,15 +924,13 @@ ShadowVertexOutput shadow_vertex(uint vertex_id: SV_VertexID, uint instance_id:
bounds.size += 2.0 * margin;
}
float4 device_position = to_device_position(unit_vertex, bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, bounds, shadow.content_mask);
float4 device_position = to_device_position(unit_vertex, clip_to_mask(bounds, shadow.content_mask));
float4 color = hsla_to_rgba(shadow.color);
ShadowVertexOutput output;
output.position = device_position;
output.color = color;
output.shadow_id = shadow_id;
output.clip_distance = clip_distance;
return output;
}
@ -1076,7 +1101,6 @@ struct UnderlineVertexOutput {
nointerpolation uint underline_id: TEXCOORD0;
float4 position: SV_Position;
nointerpolation float4 color: COLOR;
float4 clip_distance: SV_ClipDistance;
};
struct UnderlineFragmentInput {
@ -1091,16 +1115,14 @@ UnderlineVertexOutput underline_vertex(uint vertex_id: SV_VertexID, uint instanc
float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
uint underline_id = batch_start_index + instance_id;
Underline underline = underlines[underline_id];
float4 device_position = to_device_position(unit_vertex, underline.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, underline.bounds,
underline.content_mask);
float4 device_position = to_device_position(
unit_vertex, clip_to_mask(underline.bounds, underline.content_mask));
float4 color = hsla_to_rgba(underline.color);
UnderlineVertexOutput output;
output.position = device_position;
output.color = color;
output.underline_id = underline_id;
output.clip_distance = clip_distance;
return output;
}
@ -1167,10 +1189,23 @@ MonochromeSpriteVertexOutput monochrome_sprite_vertex(uint vertex_id: SV_VertexI
float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
uint sprite_id = batch_start_index + instance_id;
MonochromeSprite sprite = mono_sprites[sprite_id];
float4 device_position =
to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
float4 clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
float4 device_position;
float2 tile_position;
float4 clip_distance;
if (transform_is_axis_aligned(sprite.transformation)) {
Bounds mask = mask_in_transform_space(sprite.content_mask, sprite.transformation);
Bounds clipped = clip_to_mask(sprite.bounds, mask);
device_position = to_device_position_transformed(unit_vertex, clipped, sprite.transformation);
float2 local_position = unit_vertex * clipped.size + clipped.origin;
tile_position = to_tile_position((local_position - sprite.bounds.origin) / sprite.bounds.size, sprite.tile);
clip_distance = float4(1.0, 1.0, 1.0, 1.0);
} else {
// A rotated sprite intersected with the axis-aligned mask isn't
// representable as a quad, so fall back to hardware clip distances.
device_position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
tile_position = to_tile_position(unit_vertex, sprite.tile);
clip_distance = distance_from_clip_rect_transformed(unit_vertex, sprite.bounds, sprite.content_mask, sprite.transformation);
}
float4 color = hsla_to_rgba(sprite.color);
MonochromeSpriteVertexOutput output;
@ -1225,7 +1260,6 @@ struct PolychromeSpriteVertexOutput {
nointerpolation uint sprite_id: TEXCOORD0;
float4 position: SV_Position;
float2 tile_position: POSITION;
float4 clip_distance: SV_ClipDistance;
};
struct PolychromeSpriteFragmentInput {
@ -1240,16 +1274,15 @@ PolychromeSpriteVertexOutput polychrome_sprite_vertex(uint vertex_id: SV_VertexI
float2 unit_vertex = float2(float(vertex_id & 1u), 0.5 * float(vertex_id & 2u));
uint sprite_id = batch_start_index + instance_id;
PolychromeSprite sprite = poly_sprites[sprite_id];
float4 device_position = to_device_position(unit_vertex, sprite.bounds);
float4 clip_distance = distance_from_clip_rect(unit_vertex, sprite.bounds,
sprite.content_mask);
float2 tile_position = to_tile_position(unit_vertex, sprite.tile);
Bounds clipped = clip_to_mask(sprite.bounds, sprite.content_mask);
float4 device_position = to_device_position(unit_vertex, clipped);
float2 position = unit_vertex * clipped.size + clipped.origin;
float2 tile_position = to_tile_position((position - sprite.bounds.origin) / sprite.bounds.size, sprite.tile);
PolychromeSpriteVertexOutput output;
output.position = device_position;
output.tile_position = tile_position;
output.sprite_id = sprite_id;
output.clip_distance = clip_distance;
return output;
}