Skip to content
Open
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 @@ -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
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 Down Expand Up @@ -130,8 +136,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];
}
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
90 changes: 75 additions & 15 deletions src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#import drawtag
#import ptcl
#import tile
#import scene_segment

@group(0) @binding(0)
var<uniform> config: Config;
Expand Down Expand Up @@ -98,6 +99,30 @@ struct BinHeader {
var<private> cmd_offset: u32;
var<private> cmd_limit: u32;

// Profile record table of the draw object whose commands coarse writes now. When the scene has a
// segment table, write_path puts it in a CMD_PROFILE_TABLE before an aliased fill.
var<private> segment_profile_records: u32;

// With a segment table, sh_drawobj_ix holds the draw object index in the low 24 bits and the
// index of its segment in the high 8 bits. Each draw object is therefore resolved one time in the
// bin load loop. Without a table, it holds the plain index and these helpers return it unchanged.
fn drawobj_index(packed: u32) -> u32 {
if config.segment_count == 0u {
return packed;
}

return packed & 0xffffffu;
}

// Returns the segment table entry of a packed draw object value, or SCENE_SEGMENT_NONE.
fn drawobj_segment(packed: u32) -> u32 {
if config.segment_count == 0u {
return SCENE_SEGMENT_NONE;
}

return config.segment_table_base + (packed >> 24u) * SCENE_SEGMENT_WORDS;
}

// Ensures the current PTCL chunk has room for a command of the given size
// plus jump headroom. When the chunk is exhausted, bump-allocates a new one
// from bump.ptcl and links it with a CMD_JUMP; on overflow the coarse
Expand Down Expand Up @@ -152,10 +177,20 @@ fn write_path(tile: Tile, tile_ix: u32, path: Path, global_x: u32, global_y: u32
if n_segs != 0u {
var seg_ix = atomicAdd(&bump.segments, n_segs);
tiles[tile_ix].segment_count_or_ix = ~seg_ix;
alloc_cmd(6u);
ptcl[cmd_offset] = CMD_FILL;
let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u;
let aliased = (draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u;

// Only aliased fills read profile records in fine. With a segment table, each retained
// scene has its own record table, so a CMD_PROFILE_TABLE before the fill sets it.
let with_profile_records = aliased && config.segment_count != 0u;
alloc_cmd(select(6u, 8u, with_profile_records));
if with_profile_records {
ptcl[cmd_offset] = CMD_PROFILE_TABLE;
ptcl[cmd_offset + 1u] = segment_profile_records;
cmd_offset += 2u;
}

ptcl[cmd_offset] = CMD_FILL;
var packed_coverage_data = bitcast<u32>(coverage_data);
if aliased {
// Pack the data needed by the aliased half-pixel halo into one word:
Expand Down Expand Up @@ -462,7 +497,15 @@ fn main(
}
ix -= select(part_start_ix, sh_part_count[part_ix - 1u], part_ix > 0u);
let offset = config.bin_data_start + sh_part_offsets[part_ix];
sh_drawobj_ix[local_id.x] = info_bin_data[offset + ix];
var drawobj = info_bin_data[offset + ix];
if config.segment_count != 0u {
// This resolves the segment of the draw object one time. The table has at most
// 255 entries, and a scene with a table has fewer than 2^24 draw objects.
let entry = find_scene_segment(SCENE_SEGMENT_DRAWTAG_START, drawobj);
drawobj |= ((entry - config.segment_table_base) / SCENE_SEGMENT_WORDS) << 24u;
}

sh_drawobj_ix[local_id.x] = drawobj;
}
wr_ix = min(rd_ix + N_TILE, ready_ix);
if wr_ix - rd_ix >= N_TILE || (wr_ix >= ready_ix && partition_ix >= n_partitions) {
Expand All @@ -474,8 +517,9 @@ fn main(
var tag = DRAWTAG_NOP;
var drawobj_ix: u32;
if local_id.x + rd_ix < wr_ix {
drawobj_ix = sh_drawobj_ix[local_id.x];
tag = scene[config.drawtag_base + drawobj_ix];
let packed = sh_drawobj_ix[local_id.x];
drawobj_ix = drawobj_index(packed);
tag = scene[draw_tag_bias(drawobj_segment(packed)) + drawobj_ix];
}

var tile_count = 0u;
Expand Down Expand Up @@ -508,8 +552,9 @@ fn main(
el_ix = probe;
}
}
drawobj_ix = sh_drawobj_ix[el_ix];
tag = scene[config.drawtag_base + drawobj_ix];
let packed = sh_drawobj_ix[el_ix];
drawobj_ix = drawobj_index(packed);
tag = scene[draw_tag_bias(drawobj_segment(packed)) + drawobj_ix];
let seq_ix = ix - select(0u, sh_tile_count[el_ix - 1u], el_ix > 0u);
let path_ix = draw_monoids[drawobj_ix].path_ix;
let path = paths[path_ix];
Expand Down Expand Up @@ -581,10 +626,25 @@ fn main(
}

let el_ix = slice_ix * 32u + firstTrailingBit(bitmap);
drawobj_ix = sh_drawobj_ix[el_ix];
let packed = sh_drawobj_ix[el_ix];
drawobj_ix = drawobj_index(packed);
// clear LSB of bitmap, using bit magic
bitmap &= bitmap - 1u;
let drawtag = scene[config.drawtag_base + drawobj_ix];
let segment = drawobj_segment(packed);
let drawtag = scene[draw_tag_bias(segment) + drawobj_ix];

// A retained scene keeps the indices of its own brush data and gradient rows. Those
// follow the outer data in this dispatch, so its segment supplies the bases.
var brush_data_base = config.brush_data_base;
var gradient_index_base = 0u;
segment_profile_records = config.profile_records_base;
if segment != SCENE_SEGMENT_NONE {
brush_data_base = scene[segment + SCENE_SEGMENT_BRUSH_BASE];
// The gradient index word packs the row above the two extend-mode bits.
gradient_index_base = scene[segment + SCENE_SEGMENT_GRADIENT_ROW_BASE] << 2u;
segment_profile_records = scene[segment + SCENE_SEGMENT_PROFILE_RECORDS];
}

let dm = draw_monoids[drawobj_ix];
let dd = config.drawdata_base + dm.scene_offset;
let di = dm.info_offset;
Expand Down Expand Up @@ -617,40 +677,40 @@ fn main(
}
case DRAWTAG_FILL_RECOLOR: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
write_recolor(config.brush_data_base + scene[dd], draw_flags);
write_recolor(brush_data_base + scene[dd], draw_flags);
}
}
case DRAWTAG_FILL_LIN_GRADIENT: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
let index = scene[dd];
let index = scene[dd] + gradient_index_base;
let info_offset = di + 1u;
write_grad(CMD_LIN_GRAD, index, info_offset);
}
}
case DRAWTAG_FILL_RAD_GRADIENT: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
let index = scene[dd];
let index = scene[dd] + gradient_index_base;
let info_offset = di + 1u;
write_grad(CMD_RAD_GRAD, index, info_offset);
}
}
case DRAWTAG_FILL_ELLIPTIC_GRADIENT: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
let index = scene[dd];
let index = scene[dd] + gradient_index_base;
let info_offset = di + 1u;
write_grad(CMD_ELLIPTIC_GRAD, index, info_offset);
}
}
case DRAWTAG_FILL_SWEEP_GRADIENT: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
let index = scene[dd];
let index = scene[dd] + gradient_index_base;
let info_offset = di + 1u;
write_grad(CMD_SWEEP_GRAD, index, info_offset);
}
}
case DRAWTAG_FILL_PATH_GRADIENT: {
if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, false) {
write_path_grad(config.brush_data_base + scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags);
write_path_grad(brush_data_base + scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags);
}
}
case DRAWTAG_FILL_IMAGE: {
Expand Down
Loading
Loading