Skip to content

Commit 43cd8a2

Browse files
Merge pull request #428 from SixLabors/js/gradient-repeat-scene-fixes
Fix seals inside layers, gradient repetition and WebGPU scratch bounds
2 parents cef01ae + fd4c830 commit 43cd8a2

134 files changed

Lines changed: 9571 additions & 1310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,19 +56,17 @@ public static bool TryCreateBindGroupLayout(
5656
// 3 bump allocators (read-write; path_rows bump-allocated, failure bit set on overflow)
5757
// 4 paths (read-write; Path record written per draw object)
5858
// 5 rows (read-write; PathRow records initialized to empty spans)
59-
// 6 path_bboxes (read-only path bounds before clipping to the drawing bounds)
60-
WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7];
59+
WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[6];
6160
entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig));
6261
entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage);
6362
entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage);
6463
entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators));
6564
entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage);
6665
entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage);
67-
entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.ReadOnlyStorage);
6866

6967
WGPUBindGroupLayoutDescriptor descriptor = new()
7068
{
71-
entryCount = 7,
69+
entryCount = 6,
7270
entries = entries
7371
};
7472

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,19 @@ struct Config {
6262
// Word offset of the profile record table, or zero when the table is absent. The table starts
6363
// with the X and Y record counts. Each record then stores two axis limits and one contour link.
6464
profile_records_base: u32,
65-
profile_pad0: u32,
66-
profile_pad1: u32,
67-
profile_pad2: u32,
65+
// Number of entries in the scene segment table, or zero when the scene has no retained scene
66+
// inside a layer. With zero, every shader reads the streams through the bases above. See
67+
// scene_segment.wgsl for the table layout.
68+
segment_count: u32,
69+
// Word offset of the scene segment table in the scene buffer.
70+
segment_table_base: u32,
71+
// Position of the target in the coordinates of the encoded data, packed as x | (y << 16). The
72+
// data of an ordered scene is in the coordinates of its root target, so a scoped layer target
73+
// has a value that is not zero: path_lowering moves the lines into target space, and fine
74+
// moves sample positions back into data space for brushes and profiles. A layer target has at
75+
// most maxTextureDimension2D (8192) texels on each axis and starts inside the canvas, so each
76+
// value fits in 16 bits.
77+
target_origin: u32,
6878
}
6979

7080
// Geometry of tiles and bins

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ const STYLE_FLAGS_JOIN_ROUND: u32 = 0x20000000u;
6161
const STYLE_FLAGS_JOIN_MITER_REVERT: u32 = 0x00400000u;
6262
const STYLE_FLAGS_JOIN_MITER_ROUND: u32 = 0x00800000u;
6363

64+
// Stroke styles only: the largest interior vertex count of one round arc. The C# encoder writes
65+
// the CPU count of half a turn, so the GPU line count of a round join or cap stays inside the
66+
// CPU line estimate.
67+
const STYLE_FLAGS_ARC_LIMIT_MASK: u32 = 0x000007FFu;
68+
6469
// TODO: Declare the remaining STYLE flags here.
6570

6671
// The scan identity: all counters zero.

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ const CMD_BEGIN_CLIP = 10u;
3737
const CMD_END_CLIP = 11u;
3838
const CMD_JUMP = 12u;
3939
const CMD_PATH_GRAD = 16u;
40+
// Sets the profile record table for the next CMD_FILL. Its one payload word is the word offset of
41+
// the table. With a segment table, each retained scene keeps its own table (see
42+
// scene_segment.wgsl), so coarse writes this command before each aliased fill. Coarse writes it
43+
// only when config.segment_count is not zero. A separate command keeps CMD_FILL, the most
44+
// frequent command in fine, the same for every scene.
45+
const CMD_PROFILE_TABLE = 17u;
4046

