Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,17 @@ public static bool TryCreateBindGroupLayout(
// 3 bump allocators (read-write; path_rows bump-allocated, failure bit set on overflow)
// 4 paths (read-write; Path record written per draw object)
// 5 rows (read-write; PathRow records initialized to empty spans)
// 6 path_bboxes (read-only path bounds before clipping to the drawing bounds)
WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7];
WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[6];
entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig));
entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage);
entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage);
entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators));
entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage);
entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage);
entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.ReadOnlyStorage);

WGPUBindGroupLayoutDescriptor descriptor = new()
{
entryCount = 7,
entryCount = 6,
entries = entries
};

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

// Geometry of tiles and bins
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ const STYLE_FLAGS_JOIN_ROUND: u32 = 0x20000000u;
const STYLE_FLAGS_JOIN_MITER_REVERT: u32 = 0x00400000u;
const STYLE_FLAGS_JOIN_MITER_ROUND: u32 = 0x00800000u;

// Stroke styles only: the largest interior vertex count of one round arc. The C# encoder writes
// the CPU count of half a turn, so the GPU line count of a round join or cap stays inside the
// CPU line estimate.
const STYLE_FLAGS_ARC_LIMIT_MASK: u32 = 0x000007FFu;

// TODO: Declare the remaining STYLE flags here.

// The scan identity: all counters zero.
Expand Down
17 changes: 14 additions & 3 deletions src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ const CMD_BEGIN_CLIP = 10u;
const CMD_END_CLIP = 11u;
const CMD_JUMP = 12u;
const CMD_PATH_GRAD = 16u;
// Sets the profile record table for the next CMD_FILL. Its one payload word is the word offset of
// the table. With a segment table, each retained scene keeps its own table (see
// scene_segment.wgsl), so coarse writes this command before each aliased fill. Coarse writes it
// only when config.segment_count is not zero. A separate command keeps CMD_FILL, the most
// frequent command in fine, the same for every scene.
const CMD_PROFILE_TABLE = 17u;

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

