@@ -25,6 +25,8 @@ struct Edge {
2525 x1: i32,
2626 y1: i32,
2727 flags: i32,
28+ adj_x: i32,
29+ adj_y: i32,
2830 }
2931
3032 struct Params {
@@ -54,12 +56,8 @@ struct Params {
5456 solid_a: u32,
5557 rasterization_mode: u32,
5658 antialias_threshold: u32,
57- stroke_mode: u32,
58- stroke_half_width: u32,
59- stroke_line_cap: u32,
60- stroke_line_join: u32,
61- stroke_miter_limit: u32,
62- stroke_pad0: u32,
59+ pad0: u32,
60+ pad1: u32,
6361 };
6462
6563 struct DispatchConfig {
@@ -814,148 +812,6 @@ fn area_to_coverage(area_val: i32, fill_rule: u32, rasterization_mode: u32, anti
814812 return coverage;
815813 }
816814
817- // -----------------------------------------------------------------------
818- // Stroke distance-field helpers
819- // -----------------------------------------------------------------------
820-
821- // Returns vec2(squared_distance, unclamped_t) from point p to line segment a→b.
822- // The returned distance uses clamped t (0..1), but the unclamped t is also returned
823- // so callers can detect endpoint proximity for cap handling.
824- fn dist_to_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> vec2<f32> {
825- let ab = b - a;
826- let ap = p - a;
827- let len_sq = dot(ab, ab);
828- var unclamped_t: f32;
829- if len_sq < 1e-10 {
830- unclamped_t = 0.0;
831- } else {
832- unclamped_t = dot(ap, ab) / len_sq;
833- }
834- let t = clamp(unclamped_t, 0.0, 1.0);
835- let closest = a + ab * t;
836- let d = p - closest;
837- return vec2<f32>(dot(d, d), unclamped_t);
838- }
839-
840- // Edge flags (matches C# GpuEdge.Flags bit layout).
841- const EDGE_OPEN_START: i32 = 1; // (x0,y0) is an open path start
842- const EDGE_OPEN_END: i32 = 2; // (x1,y1) is an open path end
843-
844- // LineCap enum values.
845- const CAP_BUTT: u32 = 0u;
846- const CAP_SQUARE: u32 = 1u;
847- const CAP_ROUND: u32 = 2u;
848-
849- // Computes stroke coverage for a pixel using distance-field evaluation.
850- // Iterates all edges in relevant bands, finds min distance to centerline,
851- // applies line cap rules using edge flags, and returns antialiased coverage.
852- //
853- // Line join handling:
854- // Miter/MiterRevert/MiterRound joins are handled by extending centerline
855- // segments past interior vertices on the CPU side. The distance field then
856- // naturally produces the correct miter coverage.
857- // Round joins are the natural distance-field behavior.
858- // Bevel join coverage is very close to round and accepted as-is.
859- fn stroke_coverage(
860- dest_x_i32: i32,
861- dest_y_i32: i32,
862- command: Params,
863- half_width: f32,
864- line_cap: u32,
865- line_join: u32,
866- miter_limit: f32,
867- tile_min_x: i32,
868- tile_min_y: i32,
869- ) -> f32 {
870- let px = f32(dest_x_i32 - command.edge_origin_x) + 0.5;
871- let py = f32(dest_y_i32 - command.edge_origin_y) + 0.5;
872- let point = vec2<f32>(px, py);
873-
874- // Determine band range for this pixel. Edges are stored per 16-row band.
875- // We must check all bands whose edges could be within half_width + 1 distance.
876- let expand = i32(ceil(half_width)) + 1;
877- let pixel_y = i32(py);
878- var first_band = max((pixel_y - expand) / 16, 0);
879- if (pixel_y - expand) < 0 && ((pixel_y - expand) % 16) != 0 {
880- first_band = max(first_band - 1, 0);
881- }
882- let last_band = min((pixel_y + expand) / 16, i32(command.csr_band_count) - 1);
883- if first_band > last_band || i32(command.csr_band_count) == 0 {
884- return 0.0;
885- }
886-
887- let sentinel = half_width * half_width + half_width * 2.0 + 1.0;
888- var min_dist_sq = sentinel;
889- let inv_fixed = 1.0 / f32(FIXED_ONE);
890-
891- for (var band = first_band; band <= last_band; band++) {
892- let b_start = band_offsets[command.csr_offsets_start + u32(band)];
893- let b_end = band_offsets[command.csr_offsets_start + u32(band) + 1u];
894- for (var ei = b_start; ei < b_end; ei++) {
895- let edge = edges[command.edge_start + ei];
896- let a = vec2<f32>(f32(edge.x0) * inv_fixed, f32(edge.y0) * inv_fixed);
897- let b = vec2<f32>(f32(edge.x1) * inv_fixed, f32(edge.y1) * inv_fixed);
898- let flags = edge.flags;
899- let is_open_start = (flags & EDGE_OPEN_START) != 0;
900- let is_open_end = (flags & EDGE_OPEN_END) != 0;
901-
902- let result = dist_to_segment(point, a, b);
903- var d_sq = result.x;
904- let unclamped_t = result.y;
905-
906- // Cap handling at open path endpoints.
907- // Interior vertices (no open flags) use the natural clamped distance,
908- // which produces smooth coverage where adjacent segments meet.
909- if line_cap == CAP_BUTT {
910- // Butt cap: no coverage past the open endpoint.
911- // Exclude this edge if the pixel projects past an open end.
912- if (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end) {
913- d_sq = sentinel;
914- }
915- } else if line_cap == CAP_SQUARE {
916- // Square cap: extend the segment by half_width at open endpoints only.
917- let needs_ext = (unclamped_t < 0.0 && is_open_start) || (unclamped_t > 1.0 && is_open_end);
918- if needs_ext {
919- let seg = b - a;
920- let seg_len = length(seg);
921- if seg_len > 1e-6 {
922- let dir = seg / seg_len;
923- var ext_a = a;
924- var ext_b = b;
925- if is_open_start {
926- ext_a = a - dir * half_width;
927- }
928- if is_open_end {
929- ext_b = b + dir * half_width;
930- }
931- let ext_result = dist_to_segment(point, ext_a, ext_b);
932- d_sq = ext_result.x;
933- }
934- }
935- // For non-open endpoints or when projection is on-segment,
936- // use the natural clamped distance (d_sq unchanged).
937- }
938- // CAP_ROUND: natural clamped distance produces round caps. No change needed.
939-
940- min_dist_sq = min(min_dist_sq, d_sq);
941- }
942- }
943-
944- let min_dist = sqrt(min_dist_sq);
945-
946- // Antialiased coverage.
947- let rasterization_mode = command.rasterization_mode;
948- if rasterization_mode == 1u {
949- // Aliased mode.
950- if min_dist <= half_width {
951- return 1.0;
952- }
953- return 0.0;
954- }
955- // Antialiased: smooth transition over 1 pixel at boundary.
956- return clamp(half_width + 0.5 - min_dist, 0.0, 1.0);
957- }
958-
959815 // -----------------------------------------------------------------------
960816 // Main entry point
961817 // -----------------------------------------------------------------------
@@ -1011,31 +867,13 @@ fn cs_main(
1011867 continue;
1012868 }
1013869
1014- // Branch: stroke mode vs fill mode.
1015- let is_stroke = command.stroke_mode == 1u;
1016-
1017870 var coverage_value = 0.0;
1018- if is_stroke {
1019- // Stroke path: per-pixel distance-field evaluation.
1020- if in_bounds && dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y {
1021- let half_width = u32_to_f32(command.stroke_half_width);
1022- coverage_value = stroke_coverage(
1023- dest_x_i32, dest_y_i32, command,
1024- half_width,
1025- command.stroke_line_cap,
1026- command.stroke_line_join,
1027- u32_to_f32(command.stroke_miter_limit),
1028- tile_min_x, tile_min_y);
1029- }
1030- } else {
1031- // Fill path: scanline rasterizer.
1032871
1033- // Determine this tile's position in coverage-local space.
872+ // Tile position in edge-local ( coverage-local) space.
1034873 let band_top = tile_min_y - command.edge_origin_y;
1035874 let band_left_fixed = (tile_min_x - command.edge_origin_x) << FIXED_SHIFT;
1036875
1037- // Band lookup: when edge_origin_y is 16-aligned the tile maps to one band;
1038- // otherwise it can overlap two bands.
876+ // Multi-band lookup: tile may overlap one or two bands.
1039877 var first_band = band_top / 16;
1040878 if band_top < 0 && (band_top % 16) != 0 {
1041879 first_band -= 1;
@@ -1082,7 +920,9 @@ fn cs_main(
1082920 break;
1083921 }
1084922 let edge = edges[command.edge_start + b_start + ei];
1085- if min(edge.x0, edge.x1) >= tile_right_fixed {
923+ if edge.y0 == edge.y1 {
924+ // Skip degenerate edges (sentinel slots from stroke expand).
925+ } else if min(edge.x0, edge.x1) >= tile_right_fixed {
1086926 } else if max(edge.x0, edge.x1) < band_left_fixed {
1087927 accumulate_start_cover(edge.y0, edge.y1, clip_top, clip_bottom, tile_top_fixed);
1088928 } else {
@@ -1093,7 +933,7 @@ fn cs_main(
1093933 }
1094934 workgroupBarrier();
1095935
1096- // Compute coverage for fill .
936+ // Compute coverage.
1097937 if in_bounds {
1098938 if dest_x_i32 >= cmd_min_x && dest_x_i32 < cmd_max_x && dest_y_i32 >= cmd_min_y && dest_y_i32 < cmd_max_y {
1099939 var cover = atomicLoad(&tile_start_cover[py]);
@@ -1104,7 +944,6 @@ fn cs_main(
1104944 coverage_value = area_to_coverage(area_val, command.fill_rule_value, command.rasterization_mode, u32_to_f32(command.antialias_threshold));
1105945 }
1106946 }
1107- } // end fill path
1108947
1109948 // Compose coverage result (shared by fill and stroke paths).
1110949 if in_bounds && coverage_value > 0.0 {
0 commit comments