diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl index d04a858f..94b16c07 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl @@ -369,6 +369,16 @@ fn compose_source(backdrop: vec4, source: vec4, 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); @@ -384,7 +394,7 @@ fn compose_source(backdrop: vec4, source: vec4, 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); @@ -403,14 +413,14 @@ fn compose_source(backdrop: vec4, source: vec4, 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); @@ -420,14 +430,14 @@ fn compose_source(backdrop: vec4, source: vec4, 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); } } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl index 00ee4192..ff676e05 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl @@ -38,11 +38,17 @@ const CMD_END_CLIP = 11u; const CMD_JUMP = 12u; const CMD_PATH_GRAD = 16u; +// 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 @@ -59,7 +65,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 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl index d81e84b2..7f37732b 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl @@ -144,7 +144,7 @@ fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32) -> bool { // When emit_empty_solid is false, solid tiles whose backdrop resolves to // zero coverage are skipped entirely. Returns true when a command was // written, meaning the caller should emit the matching paint command. -fn write_path(tile: Tile, tile_ix: u32, path: Path, global_x: u32, global_y: u32, draw_flags: u32, coverage_data: f32, interest: vec4, emit_empty_solid: bool) -> bool { +fn write_path(tile: Tile, tile_ix: u32, path: Path, global_x: u32, global_y: u32, draw_flags: u32, coverage_data: f32, interest_ref: u32, emit_empty_solid: bool) -> bool { // We overload the "segments" field to store both count (written by // path_count stage) and segment allocation (used by path_tiling and // fine). @@ -152,7 +152,7 @@ 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(9u); + 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; @@ -186,44 +186,37 @@ fn write_path(tile: Tile, tile_ix: u32, path: Path, global_x: u32, global_y: u32 // size_and_rule: bit 0 = even-odd, bit 1 = aliased coverage, bits 2.. = segment count. let size_and_rule = (n_segs << 2u) | (u32(aliased) << 1u) | u32(even_odd); - let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop, packed_coverage_data, interest); - ptcl[cmd_offset + 1u] = fill.size_and_rule; - ptcl[cmd_offset + 2u] = fill.seg_data; - ptcl[cmd_offset + 3u] = u32(fill.backdrop); - ptcl[cmd_offset + 4u] = fill.coverage_data; - ptcl[cmd_offset + 5u] = bitcast(fill.interest.x); - ptcl[cmd_offset + 6u] = bitcast(fill.interest.y); - ptcl[cmd_offset + 7u] = bitcast(fill.interest.z); - ptcl[cmd_offset + 8u] = bitcast(fill.interest.w); + ptcl[cmd_offset + 1u] = size_and_rule; + ptcl[cmd_offset + 2u] = seg_ix; + ptcl[cmd_offset + 3u] = u32(tile.backdrop); + ptcl[cmd_offset + 4u] = packed_coverage_data; + ptcl[cmd_offset + 5u] = interest_ref; // The winding backdrop is now in PTCL. Reuse its tile field for the original segment // count so fine can read adjacent slices after segment_count_or_ix becomes the allocation. tiles[tile_ix].backdrop = i32(n_segs); - cmd_offset += 9u; + cmd_offset += 6u; return true; } else { if !emit_empty_solid && !solid_tile_has_coverage(draw_flags, tile.backdrop) { return false; } - alloc_cmd(5u); + alloc_cmd(2u); ptcl[cmd_offset] = CMD_SOLID; - ptcl[cmd_offset + 1u] = bitcast(interest.x); - ptcl[cmd_offset + 2u] = bitcast(interest.y); - ptcl[cmd_offset + 3u] = bitcast(interest.z); - ptcl[cmd_offset + 4u] = bitcast(interest.w); - cmd_offset += 5u; + ptcl[cmd_offset + 1u] = interest_ref; + cmd_offset += 2u; return true; } } -// Emits a CMD_COLOR paint command (binary16 RGBA color plus draw flags). -fn write_color(color: CmdColor) { - alloc_cmd(4u); +// Emits a CMD_COLOR paint command referencing the draw's packed color words in the scene +// stream and its draw flags in the info stream. +fn write_color(scene_offset: u32, info_offset: u32) { + alloc_cmd(3u); ptcl[cmd_offset] = CMD_COLOR; - ptcl[cmd_offset + 1u] = color.color_rg; - ptcl[cmd_offset + 2u] = color.color_ba; - ptcl[cmd_offset + 3u] = color.draw_flags; - cmd_offset += 4u; + ptcl[cmd_offset + 1u] = scene_offset; + ptcl[cmd_offset + 2u] = info_offset; + cmd_offset += 3u; } // Emits a CMD_RECOLOR command referencing one target-specialized auxiliary record. @@ -597,18 +590,14 @@ fn main( let di = dm.info_offset; let draw_flags = info_bin_data[di]; var coverage_data = 0.0; - var interest = vec4(0.0, 0.0, f32(config.target_width), f32(config.target_height)); + var interest_ref = CMD_INTEREST_FULL_TARGET; // Draw tags whose info block spans at least five words append a // coverage data plus interest rectangle at the end of it. let drawtag_info_size = (drawtag >> 6u) & 0xfu; if drawtag_info_size >= 5u { let interest_offset = di + drawtag_info_size - 5u; coverage_data = bitcast(info_bin_data[interest_offset]); - interest = vec4( - bitcast(info_bin_data[interest_offset + 1u]), - bitcast(info_bin_data[interest_offset + 2u]), - bitcast(info_bin_data[interest_offset + 3u]), - bitcast(info_bin_data[interest_offset + 4u])); + interest_ref = interest_offset + 1u; } if clip_zero_depth == 0u { @@ -622,50 +611,50 @@ fn main( let tile = tiles[tile_ix]; switch drawtag { case DRAWTAG_FILL_COLOR: { - if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { - write_color(CmdColor(scene[dd], scene[dd + 1u], draw_flags)); + 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_color(dd, di); } } 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, false) { + 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); } } 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, false) { + 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 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, false) { + 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 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, false) { + 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 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, false) { + 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 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, false) { + 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); } } case DRAWTAG_FILL_IMAGE: { - if write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, false) { + 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_image(di + 1u); } } @@ -689,7 +678,7 @@ fn main( case DRAWTAG_END_CLIP: { clip_depth -= 1u; let blend = scene[dd]; - write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest, true); + write_path(tile, tile_ix, path, bin_tile_x + tile_x, bin_tile_y + tile_y, draw_flags, coverage_data, interest_ref, true); let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); render_blend_depth -= 1u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl index 49572376..6741f2e1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl @@ -95,26 +95,36 @@ fn read_fill(cmd_ix: u32) -> CmdFill { let seg_data = ptcl[cmd_ix + 2u]; let backdrop = i32(ptcl[cmd_ix + 3u]); let coverage_data = ptcl[cmd_ix + 4u]; - let interest = vec4( - bitcast(ptcl[cmd_ix + 5u]), - bitcast(ptcl[cmd_ix + 6u]), - bitcast(ptcl[cmd_ix + 7u]), - bitcast(ptcl[cmd_ix + 8u])); + let interest = resolve_interest(ptcl[cmd_ix + 5u]); return CmdFill(size_and_rule, seg_data, backdrop, coverage_data, interest); } +// Resolves the raster interest rectangle referenced by a CMD_FILL or CMD_SOLID: the four +// words at the info-stream offset, or the whole target for CMD_INTEREST_FULL_TARGET. +fn resolve_interest(interest_ref: u32) -> vec4 { + if interest_ref == CMD_INTEREST_FULL_TARGET { + return vec4(0.0, 0.0, f32(config.target_width), f32(config.target_height)); + } + + return vec4( + bitcast(info[interest_ref]), + bitcast(info[interest_ref + 1u]), + bitcast(info[interest_ref + 2u]), + bitcast(info[interest_ref + 3u])); +} + // Expands one RGBA color stored as two binary16 pairs. Brush payloads use // binary16 so RgbaHalf targets are not prematurely reduced to RGBA8. fn unpack_color_f16(rg: u32, ba: u32) -> vec4 { return vec4(unpack2x16float(rg), unpack2x16float(ba)); } -// Decodes a CMD_COLOR payload: binary16 associated color and draw flags. +// Decodes a CMD_COLOR payload: the binary16 associated color from the scene stream and the +// draw flags from the info stream, both by offset. fn read_color(cmd_ix: u32) -> CmdColor { - let color_rg = ptcl[cmd_ix + 1u]; - let color_ba = ptcl[cmd_ix + 2u]; - let draw_flags = ptcl[cmd_ix + 3u]; - return CmdColor(color_rg, color_ba, draw_flags); + let scene_offset = ptcl[cmd_ix + 1u]; + let info_offset = ptcl[cmd_ix + 2u]; + return CmdColor(scene_data[scene_offset], scene_data[scene_offset + 1u], info[info_offset]); } // Decodes a CMD_RECOLOR reference to its target-specialized auxiliary record. @@ -1167,6 +1177,13 @@ fn main( var blend_stack: array, PIXELS_PER_THREAD>, BLEND_STACK_SPLIT>; var clip_depth = 0u; var area: array; + // Paint commands store their per-pixel source color and coverage here, and the loop tail + // composes them at one site. A compose call inlines the blend-mode switch, so one site per + // paint command would multiply the shader's compile time. + var paint_color: array, PIXELS_PER_THREAD>; + var paint_coverage: array; + var paint_flags = 0u; + var paint_pending = false; var cmd_ix = tile_ix * PTCL_INITIAL_ALLOC; // The first word of each tile's PTCL slot is its blend spill offset. let blend_offset = ptcl[cmd_ix]; @@ -1181,31 +1198,28 @@ fn main( case CMD_FILL: { let fill = read_fill(cmd_ix); fill_path(fill, local_xy, xy, &area); - cmd_ix += 9u; + cmd_ix += 6u; } case CMD_SOLID: { // Full coverage, restricted to the command's raster interest // rectangle. - let interest = vec4( - bitcast(ptcl[cmd_ix + 1u]), - bitcast(ptcl[cmd_ix + 2u]), - bitcast(ptcl[cmd_ix + 3u]), - bitcast(ptcl[cmd_ix + 4u])); + let interest = resolve_interest(ptcl[cmd_ix + 1u]); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let pixel = xy + vec2(f32(i), 0.0); area[i] = select(0.0, 1.0, pixel.x >= interest.x && pixel.y >= interest.y && pixel.x < interest.z && pixel.y < interest.w); } - cmd_ix += 5u; + cmd_ix += 2u; } case CMD_COLOR: { let color = read_color(cmd_ix); let fg = decode_paint_color(unpack_color_f16(color.color_rg, color.color_ba)); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - if area[i] != 0.0 { - rgba[i] = compose_draw_with_coverage(rgba[i], fg, area[i], color.draw_flags); - } + paint_color[i] = fg; + paint_coverage[i] = area[i]; } - cmd_ix += 4u; + paint_flags = color.draw_flags; + paint_pending = true; + cmd_ix += 3u; } case CMD_RECOLOR: { let recolor = read_recolor(cmd_ix); @@ -1332,6 +1346,7 @@ fn main( let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; let d = lin.line_x * (xy.x + 0.5) + lin.line_y * (xy.y + 0.5) + lin.line_c; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + paint_coverage[i] = area[i]; if area[i] != 0.0 { let my_d = d + lin.line_x * f32(i); let t = extend_mode_normalized(my_d, lin.extend_mode); @@ -1344,9 +1359,11 @@ fn main( // CPU gradient brushes return a transparent overlay for DontFill samples, // then blend it normally. Destructive composition modes must therefore // still run when no ramp texel is selected. - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + paint_color[i] = fg_rgba; } } + paint_flags = draw_flags; + paint_pending = true; cmd_ix += 3u; } case CMD_RAD_GRAD: { @@ -1403,14 +1420,18 @@ fn main( // Invalid conical solutions and DontFill both produce the transparent // overlay that the CPU still sends through coverage and composition. - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + paint_color[i] = fg_rgba; + paint_coverage[i] = area[i]; } + paint_flags = draw_flags; + paint_pending = true; cmd_ix += 3u; } case CMD_ELLIPTIC_GRAD: { let elliptic = read_elliptic_grad(cmd_ix); let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + paint_coverage[i] = area[i]; if area[i] != 0.0 { let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); if elliptic.kind == ELLIPTIC_GRAD_KIND_NORMAL { @@ -1428,7 +1449,7 @@ fn main( } } - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + paint_color[i] = fg_rgba; } else { // Keep the CPU order of operations for a collapsed ellipse: subtract // the center first, then rotate. An affine translation would introduce @@ -1448,10 +1469,12 @@ fn main( fg_rgba = textureLoad(gradients, vec2(i32(GRADIENT_WIDTH - 1), i32(elliptic.index)), 0); } - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + paint_color[i] = fg_rgba; } } } + paint_flags = draw_flags; + paint_pending = true; cmd_ix += 3u; } case CMD_SWEEP_GRAD: { @@ -1459,6 +1482,7 @@ fn main( let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; let scale = 1.0 / (sweep.t1 - sweep.t0); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + paint_coverage[i] = area[i]; if area[i] != 0.0 { let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); let local_xy = sweep.matrx.xy * my_xy.x + sweep.matrx.zw * my_xy.y + sweep.xlat; @@ -1496,26 +1520,31 @@ fn main( // DontFill is a transparent brush sample, not an omitted draw, so it // still participates in Src, Clear, and every other composition mode. - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + paint_color[i] = fg_rgba; } } + paint_flags = draw_flags; + paint_pending = true; cmd_ix += 3u; } case CMD_PATH_GRAD: { let path_grad = read_path_grad(cmd_ix); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + paint_coverage[i] = area[i]; if area[i] != 0.0 { let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); - let fg_rgba = evaluate_path_gradient(path_grad, my_xy); - rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], path_grad.draw_flags); + paint_color[i] = evaluate_path_gradient(path_grad, my_xy); } } + paint_flags = path_grad.draw_flags; + paint_pending = true; cmd_ix += 5u; } case CMD_IMAGE: { let image = read_image(cmd_ix); let draw_flags = info[ptcl[cmd_ix + 1u] - 1u]; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + paint_coverage[i] = 0.0; // We only need to load from the textures if the value will be used. if area[i] != 0.0 { let my_xy = vec2(xy.x + f32(i), xy.y); @@ -1531,15 +1560,26 @@ fn main( // sources both enter the common composition space. let atlas_color = decode_image_numeric(textureLoad(image_atlas, atlas_uv_clamped, 0), image.signed_unit); let fg_rgba = maybe_premul_alpha(atlas_color, image.alpha_type); - let fg_i = pixel_format(fg_rgba * image.alpha, image.format); - rgba[i] = compose_draw_with_coverage(rgba[i], fg_i, area[i], draw_flags); + paint_color[i] = pixel_format(fg_rgba * image.alpha, image.format); + paint_coverage[i] = area[i]; } } } + paint_flags = draw_flags; + paint_pending = true; cmd_ix += 2u; } default: {} } + + if paint_pending { + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + if paint_coverage[i] != 0.0 { + rgba[i] = compose_draw_with_coverage(rgba[i], paint_color[i], paint_coverage[i], paint_flags); + } + } + paint_pending = false; + } } for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let coords = xy_uint + vec2(i, 0u); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs index a368b491..b9c41398 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs @@ -54,6 +54,10 @@ public WebGPUNativeSurface( this.TextureCoordinateOffset = textureCoordinateOffset; this.IsPresentationSurface = isPresentationSurface; this.RequiresPresentationCopies = requiresPresentationCopies; + + // The target format is known here for the first time. Start the fine pipeline compile for it so + // the first flush finds it ready or waits only for the remaining part. + WebGPUSceneDispatch.BeginFinePipelineWarmup(WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), deviceHandle), targetDescriptor); } /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs index 6b4e37a7..33bc0542 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs @@ -184,9 +184,19 @@ internal sealed partial class DeviceSharedState : IDisposable private readonly object singlePixelSync = new(); /// - /// Signals that the one-time background pipeline warm-up no longer uses this state. + /// Set while no background pipeline warm-up uses this state. /// - private readonly ManualResetEventSlim pipelineWarmupCompleted = new(false); + private readonly ManualResetEventSlim pipelineWarmupCompleted = new(true); + + /// + /// Guards and the transitions of . + /// + private readonly object pipelineWarmupSync = new(); + + /// + /// The number of background pipeline warm-ups in flight. + /// + private int pipelineWarmupCount; /// /// Upper bound on pooled status readback buffers; returns beyond it release instead. @@ -336,10 +346,40 @@ public void MarkLost() => Volatile.Write(ref this.isLost, 1); /// - /// Signals that background pipeline warm-up has stopped using this state. + /// Registers one background pipeline warm-up that uses this state until is called. + /// + public void RegisterPipelineWarmup() + { + lock (this.pipelineWarmupSync) + { + if (this.pipelineWarmupCount++ == 0) + { + this.pipelineWarmupCompleted.Reset(); + } + } + } + + /// + /// Signals that one background pipeline warm-up has stopped using this state. /// public void CompletePipelineWarmup() - => this.pipelineWarmupCompleted.Set(); + { + lock (this.pipelineWarmupSync) + { + if (--this.pipelineWarmupCount == 0) + { + this.pipelineWarmupCompleted.Set(); + } + } + } + + /// + /// Reports whether the compute pipeline for the given key has already been created. + /// + /// The pipeline cache key. + /// when the pipeline exists; otherwise, . + public bool HasCompositeComputePipeline(string pipelineKey) + => this.compositeComputePipelines.TryGetValue(pipelineKey, out CompositeComputePipelineInfrastructure? infrastructure) && infrastructure.Pipeline is not null; /// /// Rents a pooled map-readable status buffer, or creates one when the pool has no buffer diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index 4bb2960b..03ecbefc 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -324,9 +324,16 @@ public static WebGPUEnvironmentError ProbeAvailability() try { - availabilityProbeResult = TryGetOrCreateDevice(out _, out _, out WebGPUEnvironmentError errorCode) + availabilityProbeResult = TryGetOrCreateDevice(out WebGPUDeviceHandle? probedDevice, out _, out WebGPUEnvironmentError errorCode) ? WebGPUEnvironmentError.Success : errorCode; + + // Creating the device state here starts the background compile of the scheduling + // pipelines at probe time, before the application creates its first target. + if (availabilityProbeResult == WebGPUEnvironmentError.Success && probedDevice is not null) + { + _ = GetOrCreateDeviceState(GetApi(), probedDevice); + } } catch (InvalidOperationException) { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs index 5b904750..40ab9f87 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs @@ -67,11 +67,10 @@ internal static class WebGPUSceneDispatch // validation rejects deeper nesting before anything is dispatched. private const int MaxClipStackDepth = 256; - // The PTCL word budget attributed to each estimated tile crossing when seeding the PTCL - // scratch capacity. Each crossed tile costs one CMD_FILL (9 words) plus a paint command and - // amortized jump/end overhead; the multiplier was calibrated against measured demand - // (~6.5 words per crossing on stroke-heavy scenes) with headroom for paint-heavy draws. - private const long PtclWordsPerCrossing = 8; + // Coarse allocates the dynamic PTCL tail in PTCL_INCREMENT-word chunks (Shared/ptcl.wgsl); a + // command that does not fit the remaining chunk starts a new one, so each tile can leave one + // partial chunk and each chunk can waste up to one command of headroom. + private const long PtclChunkWords = 256; /// /// Identifies the staged-scene storage binding that exceeded the device limit for one flush attempt. @@ -2446,7 +2445,9 @@ private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes curr scene.TotalPathRowCount, scene.PathCount, scene.EstimatedTileCrossings, + scene.EstimatedPtclWords, scene.EstimatedBinFootprint, + (long)scene.TileCountX * scene.TileCountY, maxStorageBufferBindingSize); /// @@ -2463,7 +2464,9 @@ private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes curr range.TotalPathRowCount, range.PathCount, range.EstimatedTileCrossings, + range.EstimatedPtclWords, range.EstimatedBinFootprint, + (long)((range.TargetBounds.Width + 15) / 16) * ((range.TargetBounds.Height + 15) / 16), maxStorageBufferBindingSize); /// @@ -2478,7 +2481,9 @@ private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes curr /// The estimated sparse path-row count. /// The encoded path count. /// The CPU-side upper bound for tile-boundary crossings. + /// The CPU-side upper bound for the dynamic PTCL words those crossings write. /// The CPU-side upper bound for per-(draw, bin) records. + /// The number of tiles in the target, each of which can leave one partial PTCL chunk. /// The device-reported maximum size of one storage-buffer binding. /// The retained capacities raised to the known CPU-side lower bounds. private static WebGPUSceneBumpSizes SeedSceneBumpSizes( @@ -2487,7 +2492,9 @@ private static WebGPUSceneBumpSizes SeedSceneBumpSizes( int totalPathRowCount, int pathCount, long estimatedTileCrossings, + long estimatedPtclWords, long estimatedBinFootprint, + long targetTileCount, ulong maxStorageBufferBindingSize) { uint lineFloor = AddSizingSlack(checked((uint)Math.Max(lineCount, 1))); @@ -2503,7 +2510,8 @@ private static WebGPUSceneBumpSizes SeedSceneBumpSizes( ClampEstimate(estimatedTileCrossings, maxStorageBufferBindingSize, (uint)Unsafe.SizeOf()), pathRowFloor); uint binningFloor = ClampEstimate(estimatedBinFootprint, maxStorageBufferBindingSize, sizeof(uint)); - uint ptclFloor = ClampEstimate(estimatedTileCrossings * PtclWordsPerCrossing, maxStorageBufferBindingSize, sizeof(uint)); + long ptclWords = ((estimatedPtclWords * 105L) / 100L) + (targetTileCount * PtclChunkWords); + uint ptclFloor = ClampEstimate(ptclWords, maxStorageBufferBindingSize, sizeof(uint)); return new WebGPUSceneBumpSizes( Math.Max(currentSizes.Lines, lineFloor), @@ -4030,23 +4038,11 @@ private static unsafe bool TryDispatchFineArea( { // A single analytic fine pass handles every flush. Aliased coverage is applied per fill inside // the shader from the draw-flags aliased bit, so there is no separate aliased pipeline variant. - PixelAlphaRepresentation alphaRepresentation = flushContext.TargetDescriptor.AlphaRepresentation; - WebGPUTargetNumericEncoding numericEncoding = flushContext.TargetDescriptor.NumericEncoding; - byte[] shaderCode = FineAreaComputeShader.GetCode(flushContext.TextureFormat, alphaRepresentation, numericEncoding); - - bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutImpl* layout, out string? layoutError) - => FineAreaComputeShader.TryCreateBindGroupLayout( - api, - device, + if (!TryResolveFinePipeline( + flushContext.DeviceState, flushContext.TextureFormat, - out layout, - out layoutError); - - if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - $"{FineAreaPipelineKey}/{flushContext.TextureFormat}/{alphaRepresentation}/{numericEncoding}", - shaderCode, - FineAreaComputeShader.EntryPoint, - LayoutFactory, + flushContext.TargetDescriptor.AlphaRepresentation, + flushContext.TargetDescriptor.NumericEncoding, out WGPUBindGroupLayoutImpl* bindGroupLayout, out WGPUComputePipelineImpl* pipeline, out error)) @@ -4425,16 +4421,15 @@ bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutIm } /// - /// Queues a background warmup that eagerly compiles every staged-scene compute pipeline for a - /// newly created device, plus the fine pipeline for the common target formats. First-ever use - /// of the pipeline set on a machine pays multi-second driver shader compilation; warming at - /// device creation moves that cost off the first flush and overlaps it with application - /// startup. The pipeline caches are thread-safe, so a flush that arrives mid-warmup simply - /// blocks on the specific pipelines it needs. + /// Queues a background warmup that compiles every scheduling compute pipeline for a newly + /// created device, in parallel. The fine pipeline depends on the target format and is compiled + /// by when a target is created. The pipeline caches are + /// thread-safe, so a flush that arrives mid-warmup waits only for the pipelines it needs. /// /// The shared device state whose pipeline caches are warmed. public static void BeginPipelineWarmup(WebGPURuntime.DeviceSharedState deviceState) { + deviceState.RegisterPipelineWarmup(); bool queued = ThreadPool.UnsafeQueueUserWorkItem( static state => { @@ -4459,74 +4454,133 @@ public static void BeginPipelineWarmup(WebGPURuntime.DeviceSharedState deviceSta } /// - /// Compiles the full staged-scene pipeline set into the shared device caches. Failures are - /// deliberately swallowed: warmup is best-effort and the flush path re-attempts creation with - /// proper error reporting. + /// Queues a background compile of the fine pipeline for one target format. A flush to a target of + /// that format then finds the pipeline ready, or waits only for the remaining part of the compile. + /// + /// The shared device state whose pipeline cache receives the pipeline. + /// The target format, alpha representation, and numeric encoding. + public static unsafe void BeginFinePipelineWarmup(WebGPURuntime.DeviceSharedState deviceState, WebGPUTargetDescriptor targetDescriptor) + { + WebGPUDrawingBackend.GetCompositeTextureFormatInfo(targetDescriptor.Format, out WGPUTextureFormat textureFormat, out _); + if (deviceState.HasCompositeComputePipeline(GetFinePipelineKey(textureFormat, targetDescriptor.AlphaRepresentation, targetDescriptor.NumericEncoding))) + { + return; + } + + deviceState.RegisterPipelineWarmup(); + (WebGPURuntime.DeviceSharedState DeviceState, WGPUTextureFormat TextureFormat, WebGPUTargetDescriptor Descriptor) warmupState = (deviceState, textureFormat, targetDescriptor); + bool queued = ThreadPool.UnsafeQueueUserWorkItem( + static (state) => + { + try + { + _ = TryResolveFinePipeline(state.DeviceState, state.TextureFormat, state.Descriptor.AlphaRepresentation, state.Descriptor.NumericEncoding, out _, out _, out _); + } + catch + { + // Best-effort warmup only; the render path surfaces real pipeline failures. + } + finally + { + state.DeviceState.CompletePipelineWarmup(); + } + }, + warmupState, + false); + + if (!queued) + { + deviceState.CompletePipelineWarmup(); + } + } + + /// + /// Builds the pipeline cache key of the fine pipeline for one target format. + /// + /// The output texture format. + /// The target alpha representation. + /// The target numeric encoding. + /// The cache key. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static string GetFinePipelineKey(WGPUTextureFormat textureFormat, PixelAlphaRepresentation alphaRepresentation, WebGPUTargetNumericEncoding numericEncoding) + => $"{FineAreaPipelineKey}/{textureFormat}/{alphaRepresentation}/{numericEncoding}"; + + /// + /// Gets or creates the fine pipeline for one target format. + /// + /// The shared device state that caches the pipeline. + /// The output texture format. + /// The target alpha representation. + /// The target numeric encoding. + /// Receives the fine bind group layout. + /// Receives the fine compute pipeline. + /// Receives the failure reason when the pipeline cannot be created. + /// when the pipeline is available; otherwise, . + private static unsafe bool TryResolveFinePipeline( + WebGPURuntime.DeviceSharedState deviceState, + WGPUTextureFormat textureFormat, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUComputePipelineImpl* pipeline, + out string? error) + { + byte[] shaderCode = FineAreaComputeShader.GetCode(textureFormat, alphaRepresentation, numericEncoding); + + bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutImpl* layout, out string? layoutError) + => FineAreaComputeShader.TryCreateBindGroupLayout(api, device, textureFormat, out layout, out layoutError); + + return deviceState.TryGetOrCreateCompositeComputePipeline( + GetFinePipelineKey(textureFormat, alphaRepresentation, numericEncoding), + shaderCode, + FineAreaComputeShader.EntryPoint, + LayoutFactory, + out bindGroupLayout, + out pipeline, + out error); + } + + /// + /// The scheduling shaders in warmup order: the most expensive compiles first, so they start earliest. + /// + private static readonly WebGPUSceneShaderId[] SchedulingWarmupOrder = + [ + WebGPUSceneShaderId.PathLowering, + WebGPUSceneShaderId.Coarse, + WebGPUSceneShaderId.DrawLeaf, + WebGPUSceneShaderId.PathCount, + WebGPUSceneShaderId.PathTiling, + WebGPUSceneShaderId.Binning, + WebGPUSceneShaderId.ClipLeaf, + WebGPUSceneShaderId.TileAlloc, + WebGPUSceneShaderId.PathRowAlloc, + WebGPUSceneShaderId.PathRowSpan, + WebGPUSceneShaderId.Backdrop, + WebGPUSceneShaderId.ClipReduce, + WebGPUSceneShaderId.DrawReduce, + WebGPUSceneShaderId.PathtagReduce, + WebGPUSceneShaderId.PathtagReduce2, + WebGPUSceneShaderId.PathtagScan1, + WebGPUSceneShaderId.PathtagScan, + WebGPUSceneShaderId.PathtagScanSmall, + WebGPUSceneShaderId.BboxClear, + WebGPUSceneShaderId.PathCountSetup, + WebGPUSceneShaderId.PathTilingSetup, + WebGPUSceneShaderId.ChunkReset, + WebGPUSceneShaderId.Prepare, + ]; + + /// + /// Compiles every scheduling pipeline into the shared device caches, in parallel. Failures are + /// swallowed: warmup is best-effort and the flush path re-attempts creation with proper error + /// reporting. /// /// The shared device state whose pipeline caches are warmed. private static unsafe void WarmPipelines(WebGPURuntime.DeviceSharedState deviceState) { try { - // Most expensive shaders first so their driver compilation starts as early as possible. - ReadOnlySpan order = - [ - WebGPUSceneShaderId.PathLowering, - WebGPUSceneShaderId.Coarse, - WebGPUSceneShaderId.DrawLeaf, - WebGPUSceneShaderId.PathCount, - WebGPUSceneShaderId.PathTiling, - WebGPUSceneShaderId.Binning, - WebGPUSceneShaderId.ClipLeaf, - WebGPUSceneShaderId.TileAlloc, - WebGPUSceneShaderId.PathRowAlloc, - WebGPUSceneShaderId.PathRowSpan, - WebGPUSceneShaderId.Backdrop, - WebGPUSceneShaderId.ClipReduce, - WebGPUSceneShaderId.DrawReduce, - WebGPUSceneShaderId.PathtagReduce, - WebGPUSceneShaderId.PathtagReduce2, - WebGPUSceneShaderId.PathtagScan1, - WebGPUSceneShaderId.PathtagScan, - WebGPUSceneShaderId.PathtagScanSmall, - WebGPUSceneShaderId.BboxClear, - WebGPUSceneShaderId.PathCountSetup, - WebGPUSceneShaderId.PathTilingSetup, - WebGPUSceneShaderId.ChunkReset, - WebGPUSceneShaderId.Prepare, - ]; - - // Warm the format/representation pairs used by the default offscreen target and by - // opaque and transparent presentation surfaces. Other supported pairs compile on demand. - ReadOnlySpan<(WGPUTextureFormat Format, PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding)> fineTargets = - [ - (WGPUTextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), - (WGPUTextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit), - (WGPUTextureFormat.BGRA8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), - (WGPUTextureFormat.BGRA8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit) - ]; - - foreach ((WGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, WebGPUTargetNumericEncoding numericEncoding) in fineTargets) - { - byte[] shaderCode = FineAreaComputeShader.GetCode(format, alphaRepresentation, numericEncoding); - - bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutImpl* layout, out string? layoutError) - => FineAreaComputeShader.TryCreateBindGroupLayout(api, device, format, out layout, out layoutError); - - _ = deviceState.TryGetOrCreateCompositeComputePipeline( - $"{FineAreaPipelineKey}/{format}/{alphaRepresentation}/{numericEncoding}", - shaderCode, - FineAreaComputeShader.EntryPoint, - LayoutFactory, - out _, - out _, - out _); - } - - foreach (WebGPUSceneShaderId shaderId in order) - { - _ = TryResolveComputeShader(deviceState, shaderId, out _, out _, out _); - } + _ = Parallel.ForEach(SchedulingWarmupOrder, shaderId => TryResolveComputeShader(deviceState, shaderId, out _, out _, out _)); } catch { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs index 68485d2b..502a4ae4 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs @@ -73,6 +73,11 @@ internal static class WebGPUSceneEncoder /// public const int TileHeight = 16; + /// + /// The PTCL word count of one CMD_FILL command. Must match write_path in coarse.wgsl. + /// + private const int PtclFillWords = 6; + /// /// Half the length of the synthetic horizontal segment substituted for point-like strokes so caps /// still render a dot. The full segment length equals , keeping @@ -853,6 +858,7 @@ private readonly struct SceneEncodingCheckpoint /// The number of non-horizontal line segments emitted so far. /// The accumulated sparse path-row estimate. /// The accumulated tile-crossing upper bound. + /// The accumulated PTCL word upper bound. /// The accumulated per-(draw, bin) record upper bound. public SceneEncodingCheckpoint( int pathTagByteCount, @@ -868,6 +874,7 @@ public SceneEncodingCheckpoint( int lineCount, long estimatedPathRowCount, long estimatedTileCrossings, + long estimatedPtclWords, long estimatedBinFootprint) { this.PathTagByteCount = pathTagByteCount; @@ -883,6 +890,7 @@ public SceneEncodingCheckpoint( this.LineCount = lineCount; this.EstimatedPathRowCount = estimatedPathRowCount; this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedPtclWords = estimatedPtclWords; this.EstimatedBinFootprint = estimatedBinFootprint; } @@ -951,6 +959,11 @@ public SceneEncodingCheckpoint( /// public long EstimatedTileCrossings { get; } + /// + /// Gets the accumulated PTCL word upper bound. + /// + public long EstimatedPtclWords { get; } + /// /// Gets the accumulated per-(draw, bin) record upper bound. /// @@ -1593,8 +1606,10 @@ private ref struct SupportedSubsetSceneEncoding // CPU-side scratch-demand estimates accumulated per draw so the first GPU attempt can be // seeded near true demand instead of discovering it through the overflow retry protocol. // estimatedTileCrossings bounds tile-boundary crossings (segment/seg-count/path-tile - // records); estimatedBinFootprint bounds per-(draw, bin) binning records. + // records); estimatedPtclWords bounds the dynamic PTCL words those crossings can write; + // estimatedBinFootprint bounds per-(draw, bin) binning records. private long estimatedTileCrossings; + private long estimatedPtclWords; private long estimatedBinFootprint; // Cache of the last emitted 10-word style record. Consecutive draws with identical @@ -1669,6 +1684,7 @@ public SupportedSubsetSceneEncoding( this.openLayerBounds = null; this.estimatedPathRowCount = 0; this.estimatedTileCrossings = 0; + this.estimatedPtclWords = 0; this.estimatedBinFootprint = 0; this.VisibleFillCount = 0; @@ -1797,6 +1813,12 @@ public SupportedSubsetSceneEncoding( /// public readonly long EstimatedTileCrossings => this.estimatedTileCrossings; + /// + /// Gets the CPU-side upper-bound estimate of the dynamic PTCL words the scene's tile + /// crossings can write: one fill command plus the draw's paint command per crossing. + /// + public readonly long EstimatedPtclWords => this.estimatedPtclWords; + /// /// Gets the CPU-side upper-bound estimate of per-(draw, bin) binning records. /// @@ -1826,6 +1848,7 @@ public readonly SceneEncodingCheckpoint CaptureCheckpoint() this.LineCount, this.estimatedPathRowCount, this.estimatedTileCrossings, + this.estimatedPtclWords, this.estimatedBinFootprint); /// @@ -1988,6 +2011,7 @@ public static WebGPUSceneRange CreateRange( end.LineCount - start.LineCount, checked((int)(end.EstimatedPathRowCount - start.EstimatedPathRowCount)), end.EstimatedTileCrossings - start.EstimatedTileCrossings, + end.EstimatedPtclWords - start.EstimatedPtclWords, end.EstimatedBinFootprint - start.EstimatedBinFootprint); /// @@ -2378,6 +2402,7 @@ public bool TryAppend(in StrokeLineSegmentCommand command, out string? error) resolved.BrushBounds, resolved.Pen, widthScale, + GetResidualStretch(segmentResidual), start, end); error = null; @@ -2422,6 +2447,7 @@ public bool TryAppend( resolved.BrushBounds, resolved.Pen, widthScale, + GetResidualStretch(polylineResidual), geometry); error = null; return true; @@ -2612,7 +2638,7 @@ private void AppendPlainFill( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); + this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings, GetPtclPaintWords(drawTag)); this.FillCount++; this.PathCount += encodedPathCount; @@ -2683,6 +2709,7 @@ private void AppendPlainStroke( Vector2 scale = MatrixUtilities.GetScale(command.Transform); geometry ??= command.Path.ToLinearGeometry(scale); float widthScale = GetTransformWidthScale(command.Transform); + float residualStretch = GetResidualStretch(MatrixUtilities.GetResidual(scale, command.Transform)); uint drawTag = GetDrawTag(brush); GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); Rectangle interestBounds = ToTargetLocal(command.RasterizerOptions.Interest, this.rootTargetBounds); @@ -2735,10 +2762,12 @@ private void AppendPlainStroke( pathDataOffset, pen, widthScale, + residualStretch, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -2758,7 +2787,7 @@ private void AppendPlainStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, -1); + this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings, GetPtclPaintWords(drawTag)); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -2793,6 +2822,7 @@ private void AppendPlainStroke( /// The absolute brush sampling bounds. /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. + /// The largest length scale of the residual transform the GPU applies to the points. /// The prepared open centerline geometry. private void AppendExplicitStroke( Brush brush, @@ -2802,6 +2832,7 @@ private void AppendExplicitStroke( Rectangle brushBounds, Pen pen, float widthScale, + float residualStretch, LinearGeometry geometry) { uint drawTag = GetDrawTag(brush); @@ -2856,10 +2887,12 @@ private void AppendExplicitStroke( pathDataOffset, pen, widthScale, + residualStretch, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -2879,7 +2912,7 @@ private void AppendExplicitStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, -1); + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings, GetPtclPaintWords(drawTag)); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -2914,6 +2947,7 @@ private void AppendExplicitStroke( /// The absolute brush sampling bounds. /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. + /// The largest length scale of the residual transform the GPU applies to the points. /// The segment start point with the transform scale applied. /// The segment end point with the transform scale applied. private void AppendExplicitStroke( @@ -2924,6 +2958,7 @@ private void AppendExplicitStroke( Rectangle brushBounds, Pen pen, float widthScale, + float residualStretch, PointF start, PointF end) { @@ -2979,10 +3014,12 @@ private void AppendExplicitStroke( pathDataOffset, pen, widthScale, + residualStretch, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -3002,7 +3039,7 @@ private void AppendExplicitStroke( this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, -1); + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings, GetPtclPaintWords(drawTag)); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -3330,7 +3367,7 @@ private bool AppendClipDescriptor(in DrawingClipDescriptor descriptor, Point des this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimateLocal(clipBounds, clipLineCount); + this.AccumulateDrawRowEstimateLocal(clipBounds, clipLineCount, -1, GetPtclPaintWords(GpuSceneDrawTag.EndClip)); this.PathCount += encodedPathCount; this.LineCount += clipLineCount; this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; @@ -3417,7 +3454,7 @@ private void AppendBeginLayer(in CompositionCommand command) this.lastStyle7 = style7; this.lastStyle8 = style8; this.lastStyle9 = style9; - this.AccumulateDrawRowEstimateLocal(layerBounds, clipLineCount); + this.AccumulateDrawRowEstimateLocal(layerBounds, clipLineCount, -1, GetPtclPaintWords(GpuSceneDrawTag.EndClip)); this.PathCount += encodedPathCount; this.LineCount += clipLineCount; this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; @@ -3434,11 +3471,12 @@ private void AppendBeginLayer(in CompositionCommand command) /// The draw object's absolute raster interest bounds. /// The draw object's linearized or GPU-expanded line count. /// - /// The exact summed per-line tile-crossing bound when the caller linearized the geometry on the CPU - /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). + /// The summed per-line tile-crossing bound of the geometry (fills and strokes), or a negative value + /// to fall back to the bounding-box diagonal bound (clips). /// - private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount, long tileCrossings = -1) - => this.AccumulateDrawRowEstimateLocal(ToTargetLocal(absoluteInterest, this.rootTargetBounds), lineCount, tileCrossings); + /// The PTCL word count of the draw's paint command. + private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount, long tileCrossings, int paintWords) + => this.AccumulateDrawRowEstimateLocal(ToTargetLocal(absoluteInterest, this.rootTargetBounds), lineCount, tileCrossings, paintWords); /// /// Adds one draw object's clipped root-target-local footprint to the sparse scratch estimates: @@ -3448,10 +3486,11 @@ private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount /// The draw object's root-target-local raster interest bounds. /// The draw object's linearized or GPU-expanded line count. /// - /// The exact summed per-line tile-crossing bound when the caller linearized the geometry on the CPU - /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). + /// The summed per-line tile-crossing bound of the geometry (fills and strokes), or a negative value + /// to fall back to the bounding-box diagonal bound (clips). /// - private void AccumulateDrawRowEstimateLocal(Rectangle localBounds, int lineCount, long tileCrossings = -1) + /// The PTCL word count of the draw's paint command. + private void AccumulateDrawRowEstimateLocal(Rectangle localBounds, int lineCount, long tileCrossings, int paintWords) { Rectangle clippedBounds = Rectangle.Intersect(localBounds, new Rectangle(0, 0, this.rootTargetBounds.Width, this.rootTargetBounds.Height)); @@ -3483,12 +3522,31 @@ private void AccumulateDrawRowEstimateLocal(Rectangle localBounds, int lineCount long crossings = tileCrossings >= 0 ? Math.Min(tileCrossings, boundingBoxCrossings) : boundingBoxCrossings; this.estimatedTileCrossings += crossings; + // Every crossing belongs to one (draw, tile) pair with segments, and such a pair writes + // one CMD_FILL plus the draw's paint command. + this.estimatedPtclWords += crossings * (PtclFillWords + paintWords); + // Binning emits one record per (draw, 16x16-tile bin) pair the draw's bounds touch. long binsWide = (clippedBounds.Width / (TileWidth * 16)) + 2; long binsHigh = (clippedBounds.Height / (TileHeight * 16)) + 2; this.estimatedBinFootprint += binsWide * binsHigh; } + /// + /// Returns the PTCL word count of the paint command coarse writes for one draw tag, matching + /// the write_* helpers in coarse.wgsl. + /// + /// The draw tag. + /// The paint command word count. + private static int GetPtclPaintWords(uint drawTag) + => drawTag switch + { + GpuSceneDrawTag.FillPathGradient => 5, + GpuSceneDrawTag.FillImage => 2, + GpuSceneDrawTag.BeginClip => 2, + _ => 3 + }; + /// /// Encodes the closing record for the next end-layer command in the retained timeline. /// @@ -3560,6 +3618,7 @@ private sealed class SceneEncodingPartition : IDisposable /// The number of emitted gradient-ramp rows. /// The CPU-side estimate of active tile rows. /// The CPU-side upper bound for tile-boundary crossings. + /// The CPU-side upper bound for the dynamic PTCL words. /// The CPU-side upper bound for per-(draw, bin) records. /// The unpadded path-tag byte count. /// The path-data word count. @@ -3592,6 +3651,7 @@ private SceneEncodingPartition( int gradientRowCount, int estimatedPathRowCount, long estimatedTileCrossings, + long estimatedPtclWords, long estimatedBinFootprint, int pathTagByteCount, int pathDataWordCount, @@ -3625,6 +3685,7 @@ private SceneEncodingPartition( this.GradientRowCount = gradientRowCount; this.EstimatedPathRowCount = estimatedPathRowCount; this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedPtclWords = estimatedPtclWords; this.EstimatedBinFootprint = estimatedBinFootprint; this.PathTagByteCount = pathTagByteCount; this.PathDataWordCount = pathDataWordCount; @@ -3753,6 +3814,11 @@ public ReadOnlySpan PathGradientData /// public long EstimatedTileCrossings { get; } + /// + /// Gets the CPU-side upper bound for the dynamic PTCL words. + /// + public long EstimatedPtclWords { get; } + /// /// Gets the CPU-side upper bound for per-(draw, bin) binning records. /// @@ -3854,6 +3920,7 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc gradientRowCount, encoding.EstimatedPathRowCount, encoding.EstimatedTileCrossings, + encoding.EstimatedPtclWords, encoding.EstimatedBinFootprint, pathTagByteCount, pathDataWordCount, @@ -4016,6 +4083,7 @@ public static WebGPUEncodedScene Resolve( DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), encoding.EstimatedTileCrossings, + encoding.EstimatedPtclWords, encoding.EstimatedBinFootprint, operations); @@ -4403,6 +4471,7 @@ public static WebGPUEncodedScene Resolve( int profileFillCount = 0; long estimatedPathRowCount = 0; long estimatedTileCrossings = 0; + long estimatedPtclWords = 0; long estimatedBinFootprint = 0; for (int i = 0; i < partitions.Length; i++) @@ -4428,6 +4497,7 @@ public static WebGPUEncodedScene Resolve( profileFillCount += partition.ProfileFills?.Count ?? 0; estimatedPathRowCount = Math.Min(estimatedPathRowCount + partition.EstimatedPathRowCount, int.MaxValue); estimatedTileCrossings += partition.EstimatedTileCrossings; + estimatedPtclWords += partition.EstimatedPtclWords; estimatedBinFootprint += partition.EstimatedBinFootprint; } @@ -4615,6 +4685,7 @@ public static WebGPUEncodedScene Resolve( DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), estimatedTileCrossings, + estimatedPtclWords, estimatedBinFootprint, []); @@ -5356,25 +5427,32 @@ private static float Sum(Vector2 value) /// The absolute destination offset applied to every point. /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. + /// The largest length scale of the residual transform the GPU applies to the points. /// The root target bounds used for target-local conversion. /// The path-tag stream. /// The path-data stream. /// Receives the estimated final line workload for the stroke. + /// Receives the bound on the tile crossings of the emitted stroke lines. /// The number of encoded path objects: 1, or 0 when no contour survived preprocessing. private static int EncodeStrokePath( LinearGeometry geometry, Point destinationOffset, Pen pen, float widthScale, + float residualStretch, in Rectangle rootTargetBounds, ref OwnedStream pathTags, ref OwnedStream pathData, - out int lineCount) + out int lineCount, + out long tileCrossings) { float pointTranslateX = destinationOffset.X - rootTargetBounds.X; float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y; lineCount = EstimateStrokeLineCount(geometry, pen, widthScale); float strokeWidth = pen.StrokeWidth * widthScale; + float halfWidth = strokeWidth * 0.5F; + float capChainLength = GetStrokeCapChainLength(pen, halfWidth); + double outlineLength = 0D; int encodedContourCount = 0; LinearContour[] contours = (LinearContour[])geometry.Contours; PointF[] geometryPoints = (PointF[])geometry.Points; @@ -5395,17 +5473,40 @@ private static int EncodeStrokePath( PointF lastKept = firstPoint; int pointIndex = 1; int keptCount = 1; + Vector2 firstDirection = default; + float firstLength = 0F; + Vector2 previousDirection = default; + float previousLength = 0F; + double contourOutlineLength = 0D; // Count the filtered points and retain only the few values needed to classify the // contour. The second pass writes them directly to the scene streams, so no temporary // point buffer is rented for each stroke geometry. - while (TryGetNextStrokePoint(contourPoints, ref pointIndex, ref previousKept, ref pointLike, out PointF point)) + while (true) { + PointF segmentStart = previousKept; + if (!TryGetNextStrokePoint(contourPoints, ref pointIndex, ref previousKept, ref pointLike, out PointF point)) + { + break; + } + + Vector2 segment = (Vector2)point - (Vector2)segmentStart; + float segmentLength = segment.Length(); + Vector2 direction = segment / segmentLength; + contourOutlineLength += 2D * segmentLength; if (keptCount == 1) { secondPoint = point; + firstDirection = direction; + firstLength = segmentLength; + } + else + { + contourOutlineLength += GetStrokeJoinChainLength(pen, halfWidth, previousDirection, previousLength, direction, segmentLength); } + previousDirection = direction; + previousLength = segmentLength; lastKept = point; keptCount++; } @@ -5430,6 +5531,7 @@ private static int EncodeStrokePath( if (segmentCount == 0) { EncodePointStrokeContour(pointLike, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + outlineLength += (4D * PointStrokeSegmentHalfLength) + (2D * capChainLength); encodedContourCount++; continue; } @@ -5439,6 +5541,7 @@ private static int EncodeStrokePath( // The CPU stroker emits these as one capped open segment even when declared closed. Span segmentPoints = [firstPoint, secondPoint]; EncodeOpenStrokeContour(segmentPoints, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + outlineLength += (2D * Vector2.Distance(firstPoint, secondPoint)) + (2D * capChainLength); encodedContourCount++; continue; } @@ -5453,6 +5556,21 @@ private static int EncodeStrokePath( (segmentCount > 1 && Vector2.DistanceSquared(lastKept, firstPoint) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon); + if (closingSegment && !duplicateClosingPoint) + { + Vector2 closingVector = (Vector2)firstPoint - (Vector2)lastKept; + float closingLength = closingVector.Length(); + Vector2 closingDirection = closingVector / closingLength; + contourOutlineLength += (2D * closingLength) + + GetStrokeJoinChainLength(pen, halfWidth, previousDirection, previousLength, closingDirection, closingLength) + + GetStrokeJoinChainLength(pen, halfWidth, closingDirection, closingLength, firstDirection, firstLength); + } + else + { + contourOutlineLength += GetStrokeJoinChainLength(pen, halfWidth, previousDirection, previousLength, firstDirection, firstLength); + } + + outlineLength += contourOutlineLength; int linetoCount = (emitCount - 1) + (closingSegment ? 1 : 0); Span contourData = pathData.GetAppendSpan(2 + (linetoCount * 2) + 2); Span contourTags = pathTags.GetAppendSpan(linetoCount + 1); @@ -5498,6 +5616,7 @@ private static int EncodeStrokePath( } int openLinetoCount = keptCount - 1; + outlineLength += contourOutlineLength + (2D * capChainLength); Span openData = pathData.GetAppendSpan(2 + (openLinetoCount * 2) + 4); Span openTags = pathTags.GetAppendSpan(openLinetoCount + 1); int openDataIndex = 0; @@ -5529,6 +5648,7 @@ private static int EncodeStrokePath( encodedContourCount++; } + tileCrossings = BoundStrokeTileCrossings(outlineLength, residualStretch, lineCount); if (encodedContourCount == 0) { return 0; @@ -5670,10 +5790,12 @@ private static void EncodePointStrokeContour( /// The absolute destination offset applied to every point. /// The pen that defines stroke width, joins, and caps. /// The transform-derived scale applied to the stroke width. + /// The largest length scale of the residual transform the GPU applies to the points. /// The root target bounds used for target-local conversion. /// The path-tag stream. /// The path-data stream. /// Receives the estimated final line workload for the stroke. + /// Receives the bound on the tile crossings of the emitted stroke lines. /// The number of encoded path objects; always 1. private static int EncodeOpenSegmentStrokePath( PointF start, @@ -5681,10 +5803,12 @@ private static int EncodeOpenSegmentStrokePath( Point destinationOffset, Pen pen, float widthScale, + float residualStretch, in Rectangle rootTargetBounds, ref OwnedStream pathTags, ref OwnedStream pathData, - out int lineCount) + out int lineCount, + out long tileCrossings) { float pointTranslateX = destinationOffset.X - rootTargetBounds.X; float pointTranslateY = destinationOffset.Y - rootTargetBounds.Y; @@ -5693,6 +5817,9 @@ private static int EncodeOpenSegmentStrokePath( EncodeOpenStrokeContour(segmentPoints, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); pathTags.Add(PackPathTag(PathTag.Path)); lineCount = EstimateStrokeLineCountForOpenSegment(pen, widthScale); + float halfWidth = pen.StrokeWidth * widthScale * 0.5F; + double outlineLength = (2D * Vector2.Distance(start, end)) + (2D * GetStrokeCapChainLength(pen, halfWidth)); + tileCrossings = BoundStrokeTileCrossings(outlineLength, residualStretch, lineCount); return 1; } @@ -6224,6 +6351,77 @@ private static int EstimateStrokeLineCount(LinearGeometry geometry, Pen pen, flo private static int EstimateStrokeLineCountForOpenSegment(Pen pen, float widthScale) => Math.Max(2 + (GetStrokeCapLineCost(pen, widthScale) * 2), 1); + /// + /// Bounds the tile crossings of the lines the GPU stroker emits for one stroke. One line crosses at most + /// (|dx| + |dy|) / 16 + 3 tiles, so the emitted outline crosses at most sqrt(2) times its device-space + /// length in tiles plus three per line. + /// + /// The bound on the emitted outline length before the residual transform. + /// The largest length scale of the residual transform the GPU applies to the points. + /// The estimated emitted line count. + /// The tile-crossing bound. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long BoundStrokeTileCrossings(double outlineLength, float residualStretch, int lineCount) + => (long)Math.Ceiling(outlineLength * residualStretch * Math.Sqrt(2D) / TileWidth) + (3L * lineCount); + + /// + /// Returns the largest length scale of a residual transform: the Frobenius norm of its 2x2 part, which is + /// at least its largest singular value. + /// + /// The residual transform the GPU applies to the points. + /// The length scale; 1 for the identity. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetResidualStretch(Matrix4x4 residual) + => residual.IsIdentity + ? 1F + : MathF.Sqrt((residual.M11 * residual.M11) + (residual.M12 * residual.M12) + (residual.M21 * residual.M21) + (residual.M22 * residual.M22)); + + /// + /// Returns the length of the line chain the GPU stroker emits for one cap. + /// + /// The pen that defines the cap style. + /// Half the transform-scaled stroke width. + /// The chain length in pixels. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float GetStrokeCapChainLength(Pen pen, float halfWidth) + => pen.StrokeOptions.LineCap switch + { + LineCap.Square => 4F * halfWidth, + LineCap.Round => MathF.PI * halfWidth, + _ => 2F * halfWidth + }; + + /// + /// Bounds the length of the line chains the GPU stroker emits for one join. The outer chain reaches at most + /// the miter apex, the miter limit, the round arc, or the bevel chord. The inner chain reaches at most the + /// miter apex or the shorter neighboring segment. + /// + /// The pen that defines the join style and miter limit. + /// Half the transform-scaled stroke width. + /// The unit direction of the segment entering the join. + /// The length of the segment entering the join. + /// The unit direction of the segment leaving the join. + /// The length of the segment leaving the join. + /// The chain length bound in pixels. + private static float GetStrokeJoinChainLength(Pen pen, float halfWidth, Vector2 incoming, float incomingLength, Vector2 outgoing, float outgoingLength) + { + float cosTheta = -Vector2.Dot(incoming, outgoing); + float sinHalfTheta = MathF.Sqrt(MathF.Max(0F, (1F - cosTheta) * 0.5F)); + float miterApex = sinHalfTheta > 1E-6F ? halfWidth / sinHalfTheta : float.PositiveInfinity; + float innerExtent = MathF.Min(miterApex, MathF.Min(incomingLength, outgoingLength)); + float miterLimit = (float)Math.Max(pen.StrokeOptions.MiterLimit, 1D); + float outerExtent = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter => halfWidth * (1F + miterLimit), + LineJoin.MiterRevert => MathF.Max(MathF.Min(miterApex, halfWidth * miterLimit), 2F * halfWidth), + LineJoin.MiterRound => MathF.Max(MathF.Min(miterApex, halfWidth * miterLimit), MathF.PI * halfWidth), + LineJoin.Round => MathF.PI * halfWidth, + _ => 2F * halfWidth + }; + + return (2F * (outerExtent + halfWidth)) + (2F * (innerExtent + halfWidth)); + } + /// /// Returns the conservative final line cost of one stroke join. /// @@ -7313,6 +7511,7 @@ public void SetProfileRegions(uint slotsBase, uint recordsBase) 0, 0L, 0L, + 0L, []); private readonly IMemoryOwner? sceneDataOwner; @@ -7363,6 +7562,7 @@ public void SetProfileRegions(uint slotsBase, uint recordsBase) /// The horizontal tile count. /// The vertical tile count. /// The CPU-side upper bound for tile-boundary crossings. + /// The CPU-side upper bound for the dynamic PTCL words. /// The CPU-side upper bound for per-(draw, bin) binning records. /// The scene operations associated with the encoded payload. public WebGPUEncodedScene( @@ -7395,6 +7595,7 @@ public WebGPUEncodedScene( int tileCountX, int tileCountY, long estimatedTileCrossings, + long estimatedPtclWords, long estimatedBinFootprint, WebGPUSceneOperation[] operations) { @@ -7428,6 +7629,7 @@ public WebGPUEncodedScene( this.TileCountX = tileCountX; this.TileCountY = tileCountY; this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedPtclWords = estimatedPtclWords; this.EstimatedBinFootprint = estimatedBinFootprint; int targetCount = operations.Length == 0 ? 0 : 1; @@ -7514,6 +7716,12 @@ public WebGPUEncodedScene( /// public long EstimatedTileCrossings { get; } + /// + /// Gets the CPU-side upper-bound estimate of the dynamic PTCL words the scene's tile crossings + /// can write: one fill command plus the draw's paint command per crossing. + /// + public long EstimatedPtclWords { get; } + /// /// Gets the CPU-side upper-bound estimate of per-(draw, bin) binning records. /// diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs index bf03ef15..04503e2b 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs @@ -205,6 +205,7 @@ internal readonly struct WebGPUSceneRange /// The line count in the range. /// The estimated sparse row count for the range. /// The CPU-side upper bound for the range's tile-boundary crossings. + /// The CPU-side upper bound for the range's dynamic PTCL words. /// The CPU-side upper bound for the range's per-(draw, bin) binning records. public WebGPUSceneRange( Rectangle targetBounds, @@ -227,6 +228,7 @@ public WebGPUSceneRange( int lineCount, int totalPathRowCount, long estimatedTileCrossings, + long estimatedPtclWords, long estimatedBinFootprint) { this.TargetBounds = targetBounds; @@ -249,6 +251,7 @@ public WebGPUSceneRange( this.LineCount = lineCount; this.TotalPathRowCount = totalPathRowCount; this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedPtclWords = estimatedPtclWords; this.EstimatedBinFootprint = estimatedBinFootprint; } @@ -352,6 +355,11 @@ public WebGPUSceneRange( /// public long EstimatedTileCrossings { get; } + /// + /// Gets the CPU-side upper bound for the range's dynamic PTCL words. + /// + public long EstimatedPtclWords { get; } + /// /// Gets the CPU-side upper bound for the range's per-(draw, bin) binning records. ///