// Continue reading the command stream at new_ix; links bump-allocated
Expand Down Expand Up @@ -130,8 +140,9 @@ struct CmdSweepGrad {
// whose edge list lives in the shared info/bin-data buffer.
struct CmdPathGrad {
// Absolute offset of the gradient data in the shared info/bin-data
// buffer; coarse rebases the scene-relative offset by
// config.brush_data_base before writing the command.
// buffer. coarse rebases the scene-relative offset by the brush base
// of the scene segment (config.brush_data_base outside a segment table)
// before it writes the command.
data_offset: u32,
edge_count: u32,
flags: u32,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.

// The scene segment table: one dispatch reads retained scenes in place.
//
// A retained scene drawn inside a layer does not copy its streams into the outer scene. The
// scene buffer holds the outer scene data, then this table, then one resident block for each
// retained scene. Each stream is a virtual stream: the outer elements before the first retained
// scene, then the retained elements, then the next outer elements, and so on. The monoid scans
// run over the virtual streams, so every monoid offset is a virtual offset.
//
// One table entry describes one segment, that is one contiguous piece of every stream. A shader
// finds the segment of an element one time, from the virtual index of its path-tag word or of
// its draw object. Then it reads each stream as scene[bias + virtual offset]. A bias is the
// physical start minus the virtual start, in wrapping u32 arithmetic. The segments are in stream
// order, and the first segment starts at virtual index zero in every stream.
//
// When config.segment_count is zero, the scene has no table and every helper returns the config
// bases. Then the cost for each element is one compare of a uniform value.
//
// Like util.wgsl, this module references the `scene` and `config` bindings by name, so the root
// shader must declare them. WGSL module-scope declarations do not depend on order, so the import
// can be at the top of the root shader. The C# writer of the table is WebGPUSceneEncoder.cs.

// Words in one table entry.
const SCENE_SEGMENT_WORDS = 13u;

// Virtual index of the first path-tag word of the segment. This is the key for path-tag elements.
const SCENE_SEGMENT_PATHTAG_START = 0u;
// Bias of the path-tag word stream.
const SCENE_SEGMENT_PATHTAG_BIAS = 1u;
// Bias of the path-data word stream.
const SCENE_SEGMENT_PATHDATA_BIAS = 2u;
// Bias of the style word stream.
const SCENE_SEGMENT_STYLE_BIAS = 3u;
// Bias of the transform stream: the physical word start minus 9 times the virtual transform index.
const SCENE_SEGMENT_TRANSFORM_BIAS = 4u;
// Virtual index of the first draw object of the segment. This is the key for draw objects.
const SCENE_SEGMENT_DRAWTAG_START = 5u;
// Bias of the draw-tag stream.
const SCENE_SEGMENT_DRAWTAG_BIAS = 6u;
// Bias of the draw-data word stream.
const SCENE_SEGMENT_DRAWDATA_BIAS = 7u;
// Word offset of the auxiliary brush data of the segment in the combined info buffer.
const SCENE_SEGMENT_BRUSH_BASE = 8u;
// First gradient-ramp row of the segment.
const SCENE_SEGMENT_GRADIENT_ROW_BASE = 9u;
// First image-atlas row of the segment.
const SCENE_SEGMENT_ATLAS_Y_BASE = 10u;
// Profile slot origin: the slot of a fill line is this value plus half the physical path-data
// word of the line start. Zero when the segment has no profile table.
const SCENE_SEGMENT_PROFILE_SLOTS = 11u;
// Word offset of the profile record table of the segment, or zero when it has none.
const SCENE_SEGMENT_PROFILE_RECORDS = 12u;

// Returned in place of a table entry when the scene has no segment table.
const SCENE_SEGMENT_NONE = 0xffffffffu;

// Returns the word offset of the table entry for the last segment whose key at `key_field` is
// less than or equal to `element_ix`. Empty segments share their key with the next segment, so
// the search selects the segment that holds the element. Call this only when
// config.segment_count is not zero.
fn find_scene_segment(key_field: u32, element_ix: u32) -> u32 {
var lo = 0u;
var hi = config.segment_count;
while lo + 1u < hi {
let mid = (lo + hi) >> 1u;
if scene[config.segment_table_base + mid * SCENE_SEGMENT_WORDS + key_field] <= element_ix {
lo = mid;
} else {
hi = mid;
}
}

return config.segment_table_base + lo * SCENE_SEGMENT_WORDS;
}

// Returns the bias of the path-tag word stream for the path-tag word `word_ix`.
fn resolve_path_tag_bias(word_ix: u32) -> u32 {
if config.segment_count == 0u {
return config.pathtag_base;
}

return scene[find_scene_segment(SCENE_SEGMENT_PATHTAG_START, word_ix) + SCENE_SEGMENT_PATHTAG_BIAS];
}

// Returns the table entry of the segment that holds draw object `ix`, or SCENE_SEGMENT_NONE
// when the scene has no segment table.
fn resolve_draw_segment(ix: u32) -> u32 {
if config.segment_count == 0u {
return SCENE_SEGMENT_NONE;
}

return find_scene_segment(SCENE_SEGMENT_DRAWTAG_START, ix);
}

// Returns the bias of the draw-tag stream for a segment entry from resolve_draw_segment.
fn draw_tag_bias(entry: u32) -> u32 {
if entry == SCENE_SEGMENT_NONE {
return config.drawtag_base;
}

return scene[entry + SCENE_SEGMENT_DRAWTAG_BIAS];
}

// Returns the bias of the draw-data stream for a segment entry from resolve_draw_segment.
fn draw_data_bias(entry: u32) -> u32 {
if entry == SCENE_SEGMENT_NONE {
return config.drawdata_base;
}

return scene[entry + SCENE_SEGMENT_DRAWDATA_BIAS];
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ struct Segment {
point1: vec2<f32>,
// Tile-relative y at which the segment meets the tile's left edge, or
// 1e9 if it does not; fine accumulates the implied vertical edge there
// to keep winding consistent after clipping.
// to keep winding consistent after clipping. A vertical segment that
// path_tiling stretched along the whole left edge keeps its real end
// here instead; its delta.x is 0, so the value adds no winding.
y_edge: f32,
// Profile identifiers used by aliased thin-feature recovery. Bits 0..15 select the profile
// based on left and right movement. Bits 16..31 select the profile based on up and down
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,12 @@ struct Path {
bbox: vec4<u32>,
// offset to the first sparse row record owned by this path
rows: u32,
// Facts about the original path that remain necessary after clipping its drawing bounds.
flags: u32,
}

// The original path begins left of its drawing bounds. The aliased row walk uses this flag when an
// interval enters through the clipped left edge and closes inside the first half pixel.
const PATH_FLAGS_CLIPPED_LEFT = 0x1u;

// Coarse packs one path-tile index and three aliased row-walk flags into CmdFill.coverage_data.
// The index occupies bits 0..28. The remaining bits describe the original left clip and the two
// Coarse packs one path-tile index and two aliased row-walk flags into CmdFill.coverage_data.
// The index occupies bits 0..28 and bit 29 is unused. Bits 30 and 31 describe the two
// neighboring tiles that contain segments for the same path row.
const ALIASED_TILE_INDEX_MASK = 0x1fffffffu;
const ALIASED_CLIPPED_LEFT_BIT = 0x20000000u;
const ALIASED_LEFT_NEIGHBOR_BIT = 0x40000000u;
const ALIASED_RIGHT_NEIGHBOR_BIT = 0x80000000u;
const INVALID_TILE_INDEX = 0xffffffffu;
Expand Down
20 changes: 13 additions & 7 deletions src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@

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

// Reads a draw tag from the scene buffer, defaulting to DRAWTAG_NOP if the given `ix` is beyond the
// range of valid draw objects (e.g this can happen if `ix` is derived from an invocation ID in a
Expand All @@ -16,11 +16,17 @@
// This function depends on the following global declarations:
// * `scene`: array<u32>
// * `config`: Config (see config.wgsl)
// * the scene_segment.wgsl module, imported before this module
fn read_draw_tag_from_scene(ix: u32) -> u32 {
return read_draw_tag_from_segment(resolve_draw_segment(ix), ix);
}

// Reads the draw tag of draw object `ix` through the segment entry that resolve_draw_segment
// returned for it, defaulting to DRAWTAG_NOP beyond the valid draw objects.
fn read_draw_tag_from_segment(entry: u32, ix: u32) -> u32 {
var tag_word: u32;
if ix < config.n_drawobj {
let tag_ix = config.drawtag_base + ix;
tag_word = scene[tag_ix];
tag_word = scene[draw_tag_bias(entry) + ix];
} else {
tag_word = DRAWTAG_NOP;
}
Expand Down
Loading
Loading