Skip to content
Open
20 changes: 15 additions & 5 deletions src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,16 @@ fn compose_source(backdrop: vec4<f32>, source: vec4<f32>, opacity: f32, mode: u3
}

let normal = mix_mode == MIX_NORMAL;

// The overlap term is evaluated once, with the operand order of the compose mode
// that consumes it. Evaluating it inside each case inlines the whole mix-mode switch
// once per case, and the driver compile time of this shader grows with every copy.
var overlap = vec3(0.0);
if !normal {
let dest_first = compose_mode == COMPOSE_DEST_OVER || compose_mode == COMPOSE_DEST_ATOP;
overlap = blend_overlap(select(backdrop, scaled, dest_first), select(scaled, backdrop, dest_first), mix_mode);
}

switch compose_mode {
case COMPOSE_CLEAR: {
return vec4(0.0);
Expand All @@ -384,7 +394,7 @@ fn compose_source(backdrop: vec4<f32>, source: vec4<f32>, opacity: f32, mode: u3
return compose_over_normal(scaled, backdrop);
}

return compose_over(scaled, backdrop, blend_overlap(scaled, backdrop, mix_mode));
return compose_over(scaled, backdrop, overlap);
}
case COMPOSE_SRC_IN: {
return compose_in(backdrop, scaled);
Expand All @@ -403,14 +413,14 @@ fn compose_source(backdrop: vec4<f32>, source: vec4<f32>, opacity: f32, mode: u3
return compose_atop_normal(backdrop, scaled);
}

return compose_atop(backdrop, scaled, blend_overlap(backdrop, scaled, mix_mode));
return compose_atop(backdrop, scaled, overlap);
}
case COMPOSE_DEST_ATOP: {
if normal {
return compose_atop_normal(scaled, backdrop);
}

return compose_atop(scaled, backdrop, blend_overlap(scaled, backdrop, mix_mode));
return compose_atop(scaled, backdrop, overlap);
}
case COMPOSE_XOR: {
return compose_xor(backdrop, scaled);
Expand All @@ -420,14 +430,14 @@ fn compose_source(backdrop: vec4<f32>, source: vec4<f32>, opacity: f32, mode: u3
return compose_plus_normal(backdrop, scaled);
}

return compose_plus(backdrop, scaled, blend_overlap(backdrop, scaled, mix_mode));
return compose_plus(backdrop, scaled, overlap);
}
default: {
if normal {
return compose_over_normal(backdrop, scaled);
}

return compose_over(backdrop, scaled, blend_overlap(backdrop, scaled, mix_mode));
return compose_over(backdrop, scaled, overlap);
}
}
}
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
23 changes: 19 additions & 4 deletions src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,24 @@ 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.
const CMD_INTEREST_FULL_TARGET = 0xffffffffu;

// The individual PTCL structs are written here, but read/write is by
// hand in the relevant shaders

// Fill coverage command: rasterize the tile's segment slice into coverage.
// Written by coarse write_path, read by fine read_fill.
// 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.
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 @@ -59,7 +71,9 @@ struct CmdJump {
new_ix: u32,
}

// Solid color paint.
// Solid color paint. The command holds two words after the tag: the scene offset of the two
// packed color words and the info offset of the draw flags, which read_color resolves into
// the struct below.
struct CmdColor {
color_rg: u32, // associated RG packed as binary16
color_ba: u32, // associated BA packed as binary16
Expand Down Expand Up @@ -122,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
Loading
Loading