From bb2095a51dddf9377fcbaa9ff18165bddf24d720 Mon Sep 17 00:00:00 2001 From: John Tur Date: Tue, 11 Aug 2026 23:04:10 -0400 Subject: [PATCH] Manually clip primitive geometry --- crates/gpui_apple/src/shaders.metal | 176 ++++++++++++++------- crates/gpui_wgpu/src/shaders.wgsl | 121 +++++++------- crates/gpui_wgpu/src/shaders_subpixel.wgsl | 23 ++- crates/gpui_windows/src/shaders.hlsl | 89 +++++++---- 4 files changed, 264 insertions(+), 145 deletions(-) diff --git a/crates/gpui_apple/src/shaders.metal b/crates/gpui_apple/src/shaders.metal index 2b52bb9ecc0..180d9889413 100644 --- a/crates/gpui_apple/src/shaders.metal +++ b/crates/gpui_apple/src/shaders.metal @@ -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 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, diff --git a/crates/gpui_wgpu/src/shaders.wgsl b/crates/gpui_wgpu/src/shaders.wgsl index 718cb7595d8..db2c1504ba7 100644 --- a/crates/gpui_wgpu/src/shaders.wgsl +++ b/crates/gpui_wgpu/src/shaders.wgsl @@ -195,17 +195,42 @@ fn distance_from_clip_rect_impl(position: vec2, clip_bounds: Bounds) -> vec return vec4(tl.x, br.x, tl.y, br.y); } -fn distance_from_clip_rect(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds) -> vec4 { - let position = unit_vertex * vec2(bounds.size) + bounds.origin; - return distance_from_clip_rect_impl(position, clip_bounds); -} - fn distance_from_clip_rect_transformed(unit_vertex: vec2, bounds: Bounds, clip_bounds: Bounds, transform: TransformationMatrix) -> vec4 { let position = unit_vertex * vec2(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(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(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) -> vec3 { let cutoff = srgb < vec3(0.04045); @@ -531,11 +556,9 @@ struct QuadVarying { @builtin(position) position: vec4, @location(0) @interpolate(flat) border_color: vec4, @location(1) @interpolate(flat) quad_id: u32, - // TODO: use `clip_distance` once Naga supports it - @location(2) clip_distances: vec4, - @location(3) @interpolate(flat) background_solid: vec4, - @location(4) @interpolate(flat) background_color0: vec4, - @location(5) @interpolate(flat) background_color1: vec4, + @location(2) @interpolate(flat) background_solid: vec4, + @location(3) @interpolate(flat) background_color0: vec4, + @location(4) @interpolate(flat) background_color1: vec4, } @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 { - // Alpha clip first, since we don't have `clip_distance`. - if (any(input.clip_distances < vec4(0.0))) { - return vec4(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, @location(0) @interpolate(flat) color: vec4, @location(1) @interpolate(flat) shadow_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, } @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 { - // Alpha clip first, since we don't have `clip_distance`. - if (any(input.clip_distances < vec4(0.0))) { - return vec4(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, @location(0) @interpolate(flat) color: vec4, @location(1) @interpolate(flat) underline_id: u32, - //TODO: use `clip_distance` once Naga supports it - @location(3) clip_distances: vec4, } @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 { 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(0.0))) { - return vec4(0.0); - } - let underline = load_underline(input.underline_id); if (underline.wavy == 0u) { @@ -1227,7 +1228,7 @@ struct MonoSpriteVarying { @builtin(position) position: vec4, @location(0) tile_position: vec2, @location(1) @interpolate(flat) color: vec4, - @location(3) clip_distances: vec4, + @location(2) clip_distances: vec4, } @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(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 { 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(0.0))) { return vec4(0.0); } @@ -1275,30 +1288,25 @@ struct PolySpriteVarying { @builtin(position) position: vec4, @location(0) tile_position: vec2, @location(1) @interpolate(flat) sprite_id: u32, - @location(3) clip_distances: vec4, } @vertex fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying { let unit_vertex = vec2(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 { let sample = textureSample(t_sprite, s_sprite, input.tile_position); - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0))) { - return vec4(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( struct SurfaceVarying { @builtin(position) position: vec4, @location(0) texture_position: vec2, - @location(3) clip_distances: vec4, } @vertex fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying { let unit_vertex = vec2(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 { - // Alpha clip after using the derivatives. - if (any(input.clip_distances < vec4(0.0))) { - return vec4(0.0); - } - let y_cb_cr = vec4( textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r, textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg, diff --git a/crates/gpui_wgpu/src/shaders_subpixel.wgsl b/crates/gpui_wgpu/src/shaders_subpixel.wgsl index 8fd936469dc..a57a28a1ac0 100644 --- a/crates/gpui_wgpu/src/shaders_subpixel.wgsl +++ b/crates/gpui_wgpu/src/shaders_subpixel.wgsl @@ -15,7 +15,7 @@ struct SubpixelSpriteOutput { @builtin(position) position: vec4, @location(0) tile_position: vec2, @location(1) @interpolate(flat) color: vec4, - @location(3) clip_distances: vec4, + @location(2) clip_distances: vec4, } 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(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(0.0))) { return SubpixelSpriteFragmentOutput(vec4(0.0), vec4(0.0)); } diff --git a/crates/gpui_windows/src/shaders.hlsl b/crates/gpui_windows/src/shaders.hlsl index d20cb317dbf..87d2b66d2fc 100644 --- a/crates/gpui_windows/src/shaders.hlsl +++ b/crates/gpui_windows/src/shaders.hlsl @@ -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; }