4147
// CMD_FILL and CMD_SOLID reference their raster interest rectangle by its offset in the
4248
// info stream. This value marks a draw without an interest block: the whole target.
@@ -48,7 +54,8 @@ const CMD_INTEREST_FULL_TARGET = 0xffffffffu;
4854
// Fill coverage command: rasterize the tile's segment slice into coverage.
4955
// Written by coarse write_path, read by fine read_fill. The command holds five words after
5056
// the tag: size_and_rule, seg_data, backdrop, coverage_data, and the info-stream offset of
51-
// the interest rectangle, which read_fill resolves into the vec4 below.
57+
// the interest rectangle, which read_fill resolves into the vec4 below. An aliased fill
58+
// holds two more words: the path-tile indices of the tiles above and below.
5259
struct CmdFill {
5360
size_and_rule: u32, // bit 0 = even-odd, bit 1 = aliased coverage, bits 2.. = segment count
5461
seg_data: u32, // index of the tile's first Segment in segment storage
@@ -57,6 +64,9 @@ struct CmdFill {
5764
// flags. Antialiased fills store the bit pattern of their coverage boost, or zero.
5865
coverage_data: u32,
5966
interest: vec4<f32>, // pixel rect (x0, y0, x1, y1); coverage outside it is zeroed
67+
// Aliased fills only: the path-tile indices of the tiles above (x) and below (y) that have
68+
// segments for the same path, or INVALID_TILE_INDEX.
69+
vertical_neighbors: vec2<u32>,
6070
}
6171

6272
// Continue reading the command stream at new_ix; links bump-allocated
@@ -130,8 +140,9 @@ struct CmdSweepGrad {
130140
// whose edge list lives in the shared info/bin-data buffer.
131141
struct CmdPathGrad {
132142
// Absolute offset of the gradient data in the shared info/bin-data
133-
// buffer; coarse rebases the scene-relative offset by
134-
// config.brush_data_base before writing the command.
143+
// buffer. coarse rebases the scene-relative offset by the brush base
144+
// of the scene segment (config.brush_data_base outside a segment table)
145+
// before it writes the command.
135146
data_offset: u32,
136147
edge_count: u32,
137148
flags: u32,
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
// The scene segment table: one dispatch reads retained scenes in place.
5+
//
6+
// A retained scene drawn inside a layer does not copy its streams into the outer scene. The
7+
// scene buffer holds the outer scene data, then this table, then one resident block for each
8+
// retained scene. Each stream is a virtual stream: the outer elements before the first retained
9+
// scene, then the retained elements, then the next outer elements, and so on. The monoid scans
10+
// run over the virtual streams, so every monoid offset is a virtual offset.
11+
//
12+
// One table entry describes one segment, that is one contiguous piece of every stream. A shader
13+
// finds the segment of an element one time, from the virtual index of its path-tag word or of
14+
// its draw object. Then it reads each stream as scene[bias + virtual offset]. A bias is the
15+
// physical start minus the virtual start, in wrapping u32 arithmetic. The segments are in stream
16+
// order, and the first segment starts at virtual index zero in every stream.
17+
//
18+
// When config.segment_count is zero, the scene has no table and every helper returns the config
19+
// bases. Then the cost for each element is one compare of a uniform value.
20+
//
21+
// Like util.wgsl, this module references the `scene` and `config` bindings by name, so the root
22+
// shader must declare them. WGSL module-scope declarations do not depend on order, so the import
23+
// can be at the top of the root shader. The C# writer of the table is WebGPUSceneEncoder.cs.
24+
25+
// Words in one table entry.
26+
const SCENE_SEGMENT_WORDS = 13u;
27+
28+
// Virtual index of the first path-tag word of the segment. This is the key for path-tag elements.
29+
const SCENE_SEGMENT_PATHTAG_START = 0u;
30+
// Bias of the path-tag word stream.
31+
const SCENE_SEGMENT_PATHTAG_BIAS = 1u;
32+
// Bias of the path-data word stream.
33+
const SCENE_SEGMENT_PATHDATA_BIAS = 2u;
34+
// Bias of the style word stream.
35+
const SCENE_SEGMENT_STYLE_BIAS = 3u;
36+
// Bias of the transform stream: the physical word start minus 9 times the virtual transform index.
37+
const SCENE_SEGMENT_TRANSFORM_BIAS = 4u;
38+
// Virtual index of the first draw object of the segment. This is the key for draw objects.
39+
const SCENE_SEGMENT_DRAWTAG_START = 5u;
40+
// Bias of the draw-tag stream.
41+
const SCENE_SEGMENT_DRAWTAG_BIAS = 6u;
42+
// Bias of the draw-data word stream.
43+
const SCENE_SEGMENT_DRAWDATA_BIAS = 7u;
44+
// Word offset of the auxiliary brush data of the segment in the combined info buffer.
45+
const SCENE_SEGMENT_BRUSH_BASE = 8u;
46+
// First gradient-ramp row of the segment.
47+
const SCENE_SEGMENT_GRADIENT_ROW_BASE = 9u;
48+
// First image-atlas row of the segment.
49+
const SCENE_SEGMENT_ATLAS_Y_BASE = 10u;
50+
// Profile slot origin: the slot of a fill line is this value plus half the physical path-data
51+
// word of the line start. Zero when the segment has no profile table.
52+
const SCENE_SEGMENT_PROFILE_SLOTS = 11u;
53+
// Word offset of the profile record table of the segment, or zero when it has none.
54+
const SCENE_SEGMENT_PROFILE_RECORDS = 12u;
55+
56+
// Returned in place of a table entry when the scene has no segment table.
57+
const SCENE_SEGMENT_NONE = 0xffffffffu;
58+
59+
// Returns the word offset of the table entry for the last segment whose key at `key_field` is
60+
// less than or equal to `element_ix`. Empty segments share their key with the next segment, so
61+
// the search selects the segment that holds the element. Call this only when
62+
// config.segment_count is not zero.
63+
fn find_scene_segment(key_field: u32, element_ix: u32) -> u32 {
64+
var lo = 0u;
65+
var hi = config.segment_count;
66+
while lo + 1u < hi {
67+
let mid = (lo + hi) >> 1u;
68+
if scene[config.segment_table_base + mid * SCENE_SEGMENT_WORDS + key_field] <= element_ix {
69+
lo = mid;
70+
} else {
71+
hi = mid;
72+
}
73+
}
74+
75+
return config.segment_table_base + lo * SCENE_SEGMENT_WORDS;
76+
}
77+
78+
// Returns the bias of the path-tag word stream for the path-tag word `word_ix`.
79+
fn resolve_path_tag_bias(word_ix: u32) -> u32 {
80+
if config.segment_count == 0u {
81+
return config.pathtag_base;
82+
}
83+
84+
return scene[find_scene_segment(SCENE_SEGMENT_PATHTAG_START, word_ix) + SCENE_SEGMENT_PATHTAG_BIAS];
85+
}
86+
87+
// Returns the table entry of the segment that holds draw object `ix`, or SCENE_SEGMENT_NONE
88+
// when the scene has no segment table.
89+
fn resolve_draw_segment(ix: u32) -> u32 {
90+
if config.segment_count == 0u {
91+
return SCENE_SEGMENT_NONE;
92+
}
93+
94+
return find_scene_segment(SCENE_SEGMENT_DRAWTAG_START, ix);
95+
}
96+
97+
// Returns the bias of the draw-tag stream for a segment entry from resolve_draw_segment.
98+
fn draw_tag_bias(entry: u32) -> u32 {
99+
if entry == SCENE_SEGMENT_NONE {
100+
return config.drawtag_base;
101+
}
102+
103+
return scene[entry + SCENE_SEGMENT_DRAWTAG_BIAS];
104+
}
105+
106+
// Returns the bias of the draw-data stream for a segment entry from resolve_draw_segment.
107+
fn draw_data_bias(entry: u32) -> u32 {
108+
if entry == SCENE_SEGMENT_NONE {
109+
return config.drawdata_base;
110+
}
111+
112+
return scene[entry + SCENE_SEGMENT_DRAWDATA_BIAS];
113+
}

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ struct Segment {
1616
point1: vec2<f32>,
1717
// Tile-relative y at which the segment meets the tile's left edge, or
1818
// 1e9 if it does not; fine accumulates the implied vertical edge there
19-
// to keep winding consistent after clipping.
19+
// to keep winding consistent after clipping. A vertical segment that
20+
// path_tiling stretched along the whole left edge keeps its real end
21+
// here instead; its delta.x is 0, so the value adds no winding.
2022
y_edge: f32,
2123
// Profile identifiers used by aliased thin-feature recovery. Bits 0..15 select the profile
2224
// based on left and right movement. Bits 16..31 select the profile based on up and down
@@ -50,6 +52,10 @@ struct LineSoup {
5052
p1: vec2<f32>,
5153
}
5254

55+
// The line index of a SegmentCount record that path_count reserved for a crossing that no tile
56+
// keeps. path_tiling skips it, so every reserved record holds a written value.
57+
const SKIPPED_SEGMENT_LINE = 0xffffffffu;
58+
5359
// An intermediate data structure for sorting tile segments.
5460
struct SegmentCount {
5561
// Reference to element of LineSoup array

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,12 @@ struct Path {
1717
bbox: vec4<u32>,
1818
// offset to the first sparse row record owned by this path
1919
rows: u32,
20-
// Facts about the original path that remain necessary after clipping its drawing bounds.
21-
flags: u32,
2220
}
2321

24-
// The original path begins left of its drawing bounds. The aliased row walk uses this flag when an
25-
// interval enters through the clipped left edge and closes inside the first half pixel.
26-
const PATH_FLAGS_CLIPPED_LEFT = 0x1u;
27-
28-
// Coarse packs one path-tile index and three aliased row-walk flags into CmdFill.coverage_data.
29-
// The index occupies bits 0..28. The remaining bits describe the original left clip and the two
22+
// Coarse packs one path-tile index and two aliased row-walk flags into CmdFill.coverage_data.
23+
// The index occupies bits 0..28 and bit 29 is unused. Bits 30 and 31 describe the two
3024
// neighboring tiles that contain segments for the same path row.
3125
const ALIASED_TILE_INDEX_MASK = 0x1fffffffu;
32-
const ALIASED_CLIPPED_LEFT_BIT = 0x20000000u;
3326
const ALIASED_LEFT_NEIGHBOR_BIT = 0x40000000u;
3427
const ALIASED_RIGHT_NEIGHBOR_BIT = 0x80000000u;
3528
const INVALID_TILE_INDEX = 0xffffffffu;

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
// Utility functions that interact with host-shareable buffer objects.
55
//
6-
// Unlike the other shared modules, this one references bindings by name, so
7-
// it must be imported once, after the resource binding declarations, in the
8-
// shader module that accesses them. Imported by draw_leaf.wgsl and
9-
// draw_reduce.wgsl. Ported from Vello's shader/shared/util.wgsl
10-
// (linebender/vello).
6+
// Like scene_segment.wgsl, and unlike the other shared modules, this one
7+
// references bindings by name, so the root shader must declare them. It calls
8+
// the scene_segment.wgsl helpers, so the root shader imports scene_segment too.
9+
// Imported by draw_leaf.wgsl and draw_reduce.wgsl. Ported from Vello's
10+
// shader/shared/util.wgsl (linebender/vello).
1111

1212
// Reads a draw tag from the scene buffer, defaulting to DRAWTAG_NOP if the given `ix` is beyond the
1313
// range of valid draw objects (e.g this can happen if `ix` is derived from an invocation ID in a
@@ -16,11 +16,17 @@
1616
// This function depends on the following global declarations:
1717
// * `scene`: array<u32>
1818
// * `config`: Config (see config.wgsl)
19+
// * the scene_segment.wgsl module, imported before this module
1920
fn read_draw_tag_from_scene(ix: u32) -> u32 {
21+
return read_draw_tag_from_segment(resolve_draw_segment(ix), ix);
22+
}
23+
24+
// Reads the draw tag of draw object `ix` through the segment entry that resolve_draw_segment
25+
// returned for it, defaulting to DRAWTAG_NOP beyond the valid draw objects.
26+
fn read_draw_tag_from_segment(entry: u32, ix: u32) -> u32 {
2027
var tag_word: u32;
2128
if ix < config.n_drawobj {
22-
let tag_ix = config.drawtag_base + ix;
23-
tag_word = scene[tag_ix];
29+
tag_word = scene[draw_tag_bias(entry) + ix];
2430
} else {
2531
tag_word = DRAWTAG_NOP;
2632
}

‎src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ fn main(
145145
// antialiased bin coverage unchanged.
146146
let aliased_right_pad = select(0.0, 1.0, (path_bbox.draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u);
147147
let pb = vec4<f32>(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1) + aliased_right_pad, f32(path_bbox.y1));
148-
let bbox = bbox_intersect(clip_bbox, bbox_intersect(pb, path_bbox.interest));
148+
149+
// An aliased fill sets the midpoint pixel of a thin feature between two pixel centres. The
150+
// feature can cross the edge of the interest or the clip, and the pixel that it sets inside
151+
// depends on the geometry up to one pixel outside. Keep one more pixel on each side, as the
152+
// CPU rasterizer does. Fine clears the coverage outside the interest, and the clip layer
153+
// masks the coverage outside the clip.
154+
let halo = vec4(-aliased_right_pad, -aliased_right_pad, aliased_right_pad, aliased_right_pad);
155+
let bbox = bbox_intersect(clip_bbox + halo, bbox_intersect(pb, path_bbox.interest + halo));
149156

150157
// Only the first bin-chunk workgroup writes the intersected bbox, since
151158
// the value is identical across chunks and later stages read it once.

0 commit comments

Comments
 (0)