Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 Expand Up @@ -50,6 +52,10 @@ struct LineSoup {
p1: vec2<f32>,
}

// The line index of a SegmentCount record that path_count reserved for a crossing that no tile
// keeps. path_tiling skips it, so every reserved record holds a written value.
const SKIPPED_SEGMENT_LINE = 0xffffffffu;

// An intermediate data structure for sorting tile segments.
struct SegmentCount {
// Reference to element of LineSoup array
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
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ fn main(
// antialiased bin coverage unchanged.
let aliased_right_pad = select(0.0, 1.0, (path_bbox.draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u);
let pb = vec4<f32>(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1) + aliased_right_pad, f32(path_bbox.y1));
let bbox = bbox_intersect(clip_bbox, bbox_intersect(pb, path_bbox.interest));

// An aliased fill sets the midpoint pixel of a thin feature between two pixel centres. The
// feature can cross the edge of the interest or the clip, and the pixel that it sets inside
// depends on the geometry up to one pixel outside. Keep one more pixel on each side, as the
// CPU rasterizer does. Fine clears the coverage outside the interest, and the clip layer
// masks the coverage outside the clip.
let halo = vec4(-aliased_right_pad, -aliased_right_pad, aliased_right_pad, aliased_right_pad);
let bbox = bbox_intersect(clip_bbox + halo, bbox_intersect(pb, path_bbox.interest + halo));

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