diff --git a/.gitattributes b/.gitattributes index 7de2c1597..031420e53 100644 --- a/.gitattributes +++ b/.gitattributes @@ -84,12 +84,15 @@ # treat as binary ############################################################################### *.basis binary +*.a binary *.dll binary +*.dylib binary *.exe binary *.pdf binary *.ppt binary *.pptx binary *.pvr binary +*.so binary *.snk binary *.xls binary *.xlsx binary diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..5f9f69435 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +# GitHub Copilot Instructions + +Read and follow [AGENTS.md](../AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Prefer existing local patterns and repository configuration whenever generated code or suggestions are accepted. diff --git a/.gitignore b/.gitignore index 4560e54e3..4e61c3c8e 100644 --- a/.gitignore +++ b/.gitignore @@ -231,3 +231,6 @@ SixLabors.Shapes.Coverage.xml .dotnet .codex-* .claude +/plans/ + +samples/Avalonia** diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..814e3ce26 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,41 @@ +# Six Labors AI Coding Guidelines + +These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree. + +## Working Practices + +- Inspect the relevant implementation, tests, benchmarks, project files, and nearby code before proposing or making changes. Do not infer current behavior when the source is available. +- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn. +- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative. +- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details. +- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested. +- Build .NET projects in Release configuration unless explicitly instructed otherwise. + +## Performance + +- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths. +- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data. +- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits. +- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness. +- Do not trade correctness or maintainability for assumed speed. Support non-obvious optimizations with measurements or clear evidence, and add or update benchmarks when performance is the purpose of the change. +- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes. + +## C# Conventions + +- Follow the existing code around the change; local patterns take precedence over generic preferences. +- Do not use `record` or `record struct` types. +- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states. +- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction. +- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace. +- Document every method, constructor, and property, regardless of whether it is public, internal, protected, or private. Keep public API documentation limited to observable behavior; use private and internal documentation to capture the contract and intent needed to maintain the code. +- Add inline comments throughout complex code. Explain algorithms, formulas, invariants, ownership, compatibility behavior, and performance tradeoffs at the operations and decisions they govern. Explain why the code is shaped that way rather than narrating the syntax. +- Document SIMD code especially thoroughly. Explain the vector layout, lane meaning, widening or narrowing, masks, shuffles, constants, alignment or remainder handling, supported instruction paths, scalar equivalence, and the reason each non-obvious operation is correct. +- Write algorithm and SIMD comments for a maintainer who is unfamiliar with the implementation. The reader should not need to reconstruct intent from external documentation, issue history, or benchmark results. + +## Verification + +- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project. +- Never hack, weaken, skip, conditionally bypass, or otherwise manipulate a test to make it pass. Fix the production defect or the genuine test defect while preserving the test's intended coverage and sensitivity. +- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts to resolve a test failure. Treat a mismatch as evidence to investigate and correct the implementation. +- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change. +- Report what changed, the verification performed, and any remaining risks or unverified assumptions. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..5f08a449a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# Claude Code Instructions + +Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `CLAUDE.md` found below the files being changed. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 000000000..b621d2652 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,3 @@ +# Gemini CLI Instructions + +Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `GEMINI.md` found below the files being changed. diff --git a/samples/DrawShapesWithImageSharp/Program.cs b/samples/DrawShapesWithImageSharp/Program.cs index 431680d5b..04280428c 100644 --- a/samples/DrawShapesWithImageSharp/Program.cs +++ b/samples/DrawShapesWithImageSharp/Program.cs @@ -157,24 +157,15 @@ private static void DrawPosterComposition() canvas.Fill(Brushes.Solid(Color.SeaGreen), shorelineShape); - // Clipping demo: canvas.Save accepts a clip path plus DrawingOptions whose - // BooleanOperation controls how the new clip combines with the existing one. Using - // Intersection means subsequent draws are masked to the oval highlight shape, so a - // rectangular gradient fill can be reused without re-shaping it as an ellipse. EllipsePolygon lakeHighlight = new(725, 512, 285, 86); - DrawingOptions lakeHighlightClipOptions = new() - { - ShapeOptions = new ShapeOptions - { - BooleanOperation = BooleanOperation.Intersection - } - }; RectangleF lakeHighlightBounds = lakeHighlight.Bounds; - // Save pushes a clipping state; Restore pops it. Anything drawn between the two is + // Save pushes a state; Clip narrows it. Anything drawn between the two is // confined to lakeHighlight even though the brush spans its full bounding rectangle. - canvas.Save(lakeHighlightClipOptions, lakeHighlight); + canvas.Save(); + canvas.Clip(lakeHighlight); + canvas.Fill(Brushes.ForwardDiagonal(Color.White.WithAlpha(.36F), Color.Transparent), new RectanglePolygon(lakeHighlightBounds)); canvas.Restore(); @@ -910,7 +901,7 @@ private static void DrawImageProcessingMask() canvas.DrawText( new RichTextOptions(subtitleFont) { Origin = new PointF(40, 66) }, - "Apply() runs a processor inside an IPath, ImageBrush fills one with a photo, and Save() clips drawing to one.", + "Apply() runs a processor inside an IPath, ImageBrush fills one with a photo, and Clip() limits drawing to one.", Brushes.Solid(secondaryColor), pen: null); @@ -1068,20 +1059,15 @@ static void DrawPhotoInTextPanel( imageArea.Y + ((imageArea.Height - textBounds.Height) / 2F) - textBounds.Y), }; - // GeneratePaths returns one IPath per glyph. canvas.Save accepts params IPath[] so - // the whole collection becomes a compound clip, but ShapeOptions.BooleanOperation - // must be set to Intersection: the default Difference would cut the glyph shapes - // OUT of the photograph, the opposite of "image inside text". + // GeneratePaths returns one IPath per glyph. Clip accepts params IPath[] so + // the whole collection becomes one compound clip for the photograph. IPathCollection letters = TextBuilder.GeneratePaths("MASK", glyphOptions); IPath[] glyphClips = [.. letters]; - DrawingOptions clipToGlyphs = new() - { - ShapeOptions = new ShapeOptions { BooleanOperation = BooleanOperation.Intersection }, - }; - canvas.Fill(Brushes.Solid(Color.ParseHex("#E2DCC2")), new RectanglePolygon(imageArea)); - canvas.Save(clipToGlyphs, glyphClips); + canvas.Save(); + canvas.Clip(glyphClips); + canvas.DrawImage(source, source.Bounds, imageArea, null); canvas.Restore(); diff --git a/samples/DrawShapesWithImageSharp/README.md b/samples/DrawShapesWithImageSharp/README.md index a9b8a286b..a69e86010 100644 --- a/samples/DrawShapesWithImageSharp/README.md +++ b/samples/DrawShapesWithImageSharp/README.md @@ -9,7 +9,7 @@ Poster-style landscape that exercises the building blocks: - `LinearGradientBrush` and `RadialGradientBrush` for the sky, lake, and sun. - `PathBuilder` + `CloseFigure` for the mountain ridges, lake, and shoreline. -- `canvas.Save(options, IPath)` with `BooleanOperation.Intersection` for the lake highlight clip. +- `canvas.Save()` plus `canvas.Clip(IPath)` for the lake highlight clip. - `canvas.Save` + `canvas.SaveLayer` to apply a Z rotation and then composite the title panel and text together with `GraphicsOptions.BlendPercentage`. - `TextMeasurer.MeasureRenderableBounds` to size the title panel to the laid-out text. @@ -38,7 +38,7 @@ Image-compositing scene demonstrating four ways a photograph (`tests/Images/Inpu - **Before / after wipe** — `canvas.Apply(rightHalfRect, ctx => ctx.OilPaint(15, 5))` scopes an `OilPaint` processor to the right half of the photograph. - **Privacy redaction** — `canvas.Apply(ellipse, ctx => ctx.Pixelate(10))` pixelates an elliptical face-shaped region and leaves the rest untouched. - **Image as a brush** — `new ImageBrush(source, source.Bounds, brushOffset)` wraps the photograph as a `Brush` so a `StarPolygon` path can be filled with it as a texture; the brush offset aligns the mountain in the photograph with the star's centre. -- **Photo in text** — `TextBuilder.GeneratePaths("MASK", ...)` produces one `IPath` per glyph; `canvas.Save(intersectionOptions, glyphPaths)` uses them as a compound clip so `DrawImage` only renders inside the letterforms. +- **Photo in text** — `TextBuilder.GeneratePaths("MASK", ...)` produces one `IPath` per glyph; `canvas.Clip(glyphPaths)` uses them as a compound clip so `DrawImage` only renders inside the letterforms. ## Running diff --git a/samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs b/samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs index b82a32a5d..9fa48a9fd 100644 --- a/samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs +++ b/samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs @@ -17,7 +17,9 @@ public sealed partial class WebGPURenderControl : Control { private const int WM_MOVING = 0x0216; private const int WM_EXITSIZEMOVE = 0x0232; + private static readonly nint ProcessModuleHandle = GetModuleHandle(null); + private readonly WebGPUSurfaceSession surfaceSession; private WebGPUExternalSurface? surface; private Size framebufferSize; private bool idleHooked; @@ -29,8 +31,11 @@ public sealed partial class WebGPURenderControl : Control /// /// Initializes a new instance of the class. /// - public WebGPURenderControl() + /// The session shared by the application's related presentation surfaces. + public WebGPURenderControl(WebGPUSurfaceSession surfaceSession) { + this.surfaceSession = surfaceSession; + // WebGPU presents directly to the native surface. Normal WinForms buffering and background // painting would add flicker or unnecessary work, so the control opts into direct user painting. this.SetStyle( @@ -81,12 +86,12 @@ protected override void OnHandleCreated(EventArgs e) Math.Max(this.framebufferSize.Width, 1), Math.Max(this.framebufferSize.Height, 1)); - // The module handle is required by the Win32 surface descriptor. It identifies the process module - // that owns the window class backing this control. - this.surface = new WebGPUExternalSurface( + // WinForms registers its window classes against the process executable module. Use that same + // HINSTANCE in the WebGPU descriptor instead of the managed assembly's module handle. + this.surface = this.surfaceSession.CreateSurface( WebGPUSurfaceHost.Win32( this.Handle, - Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)), + ProcessModuleHandle), initialFramebufferSize, new WebGPUExternalSurfaceOptions { @@ -322,5 +327,8 @@ private struct NativeMessage [LibraryImport("user32.dll", EntryPoint = "PeekMessageW")] private static partial int PeekMessage(out NativeMessage msg, nint hWnd, uint messageFilterMin, uint messageFilterMax, uint flags); + [LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)] + private static partial nint GetModuleHandle(string? moduleName); + private static bool IsApplicationIdle() => PeekMessage(out _, 0, 0, 0, 0) == 0; } diff --git a/samples/WebGPUExternalSurfaceDemo/MainForm.cs b/samples/WebGPUExternalSurfaceDemo/MainForm.cs index 417530808..0331c20ec 100644 --- a/samples/WebGPUExternalSurfaceDemo/MainForm.cs +++ b/samples/WebGPUExternalSurfaceDemo/MainForm.cs @@ -1,39 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Drawing.Processing; -using WebGPUExternalSurfaceDemo.Controls; +using SixLabors.ImageSharp.Drawing.Processing.Backends; using WebGPUExternalSurfaceDemo.Scenes; -using ImageSharpColor = SixLabors.ImageSharp.Color; namespace WebGPUExternalSurfaceDemo; /// -/// Main window for the sample. A tab control switches between demo scenes, each displayed in its own -/// instance. This demonstrates that multiple independent external surfaces -/// can coexist in the same WinForms process without managing surfaces or swapchains in user code. +/// Hosts the external-surface sample scenes on tabs that share one WebGPU device session. /// internal sealed class MainForm : Form { - private readonly WebGPURenderControl clockControl; - private readonly WebGPURenderControl tigerControl; - private readonly WebGPURenderControl applyControl; - private readonly WebGPURenderControl manualTextFlowControl; - private readonly WebGPURenderControl richTextControl; - private readonly ClockScene clockScene; - private readonly TigerViewerScene tigerScene; - private readonly ApplyReadbackScene applyScene; - private readonly ManualTextFlowScene manualTextFlowScene; - private readonly RichTextEditorScene richTextScene; - private readonly Label tigerStatusLabel; - private readonly ComboBox manualTextShapeComboBox; - private readonly CheckBox boldButton; - private readonly CheckBox italicButton; - private readonly CheckBox underlineButton; - private readonly CheckBox strikeoutButton; - private readonly ComboBox fontFamilyComboBox; - private readonly Label fontSizeLabel; - private readonly Label selectionStatusLabel; + private readonly WebGPUSurfaceSession surfaceSession = new(); + private readonly RenderScene[] scenes; public MainForm() { @@ -42,369 +21,52 @@ public MainForm() this.StartPosition = FormStartPosition.CenterScreen; this.BackColor = Color.FromArgb(11, 18, 32); - this.clockScene = new ClockScene(); - this.tigerScene = new TigerViewerScene(); - this.applyScene = new ApplyReadbackScene(); - this.manualTextFlowScene = new ManualTextFlowScene(); - this.richTextScene = new RichTextEditorScene(); + this.scenes = + [ + new ClockScene(), + new ShaderEffectsScene(), + new TigerViewerScene(), + new ApplyReadbackScene(), + new ManualTextFlowScene(), + new RichTextEditorScene(), + ]; - // Each tab gets its own render control and external surface. This mirrors real UI applications where - // separate controls or panels own their own native drawable areas. - this.clockControl = new WebGPURenderControl { Dock = DockStyle.Fill, RenderMode = WebGPURenderMode.Continuous }; - this.clockControl.PaintFrame += this.OnPaintClock; - - this.tigerControl = new WebGPURenderControl { Dock = DockStyle.Fill }; - this.tigerControl.PaintFrame += this.OnPaintTiger; - - this.applyControl = new WebGPURenderControl { Dock = DockStyle.Fill }; - this.applyControl.PaintFrame += this.OnPaintApply; - - this.manualTextFlowControl = new WebGPURenderControl { Dock = DockStyle.Fill }; - this.manualTextFlowControl.PaintFrame += this.OnPaintManualTextFlow; - - this.richTextControl = new WebGPURenderControl { Dock = DockStyle.Fill, TabStop = true }; - this.richTextControl.PaintFrame += this.OnPaintRichText; - - // The Apply scene reacts to pointer movement so readback cost can be assessed interactively. - this.applyControl.MouseDown += (_, e) => - { - this.applyScene.OnMouseDown(e); - this.applyControl.Invalidate(); - }; - this.applyControl.MouseMove += (_, e) => - { - this.applyScene.OnMouseMove(e); - this.applyControl.Invalidate(); - }; - this.applyControl.MouseWheel += (_, e) => - { - this.applyScene.OnMouseWheel(e); - this.applyControl.Invalidate(); - }; - - this.tigerStatusLabel = new Label - { - AutoSize = false, - Size = new Size(350, 100), - BackColor = Color.FromArgb(160, 0, 0, 0), - ForeColor = Color.White, - Font = new Font(FontFamily.GenericMonospace, 9f), - Padding = new Padding(6), - Location = new Point(6, 6), - Text = string.Empty, - }; - this.tigerControl.Controls.Add(this.tigerStatusLabel); - - // The manual flow scene is intentionally controlled from ordinary WinForms - // widgets. Changing this dropdown swaps the obstacle path, while the scene - // keeps using the same retained TextBlock and per-line layout enumerator. - this.manualTextShapeComboBox = new ComboBox - { - DropDownStyle = ComboBoxStyle.DropDownList, - Width = 140, - Margin = new Padding(0, 0, 8, 0), - }; - - foreach (ManualTextFlowObstacleShape shape in Enum.GetValues()) - { - this.manualTextShapeComboBox.Items.Add(shape); - } - - this.manualTextShapeComboBox.SelectedItem = this.manualTextFlowScene.ObstacleShape; - this.manualTextShapeComboBox.SelectedIndexChanged += (_, _) => - { - if (this.manualTextShapeComboBox.SelectedItem is ManualTextFlowObstacleShape shape) - { - this.manualTextFlowScene.ObstacleShape = shape; - this.manualTextFlowControl.Invalidate(); - } - }; - - FlowLayoutPanel manualTextFlowToolbar = new() - { - Dock = DockStyle.Top, - AutoSize = true, - Padding = new Padding(8), - }; - - manualTextFlowToolbar.Controls.Add(new Label - { - AutoSize = true, - Margin = new Padding(0, 5, 8, 0), - Text = "Shape", - }); - manualTextFlowToolbar.Controls.Add(this.manualTextShapeComboBox); - - Panel manualTextFlowPanel = new() { Dock = DockStyle.Fill }; - manualTextFlowPanel.Controls.Add(this.manualTextFlowControl); - manualTextFlowPanel.Controls.Add(manualTextFlowToolbar); - - this.boldButton = this.CreateEditorToggleButton("B", () => this.richTextScene.ToggleBold()); - this.italicButton = this.CreateEditorToggleButton("I", () => this.richTextScene.ToggleItalic()); - this.underlineButton = this.CreateEditorToggleButton("U", () => this.richTextScene.ToggleUnderline()); - this.strikeoutButton = this.CreateEditorToggleButton("S", () => this.richTextScene.ToggleStrikeout()); - this.fontFamilyComboBox = new ComboBox - { - DropDownStyle = ComboBoxStyle.DropDownList, - Width = 240, - Margin = new Padding(0, 0, 8, 0), - }; - - foreach (string name in SixLabors.Fonts.SystemFonts.Collection.Families.Select(x => x.Name).Order()) + TabControl tabs = new() { Dock = DockStyle.Fill }; + foreach (RenderScene scene in this.scenes) { - this.fontFamilyComboBox.Items.Add(name); + // Each scene owns its controls and behavior. The form supplies only the shared + // device session and a tab in which the complete scene view can be hosted. + TabPage tab = new(scene.DisplayName); + tab.Controls.Add(scene.CreateView(this.surfaceSession)); + tabs.TabPages.Add(tab); } - this.fontFamilyComboBox.SelectedItem = this.richTextScene.FontFamilyName; - this.fontFamilyComboBox.SelectedIndexChanged += (_, _) => - { - if (this.fontFamilyComboBox.SelectedItem is string name) - { - this.richTextScene.SetFontFamily(name); - this.richTextControl.Focus(); - this.richTextControl.Invalidate(); - } - }; - - this.selectionStatusLabel = new Label - { - AutoSize = true, - Margin = new Padding(10, 5, 0, 0), - }; - this.fontSizeLabel = new Label - { - AutoSize = true, - Margin = new Padding(0, 5, 8, 0), - }; - - FlowLayoutPanel editorToolbar = new() - { - Dock = DockStyle.Top, - AutoSize = true, - Padding = new Padding(8), - }; - - editorToolbar.Controls.Add(this.fontFamilyComboBox); - editorToolbar.Controls.Add(this.boldButton); - editorToolbar.Controls.Add(this.italicButton); - editorToolbar.Controls.Add(this.underlineButton); - editorToolbar.Controls.Add(this.strikeoutButton); - editorToolbar.Controls.Add(this.CreateEditorButton("A-", () => this.richTextScene.ChangeFontSize(-2F))); - editorToolbar.Controls.Add(this.fontSizeLabel); - editorToolbar.Controls.Add(this.CreateEditorButton("A+", () => this.richTextScene.ChangeFontSize(2F))); - editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.Black, ImageSharpColor.ParseHex("#17212B"))); - editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.RoyalBlue, ImageSharpColor.ParseHex("#145DA0"))); - editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.Firebrick, ImageSharpColor.ParseHex("#B33A3A"))); - editorToolbar.Controls.Add(this.CreateEditorColorButton(Color.SeaGreen, ImageSharpColor.ParseHex("#2B7A4B"))); - editorToolbar.Controls.Add(this.selectionStatusLabel); - - Panel richTextPanel = new() { Dock = DockStyle.Fill }; - richTextPanel.Controls.Add(this.richTextControl); - richTextPanel.Controls.Add(editorToolbar); - - // Mouse input stays in WinForms coordinates. The scene converts it into its own world transform. - this.tigerControl.MouseDown += (_, e) => - { - this.tigerScene.OnMouseDown(e); - this.tigerControl.Invalidate(); - }; - this.tigerControl.MouseMove += (_, e) => - { - this.tigerScene.OnMouseMove(e); - this.tigerStatusLabel.Text = this.tigerScene.StatusText; - this.tigerControl.Invalidate(); - }; - this.tigerControl.MouseUp += (_, e) => - { - this.tigerScene.OnMouseUp(e); - this.tigerControl.Invalidate(); - }; - this.tigerControl.MouseWheel += (_, e) => - { - this.tigerScene.OnMouseWheel(e); - this.tigerStatusLabel.Text = this.tigerScene.StatusText; - this.tigerControl.Invalidate(); - }; - - // The manual text-flow scene keeps prepared text static and lets the mouse - // move only the obstacle that determines each per-line wrapping width. - this.manualTextFlowControl.MouseDown += (_, e) => - { - this.manualTextFlowScene.OnMouseDown(e); - this.manualTextFlowControl.Invalidate(); - }; - this.manualTextFlowControl.MouseMove += (_, e) => - { - this.manualTextFlowScene.OnMouseMove(e); - this.manualTextFlowControl.Invalidate(); - }; - - this.richTextControl.MouseDown += (_, e) => - { - this.richTextControl.Focus(); - if (e.Button == MouseButtons.Left) - { - this.richTextControl.Capture = true; - } - - this.richTextScene.OnMouseDown(e); - this.UpdateEditorToolbar(); - this.richTextControl.Invalidate(); - }; - - this.richTextControl.MouseMove += (_, e) => - { - this.richTextScene.OnMouseMove(e); - this.UpdateEditorToolbar(); - this.richTextControl.Invalidate(); - }; - - this.richTextControl.MouseUp += (_, e) => - { - this.richTextScene.OnMouseUp(e); - if (e.Button == MouseButtons.Left) - { - this.richTextControl.Capture = false; - } - - this.UpdateEditorToolbar(); - this.richTextControl.Invalidate(); - }; - - this.richTextControl.PreviewKeyDown += (_, e) => - { - e.IsInputKey = e.KeyCode is Keys.Left or Keys.Right or Keys.Up or Keys.Down or Keys.Home or Keys.End; - }; - - this.richTextControl.KeyDown += this.OnRichTextKeyDown; - this.richTextControl.KeyPress += this.OnRichTextKeyPress; - - TabControl tabs = new() { Dock = DockStyle.Fill }; - - TabPage clockTab = new(this.clockScene.DisplayName); - clockTab.Controls.Add(this.clockControl); - tabs.TabPages.Add(clockTab); - - TabPage tigerTab = new(this.tigerScene.DisplayName); - tigerTab.Controls.Add(this.tigerControl); - tabs.TabPages.Add(tigerTab); - - TabPage applyTab = new(this.applyScene.DisplayName); - applyTab.Controls.Add(this.applyControl); - tabs.TabPages.Add(applyTab); - - TabPage manualTextFlowTab = new(this.manualTextFlowScene.DisplayName); - manualTextFlowTab.Controls.Add(manualTextFlowPanel); - tabs.TabPages.Add(manualTextFlowTab); - - TabPage richTextTab = new(this.richTextScene.DisplayName); - richTextTab.Controls.Add(richTextPanel); - tabs.TabPages.Add(richTextTab); - this.Controls.Add(tabs); - this.UpdateEditorToolbar(); } - private void OnPaintClock(DrawingCanvas canvas, TimeSpan delta) - => this.clockScene.Paint(canvas, delta); - - private void OnPaintTiger(DrawingCanvas canvas, TimeSpan delta) + /// + protected override void OnLoad(EventArgs e) { - this.tigerScene.Paint(canvas, delta); - this.tigerStatusLabel.Text = this.tigerScene.StatusText; - } - - private void OnPaintApply(DrawingCanvas canvas, TimeSpan delta) - => this.applyScene.Paint(canvas, delta); + base.OnLoad(e); - private void OnPaintManualTextFlow(DrawingCanvas canvas, TimeSpan delta) - => this.manualTextFlowScene.Paint(canvas, delta); - - private void OnPaintRichText(DrawingCanvas canvas, TimeSpan delta) - => this.richTextScene.Paint(canvas, delta); - - private void OnRichTextKeyDown(object? sender, KeyEventArgs e) - { - if (!this.richTextScene.OnKeyDown(e)) + // Surface creation initializes the shared device while the form is loaded. Scenes + // can now precompile pipelines without exposing their shader details to the host. + foreach (RenderScene scene in this.scenes) { - return; + scene.OnHostLoaded(this.surfaceSession.DeviceContext); } - - e.SuppressKeyPress = true; - this.UpdateEditorToolbar(); - this.richTextControl.Invalidate(); } - private void OnRichTextKeyPress(object? sender, KeyPressEventArgs e) + /// + protected override void Dispose(bool disposing) { - if (!this.richTextScene.OnKeyPress(e.KeyChar)) - { - return; - } - - e.Handled = true; - this.UpdateEditorToolbar(); - this.richTextControl.Invalidate(); - } - - private CheckBox CreateEditorToggleButton(string text, Action action) - { - CheckBox button = new() - { - Appearance = Appearance.Button, - AutoSize = true, - Text = text, - Font = new Font(FontFamily.GenericSansSerif, 9F, FontStyle.Bold), - Margin = new Padding(0, 0, 6, 0), - }; - - button.Click += (_, _) => this.InvokeEditorCommand(action); - return button; - } - - private Button CreateEditorButton(string text, Action action) - { - Button button = new() - { - AutoSize = true, - Text = text, - Margin = new Padding(0, 0, 6, 0), - }; + // Child controls release their surfaces during base disposal; the shared device + // session must remain alive until every scene surface has been released. + base.Dispose(disposing); - button.Click += (_, _) => this.InvokeEditorCommand(action); - return button; - } - - private Button CreateEditorColorButton(Color color, ImageSharpColor textColor) - { - Button button = new() + if (disposing) { - BackColor = color, - FlatStyle = FlatStyle.Flat, - Margin = new Padding(0, 1, 6, 0), - Size = new Size(28, 24), - UseVisualStyleBackColor = false, - }; - - button.Click += (_, _) => this.InvokeEditorCommand(() => this.richTextScene.SetFillColor(textColor)); - return button; - } - - private void InvokeEditorCommand(Action action) - { - action(); - this.UpdateEditorToolbar(); - this.richTextControl.Focus(); - this.richTextControl.Invalidate(); - } - - private void UpdateEditorToolbar() - { - this.boldButton.Checked = this.richTextScene.IsBold; - this.italicButton.Checked = this.richTextScene.IsItalic; - this.underlineButton.Checked = this.richTextScene.IsUnderline; - this.strikeoutButton.Checked = this.richTextScene.IsStrikeout; - this.fontSizeLabel.Text = $"{this.richTextScene.CurrentFontSize:0.#} pt"; - this.selectionStatusLabel.Text = $"Selected: {this.richTextScene.SelectionLength}"; + this.surfaceSession.Dispose(); + } } } diff --git a/samples/WebGPUExternalSurfaceDemo/README.md b/samples/WebGPUExternalSurfaceDemo/README.md index 28f8ce4b3..b9974bb48 100644 --- a/samples/WebGPUExternalSurfaceDemo/README.md +++ b/samples/WebGPUExternalSurfaceDemo/README.md @@ -13,7 +13,7 @@ It exists to demonstrate: ## Running ```bash -dotnet run --project samples/WebGPUExternalSurfaceDemo -c Debug +dotnet run --project samples/WebGPUExternalSurfaceDemo -c Release ``` Requirements: @@ -23,9 +23,10 @@ Requirements: - a WebGPU-capable desktop backend such as D3D12 or Vulkan - adapter support for the storage-capable BGRA format selected by the sample -When the sample starts you should see a WinForms window with five tabs: +When the sample starts you should see a WinForms window with six tabs: - `Clock`: a continuously-rendered animated clock scene +- `Custom WGSL · Living Nebula`: an animated procedural galaxy with domain-warped clouds, fire-bright cores, two twinkling star fields, pointer swirl, click shockwaves, wheel zoom, and an equivalent CPU fallback - `Tiger`: an interactive SVG tiger viewer with pan and zoom - `Apply`: a reactive external-surface readback scene using `DrawingCanvas.Apply(...)`; move the mouse to move the edge-detect and blur regions, and use the mouse wheel to resize them - `Manual Text Flow`: prepared text is laid out one line at a time around a selectable path obstacle that follows mouse movement @@ -56,13 +57,13 @@ The reusable integration point is [WebGPURenderControl.cs](d:/GitHub/SixLabors/I ### Surface Creation -`WebGPURenderControl.OnHandleCreated(...)` creates the external surface from the WinForms control handle: +`WebGPURenderControl.OnHandleCreated(...)` creates the external surface from the WinForms control handle. Every control receives the session owned by the form so all tabs share one adapter, device, queue, and pipeline cache: ```csharp -this.surface = new WebGPUExternalSurface( +this.surface = this.surfaceSession.CreateSurface( WebGPUSurfaceHost.Win32( this.Handle, - Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)), + ProcessModuleHandle), initialFramebufferSize, new WebGPUExternalSurfaceOptions { @@ -103,17 +104,18 @@ Disposing the frame renders pending canvas work, presents the surface texture, a ### Rendering Loop -`WebGPURenderControl` supports on-demand and continuous rendering. On-demand controls render from normal WinForms invalidation, which keeps static scenes idle until input, resize, or another event asks them to repaint. Continuous controls hook `Application.Idle` and render while the WinForms message queue is empty; the clock scene uses this mode because it animates without input. +`WebGPURenderControl` supports on-demand and continuous rendering. On-demand controls render from normal WinForms invalidation, which keeps static scenes idle until input, resize, or another event asks them to repaint. Continuous controls hook `Application.Idle` and render while the WinForms message queue is empty; the clock and living-nebula scenes use this mode because they animate without input. Frame pacing is delegated to the present mode. With the default `WebGPUPresentMode.Fifo`, frame acquisition naturally waits for display presentation instead of using a separate software timer. ## Scene Code -[MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/MainForm.cs) creates independent `WebGPURenderControl` instances, one per tab. Each control owns its own external surface. +[MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/MainForm.cs) supplies a shared `WebGPUSurfaceSession` and hosts each scene's complete view on a tab. [RenderScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/RenderScene.cs) creates the common render control and input plumbing, while each concrete scene owns its frame cadence, controls, overlays, and device initialization. Each render control owns its presentation surface, while the form-owned session shares device resources between them. The scenes are deliberately ordinary canvas code: - [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs): animated vector clock +- [ShaderEffectsScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs): interactive procedural nebula implemented as a custom WGSL layer effect with named uniforms and a CPU fallback - [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs): pan and zoom SVG tiger viewer - [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs): `Apply(...)` scene that reads the external surface back into CPU processing - [ManualTextFlowScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs): interactive manual text flow using prepared line layout enumeration and a selectable closed-path obstacle @@ -128,7 +130,9 @@ Each scene receives: - [WebGPURenderControl.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs): reusable WinForms external-surface control - [MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/MainForm.cs): tabs and scene wiring +- [RenderScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/RenderScene.cs): common scene view, frame scheduling, and pointer-input plumbing - [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs): clock scene +- [ShaderEffectsScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs): custom shader scene - [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs): tiger viewer scene - [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs): apply readback scene - [ManualTextFlowScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs): manual text-flow scene diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs index b51189372..f88c1d509 100644 --- a/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs @@ -3,7 +3,6 @@ using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; using Color = SixLabors.ImageSharp.Color; diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs index 19fcf52f3..8f7a70895 100644 --- a/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/ClockScene.cs @@ -5,7 +5,7 @@ using System.Numerics; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; +using WebGPUExternalSurfaceDemo.Controls; using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; using Color = SixLabors.ImageSharp.Color; using Font = SixLabors.Fonts.Font; @@ -40,6 +40,9 @@ internal sealed class ClockScene : RenderScene private readonly Font numeralFont; + /// + protected override WebGPURenderMode RenderMode => WebGPURenderMode.Continuous; + public ClockScene() { // Resolve the font family once. The actual font size is derived from the current framebuffer size diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs index b0641a54f..5bee334bf 100644 --- a/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs @@ -5,6 +5,7 @@ using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; +using WebGPUExternalSurfaceDemo.Controls; using Brush = SixLabors.ImageSharp.Drawing.Processing.Brush; using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; using Color = SixLabors.ImageSharp.Color; @@ -115,6 +116,55 @@ public ManualTextFlowScene() public override string DisplayName => "Manual Text Flow"; + /// + protected override Control CreateContent(WebGPURenderControl renderControl) + { + ComboBox shapeSelector = new() + { + DropDownStyle = ComboBoxStyle.DropDownList, + Width = 140, + Margin = new Padding(0, 0, 8, 0), + }; + + foreach (ManualTextFlowObstacleShape shape in Enum.GetValues()) + { + shapeSelector.Items.Add(shape); + } + + shapeSelector.SelectedItem = this.ObstacleShape; + shapeSelector.SelectedIndexChanged += (_, _) => + { + if (shapeSelector.SelectedItem is ManualTextFlowObstacleShape shape) + { + // The selector demonstrates that any closed path can drive the same + // line-slot algorithm without changing the shared sample host. + this.ObstacleShape = shape; + renderControl.Invalidate(); + } + }; + + FlowLayoutPanel toolbar = new() + { + Dock = DockStyle.Top, + AutoSize = true, + Padding = new Padding(8), + }; + + toolbar.Controls.Add(new Label + { + AutoSize = true, + Margin = new Padding(0, 5, 8, 0), + Text = "Shape", + }); + + toolbar.Controls.Add(shapeSelector); + + Panel panel = new() { Dock = DockStyle.Fill }; + panel.Controls.Add(renderControl); + panel.Controls.Add(toolbar); + return panel; + } + /// /// Gets or sets the closed shape used as the flow obstacle. /// @@ -402,7 +452,7 @@ private static void AddBandScanLines( /// The scanline collection for the current band. private static void AddScanY(float y, float bandTop, float bandBottom, List scanYs) { - const float DuplicateTolerance = 0.5F; + const float duplicateTolerance = 0.5F; // Scanlines exactly on the edge of the row do not tell us whether the // row itself is obstructed. The top/bottom samples are added with a small @@ -417,7 +467,7 @@ private static void AddScanY(float y, float bandTop, float bandBottom, List /// Base class for a demo scene rendered into a . -/// The host owns frame acquisition; the scene owns only drawing and input state. +/// Each scene owns its view, frame cadence, input, and any controls that explain or manipulate it. /// internal abstract class RenderScene { @@ -18,6 +18,83 @@ internal abstract class RenderScene /// public abstract string DisplayName { get; } + /// + /// Gets the frame scheduling mode required by this scene. + /// + protected virtual WebGPURenderMode RenderMode => WebGPURenderMode.OnDemand; + + /// + /// Creates the complete WinForms view for this scene. + /// + /// The device session shared by all sample scenes. + /// The control inserted into the scene's tab. + public Control CreateView(WebGPUSurfaceSession surfaceSession) + { + WebGPURenderControl renderControl = new(surfaceSession) + { + Dock = DockStyle.Fill, + RenderMode = this.RenderMode, + }; + + // Register scene painting before scene-specific overlays. An overlay that listens + // to PaintFrame therefore observes state produced by the frame it describes. + renderControl.PaintFrame += this.Paint; + + Control content = this.CreateContent(renderControl); + this.ConfigureControl(renderControl); + + // Every scene uses the same pointer plumbing. Keeping it here lets individual + // scenes concentrate on their own interaction and presentation. + renderControl.MouseDown += (_, e) => + { + this.OnMouseDown(e); + renderControl.Invalidate(); + }; + + renderControl.MouseMove += (_, e) => + { + this.OnMouseMove(e); + renderControl.Invalidate(); + }; + + renderControl.MouseUp += (_, e) => + { + this.OnMouseUp(e); + renderControl.Invalidate(); + }; + + renderControl.MouseWheel += (_, e) => + { + this.OnMouseWheel(e); + renderControl.Invalidate(); + }; + + return content; + } + + /// + /// Initializes resources that require the shared WebGPU device after the host is loaded. + /// + /// The initialized shared device context. + public virtual void OnHostLoaded(WebGPUDeviceContext deviceContext) + { + } + + /// + /// Creates any scene-specific chrome around the render control. + /// + /// The control that presents the scene. + /// The complete scene content. + protected virtual Control CreateContent(WebGPURenderControl renderControl) => renderControl; + + /// + /// Configures scene-specific control behavior such as keyboard input. + /// + /// The control that presents the scene. + protected virtual void ConfigureControl(WebGPURenderControl renderControl) + { + } + /// /// Draws the scene into for the current frame. /// diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs index 68cee3458..0d1f91a3c 100644 --- a/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/RichTextEditorScene.cs @@ -6,6 +6,7 @@ using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; +using WebGPUExternalSurfaceDemo.Controls; using Brush = SixLabors.ImageSharp.Drawing.Processing.Brush; using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; using Color = SixLabors.ImageSharp.Color; @@ -57,6 +58,13 @@ internal sealed class RichTextEditorScene : RenderScene private static readonly Pen GuidePen = Pens.Solid(GuideColor, 1F); private readonly Dictionary fontCache = []; + private readonly CheckBox boldButton = new(); + private readonly CheckBox italicButton = new(); + private readonly CheckBox underlineButton = new(); + private readonly CheckBox strikeoutButton = new(); + private readonly ComboBox fontFamilyComboBox = new(); + private readonly Label fontSizeLabel = new(); + private readonly Label selectionStatusLabel = new(); // Style runs use source grapheme ranges, not UTF-16 indices. The Fonts APIs // expose grapheme indices for interaction, so the editor can apply formatting @@ -129,6 +137,116 @@ public RichTextEditorScene() /// public override string DisplayName => "Rich Text Editor"; + /// + protected override Control CreateContent(WebGPURenderControl renderControl) + { + this.ConfigureToggleButton(this.boldButton, "B", this.ToggleBold, renderControl); + this.ConfigureToggleButton(this.italicButton, "I", this.ToggleItalic, renderControl); + this.ConfigureToggleButton(this.underlineButton, "U", this.ToggleUnderline, renderControl); + this.ConfigureToggleButton(this.strikeoutButton, "S", this.ToggleStrikeout, renderControl); + + this.fontFamilyComboBox.DropDownStyle = ComboBoxStyle.DropDownList; + this.fontFamilyComboBox.Width = 240; + this.fontFamilyComboBox.Margin = new Padding(0, 0, 8, 0); + foreach (string name in SystemFonts.Collection.Families.Select(x => x.Name).Order()) + { + this.fontFamilyComboBox.Items.Add(name); + } + + this.fontFamilyComboBox.SelectedItem = this.FontFamilyName; + this.fontFamilyComboBox.SelectedIndexChanged += (_, _) => + { + if (this.fontFamilyComboBox.SelectedItem is string name) + { + this.SetFontFamily(name); + renderControl.Focus(); + renderControl.Invalidate(); + } + }; + + this.fontSizeLabel.AutoSize = true; + this.fontSizeLabel.Margin = new Padding(0, 5, 8, 0); + this.selectionStatusLabel.AutoSize = true; + this.selectionStatusLabel.Margin = new Padding(10, 5, 0, 0); + + FlowLayoutPanel toolbar = new() + { + Dock = DockStyle.Top, + AutoSize = true, + Padding = new Padding(8), + }; + + toolbar.Controls.Add(this.fontFamilyComboBox); + toolbar.Controls.Add(this.boldButton); + toolbar.Controls.Add(this.italicButton); + toolbar.Controls.Add(this.underlineButton); + toolbar.Controls.Add(this.strikeoutButton); + toolbar.Controls.Add(this.CreateButton("A-", () => this.ChangeFontSize(-2F), renderControl)); + toolbar.Controls.Add(this.fontSizeLabel); + toolbar.Controls.Add(this.CreateButton("A+", () => this.ChangeFontSize(2F), renderControl)); + toolbar.Controls.Add(this.CreateColorButton(System.Drawing.Color.Black, Color.ParseHex("#17212B"), renderControl)); + toolbar.Controls.Add(this.CreateColorButton(System.Drawing.Color.RoyalBlue, Color.ParseHex("#145DA0"), renderControl)); + toolbar.Controls.Add(this.CreateColorButton(System.Drawing.Color.Firebrick, Color.ParseHex("#B33A3A"), renderControl)); + toolbar.Controls.Add(this.CreateColorButton(System.Drawing.Color.SeaGreen, Color.ParseHex("#2B7A4B"), renderControl)); + toolbar.Controls.Add(this.selectionStatusLabel); + + Panel panel = new() { Dock = DockStyle.Fill }; + panel.Controls.Add(renderControl); + panel.Controls.Add(toolbar); + this.UpdateToolbar(); + return panel; + } + + /// + protected override void ConfigureControl(WebGPURenderControl renderControl) + { + renderControl.TabStop = true; + + // The scene owns editor focus and capture because these are part of selection + // behavior, not responsibilities of the generic external-surface host. + renderControl.MouseDown += (_, e) => + { + renderControl.Focus(); + if (e.Button == MouseButtons.Left) + { + renderControl.Capture = true; + } + }; + + renderControl.MouseUp += (_, e) => + { + if (e.Button == MouseButtons.Left) + { + renderControl.Capture = false; + } + }; + + renderControl.PreviewKeyDown += (_, e) => + { + e.IsInputKey = e.KeyCode is Keys.Left or Keys.Right or Keys.Up or Keys.Down or Keys.Home or Keys.End; + }; + + renderControl.KeyDown += (_, e) => + { + if (this.OnKeyDown(e)) + { + e.SuppressKeyPress = true; + this.UpdateToolbar(); + renderControl.Invalidate(); + } + }; + + renderControl.KeyPress += (_, e) => + { + if (this.OnKeyPress(e.KeyChar)) + { + e.Handled = true; + this.UpdateToolbar(); + renderControl.Invalidate(); + } + }; + } + /// /// Gets the active font family name. /// @@ -217,7 +335,9 @@ public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime) // Selection is painted before glyphs, matching normal editor behavior. // The clipping scope applies only to text so the editor chrome remains crisp. this.DrawSelection(canvas, metrics); - canvas.Save(new DrawingOptions(), editorClip); + canvas.Save(new DrawingOptions()); + canvas.Clip(editorClip); + canvas.DrawText(textOptions, this.text, DefaultTextBrush, pen: null); canvas.Restore(); this.DrawCaret(canvas); @@ -287,6 +407,7 @@ public override void OnMouseDown(MouseEventArgs e) { this.BeginSelection(e.X, e.Y); this.draggingSelection = e.Button == MouseButtons.Left; + this.UpdateToolbar(); } /// @@ -299,10 +420,15 @@ public override void OnMouseMove(MouseEventArgs e) this.draggingSelection = true; this.ExtendSelection(e.X, e.Y); + this.UpdateToolbar(); } /// - public override void OnMouseUp(MouseEventArgs e) => this.draggingSelection = false; + public override void OnMouseUp(MouseEventArgs e) + { + this.draggingSelection = false; + this.UpdateToolbar(); + } /// /// Starts a pointer selection operation at the supplied control coordinates. @@ -439,6 +565,69 @@ public void SetFontFamily(string name) this.caretGeometryDirty = true; } + private void ConfigureToggleButton( + CheckBox button, + string text, + Action action, + WebGPURenderControl renderControl) + { + button.Appearance = Appearance.Button; + button.AutoSize = true; + button.Text = text; + button.Font = new System.Drawing.Font(System.Drawing.FontFamily.GenericSansSerif, 9F, System.Drawing.FontStyle.Bold); + button.Margin = new Padding(0, 0, 6, 0); + button.Click += (_, _) => this.InvokeEditorCommand(action, renderControl); + } + + private Button CreateButton(string text, Action action, WebGPURenderControl renderControl) + { + Button button = new() + { + AutoSize = true, + Text = text, + Margin = new Padding(0, 0, 6, 0), + }; + + button.Click += (_, _) => this.InvokeEditorCommand(action, renderControl); + return button; + } + + private Button CreateColorButton( + System.Drawing.Color color, + Color textColor, + WebGPURenderControl renderControl) + { + Button button = new() + { + BackColor = color, + FlatStyle = FlatStyle.Flat, + Margin = new Padding(0, 1, 6, 0), + Size = new System.Drawing.Size(28, 24), + UseVisualStyleBackColor = false, + }; + + button.Click += (_, _) => this.InvokeEditorCommand(() => this.SetFillColor(textColor), renderControl); + return button; + } + + private void InvokeEditorCommand(Action action, WebGPURenderControl renderControl) + { + action(); + this.UpdateToolbar(); + renderControl.Focus(); + renderControl.Invalidate(); + } + + private void UpdateToolbar() + { + this.boldButton.Checked = this.IsBold; + this.italicButton.Checked = this.IsItalic; + this.underlineButton.Checked = this.IsUnderline; + this.strikeoutButton.Checked = this.IsStrikeout; + this.fontSizeLabel.Text = $"{this.CurrentFontSize:0.#} pt"; + this.selectionStatusLabel.Text = $"Selected: {this.SelectionLength}"; + } + private bool HandleControlKey(Keys keyCode) { switch (keyCode) diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs new file mode 100644 index 000000000..f79114577 --- /dev/null +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs @@ -0,0 +1,632 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using WebGPUExternalSurfaceDemo.Controls; +using Brush = SixLabors.ImageSharp.Drawing.Processing.Brush; +using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; +using Color = SixLabors.ImageSharp.Color; +using GraphicsOptions = SixLabors.ImageSharp.GraphicsOptions; +using Size = SixLabors.ImageSharp.Size; + +namespace WebGPUExternalSurfaceDemo.Scenes; + +/// +/// Displays an interactive, animated nebula generated by a custom WGSL layer effect. +/// Moving the pointer stirs the gas, clicking launches a shockwave, and the wheel changes scale. +/// The same effect can execute through its equivalent CPU fallback when WebGPU is unavailable. +/// +internal sealed class ShaderEffectsScene : RenderScene +{ + private static readonly GraphicsOptions LayerOptions = new(); + private static readonly Brush OpaqueLayerBrush = Brushes.Solid(Color.Black); + + // The effect is an immutable uniform snapshot. Animation replaces this small object, + // while WebGPUShaderProgram continues to reuse the source/layout compiled below. + private LivingNebulaShaderEffect shaderEffect = new( + new LivingNebulaShaderOptions + { + Size = new Size(1280, 800), + SwirlCenter = new Vector2(640F, 400F), + Zoom = 1F, + PulseOrigin = new Vector2(640F, 400F), + PulseAge = 10F, + }); + + // Input is retained in device pixels because the shader receives effect-local pixel + // coordinates and performs its own aspect-correct normalization from the viewport size. + private Vector2 pointer; + private Vector2 smoothedSwirl; + private Vector2 pulseOrigin; + private float elapsedSeconds; + private float pulseStart = -10F; + private float zoom = 1F; + private bool hasPointer; + private bool pointerDown; + + /// + public override string DisplayName => "Custom WGSL · Living Nebula"; + + /// + protected override WebGPURenderMode RenderMode => WebGPURenderMode.Continuous; + + /// + protected override Control CreateContent(WebGPURenderControl renderControl) + { + // The explanation belongs to the scene because it describes the generated image + // and gestures. WinForms owns its lifetime after it joins the control tree. + Label descriptionLabel = new() + { + AutoSize = true, + BackColor = System.Drawing.Color.FromArgb(160, 0, 0, 0), + ForeColor = System.Drawing.Color.White, + Font = new Font(FontFamily.GenericSansSerif, 10F), + Padding = new Padding(10), + Location = new Point(10, 10), + Text = + "LIVING NEBULA — CUSTOM WGSL\n" + + "Domain-warped clouds, fire cores, and two twinkling star fields\n" + + "Move to stir · Hold to intensify · Click for shockwave · Wheel to zoom", + }; + + renderControl.Controls.Add(descriptionLabel); + return renderControl; + } + + /// + public override void OnHostLoaded(WebGPUDeviceContext deviceContext) => + + // Compile the immutable WGSL program while the first tab is visible. Uniform values + // change every frame, but every snapshot shares this cached program and pipeline. + deviceContext.Precompile(this.shaderEffect); + + /// + public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime) + { + float deltaSeconds = Math.Clamp((float)deltaTime.TotalSeconds, 0F, 0.1F); + this.elapsedSeconds = (this.elapsedSeconds + deltaSeconds) % 1000F; + + Size viewport = canvas.Bounds.Size; + Vector2 viewportCenter = new(viewport.Width * 0.5F, viewport.Height * 0.5F); + Vector2 targetSwirl = this.hasPointer ? this.pointer : viewportCenter; + if (this.smoothedSwirl == Vector2.Zero) + { + this.smoothedSwirl = targetSwirl; + this.pulseOrigin = viewportCenter; + } + + // Exponential smoothing is frame-rate independent: the cursor feels equally + // fluid on a 60 Hz display and on a faster or temporarily delayed frame loop. + float follow = 1F - MathF.Exp(-7F * deltaSeconds); + this.smoothedSwirl = Vector2.Lerp(this.smoothedSwirl, targetSwirl, follow); + + float pulseAge = this.elapsedSeconds - this.pulseStart; + if (pulseAge < 0F) + { + // elapsedSeconds wraps to keep long-running shader coordinates precise. A pulse + // crossing that wrap is still measured against the same thousand-second period. + pulseAge += 1000F; + } + + // Effects are immutable snapshots. Rebuilding the small uniform payload does not + // recompile WGSL: the source and layout resolve to the same cached shader program. + this.shaderEffect = new LivingNebulaShaderEffect( + new LivingNebulaShaderOptions + { + Size = viewport, + Time = this.elapsedSeconds, + SwirlCenter = this.smoothedSwirl, + PointerDown = this.pointerDown, + Zoom = this.zoom, + PulseOrigin = this.pulseOrigin, + PulseAge = pulseAge, + }); + + // Filling the complete layer gives the procedural shader a pixel at every output + // position. The effect intentionally generates an opaque scene and ignores input color. + canvas.SaveLayer(LayerOptions, canvas.Bounds, this.shaderEffect); + canvas.Fill(OpaqueLayerBrush, canvas.Bounds); + canvas.Restore(); + } + + /// + public override void OnMouseDown(MouseEventArgs e) + { + this.pointer = new Vector2(e.X, e.Y); + this.hasPointer = true; + this.pointerDown = true; + + if (e.Button == MouseButtons.Left) + { + this.pulseOrigin = this.pointer; + this.pulseStart = this.elapsedSeconds; + } + } + + /// + public override void OnMouseMove(MouseEventArgs e) + { + this.pointer = new Vector2(e.X, e.Y); + this.hasPointer = true; + this.pointerDown = (e.Button & MouseButtons.Left) != 0; + } + + /// + public override void OnMouseUp(MouseEventArgs e) => this.pointerDown = false; + + /// + public override void OnMouseWheel(MouseEventArgs e) + { + this.pointer = new Vector2(e.X, e.Y); + this.hasPointer = true; + + // The shader multiplies its centered coordinates by zoom. Smaller values + // therefore reveal a closer view while larger values reveal more space. + float factor = e.Delta > 0 ? 0.88F : 1F / 0.88F; + this.zoom = Math.Clamp(this.zoom * factor, 0.42F, 3F); + } + + /// + /// Contains one immutable set of values shared by a shader pass and its CPU fallback. + /// + private readonly struct LivingNebulaShaderOptions + { + /// Gets the effect-local output size in pixels. + public Size Size { get; init; } + + /// Gets the animation time in seconds. + public float Time { get; init; } + + /// Gets the smoothed vortex center in effect-local pixels. + public Vector2 SwirlCenter { get; init; } + + /// Gets a value indicating whether a pressed pointer button should intensify the vortex. + public bool PointerDown { get; init; } + + /// Gets the scale applied to the procedural coordinate system. + public float Zoom { get; init; } + + /// Gets the most recent shockwave origin in effect-local pixels. + public Vector2 PulseOrigin { get; init; } + + /// Gets the elapsed time since the most recent shockwave started. + public float PulseAge { get; init; } + } + + /// + /// Generates the living-nebula image from domain-warped noise and sparse procedural stars. + /// + private sealed class LivingNebulaShaderEffect : WebGPUShaderLayerEffect + { + private const string ShaderSource = """ + fn hash21(value: vec2) -> f32 { + // Wrapping lattice coordinates prevents the sine hash losing useful low bits + // when animated coordinates grow. Its 256-cell period is larger than the + // procedural domain visible in this scene. + let wrapped = value - (floor(value / 256.0) * 256.0); + return fract(sin(dot(wrapped, vec2(127.1, 311.7))) * 43758.5453); + } + + fn value_noise(position: vec2) -> f32 { + let cell = floor(position); + let local = fract(position); + let blend = local * local * (vec2(3.0) - (2.0 * local)); + let lower_left = hash21(cell); + let lower_right = hash21(cell + vec2(1.0, 0.0)); + let upper_left = hash21(cell + vec2(0.0, 1.0)); + let upper_right = hash21(cell + vec2(1.0, 1.0)); + return mix( + mix(lower_left, lower_right, blend.x), + mix(upper_left, upper_right, blend.x), + blend.y); + } + + fn rotate(position: vec2, angle: f32) -> vec2 { + let cosine = cos(angle); + let sine = sin(angle); + return vec2( + (cosine * position.x) - (sine * position.y), + (sine * position.x) + (cosine * position.y)); + } + + fn fbm(initial_position: vec2) -> f32 { + var position = initial_position; + var value = 0.0; + var amplitude = 0.5; + + // Six octaves retain fine cloud structure without turning the sample into + // a shader stress test. Rotation prevents obvious axis-aligned repetition. + for (var octave: i32 = 0; octave < 6; octave = octave + 1) { + value = value + (amplitude * value_noise(position)); + position = (rotate(position, 0.6435011) * 2.0) + vec2(1.7); + amplitude = amplitude * 0.5; + } + + return value; + } + + fn star_layer(position: vec2, scale: f32, time: f32, layer: f32) -> f32 { + let star_position = position / scale; + let cell = floor(star_position); + let random = hash21(cell + vec2(layer * 13.0, layer * 7.0)); + let threshold = 0.93 + (layer * 0.02); + if (random <= threshold) { + return 0.0; + } + + // Each occupied cell gets a stable sub-cell offset. Only brightness changes, + // so stars twinkle without swimming across the image during animation. + let jitter = vec2( + hash21(cell + vec2(1.7 + layer, 3.1)), + hash21(cell + vec2(4.2, 8.3 + layer))) - vec2(0.5); + let distance_to_star = length(fract(star_position) - vec2(0.5) - (jitter * 0.6)); + let twinkle = 0.55 + (0.45 * sin((time * (2.0 + (random * 7.0))) + (random * 40.0))); + let point = 1.0 - smoothstep(0.0, 0.42, distance_to_star); + return point * twinkle * (0.7 + (0.6 * random)); + } + + fn layer_effect(position: vec2) -> vec4 { + let resolution = imagesharp_uniforms.resolution; + let zoom = imagesharp_uniforms.zoom; + let time = imagesharp_uniforms.time; + var uv = ((position - (resolution * 0.5)) / resolution.y) * zoom; + let swirl_center = ((imagesharp_uniforms.swirl_center - (resolution * 0.5)) / resolution.y) * zoom; + let from_swirl = uv - swirl_center; + let swirl_distance = length(from_swirl); + + // Space is rotated around the pointer before noise is evaluated. Holding the + // button increases the deformation while its exponential falloff keeps distant + // gas stable and makes the gesture read as a local vortex. + let swirl_strength = (0.55 + (imagesharp_uniforms.pointer_down * 0.9)) * exp(-swirl_distance * 2.8); + let swirl_angle = swirl_strength / ((swirl_distance * 2.4) + 0.16); + uv = swirl_center + rotate(from_swirl, swirl_angle); + + let drift_time = time * 0.06; + let noise_position = (uv + vec2(drift_time * 0.35, drift_time * 0.12)) * 1.6; + + // Domain warping feeds two noise fields into a third. The resulting coordinate + // distortion creates curling filaments rather than ordinary cloudy value noise. + let first_warp = vec2( + fbm(noise_position + vec2(0.0, drift_time)), + fbm(noise_position + vec2(5.2, 1.3) - vec2(drift_time))); + let second_warp = vec2( + fbm(noise_position + (3.0 * first_warp) + vec2(1.7, 9.2) + vec2(drift_time * 0.5)), + fbm(noise_position + (3.0 * first_warp) + vec2(8.3, 2.8) - vec2(drift_time * 0.4))); + let warped_noise = fbm(noise_position + (2.4 * second_warp)); + + var density = warped_noise * (0.6 + (0.7 * length(second_warp))); + density = clamp((density - 0.34) * 2.0, 0.0, 1.0); + density = pow(density, 2.1); + + // A click emits a two-second ring in the same aspect-correct coordinate system. + // It brightens only a narrow band instead of filling the area behind the wave. + var wave = 0.0; + if (imagesharp_uniforms.pulse_age >= 0.0 && imagesharp_uniforms.pulse_age < 2.0) { + let pulse_position = ((imagesharp_uniforms.pulse_origin - (resolution * 0.5)) / resolution.y) * zoom; + let pulse_distance = length(uv - pulse_position); + let wave_radius = imagesharp_uniforms.pulse_age * 1.1; + let ring = exp(-pow((pulse_distance - wave_radius) * 12.0, 2.0)); + wave = ring * (1.0 - (imagesharp_uniforms.pulse_age * 0.5)); + density = density + (wave * 0.35); + } + + let void_color = vec3(0.006, 0.010, 0.035); + let indigo = vec3(0.05, 0.07, 0.24); + let violet = vec3(0.26, 0.09, 0.46); + let magenta = vec3(0.72, 0.16, 0.50); + let ember = vec3(0.95, 0.45, 0.28); + var color = mix(void_color, indigo, smoothstep(0.02, 0.22, density)); + color = mix(color, violet, smoothstep(0.20, 0.50, density)); + color = mix(color, magenta, smoothstep(0.52, 0.82, density)); + color = mix(color, ember, smoothstep(0.88, 1.0, density) * (0.25 + (0.45 * second_warp.x))); + + // A cool boundary color separates adjacent warped fields, while the orange + // grade remains confined to the densest knots and reads as fire inside gas. + let boundary = 1.0 - smoothstep(0.0, 0.12, abs(first_warp.x - first_warp.y)); + color = color + (vec3(0.04, 0.16, 0.24) * boundary * 0.35); + color = color + (vec3(0.40, 0.70, 1.0) * wave * 0.9); + + var stars = star_layer(position, 7.0, time, 0.0); + stars = stars + star_layer(position + vec2(19.0, 31.0), 13.0, time, 1.0); + stars = stars * (1.0 - (smoothstep(0.35, 0.85, density) * 0.85)); + color = color + (vec3(0.85, 0.92, 1.0) * stars); + + let core = exp(-length(uv) * 2.6); + color = color + (vec3(0.28, 0.10, 0.40) * core * 0.35); + + let viewport_position = (position / resolution) - vec2(0.5); + let vignette = 1.0 - smoothstep(0.30, 1.20, length(viewport_position)); + color = color * vignette * 0.85; + + // This filmic curve rolls intense cloud cores toward white while preserving + // saturated mid-tones. Clamp only after tone mapping to keep highlights smooth. + color = (color * ((2.51 * color) + vec3(0.03))) / + ((color * ((2.43 * color) + vec3(0.59))) + vec3(0.14)); + return vec4(clamp(color, vec3(0.0), vec3(1.0)), 1.0); + } + """; + + // These names form the complete contract between the immutable C# options snapshot + // and imagesharp_uniforms in WGSL. The same values also feed the CPU implementation. + private static readonly WebGPUShaderUniformLayout UniformLayout = new( + [ + new WebGPUShaderUniform("time", WebGPUShaderUniformType.Float32, 1), + new WebGPUShaderUniform("resolution", WebGPUShaderUniformType.Vector2, 1), + new WebGPUShaderUniform("swirl_center", WebGPUShaderUniformType.Vector2, 1), + new WebGPUShaderUniform("pointer_down", WebGPUShaderUniformType.Float32, 1), + new WebGPUShaderUniform("zoom", WebGPUShaderUniformType.Float32, 1), + new WebGPUShaderUniform("pulse_origin", WebGPUShaderUniformType.Vector2, 1), + new WebGPUShaderUniform("pulse_age", WebGPUShaderUniformType.Float32, 1), + ]); + + /// + /// Initializes a shader effect from one immutable animation and interaction snapshot. + /// + /// The values shared by the WGSL pass and CPU fallback. + public LivingNebulaShaderEffect(LivingNebulaShaderOptions options) + : base(ShaderSource, UniformLayout, CreateFallback(options)) => + + // The base type creates and freezes the pass. The effect supplies only values + // matching its declared layout, which is the complete custom-shader workflow. + this.AddShaderPass(uniforms => + { + uniforms.SetFloat32("time", options.Time); + uniforms.SetVector2("resolution", new Vector2(options.Size.Width, options.Size.Height)); + uniforms.SetVector2("swirl_center", options.SwirlCenter); + uniforms.SetFloat32("pointer_down", options.PointerDown ? 1F : 0F); + uniforms.SetFloat32("zoom", options.Zoom); + uniforms.SetVector2("pulse_origin", options.PulseOrigin); + uniforms.SetFloat32("pulse_age", options.PulseAge); + }); + + /// + /// Creates the CPU image operation that reproduces the procedural WGSL pass. + /// + /// The immutable animation and interaction values for the frame. + /// The equivalent CPU image operation. + private static Action CreateFallback(LivingNebulaShaderOptions options) + => context => context.ProcessPixelRowsAsVector4( + (span, origin) => + { + Vector2 resolution = new(options.Size.Width, options.Size.Height); + for (int x = 0; x < span.Length; x++) + { + // Pixel-row processing supplies integer row origins. The half-pixel offset + // matches layer_effect's pixel-center coordinate contract on WebGPU. + Vector2 position = new(origin.X + x + 0.5F, origin.Y + 0.5F); + span[x] = new Vector4(RenderPixel(position, resolution, options), 1F); + } + }, + PixelConversionModifiers.Scale | PixelConversionModifiers.UnPremultiply); + + /// + /// Evaluates the nebula color for one effect-local pixel. + /// + /// The effect-local pixel-center position. + /// The effect-local output size. + /// The immutable animation and interaction values for the frame. + /// The unassociated opaque RGB color. + private static Vector3 RenderPixel(Vector2 position, Vector2 resolution, LivingNebulaShaderOptions options) + { + // Normalize against viewport height so circles and radial falloffs remain round + // on non-square surfaces. Zoom scales this effect-local coordinate system only. + Vector2 uv = ((position - (resolution * 0.5F)) / resolution.Y) * options.Zoom; + Vector2 swirlPosition = ((options.SwirlCenter - (resolution * 0.5F)) / resolution.Y) * options.Zoom; + Vector2 fromSwirl = uv - swirlPosition; + float swirlDistance = fromSwirl.Length(); + + // Rotate sample space around the smoothed pointer. The exponential falloff + // confines deformation to nearby gas; holding the pointer strengthens it. + float swirlStrength = (0.55F + (options.PointerDown ? 0.9F : 0F)) * MathF.Exp(-swirlDistance * 2.8F); + float swirlAngle = swirlStrength / ((swirlDistance * 2.4F) + 0.16F); + uv = swirlPosition + Rotate(fromSwirl, swirlAngle); + + // Slow translation makes the nebula breathe without causing visible texture + // swimming. The coordinates stay small enough for the sine hash below. + float driftTime = options.Time * 0.06F; + Vector2 noisePosition = (uv + new Vector2(driftTime * 0.35F, driftTime * 0.12F)) * 1.6F; + + // Domain warping evaluates two vector-valued noise stages and feeds them into + // the final fBm lookup. This bends ordinary noise into long curling filaments. + Vector2 firstWarp = new( + Fbm(noisePosition + new Vector2(0F, driftTime)), + Fbm(noisePosition + new Vector2(5.2F, 1.3F) - new Vector2(driftTime))); + + Vector2 secondWarp = new( + Fbm(noisePosition + (3F * firstWarp) + new Vector2(1.7F, 9.2F) + new Vector2(driftTime * 0.5F)), + Fbm(noisePosition + (3F * firstWarp) + new Vector2(8.3F, 2.8F) - new Vector2(driftTime * 0.4F))); + + // Remove the low fBm floor, expand the useful range, and apply a soft gamma. + // Empty regions remain near-black while only folded ridges become luminous. + float density = Fbm(noisePosition + (2.4F * secondWarp)) * (0.6F + (0.7F * secondWarp.Length())); + density = Math.Clamp((density - 0.34F) * 2F, 0F, 1F); + density = MathF.Pow(density, 2.1F); + + float wave = 0F; + if (options.PulseAge is >= 0F and < 2F) + { + // Evaluate the click in the same aspect-correct space as the clouds. Squaring + // the signed distance to the expanding radius produces a narrow Gaussian ring. + Vector2 pulsePosition = ((options.PulseOrigin - (resolution * 0.5F)) / resolution.Y) * options.Zoom; + float pulseDistance = Vector2.Distance(uv, pulsePosition); + float waveRadius = options.PulseAge * 1.1F; + float ringDistance = (pulseDistance - waveRadius) * 12F; + wave = MathF.Exp(-(ringDistance * ringDistance)) * (1F - (options.PulseAge * 0.5F)); + density += wave * 0.35F; + } + + // Layer color bands over the shaped density instead of mapping noise directly + // to RGB. This keeps the void cool, the cloud body saturated, and only the + // densest folded knots hot enough to reach the orange fire color. + Vector3 color = Vector3.Lerp(new Vector3(0.006F, 0.010F, 0.035F), new Vector3(0.05F, 0.07F, 0.24F), SmoothStep(0.02F, 0.22F, density)); + color = Vector3.Lerp(color, new Vector3(0.26F, 0.09F, 0.46F), SmoothStep(0.20F, 0.50F, density)); + color = Vector3.Lerp(color, new Vector3(0.72F, 0.16F, 0.50F), SmoothStep(0.52F, 0.82F, density)); + color = Vector3.Lerp( + color, + new Vector3(0.95F, 0.45F, 0.28F), + SmoothStep(0.88F, 1F, density) * (0.25F + (0.45F * secondWarp.X))); + + // A teal seam where the first warp channels meet adds depth between nearby + // folds. The shockwave is then graded cyan-white independently of density. + float boundary = 1F - SmoothStep(0F, 0.12F, MathF.Abs(firstWarp.X - firstWarp.Y)); + color += new Vector3(0.04F, 0.16F, 0.24F) * boundary * 0.35F; + color += new Vector3(0.40F, 0.70F, 1F) * wave * 0.9F; + + // Two cell sizes create near and far star fields. Dense gas occludes most of + // their light so stars appear behind the nebula rather than painted over it. + float stars = StarLayer(position, 7F, options.Time, 0F) + StarLayer(position + new Vector2(19F, 31F), 13F, options.Time, 1F); + stars *= 1F - (SmoothStep(0.35F, 0.85F, density) * 0.85F); + color += new Vector3(0.85F, 0.92F, 1F) * stars; + + float core = MathF.Exp(-uv.Length() * 2.6F); + color += new Vector3(0.28F, 0.10F, 0.40F) * core * 0.35F; + + // The vignette anchors the composition without clipping the corners abruptly. + Vector2 viewportPosition = (position / resolution) - new Vector2(0.5F); + float vignette = 1F - SmoothStep(0.30F, 1.20F, viewportPosition.Length()); + color *= vignette * 0.85F; + + // Apply the same rational filmic curve as WGSL. Values remain unconstrained + // until this final stage so bright filaments roll smoothly toward white. + Vector3 numerator = color * ((2.51F * color) + new Vector3(0.03F)); + Vector3 denominator = (color * ((2.43F * color) + new Vector3(0.59F))) + new Vector3(0.14F); + return Vector3.Clamp(numerator / denominator, Vector3.Zero, Vector3.One); + } + + /// + /// Evaluates six octaves of fractal Brownian motion at a procedural coordinate. + /// + /// The procedural noise coordinate. + /// The accumulated noise value. + private static float Fbm(Vector2 position) + { + float value = 0F; + float amplitude = 0.5F; + + // Each octave doubles frequency and halves amplitude. Rotating between + // octaves prevents repeated horizontal and vertical bands in the cloud. + for (int octave = 0; octave < 6; octave++) + { + value += amplitude * ValueNoise(position); + position = (Rotate(position, 0.6435011F) * 2F) + new Vector2(1.7F); + amplitude *= 0.5F; + } + + return value; + } + + /// + /// Evaluates smoothly interpolated two-dimensional value noise. + /// + /// The procedural noise coordinate. + /// The interpolated lattice value. + private static float ValueNoise(Vector2 position) + { + // Interpolate deterministic values assigned to the four lattice corners. + // Cubic Hermite weights remove derivative discontinuities at cell edges. + Vector2 cell = new(MathF.Floor(position.X), MathF.Floor(position.Y)); + Vector2 local = new(Fract(position.X), Fract(position.Y)); + Vector2 blend = local * local * (new Vector2(3F) - (2F * local)); + float lowerLeft = Hash21(cell); + float lowerRight = Hash21(cell + new Vector2(1F, 0F)); + float upperLeft = Hash21(cell + new Vector2(0F, 1F)); + float upperRight = Hash21(cell + Vector2.One); + float lower = lowerLeft + ((lowerRight - lowerLeft) * blend.X); + float upper = upperLeft + ((upperRight - upperLeft) * blend.X); + return lower + ((upper - lower) * blend.Y); + } + + /// + /// Maps a two-dimensional lattice coordinate to a deterministic scalar value. + /// + /// The lattice coordinate. + /// A repeatable value in the range zero to one. + private static float Hash21(Vector2 value) + { + // Wrap before the sine hash so large animated coordinates do not lose the + // fractional precision that differentiates adjacent lattice cells. + Vector2 wrapped = value - new Vector2( + MathF.Floor(value.X / 256F) * 256F, + MathF.Floor(value.Y / 256F) * 256F); + + return Fract(MathF.Sin(Vector2.Dot(wrapped, new Vector2(127.1F, 311.7F))) * 43758.5453F); + } + + /// + /// Evaluates one sparse, independently twinkling procedural star field. + /// + /// The effect-local pixel-center position. + /// The size of each candidate star cell. + /// The animation time in seconds. + /// The stable offset and density selector for this field. + /// The star brightness at the supplied position. + private static float StarLayer(Vector2 position, float scale, float time, float layer) + { + // The hash decides whether a cell contains a star. The higher threshold on + // the larger second layer keeps both apparent depths similarly sparse. + Vector2 starPosition = position / scale; + Vector2 cell = new(MathF.Floor(starPosition.X), MathF.Floor(starPosition.Y)); + float random = Hash21(cell + new Vector2(layer * 13F, layer * 7F)); + if (random <= 0.93F + (layer * 0.02F)) + { + return 0F; + } + + // A stable per-cell jitter avoids a visible grid. Only the intensity is + // animated, so twinkling never changes a star's position or footprint. + Vector2 jitter = new( + Hash21(cell + new Vector2(1.7F + layer, 3.1F)), + Hash21(cell + new Vector2(4.2F, 8.3F + layer))); + + jitter -= new Vector2(0.5F); + Vector2 local = new(Fract(starPosition.X), Fract(starPosition.Y)); + float distance = (local - new Vector2(0.5F) - (jitter * 0.6F)).Length(); + float twinkle = 0.55F + (0.45F * MathF.Sin((time * (2F + (random * 7F))) + (random * 40F))); + float point = 1F - SmoothStep(0F, 0.42F, distance); + return point * twinkle * (0.7F + (0.6F * random)); + } + + /// + /// Rotates a two-dimensional coordinate around the origin. + /// + /// The coordinate to rotate. + /// The clockwise angle in radians. + /// The rotated coordinate. + private static Vector2 Rotate(Vector2 position, float angle) + { + // Keep this explicit two-component rotation identical to the WGSL helper; + // neither path depends on matrix packing or multiplication conventions. + float cosine = MathF.Cos(angle); + float sine = MathF.Sin(angle); + return new Vector2( + (cosine * position.X) - (sine * position.Y), + (sine * position.X) + (cosine * position.Y)); + } + + /// + /// Interpolates between two edges using a clamped cubic Hermite curve. + /// + /// The value mapped to zero. + /// The value mapped to one. + /// The value to interpolate. + /// The smoothed value in the range zero to one. + private static float SmoothStep(float edge0, float edge1, float value) + { + // This is WGSL smoothstep's clamped cubic Hermite interpolation and keeps + // every CPU color boundary aligned with the corresponding shader stage. + float position = Math.Clamp((value - edge0) / (edge1 - edge0), 0F, 1F); + return position * position * (3F - (2F * position)); + } + + /// + /// Extracts the positive fractional component of a floating-point value. + /// + /// The value to split. + /// The fractional component in the range zero to one. + private static float Fract(float value) => value - MathF.Floor(value); + } +} diff --git a/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs b/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs index 68e22d5d7..13483b5c5 100644 --- a/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs +++ b/samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs @@ -5,7 +5,7 @@ using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests; -using SixLabors.ImageSharp.PixelFormats; +using WebGPUExternalSurfaceDemo.Controls; using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes; using Color = SixLabors.ImageSharp.Color; using PointF = SixLabors.ImageSharp.PointF; @@ -57,7 +57,7 @@ public TigerViewerScene() /// Gets a multi-line diagnostic string describing the current zoom, pan, and mouse-in-world values /// so the host can overlay it for debugging. /// - public string StatusText + private string StatusText { get { @@ -117,6 +117,27 @@ public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime) canvas.Restore(); } + /// + protected override Control CreateContent(WebGPURenderControl renderControl) + { + // The diagnostics describe this scene's world transform, so the scene owns the + // overlay behavior. WinForms owns its lifetime through the child-control tree. + Label statusLabel = new() + { + AutoSize = false, + Size = new System.Drawing.Size(350, 100), + BackColor = System.Drawing.Color.FromArgb(160, 0, 0, 0), + ForeColor = System.Drawing.Color.White, + Font = new Font(FontFamily.GenericMonospace, 9F), + Padding = new Padding(6), + Location = new Point(6, 6), + }; + + renderControl.PaintFrame += (_, _) => statusLabel.Text = this.StatusText; + renderControl.Controls.Add(statusLabel); + return renderControl; + } + public override void OnMouseDown(MouseEventArgs e) { if (e.Button == MouseButtons.Left) diff --git a/samples/WebGPUWindowDemo/Program.cs b/samples/WebGPUWindowDemo/Program.cs index 4c090b0bf..a043ae383 100644 --- a/samples/WebGPUWindowDemo/Program.cs +++ b/samples/WebGPUWindowDemo/Program.cs @@ -4,11 +4,11 @@ using System.Diagnostics; using System.Numerics; using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; -using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; using Color = SixLabors.ImageSharp.Color; @@ -45,37 +45,45 @@ private sealed class DemoApp private const int BallCount = 1000; private static readonly TimeSpan FpsUpdateInterval = TimeSpan.FromSeconds(1); private static readonly Brush BackgroundBrush = Brushes.Solid(Color.FromPixel(new Bgra32(30, 30, 40, 255))); - private static readonly Brush TextBrush = Brushes.Solid(Color.FromPixel(new Bgra32(70, 70, 100, 255))); + private static readonly Brush TextBrush = Brushes.Solid(Color.FromPixel(new Bgra32(242, 247, 255, 255))); + private static readonly Brush CyanBrush = Brushes.Solid(Color.FromPixel(new Bgra32(68, 236, 255, 255))); + private static readonly Brush MagentaBrush = Brushes.Solid(Color.FromPixel(new Bgra32(255, 104, 216, 255))); + private static readonly Brush GoldBrush = Brushes.Solid(Color.FromPixel(new Bgra32(255, 218, 92, 255))); + private static readonly Brush MintBrush = Brushes.Solid(Color.FromPixel(new Bgra32(112, 255, 185, 255))); + + // The shader-accelerated acrylic effect blurs the animated scene beneath the document + // before applying the dark tint, preserving visible motion without sacrificing contrast. + private static readonly WebGPUBackdropAcrylicLayerEffect TextBackdropEffect = new( + 2F, + Color.FromPixel(new Bgra32(8, 12, 24, 150))); private readonly WebGPUWindow window; + private readonly DrawingOptions drawingOptions = new(); + private readonly DrawingTextCache textCache = new(); + private readonly TextBlock scrollTextBlock; private readonly Random rng = new(42); private readonly Stopwatch fpsWindow = Stopwatch.StartNew(); private Ball[] balls = []; private int frameCount; - private IPathCollection scrollPaths = new PathCollection(); private float scrollOffset; private float scrollTextHeight; + private float scrollWrappingLength; private const string ScrollText = - "ImageSharp.Drawing on WebGPU\n\n" + - "Real-time GPU-accelerated 2D vector graphics " + - "rendered directly to a native window.\n\n" + - "The canvas API provides a familiar drawing model: " + - "Fill, Draw, DrawText, Clip, and Transform - " + - "all composited on the GPU via compute shaders.\n\n" + - "Text is shaped once using SixLabors.Fonts and " + - "converted to vector paths via TextBuilder. " + - "Each frame simply translates the cached geometry.\n\n" + - "Shapes are rasterized into coverage masks on the " + - "CPU, uploaded to GPU textures, then composited " + - "using a WebGPU compute pipeline that evaluates " + - "Porter-Duff blending per pixel.\n\n" + - "The drawing backend automatically manages texture " + - "atlases, bind groups, and pipeline state for the " + - "native target selected by the window.\n\n" + + "ImageSharp.Drawing on WebGPU\n" + + "RICH TEXT • SHARED CACHE • NATIVE PRESENTATION\n\n" + + "One prepared TextBlock combines bold emphasis, italic asides, " + + "outlined accents, large callouts, and small annotations.\n\n" + + "Decorations can underline links, overline headings, and strike out revisions " + + "without splitting the document into separate draw calls.\n\n" + + "Unicode shaping and font fallback: العربية • עברית • हिंदी • 中文 • 日本語 • 😀\n\n" + + "SixLabors.Fonts shapes the text once. A DrawingTextCache owned by " + + "the application then preserves reusable glyph and run geometry as " + + "new frame canvases scroll the document.\n\n" + + "The ordinary DrawingCanvas API still supplies fills, paths, text, " + + "clipping, transforms, and Porter-Duff compositing.\n\n" + "SixLabors ImageSharp.Drawing\n" + - "github.com/SixLabors/ImageSharp.Drawing\n\n" + - "Built on the WebGPUWindow wrapper."; + "github.com/SixLabors/ImageSharp.Drawing"; /// /// Initializes a new instance of the class. @@ -85,37 +93,26 @@ public DemoApp(WebGPUWindow window) { this.window = window; this.window.Update += this.OnUpdate; + this.scrollTextBlock = CreateScrollTextBlock(); this.InitializeScene(); } /// /// Starts the window-owned render loop for the demo. /// - public void Run() => this.window.Run(this.OnRender); + public void Run() => this.window.Run(this.drawingOptions, this.textCache, this.OnRender); /// /// Builds the one-time scene state used by the demo. /// /// - /// The scrolling text is shaped once up front and reused every frame so the steady-state loop only applies - /// a translation and submits visible paths. + /// The scrolling text is shaped once up front and reused by every frame canvas through one caller-owned cache. /// private void InitializeScene() { - Font scrollFont = SystemFonts.CreateFont("Arial", 24); Size framebufferSize = this.window.FramebufferSize; - TextOptions textOptions = new(scrollFont) - { - Origin = new Vector2(framebufferSize.Width / 2F, 0), - WrappingLength = framebufferSize.Width - 80, - HorizontalAlignment = HorizontalAlignment.Center, - LineSpacing = 1.6F, - }; - - this.scrollPaths = TextBuilder.GeneratePaths(ScrollText, textOptions); - FontRectangle bounds = TextMeasurer.MeasureBounds(ScrollText, textOptions); - FontRectangle size = new(0, 0, bounds.Width, bounds.Height); - this.scrollTextHeight = size.Height; + this.scrollWrappingLength = Math.Max(1F, framebufferSize.Width - 80F); + this.scrollTextHeight = this.scrollTextBlock.MeasureAdvance(this.scrollWrappingLength).Height; Ball[] balls = new Ball[BallCount]; for (int i = 0; i < balls.Length; i++) @@ -177,49 +174,144 @@ private void OnRender(WebGPUSurfaceFrame frame) } /// - /// Draws the cached scrolling text block with simple viewport culling. + /// Draws the prepared scrolling rich-text block. /// /// The destination canvas for the current frame. /// The current framebuffer width. /// The current framebuffer height. private void DrawScrollingText(DrawingCanvas canvas, int width, int height) { - if (this.scrollTextHeight <= 0) + float wrappingLength = Math.Max(1F, width - 80F); + if (wrappingLength != this.scrollWrappingLength) { - return; + // TextBlock retains shaping, so a resize only recomputes line breaking and height; + // it does not reshape the document or discard the shared glyph/run cache. + this.scrollWrappingLength = wrappingLength; + this.scrollTextHeight = this.scrollTextBlock.MeasureAdvance(wrappingLength).Height; } float totalCycle = height + this.scrollTextHeight; float wrappedOffset = this.scrollOffset % totalCycle; float y = height - wrappedOffset; - Matrix3x2 translation = Matrix3x2.CreateTranslation(0, y); - RectangleF viewport = new(0, 0, width, height); - DrawingOptions translatedOptions = new() + // Limit the acrylic work to the visible part of the scrolling document. The extra + // padding keeps blur samples around the first and last glyph rows inside the layer. + Rectangle layerBounds = Rectangle.Intersect( + canvas.Bounds, + Rectangle.Round(new RectangleF(24F, y - 16F, width - 48F, this.scrollTextHeight + 32F))); + + canvas.SaveLayer(this.drawingOptions.GraphicsOptions, layerBounds, TextBackdropEffect); + + // Rich runs supply their own paint where specified; TextBrush paints the remaining body. + // The cache keys run geometry in local space, so changing y does not invalidate it. + canvas.DrawText(this.scrollTextBlock, new PointF(40F, y), wrappingLength, TextBrush, pen: null); + canvas.Restore(); + } + + /// + /// Creates the prepared rich-text document reused throughout the render loop. + /// + /// The prepared text block. + private static TextBlock CreateScrollTextBlock() + { + Font bodyFont = SystemFonts.CreateFont("Arial", 24); + Font titleFont = bodyFont.Family.CreateFont(44, FontStyle.Bold); + Font boldFont = bodyFont.Family.CreateFont(24, FontStyle.Bold); + Font italicFont = bodyFont.Family.CreateFont(24, FontStyle.Italic); + Font calloutFont = bodyFont.Family.CreateFont(30, FontStyle.Bold); + Font annotationFont = bodyFont.Family.CreateFont(18, FontStyle.Italic); + + List fallbackFontFamilies = []; + AddFallbackFont(fallbackFontFamilies, bodyFont.Family, new CodePoint('ع')); + AddFallbackFont(fallbackFontFamilies, bodyFont.Family, new CodePoint('ע')); + AddFallbackFont(fallbackFontFamilies, bodyFont.Family, new CodePoint('ह')); + AddFallbackFont(fallbackFontFamilies, bodyFont.Family, new CodePoint('中')); + AddFallbackFont(fallbackFontFamilies, bodyFont.Family, new CodePoint(0x1F600)); + + RichTextRun title = CreateTextRun("ImageSharp.Drawing on WebGPU", titleFont, CyanBrush); + title.Pen = Pens.Solid(Color.FromPixel(new Bgra32(5, 24, 42, 255)), 2.25F); + + RichTextRun subtitle = CreateTextRun("RICH TEXT • SHARED CACHE • NATIVE PRESENTATION", boldFont, GoldBrush); + subtitle.TextDecorations = TextDecorations.Overline; + subtitle.OverlinePen = Pens.Solid(Color.FromPixel(new Bgra32(255, 218, 92, 255)), 2F); + + RichTextRun bold = CreateTextRun("bold emphasis", boldFont, CyanBrush); + RichTextRun italic = CreateTextRun("italic asides", italicFont, MagentaBrush); + RichTextRun outlined = CreateTextRun("outlined accents", boldFont, BackgroundBrush); + outlined.Pen = Pens.Solid(Color.FromPixel(new Bgra32(255, 218, 92, 255)), 2.25F); + + RichTextRun callout = CreateTextRun("large callouts", calloutFont, GoldBrush); + RichTextRun annotation = CreateTextRun("small annotations", annotationFont, MagentaBrush); + + RichTextRun underline = CreateTextRun("underline links", boldFont, CyanBrush); + underline.TextDecorations = TextDecorations.Underline; + underline.UnderlinePen = Pens.Solid(Color.FromPixel(new Bgra32(68, 236, 255, 255)), 2F); + + RichTextRun overline = CreateTextRun("overline headings", boldFont, GoldBrush); + overline.TextDecorations = TextDecorations.Overline; + overline.OverlinePen = Pens.Solid(Color.FromPixel(new Bgra32(255, 218, 92, 255)), 2F); + + RichTextRun strikeout = CreateTextRun("strike out revisions", italicFont, MagentaBrush); + strikeout.TextDecorations = TextDecorations.Strikeout; + strikeout.StrikeoutPen = Pens.Solid(Color.FromPixel(new Bgra32(255, 104, 216, 255)), 2F); + + RichTextRun fallback = CreateTextRun("العربية • עברית • हिंदी • 中文 • 日本語 • 😀", boldFont, MintBrush); + + RichTextRun link = CreateTextRun("github.com/SixLabors/ImageSharp.Drawing", boldFont, CyanBrush); + link.TextDecorations = TextDecorations.Underline; + link.UnderlinePen = Pens.Solid(Color.FromPixel(new Bgra32(86, 225, 255, 255)), 2F); + + RichTextOptions options = new(bodyFont) { - Transform = new Matrix4x4(translation), + FallbackFontFamilies = fallbackFontFamilies, + LineSpacing = 1.5F, + TextRuns = [title, subtitle, bold, italic, outlined, callout, annotation, underline, overline, strikeout, fallback, link], }; - // Save once with the frame-local translation, then submit only paths that are actually visible. - canvas.Save(translatedOptions); - foreach (IPath path in this.scrollPaths) + return new TextBlock(ScrollText, options); + } + + /// + /// Adds the installed system family selected by the platform text matcher for a representative character. + /// + /// The ordered fallback list being built. + /// The primary family already used by the document. + /// A character requiring coverage from the selected fallback family. + private static void AddFallbackFont(List fallbackFontFamilies, FontFamily primaryFamily, CodePoint codePoint) + { + // TextOptions consumes an explicit fallback list; it does not invoke the platform + // matcher while shaping. Resolve representative characters once so DirectWrite, + // CoreText, or Fontconfig can choose installed families appropriate to this machine. + if (SystemFonts.TryMatchCharacter(codePoint, FontStyle.Regular, primaryFamily.Name, culture: null, out FontMatch match) && + !fallbackFontFamilies.Contains(match.Family)) { - RectangleF pathBounds = path.Bounds; - RectangleF translated = new( - pathBounds.X + translation.M31, - pathBounds.Y + translation.M32, - pathBounds.Width, - pathBounds.Height); - - if (!viewport.IntersectsWith(translated)) - { - continue; - } - - canvas.Fill(TextBrush, path); + fallbackFontFamilies.Add(match.Family); } + } - canvas.Restore(); + /// + /// Creates a rich-text run for the unique marker text within . + /// + /// The text covered by the run. + /// The font applied to the run. + /// The brush applied to the run. + /// The configured rich-text run. + private static RichTextRun CreateTextRun(string marker, Font font, Brush brush) + { + int stringStart = ScrollText.IndexOf(marker, StringComparison.Ordinal); + + // RichTextRun uses grapheme indices rather than UTF-16 offsets. Computing both bounds + // through Fonts keeps the markers correct when the sample contains bullets or other + // multi-code-unit user-perceived characters. + int start = ScrollText.AsSpan(0, stringStart).GetGraphemeCount(); + + return new RichTextRun + { + Start = start, + End = start + marker.GetGraphemeCount(), + Font = font, + Brush = brush, + }; } } diff --git a/samples/WebGPUWindowDemo/README.md b/samples/WebGPUWindowDemo/README.md index f3199a502..80011afe3 100644 --- a/samples/WebGPUWindowDemo/README.md +++ b/samples/WebGPUWindowDemo/README.md @@ -9,7 +9,7 @@ It exists to show the intended shape of a real-time app: - draw with the normal `DrawingCanvas` API - present by ending the acquired frame -The sample opens an `800x600` window, draws a dark background, animates 1000 bouncing ellipses, scrolls a block of pre-shaped text, and updates the window title with frame timing statistics. +The sample opens an `800x600` window, draws a dark background, animates 1000 bouncing ellipses, scrolls a prepared rich-text document, and updates the window title with frame timing statistics. ## Why this sample matters @@ -38,7 +38,7 @@ When the sample starts you should see: - a native window titled `ImageSharp.Drawing WebGPU Demo` - animated semi-transparent balls bouncing around the viewport -- a large scrolling text block in the background +- a high-contrast scrolling rich-text document over a shader-accelerated frosted acrylic backdrop, with multiple sizes, bold and italic runs, multilingual fallback text, fills, outlines, and underline/overline/strikeout pens - the title bar updating once per second with current frame time, current FPS, mean FPS, and FPS standard deviation ## Code Tour @@ -72,18 +72,16 @@ Important details: - the window reference - a deterministic `Random` - the `Ball[]` animation state -- cached text paths +- one prepared `TextBlock` and a caller-owned `DrawingTextCache` shared across frames - FPS accumulation state `InitializeScene()` does the expensive one-time work: -- creates an `Arial` font at 24px -- builds `TextOptions` using the current framebuffer width -- shapes the scrolling text once with `TextBuilder.GeneratePaths(...)` -- measures the total text height with `TextMeasurer.MeasureSize(...)` +- creates a prepared `TextBlock` with several `RichTextRun` entries +- measures its initial wrapped height - creates 1000 random balls sized and positioned for the current framebuffer -The important pattern here is that text shaping is not done every frame. The sample converts the whole text block into vector paths once, then reuses that geometry as the text scrolls. +The important pattern is that text shaping is not done every frame. `DemoApp` also owns one `DrawingTextCache` and passes it to the window loop, allowing each short-lived frame canvas to reuse glyph and run geometry from previous frames. ### 3. Update loop @@ -106,7 +104,7 @@ Separating animation from rendering keeps the sample structure close to a normal `Run()` calls: ```csharp -this.window.Run(this.OnRender); +this.window.Run(this.drawingOptions, this.textCache, this.OnRender); ``` `WebGPUWindow.Run(...)` acquires one `WebGPUSurfaceFrame` per render callback and disposes it automatically after your callback returns. In this sample that means you do not call `Flush()` yourself. @@ -115,8 +113,8 @@ Inside `OnRender(...)` the sample: 1. grabs `DrawingCanvas canvas = frame.Canvas` 2. fills the full frame with a solid background color -3. draws the scrolling text block -4. fills one ellipse per ball +3. fills one ellipse per ball +4. draws the scrolling text block inside a clipped `WebGPUBackdropAcrylicLayerEffect` 5. updates the window title once per second with timing statistics The drawing code is intentionally plain `DrawingCanvas` API usage: @@ -127,20 +125,11 @@ The drawing code is intentionally plain `DrawingCanvas` API usage: That is the point of the sample: the WebGPU path should feel like normal ImageSharp.Drawing usage, not a separate graphics API. -### 5. Scrolling text path reuse +### 5. Prepared rich text and shared geometry `DrawScrollingText(...)` shows the most important optimization in the sample. -Instead of rebuilding glyphs every frame, it: - -- computes a wrapped vertical scroll offset -- builds a translation matrix for the current frame -- saves a transformed canvas state with `canvas.Save(translatedOptions)` -- culls any path whose translated bounds are outside the viewport -- fills only the visible paths -- restores the prior canvas state with `canvas.Restore()` - -The culling is simple but effective: large amounts of off-screen text never get submitted for rasterization. +Instead of reshaping text or rebuilding glyph paths every frame, it computes the wrapped vertical position and draws the prepared `TextBlock`. The text is isolated in a layer clipped to its visible bounds; `WebGPUBackdropAcrylicLayerEffect` blurs and tints the animated balls beneath that layer before the sharp text is composited over them. Rich runs demonstrate size and style changes, multilingual fallback, independent fills and outlines, and all three text decorations within one document. The caller-owned cache is deliberately independent of frame lifetime, so disposing a presented frame does not discard reusable text geometry. ## Frame lifetime and rendering @@ -191,15 +180,13 @@ Notes: ## Resize behavior -The sample builds the scrolling text layout once from the startup framebuffer size. That keeps the demo simple and avoids reshaping text during the steady-state render loop. +The sample shapes the scrolling text once. A resize changes only the wrapping length and measured height; it does not reshape the document. As a result: - the animation keeps working after resize because balls update against the current framebuffer size - the text continues to render -- the text wrapping width is based on the initial framebuffer width, not a reflowed width after resize - -That tradeoff is acceptable for a demo because the sample is trying to show rendering flow, cached path reuse, and frame presentation rather than full responsive layout management. +- the rich text reflows to the current framebuffer width ## Files diff --git a/shared-infrastructure b/shared-infrastructure index 7ac570345..74b7f32b8 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 7ac5703452348d9295db31fc0912c2bd9e419dc9 +Subproject commit 74b7f32b8e41fdf8fe2f3eda54fd5a82ebbedfbc diff --git a/src/ImageSharp.Drawing.WebGPU/.config/dotnet-tools.json b/src/ImageSharp.Drawing.WebGPU/.config/dotnet-tools.json new file mode 100644 index 000000000..69951e8cd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "clangsharppinvokegenerator": { + "version": "21.1.8.3", + "commands": [ + "ClangSharpPInvokeGenerator" + ] + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/BufferUsage.cs b/src/ImageSharp.Drawing.WebGPU/BufferUsage.cs new file mode 100644 index 000000000..608ee4717 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/BufferUsage.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes how a WebGPU buffer may be used. +/// +[Flags] +internal enum BufferUsage : ulong +{ + /// + /// The buffer has no permitted usage. + /// + None = 0, + + /// + /// The buffer can be mapped for CPU reads. + /// + MapRead = 1, + + /// + /// The buffer can be mapped for CPU writes. + /// + MapWrite = 2, + + /// + /// The buffer can be the source of a copy operation. + /// + CopySrc = 4, + + /// + /// The buffer can be the destination of a copy operation. + /// + CopyDst = 8, + + /// + /// The buffer can supply index data. + /// + Index = 16, + + /// + /// The buffer can supply vertex data. + /// + Vertex = 32, + + /// + /// The buffer can supply uniform data. + /// + Uniform = 64, + + /// + /// The buffer can be bound as storage. + /// + Storage = 128, + + /// + /// The buffer can supply indirect command arguments. + /// + Indirect = 256, + + /// + /// The buffer can receive resolved query results. + /// + QueryResolve = 512 +} diff --git a/src/ImageSharp.Drawing.WebGPU/ColorWriteMask.cs b/src/ImageSharp.Drawing.WebGPU/ColorWriteMask.cs new file mode 100644 index 000000000..a221eaad7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/ColorWriteMask.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies the color channels written by a render target. +/// +[Flags] +internal enum ColorWriteMask : ulong +{ + /// + /// No color channel is written. + /// + None = 0, + + /// + /// The red channel is written. + /// + Red = 1, + + /// + /// The green channel is written. + /// + Green = 2, + + /// + /// The blue channel is written. + /// + Blue = 4, + + /// + /// The alpha channel is written. + /// + Alpha = 8, + + /// + /// Every color channel is written. + /// + All = Red | Green | Blue | Alpha +} diff --git a/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawMonoid.cs b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawMonoid.cs new file mode 100644 index 000000000..a4647fcfb --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawMonoid.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Additive monoid scanned over the draw-tag stream to derive scene offsets for later stages. +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct GpuSceneDrawMonoid +{ + /// + /// Initializes a new instance of the struct. + /// + /// The path index increment. + /// The clip index increment. + /// The scene-data offset increment. + /// The info-data offset increment. + public GpuSceneDrawMonoid(uint pathIndex, uint clipIndex, uint sceneOffset, uint infoOffset) + { + this.PathIndex = pathIndex; + this.ClipIndex = clipIndex; + this.SceneOffset = sceneOffset; + this.InfoOffset = infoOffset; + } + + /// + /// Gets the path index increment. + /// + public uint PathIndex { get; } + + /// + /// Gets the clip index increment. + /// + public uint ClipIndex { get; } + + /// + /// Gets the scene-data offset increment. + /// + public uint SceneOffset { get; } + + /// + /// Gets the info-data offset increment. + /// + public uint InfoOffset { get; } + + /// + /// Combines two draw monoids by adding each offset/count component independently. + /// + /// The first draw monoid. + /// The second draw monoid. + /// The combined draw monoid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuSceneDrawMonoid Combine(in GpuSceneDrawMonoid a, in GpuSceneDrawMonoid b) + => new( + a.PathIndex + b.PathIndex, + a.ClipIndex + b.ClipIndex, + a.SceneOffset + b.SceneOffset, + a.InfoOffset + b.InfoOffset); +} diff --git a/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs new file mode 100644 index 000000000..53f3073fd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/GpuSceneDrawTag.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Encoded draw-tag constants matching the staged-scene shader contract. +/// +internal static class GpuSceneDrawTag +{ + // These values are not a plain enum because each word also encodes the path-count, clip-count, + // scene-word-count, and info-word-count increments consumed by the scan/reduction stages. + // Visible-fill tags carry five extra info words: coverage threshold plus raster interest. Must match drawtag.wgsl. + public const uint Nop = 0U; + public const uint FillColor = 0x188U; + public const uint FillRecolor = 0x184U; + public const uint FillLinGradient = 0x254U; + public const uint FillRadGradient = 0x3DCU; + public const uint FillEllipticGradient = 0x35CU; + public const uint FillSweepGradient = 0x394U; + public const uint FillPathGradient = 0x190U; + public const uint FillImage = 0x3D4U; + public const uint BeginClip = 0x49U; + public const uint EndClip = 0x21U; + public const uint FillInfoFlagsFillRuleBit = 1U; + public const uint FillInfoFlagsAliasedBit = 0x40000000U; + + /// + /// Decodes one packed draw tag into the additive monoid scanned by later scheduling passes. + /// + /// The packed draw tag word. + /// The decoded draw monoid. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static GpuSceneDrawMonoid Map(uint tagWord) + => new( + tagWord != Nop ? 1U : 0U, + tagWord & 1U, + (tagWord >> 2) & 0x07U, + (tagWord >> 6) & 0x0FU); +} diff --git a/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffect.cs b/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffect.cs new file mode 100644 index 000000000..17b1b9e6c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffect.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies a layer effect that supplies a native WebGPU implementation and a CPU fallback. +/// +/// +/// Derive from or to implement +/// this contract. Pass an instance to to compile +/// its pipelines before first use. +/// +public interface IWebGPUShaderEffect +{ +} diff --git a/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffectSource.cs b/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffectSource.cs new file mode 100644 index 000000000..64e25177a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/IWebGPUShaderEffectSource.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Exposes the internal pass sequence owned by a public shader-effect base class. +/// +internal interface IWebGPUShaderEffectSource +{ + /// + /// Gets the ordered shader passes. + /// + /// The passes in execution order. + public ReadOnlySpan GetShaderPasses(); +} diff --git a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj index e3f180f55..e6fada2a3 100644 --- a/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj +++ b/src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj @@ -23,9 +23,8 @@ true $(WarningsNotAsErrors);8002;NU1900 @@ -49,24 +48,35 @@ + + + <_WebGPUNativeRuntimeIdentifier Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier) + <_WebGPUNativeRuntimeIdentifier Condition="'$(_WebGPUNativeRuntimeIdentifier)' == ''">$(NETCoreSdkRuntimeIdentifier) + + - + + + + + + - - - + @@ -80,4 +90,17 @@ + + + + + + + + + + diff --git a/src/ImageSharp.Drawing.WebGPU/MacOSMetalLayerFactory.cs b/src/ImageSharp.Drawing.WebGPU/MacOSMetalLayerFactory.cs new file mode 100644 index 000000000..95b2fc694 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/MacOSMetalLayerFactory.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Attaches a Core Animation metal layer to a Cocoa window for WebGPU presentation. +/// +internal static partial class MacOSMetalLayerFactory +{ + /// + /// The Objective-C runtime library used to construct and attach the Core Animation layer. + /// + private const string ObjectiveCLibrary = "/usr/lib/libobjc.A.dylib"; + + /// + /// Creates and attaches a metal layer to a Cocoa window. + /// + /// The Cocoa NSWindow*. + /// The attached CAMetalLayer*. + /// + /// The returned layer carries the ownership acquired by alloc/init. Call after + /// wgpu-native has created its surface; the AppKit content view retains the attached layer independently. + /// + public static nint Create(nint nsWindow) + { + nint metalLayerClass = GetClass("CAMetalLayer"); + nint metalLayer = SendPointer(SendPointer(metalLayerClass, RegisterSelector("alloc")), RegisterSelector("init")); + nint contentView = SendPointer(nsWindow, RegisterSelector("contentView")); + + // WebGPU presents through a CAMetalLayer. AppKit creates a default backing layer only + // after wantsLayer is enabled, so install the explicitly-created metal layer afterwards. + SendByte(contentView, RegisterSelector("setWantsLayer:"), 1); + SendPointer(contentView, RegisterSelector("setLayer:"), metalLayer); + return metalLayer; + } + + /// + /// Releases the factory's ownership of a metal layer after its AppKit view has retained it. + /// + /// The CAMetalLayer* to release. + public static void Release(nint metalLayer) + => SendVoid(metalLayer, RegisterSelector("release")); + + /// + /// Resolves an Objective-C class by name. + /// + /// The Objective-C class name. + /// The Objective-C class object. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "objc_getClass", StringMarshalling = StringMarshalling.Utf8)] + private static partial nint GetClass(string name); + + /// + /// Registers an Objective-C selector by name. + /// + /// The selector name. + /// The registered selector. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "sel_registerName", StringMarshalling = StringMarshalling.Utf8)] + private static partial nint RegisterSelector(string name); + + /// + /// Sends a parameterless Objective-C message that returns an object pointer. + /// + /// The receiving object or class. + /// The selector to invoke. + /// The returned object pointer. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "objc_msgSend")] + private static partial nint SendPointer(nint receiver, nint selector); + + /// + /// Sends an Objective-C message with one object-pointer argument and no return value. + /// + /// The receiving object. + /// The selector to invoke. + /// The object pointer passed to the selector. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "objc_msgSend")] + private static partial void SendPointer(nint receiver, nint selector, nint value); + + /// + /// Sends an Objective-C message with one Boolean-sized argument and no return value. + /// + /// The receiving object. + /// The selector to invoke. + /// The Objective-C BOOL value passed to the selector. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "objc_msgSend")] + private static partial void SendByte(nint receiver, nint selector, byte value); + + /// + /// Sends a parameterless Objective-C message with no return value. + /// + /// The receiving object. + /// The selector to invoke. + [LibraryImport(ObjectiveCLibrary, EntryPoint = "objc_msgSend")] + private static partial void SendVoid(nint receiver, nint selector); +} diff --git a/src/ImageSharp.Drawing.WebGPU/MapMode.cs b/src/ImageSharp.Drawing.WebGPU/MapMode.cs new file mode 100644 index 000000000..d671d6f61 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/MapMode.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes the CPU access requested when mapping a WebGPU buffer. +/// +[Flags] +internal enum MapMode : ulong +{ + /// + /// No CPU mapping access is requested. + /// + None = 0, + + /// + /// CPU read access is requested. + /// + Read = 1, + + /// + /// CPU write access is requested. + /// + Write = 2 +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Download-WebGPUNative.ps1 b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Download-WebGPUNative.ps1 new file mode 100644 index 000000000..88d69ee4a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Download-WebGPUNative.ps1 @@ -0,0 +1,258 @@ +<# +.SYNOPSIS +Downloads the native libraries and headers used by the WebGPU backend. + +.DESCRIPTION +Downloads the configured official wgpu-native release archives, verifies each archive against its +published SHA-256 digest, and copies the native libraries into the project's runtimes directory +using standard .NET runtime identifiers. The configured Microsoft DirectX Shader Compiler package +supplies the Windows shader compiler runtime. Library filenames remain unchanged from upstream. + +The Windows x64 archive supplies the checked-in webgpu.h and wgpu.h files used for binding +generation. This is safe because every platform archive for the configured release contains +identical headers. + +Downloaded archives and extracted files are cached beneath obj/WebGPUNative. They are generation +inputs only and are not included in packages. + +.EXAMPLE +dotnet msbuild ImageSharp.Drawing.WebGPU.csproj -t:DownloadWebGPUNative -p:Configuration=Release + +.NOTES +Run the script through the DownloadWebGPUNative MSBuild target so its repository entry point stays +consistent with binding generation. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$releaseVersion = "v29.0.1.1" +$releaseBaseUri = "https://github.com/gfx-rs/wgpu-native/releases/download/$releaseVersion" +$dxcPackageVersion = "1.8.2505.32" +$dxcPackageSha256 = "C6E82B70C14552F1DD58E4A79C93EEAB1567EEB0A9EE63A51564C410429BCE3E" +$dxcPackageName = "microsoft.direct3d.dxc.$dxcPackageVersion.nupkg" +$dxcPackageUri = "https://api.nuget.org/v3-flatcontainer/microsoft.direct3d.dxc/$dxcPackageVersion/$dxcPackageName" +$projectDirectory = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$cacheDirectory = Join-Path $projectDirectory "obj\WebGPUNative\$releaseVersion" +$dxcCacheDirectory = Join-Path $projectDirectory "obj\WebGPUNative\DXC\$dxcPackageVersion" +$runtimeDirectory = Join-Path $projectDirectory "runtimes" +$headerDirectory = Join-Path $PSScriptRoot "Headers" + +# GitHub's release architecture names differ from .NET runtime identifiers. This table is also the +# authoritative supported-runtime matrix for the downloaded assets. Digests are the SHA-256 values +# published for the configured release. +$assets = @( + @{ + RuntimeIdentifier = "win-x64" + ArchiveName = "wgpu-windows-x86_64-msvc-release.zip" + LibraryName = "wgpu_native.dll" + Sha256 = "7e67d7445c42aeb85e30f88930fd8d7d83ee769e3390aeb1ada75ebf3cf78132" + }, + @{ + RuntimeIdentifier = "win-x86" + ArchiveName = "wgpu-windows-i686-msvc-release.zip" + LibraryName = "wgpu_native.dll" + Sha256 = "ad59d4eadfcfe667999a37e096cc551ecf3f56c387b5a7fd5f61baebf105f54a" + }, + @{ + RuntimeIdentifier = "win-arm64" + ArchiveName = "wgpu-windows-aarch64-msvc-release.zip" + LibraryName = "wgpu_native.dll" + Sha256 = "4a876421a8c1e5fe72f849b3722214280fe485cb1c56f77f8b0c82414be5b29f" + }, + @{ + RuntimeIdentifier = "linux-x64" + ArchiveName = "wgpu-linux-x86_64-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "95a4d90c071005a98d03eab348beaa6b07e16eb00d1dcdb9f8348f75eb97ec5a" + }, + @{ + RuntimeIdentifier = "linux-arm64" + ArchiveName = "wgpu-linux-aarch64-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "015fcdf1dbae82e614a783cc38017e5399ae0927a889fe9b69c9b664bc61b47a" + }, + @{ + RuntimeIdentifier = "osx-x64" + ArchiveName = "wgpu-macos-x86_64-release.zip" + LibraryName = "libwgpu_native.dylib" + Sha256 = "8e2f7378548ddd0e2cf21e7d864dda46e953f0af724855a33778b85ead206d41" + }, + @{ + RuntimeIdentifier = "osx-arm64" + ArchiveName = "wgpu-macos-aarch64-release.zip" + LibraryName = "libwgpu_native.dylib" + Sha256 = "a5797a37b1adf720bcd5dcffb291edbbd5b7b14be0a3874c28e6393a655a7a3e" + }, + @{ + RuntimeIdentifier = "android-x64" + ArchiveName = "wgpu-android-x86_64-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "ef16fc0644bf0e308a39ac4516742da8e22d8c201d3a542cc5baf533d272c491" + }, + @{ + RuntimeIdentifier = "android-x86" + ArchiveName = "wgpu-android-i686-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "593b94875bc4fcc1506ea0b6714dd12b96b7c852921caa63f45eb61517793312" + }, + @{ + RuntimeIdentifier = "android-arm64" + ArchiveName = "wgpu-android-aarch64-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "721741f1b05a20c1738166bedf7a5efb2ba4b382da689526d3fc33de22bdd573" + }, + @{ + RuntimeIdentifier = "android-arm" + ArchiveName = "wgpu-android-armv7-release.zip" + LibraryName = "libwgpu_native.so" + Sha256 = "f9d76c77b3fda3f7121476884eb16ec067f7dada83276298a3cc8bf6a8403d60" + }, + @{ + RuntimeIdentifier = "ios-arm64" + ArchiveName = "wgpu-ios-aarch64-release.zip" + LibraryName = "libwgpu_native.a" + Sha256 = "e36c9913b9e5095a530fa9121c50b16a4e3dd020e1eebf601f2f47ce24d56941" + }, + @{ + RuntimeIdentifier = "iossimulator-arm64" + ArchiveName = "wgpu-ios-aarch64-simulator-release.zip" + LibraryName = "libwgpu_native.a" + Sha256 = "750e706765bef3744313745194774d095c916fc21d2a0e7d4d7b0bc4d0c92789" + }, + @{ + RuntimeIdentifier = "iossimulator-x64" + ArchiveName = "wgpu-ios-x86_64-simulator-release.zip" + LibraryName = "libwgpu_native.a" + Sha256 = "94f67e1b268e8dd31b8e59b32f211ac469f09ed7950fceee52bd84f0623da3d9" + }) + +New-Item -ItemType Directory -Path $cacheDirectory -Force | Out-Null +New-Item -ItemType Directory -Path $dxcCacheDirectory -Force | Out-Null + +foreach ($asset in $assets) +{ + $archivePath = Join-Path $cacheDirectory $asset.ArchiveName + $archiveUri = "$releaseBaseUri/$($asset.ArchiveName)" + + # A cached archive is reusable only when it matches the release digest. A missing or damaged + # cache entry is downloaded again before extraction. + $archiveIsCurrent = (Test-Path $archivePath) -and + ((Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash -eq $asset.Sha256) + + if (!$archiveIsCurrent) + { + Write-Host "Downloading $($asset.ArchiveName)" + Invoke-WebRequest -Uri $archiveUri -OutFile $archivePath -MaximumRedirection 5 + } + + $actualHash = (Get-FileHash -LiteralPath $archivePath -Algorithm SHA256).Hash + + # Archive contents are an external boundary. Reject a digest mismatch before any native file + # can enter the runtime asset tree. + if ($actualHash -ne $asset.Sha256) + { + throw "SHA-256 verification failed for '$($asset.ArchiveName)'." + } + + $extractDirectory = Join-Path $cacheDirectory $asset.RuntimeIdentifier + New-Item -ItemType Directory -Path $extractDirectory -Force | Out-Null + Expand-Archive -LiteralPath $archivePath -DestinationPath $extractDirectory -Force + + $libraryCandidates = @(Get-ChildItem -LiteralPath $extractDirectory -Recurse -File -Filter $asset.LibraryName) + + # Each official archive must contain exactly one runtime library with the upstream name. This + # prevents an archive layout change from silently selecting an import library or stale file. + if ($libraryCandidates.Count -ne 1) + { + throw "Expected exactly one '$($asset.LibraryName)' in '$($asset.ArchiveName)' but found $($libraryCandidates.Count)." + } + + $runtimeAssetDirectory = Join-Path $runtimeDirectory "$($asset.RuntimeIdentifier)\native" + New-Item -ItemType Directory -Path $runtimeAssetDirectory -Force | Out-Null + Copy-Item -LiteralPath $libraryCandidates[0].FullName -Destination (Join-Path $runtimeAssetDirectory $asset.LibraryName) -Force +} + +# Every platform archive for the configured release contains the same C headers, so the verified +# Windows x64 archive is the single source for binding generation. +$windowsHeaderRoot = Join-Path $cacheDirectory "win-x64\include\webgpu" +$headerNames = @("webgpu.h", "wgpu.h") + +foreach ($headerName in $headerNames) +{ + $headerSource = Join-Path $windowsHeaderRoot $headerName + + if (!(Test-Path $headerSource)) + { + throw "The verified Windows x64 archive does not contain '$headerName' at the expected path." + } + + Copy-Item -LiteralPath $headerSource -Destination (Join-Path $headerDirectory $headerName) -Force +} + +$dxcPackagePath = Join-Path $dxcCacheDirectory $dxcPackageName +$dxcPackageIsCurrent = (Test-Path $dxcPackagePath) -and + ((Get-FileHash -LiteralPath $dxcPackagePath -Algorithm SHA256).Hash -eq $dxcPackageSha256) + +if (!$dxcPackageIsCurrent) +{ + Write-Host "Downloading $dxcPackageName" + Invoke-WebRequest -Uri $dxcPackageUri -OutFile $dxcPackagePath -MaximumRedirection 5 +} + +$actualDxcPackageHash = (Get-FileHash -LiteralPath $dxcPackagePath -Algorithm SHA256).Hash + +# The compiler binaries execute inside the application process, so reject a package whose content +# does not match the configured Microsoft package before copying any file into the runtime tree. +if ($actualDxcPackageHash -ne $dxcPackageSha256) +{ + throw "SHA-256 verification failed for '$dxcPackageName'." +} + +$dxcExtractDirectory = Join-Path $dxcCacheDirectory "package" +New-Item -ItemType Directory -Path $dxcExtractDirectory -Force | Out-Null + +# NuGet packages are ZIP archives with a different extension. Use the framework ZIP API so the +# downloader does not need to rename the verified package or depend on NuGet tooling being present. +[System.IO.Compression.ZipFile]::ExtractToDirectory($dxcPackagePath, $dxcExtractDirectory, $true) + +$dxcRuntimeAssets = @( + @{ + RuntimeIdentifier = "win-x64" + PackageArchitecture = "x64" + }, + @{ + RuntimeIdentifier = "win-x86" + PackageArchitecture = "x86" + }, + @{ + RuntimeIdentifier = "win-arm64" + PackageArchitecture = "arm64" + }) + +$dxcLibraryNames = @("dxcompiler.dll", "dxil.dll") + +foreach ($dxcRuntimeAsset in $dxcRuntimeAssets) +{ + $dxcPackageBinDirectory = Join-Path $dxcExtractDirectory "build\native\bin\$($dxcRuntimeAsset.PackageArchitecture)" + $runtimeAssetDirectory = Join-Path $runtimeDirectory "$($dxcRuntimeAsset.RuntimeIdentifier)\native" + New-Item -ItemType Directory -Path $runtimeAssetDirectory -Force | Out-Null + + foreach ($dxcLibraryName in $dxcLibraryNames) + { + $dxcLibrarySource = Join-Path $dxcPackageBinDirectory $dxcLibraryName + + # The configured compiler package contract supplies both the compiler and its validator for + # each supported Windows architecture; a partial runtime would fail only after deployment. + if (!(Test-Path $dxcLibrarySource)) + { + throw "The verified DXC package does not contain '$dxcLibraryName' for '$($dxcRuntimeAsset.PackageArchitecture)'." + } + + Copy-Item -LiteralPath $dxcLibrarySource -Destination (Join-Path $runtimeAssetDirectory $dxcLibraryName) -Force + } +} + +Write-Host "Downloaded and verified $($assets.Count) wgpu-native runtime assets for $releaseVersion and the configured DXC runtime." diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generate-WebGPUBindings.ps1 b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generate-WebGPUBindings.ps1 new file mode 100644 index 000000000..8c39263af --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generate-WebGPUBindings.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS +Regenerates the internal C# bindings for the WebGPU backend. + +.DESCRIPTION +Restores the repository-pinned ClangSharp generator and translates the checked-in wgpu-native +headers into Native/Bindings/Generated/WebGPUNative.g.cs. The script never downloads or replaces +headers, so updating the native API remains a separate and reviewable source change. + +On Windows, ClangSharp also needs the C standard-library headers supplied by MSVC and the Windows +SDK. The script discovers those installations and passes their include directories only to +libclang; machine-specific paths are not written to the generated source. + +.EXAMPLE +dotnet msbuild ImageSharp.Drawing.WebGPU.csproj -t:GenerateWebGPUBindings -p:Configuration=Release + +.NOTES +Run the script through the GenerateWebGPUBindings MSBuild target so the documented repository +entry point and the generator use the same project working directory. +#> +[CmdletBinding()] +param() + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +# Paths in generate.rsp are project-relative. Resolve the project from this script rather than +# relying on the caller's current directory. +$projectDirectory = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$toolManifestPath = Join-Path $projectDirectory ".config\dotnet-tools.json" + +# Keep stable generator options in the response file so the command line, reviewable settings, +# and local regeneration path cannot drift apart. +$generatorArguments = @( + "tool", + "run", + "ClangSharpPInvokeGenerator", + "@Native/Bindings/generate.rsp") + +$runningOnWindows = [System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform( + [System.Runtime.InteropServices.OSPlatform]::Windows) + +if ($runningOnWindows) +{ + # libclang does not ship the Microsoft C standard-library headers. The official Windows + # wgpu-native headers transitively include stdint.h, stddef.h, and math.h, so generation must + # parse them using the installed MSVC and Windows SDK definitions. + $vsWherePath = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" + + if (-not (Test-Path $vsWherePath)) + { + throw "Visual Studio Installer's vswhere.exe is required to locate the MSVC headers." + } + + # Ask Visual Studio Installer for the newest installation that actually contains the C++ + # toolchain instead of assuming an edition-specific installation directory. + $visualStudioDirectory = & $vsWherePath ` + -latest ` + -products * ` + -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` + -property installationPath + + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($visualStudioDirectory)) + { + throw "Visual Studio C++ Build Tools are required to generate the WebGPU bindings." + } + + $msvcToolsDirectory = Join-Path $visualStudioDirectory "VC\Tools\MSVC" + + # Side-by-side Visual Studio servicing can leave multiple toolsets installed. Parsing version + # directory names selects the active newest headers without depending on enumeration order. + $msvcVersionDirectory = Get-ChildItem $msvcToolsDirectory -Directory | + Sort-Object { [version]$_.Name } -Descending | + Select-Object -First 1 + + if ($null -eq $msvcVersionDirectory) + { + throw "No MSVC toolset was found under '$msvcToolsDirectory'." + } + + # The Windows Kits registry value is the supported machine-wide root. SDK versions are also + # installed side by side, so select the newest version containing the required UCRT headers. + $windowsKitsRoot = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows Kits\Installed Roots").KitsRoot10 + $windowsSdkIncludeDirectory = Join-Path $windowsKitsRoot "Include" + $windowsSdkVersionDirectory = Get-ChildItem $windowsSdkIncludeDirectory -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "ucrt\stddef.h") } | + Sort-Object { [version]$_.Name } -Descending | + Select-Object -First 1 + + if ($null -eq $windowsSdkVersionDirectory) + { + throw "No Windows SDK containing the Universal C Runtime headers was found." + } + + # These arguments affect parsing only. ClangSharp emits declarations from the WebGPU headers, + # so absolute toolchain paths do not become part of the checked-in binding file. + $generatorArguments += @( + "--include-directory", + (Join-Path $msvcVersionDirectory.FullName "include"), + "--include-directory", + (Join-Path $windowsSdkVersionDirectory.FullName "ucrt"), + "--include-directory", + (Join-Path $windowsSdkVersionDirectory.FullName "shared")) +} + +# generate.rsp deliberately uses project-relative paths to keep its output stable across machines. +Push-Location $projectDirectory + +try +{ + # Restore from the project-scoped manifest so an ambient global tool or ClangSharp upgrade + # cannot silently change generated declarations or native ABI layouts. + & dotnet tool restore --tool-manifest $toolManifestPath + + # dotnet is an external process, so PowerShell's error preference does not convert a non-zero + # exit code into an exception. Fail the MSBuild target explicitly at this process boundary. + if ($LASTEXITCODE -ne 0) + { + throw "ClangSharpPInvokeGenerator restore failed with exit code $LASTEXITCODE." + } + + & dotnet @generatorArguments + + if ($LASTEXITCODE -ne 0) + { + throw "WebGPU binding generation failed with exit code $LASTEXITCODE." + } +} +finally +{ + Pop-Location +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generated/WebGPUNative.g.cs b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generated/WebGPUNative.g.cs new file mode 100644 index 000000000..a0b46d90d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Generated/WebGPUNative.g.cs @@ -0,0 +1,3496 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +// + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native +{ + internal enum WGPUNativeSType + { + WGPUSType_DeviceExtras = 0x00030001, + WGPUSType_NativeLimits = 0x00030002, + WGPUSType_ShaderSourceGLSL = 0x00030003, + WGPUSType_InstanceExtras = 0x00030004, + WGPUSType_BindGroupEntryExtras = 0x00030005, + WGPUSType_BindGroupLayoutEntryExtras = 0x00030006, + WGPUSType_QuerySetDescriptorExtras = 0x00030007, + WGPUSType_SurfaceConfigurationExtras = 0x00030008, + WGPUSType_SurfaceSourceSwapChainPanel = 0x00030009, + WGPUSType_PrimitiveStateExtras = 0x0003000A, + WGPUSType_SamplerDescriptorExtras = 0x0003000B, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUNativeSurfaceGetCurrentTextureStatus + { + WGPUSurfaceGetCurrentTextureStatus_Occluded = 0x00030001, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUNativeFeature + { + Immediates = 0x00030001, + TextureAdapterSpecificFormatFeatures = 0x00030002, + MultiDrawIndirectCount = 0x00030004, + VertexWritableStorage = 0x00030005, + TextureBindingArray = 0x00030006, + SampledTextureAndStorageBufferArrayNonUniformIndexing = 0x00030007, + PipelineStatisticsQuery = 0x00030008, + StorageResourceBindingArray = 0x00030009, + PartiallyBoundBindingArray = 0x0003000A, + TextureFormat16bitNorm = 0x0003000B, + TextureCompressionAstcHdr = 0x0003000C, + MappablePrimaryBuffers = 0x0003000E, + BufferBindingArray = 0x0003000F, + StorageTextureArrayNonUniformIndexing = 0x00030010, + AddressModeClampToZero = 0x00030011, + AddressModeClampToBorder = 0x00030012, + PolygonModeLine = 0x00030013, + PolygonModePoint = 0x00030014, + ConservativeRasterization = 0x00030015, + ClearTexture = 0x00030016, + Multiview = 0x00030018, + VertexAttribute64bit = 0x00030019, + TextureFormatNv12 = 0x0003001A, + RayQuery = 0x0003001C, + ShaderF64 = 0x0003001D, + ShaderI16 = 0x0003001E, + ShaderEarlyDepthTest = 0x00030020, + Subgroup = 0x00030021, + SubgroupVertex = 0x00030022, + SubgroupBarrier = 0x00030023, + TimestampQueryInsideEncoders = 0x00030024, + TimestampQueryInsidePasses = 0x00030025, + ShaderInt64 = 0x00030026, + ShaderFloat32Atomic = 0x00030027, + TextureAtomic = 0x00030028, + TextureFormatP010 = 0x00030029, + PipelineCache = 0x0003002B, + ShaderInt64AtomicMinMax = 0x0003002C, + ShaderInt64AtomicAllOps = 0x0003002D, + TextureInt64Atomic = 0x00030030, + ShaderBarycentrics = 0x00030037, + SelectiveMultiview = 0x00030038, + MultisampleArray = 0x0003003A, + CooperativeMatrix = 0x0003003B, + ShaderPerVertex = 0x0003003C, + ShaderDrawIndex = 0x0003003D, + AccelerationStructureBindingArray = 0x0003003E, + MemoryDecorationCoherent = 0x0003003F, + MemoryDecorationVolatile = 0x00030040, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPULogLevel + { + Off = 0x00000000, + Error = 0x00000001, + Warn = 0x00000002, + Info = 0x00000003, + Debug = 0x00000004, + Trace = 0x00000005, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUDx12Compiler + { + Undefined = 0x00000000, + Fxc = 0x00000001, + Dxc = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUGles3MinorVersion + { + Automatic = 0x00000000, + Version0 = 0x00000001, + Version1 = 0x00000002, + Version2 = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPipelineStatisticName + { + VertexShaderInvocations = 0x00000000, + ClipperInvocations = 0x00000001, + ClipperPrimitivesOut = 0x00000002, + FragmentShaderInvocations = 0x00000003, + ComputeShaderInvocations = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUNativeQueryType + { + PipelineStatistics = 0x00030000, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUDxcMaxShaderModel + { + V6_0 = 0x00000000, + V6_1 = 0x00000001, + V6_2 = 0x00000002, + V6_3 = 0x00000003, + V6_4 = 0x00000004, + V6_5 = 0x00000005, + V6_6 = 0x00000006, + V6_7 = 0x00000007, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUGLFenceBehaviour + { + Normal = 0x00000000, + AutoFinish = 0x00000001, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUDx12SwapchainKind + { + Undefined = 0x00000000, + DxgiFromHwnd = 0x00000001, + DxgiFromVisual = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUNativeDisplayHandleType + { + None = 0x00000000, + Xlib = 0x00000001, + Xcb = 0x00000002, + Wayland = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal unsafe partial struct WGPUXlibDisplayHandle + { + public void* display; + + public int screen; + } + + internal unsafe partial struct WGPUXcbDisplayHandle + { + public void* connection; + + public int screen; + } + + internal unsafe partial struct WGPUWaylandDisplayHandle + { + public void* display; + } + + internal partial struct WGPUNativeDisplayHandle + { + public WGPUNativeDisplayHandleType type; + + [NativeTypeName("__AnonymousRecord_wgpu_L1116_C5")] + public _data_e__Union data; + + [StructLayout(LayoutKind.Explicit)] + internal partial struct _data_e__Union + { + [FieldOffset(0)] + public WGPUXlibDisplayHandle xlib; + + [FieldOffset(0)] + public WGPUXcbDisplayHandle xcb; + + [FieldOffset(0)] + public WGPUWaylandDisplayHandle wayland; + } + } + + internal unsafe partial struct WGPUInstanceExtras + { + public WGPUChainedStruct chain; + + [NativeTypeName("WGPUInstanceBackend")] + public ulong backends; + + [NativeTypeName("WGPUInstanceFlag")] + public ulong flags; + + public WGPUDx12Compiler dx12ShaderCompiler; + + public WGPUGles3MinorVersion gles3MinorVersion; + + public WGPUGLFenceBehaviour glFenceBehaviour; + + public WGPUStringView dxcPath; + + public WGPUDxcMaxShaderModel dxcMaxShaderModel; + + public WGPUDx12SwapchainKind dx12PresentationSystem; + + [NativeTypeName("const uint8_t *")] + public byte* budgetForDeviceCreation; + + [NativeTypeName("const uint8_t *")] + public byte* budgetForDeviceLoss; + + public WGPUNativeDisplayHandle displayHandle; + } + + internal partial struct WGPUDeviceExtras + { + public WGPUChainedStruct chain; + + public WGPUStringView tracePath; + } + + internal partial struct WGPUNativeLimits + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint32_t")] + public uint maxNonSamplerBindings; + + [NativeTypeName("uint32_t")] + public uint maxBindingArrayElementsPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxBindingArraySamplerElementsPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxMultiviewViewCount; + } + + internal partial struct WGPUShaderDefine + { + public WGPUStringView name; + + public WGPUStringView value; + } + + internal unsafe partial struct WGPUShaderSourceGLSL + { + public WGPUChainedStruct chain; + + [NativeTypeName("WGPUShaderStage")] + public ulong stage; + + public WGPUStringView code; + + [NativeTypeName("uint32_t")] + public uint defineCount; + + [NativeTypeName("const WGPUShaderDefine *")] + public WGPUShaderDefine* defines; + } + + internal unsafe partial struct WGPUShaderModuleDescriptorSpirV + { + public WGPUStringView label; + + [NativeTypeName("uint32_t")] + public uint sourceSize; + + [NativeTypeName("const uint32_t *")] + public uint* source; + } + + internal partial struct WGPURegistryReport + { + [NativeTypeName("size_t")] + public nuint numAllocated; + + [NativeTypeName("size_t")] + public nuint numKeptFromUser; + + [NativeTypeName("size_t")] + public nuint numReleasedFromUser; + + [NativeTypeName("size_t")] + public nuint elementSize; + } + + internal partial struct WGPUHubReport + { + public WGPURegistryReport adapters; + + public WGPURegistryReport devices; + + public WGPURegistryReport queues; + + public WGPURegistryReport pipelineLayouts; + + public WGPURegistryReport shaderModules; + + public WGPURegistryReport bindGroupLayouts; + + public WGPURegistryReport bindGroups; + + public WGPURegistryReport commandBuffers; + + public WGPURegistryReport renderBundles; + + public WGPURegistryReport renderPipelines; + + public WGPURegistryReport computePipelines; + + public WGPURegistryReport pipelineCaches; + + public WGPURegistryReport querySets; + + public WGPURegistryReport buffers; + + public WGPURegistryReport textures; + + public WGPURegistryReport textureViews; + + public WGPURegistryReport samplers; + } + + internal partial struct WGPUGlobalReport + { + public WGPURegistryReport surfaces; + + public WGPUHubReport hub; + } + + internal unsafe partial struct WGPUInstanceEnumerateAdapterOptions + { + [NativeTypeName("const WGPUChainedStruct *")] + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUInstanceBackend")] + public ulong backends; + } + + internal unsafe partial struct WGPUBindGroupEntryExtras + { + public WGPUChainedStruct chain; + + [NativeTypeName("const WGPUBuffer *")] + public WGPUBufferImpl** buffers; + + [NativeTypeName("size_t")] + public nuint bufferCount; + + [NativeTypeName("const WGPUSampler *")] + public WGPUSamplerImpl** samplers; + + [NativeTypeName("size_t")] + public nuint samplerCount; + + [NativeTypeName("const WGPUTextureView *")] + public WGPUTextureViewImpl** textureViews; + + [NativeTypeName("size_t")] + public nuint textureViewCount; + } + + internal partial struct WGPUBindGroupLayoutEntryExtras + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint32_t")] + public uint count; + } + + internal unsafe partial struct WGPUQuerySetDescriptorExtras + { + public WGPUChainedStruct chain; + + [NativeTypeName("const WGPUPipelineStatisticName *")] + public WGPUPipelineStatisticName* pipelineStatistics; + + [NativeTypeName("size_t")] + public nuint pipelineStatisticCount; + } + + internal partial struct WGPUSurfaceConfigurationExtras + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint32_t")] + public uint desiredMaximumFrameLatency; + } + + internal unsafe partial struct WGPUSurfaceSourceSwapChainPanel + { + public WGPUChainedStruct chain; + + public void* panelNative; + } + + internal enum WGPUPolygonMode + { + Fill = 0, + Line = 1, + Point = 2, + } + + internal partial struct WGPUPrimitiveStateExtras + { + public WGPUChainedStruct chain; + + public WGPUPolygonMode polygonMode; + + [NativeTypeName("WGPUBool")] + public uint conservative; + } + + internal enum WGPUNativeTextureFormat + { + NV12 = 0x00030007, + P010 = 0x00030008, + } + + internal partial struct WGPUImageSubresourceRange + { + public WGPUTextureAspect aspect; + + [NativeTypeName("uint32_t")] + public uint baseMipLevel; + + [NativeTypeName("uint32_t")] + public uint mipLevelCount; + + [NativeTypeName("uint32_t")] + public uint baseArrayLayer; + + [NativeTypeName("uint32_t")] + public uint arrayLayerCount; + } + + internal enum WGPUNativeAddressMode + { + ClampToBorder = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUSamplerBorderColor + { + Undefined = 0x00000000, + TransparentBlack = 0x00000001, + OpaqueBlack = 0x00000002, + OpaqueWhite = 0x00000003, + Zero = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal partial struct WGPUSamplerDescriptorExtras + { + public WGPUChainedStruct chain; + + public WGPUSamplerBorderColor samplerBorderColor; + } + + internal unsafe partial struct WGPUStringView + { + [NativeTypeName("const char *")] + public sbyte* data; + + [NativeTypeName("size_t")] + public nuint length; + } + + internal partial struct WGPUAdapterImpl + { + } + + internal partial struct WGPUBindGroupImpl + { + } + + internal partial struct WGPUBindGroupLayoutImpl + { + } + + internal partial struct WGPUBufferImpl + { + } + + internal partial struct WGPUCommandBufferImpl + { + } + + internal partial struct WGPUCommandEncoderImpl + { + } + + internal partial struct WGPUComputePassEncoderImpl + { + } + + internal partial struct WGPUComputePipelineImpl + { + } + + internal partial struct WGPUDeviceImpl + { + } + + internal partial struct WGPUExternalTextureImpl + { + } + + internal partial struct WGPUInstanceImpl + { + } + + internal partial struct WGPUPipelineLayoutImpl + { + } + + internal partial struct WGPUQuerySetImpl + { + } + + internal partial struct WGPUQueueImpl + { + } + + internal partial struct WGPURenderBundleImpl + { + } + + internal partial struct WGPURenderBundleEncoderImpl + { + } + + internal partial struct WGPURenderPassEncoderImpl + { + } + + internal partial struct WGPURenderPipelineImpl + { + } + + internal partial struct WGPUSamplerImpl + { + } + + internal partial struct WGPUShaderModuleImpl + { + } + + internal partial struct WGPUSurfaceImpl + { + } + + internal partial struct WGPUTextureImpl + { + } + + internal partial struct WGPUTextureViewImpl + { + } + + internal enum WGPUAdapterType + { + DiscreteGPU = 0x00000001, + IntegratedGPU = 0x00000002, + CPU = 0x00000003, + Unknown = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUAddressMode + { + Undefined = 0x00000000, + ClampToEdge = 0x00000001, + Repeat = 0x00000002, + MirrorRepeat = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUBackendType + { + Undefined = 0x00000000, + Null = 0x00000001, + WebGPU = 0x00000002, + D3D11 = 0x00000003, + D3D12 = 0x00000004, + Metal = 0x00000005, + Vulkan = 0x00000006, + OpenGL = 0x00000007, + OpenGLES = 0x00000008, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUBlendFactor + { + Undefined = 0x00000000, + Zero = 0x00000001, + One = 0x00000002, + Src = 0x00000003, + OneMinusSrc = 0x00000004, + SrcAlpha = 0x00000005, + OneMinusSrcAlpha = 0x00000006, + Dst = 0x00000007, + OneMinusDst = 0x00000008, + DstAlpha = 0x00000009, + OneMinusDstAlpha = 0x0000000A, + SrcAlphaSaturated = 0x0000000B, + Constant = 0x0000000C, + OneMinusConstant = 0x0000000D, + Src1 = 0x0000000E, + OneMinusSrc1 = 0x0000000F, + Src1Alpha = 0x00000010, + OneMinusSrc1Alpha = 0x00000011, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUBlendOperation + { + Undefined = 0x00000000, + Add = 0x00000001, + Subtract = 0x00000002, + ReverseSubtract = 0x00000003, + Min = 0x00000004, + Max = 0x00000005, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUBufferBindingType + { + BindingNotUsed = 0x00000000, + Undefined = 0x00000001, + Uniform = 0x00000002, + Storage = 0x00000003, + ReadOnlyStorage = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUBufferMapState + { + Unmapped = 0x00000001, + Pending = 0x00000002, + Mapped = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCallbackMode + { + WaitAnyOnly = 0x00000001, + AllowProcessEvents = 0x00000002, + AllowSpontaneous = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCompareFunction + { + Undefined = 0x00000000, + Never = 0x00000001, + Less = 0x00000002, + Equal = 0x00000003, + LessEqual = 0x00000004, + Greater = 0x00000005, + NotEqual = 0x00000006, + GreaterEqual = 0x00000007, + Always = 0x00000008, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCompilationInfoRequestStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCompilationMessageType + { + Error = 0x00000001, + Warning = 0x00000002, + Info = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUComponentSwizzle + { + Undefined = 0x00000000, + Zero = 0x00000001, + One = 0x00000002, + R = 0x00000003, + G = 0x00000004, + B = 0x00000005, + A = 0x00000006, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCompositeAlphaMode + { + Auto = 0x00000000, + Opaque = 0x00000001, + Premultiplied = 0x00000002, + Unpremultiplied = 0x00000003, + Inherit = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCreatePipelineAsyncStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + ValidationError = 0x00000003, + InternalError = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUCullMode + { + Undefined = 0x00000000, + None = 0x00000001, + Front = 0x00000002, + Back = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUDeviceLostReason + { + Unknown = 0x00000001, + Destroyed = 0x00000002, + CallbackCancelled = 0x00000003, + FailedCreation = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUErrorFilter + { + Validation = 0x00000001, + OutOfMemory = 0x00000002, + Internal = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUErrorType + { + NoError = 0x00000001, + Validation = 0x00000002, + OutOfMemory = 0x00000003, + Internal = 0x00000004, + Unknown = 0x00000005, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUFeatureLevel + { + Undefined = 0x00000000, + Compatibility = 0x00000001, + Core = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUFeatureName + { + CoreFeaturesAndLimits = 0x00000001, + DepthClipControl = 0x00000002, + Depth32FloatStencil8 = 0x00000003, + TextureCompressionBC = 0x00000004, + TextureCompressionBCSliced3D = 0x00000005, + TextureCompressionETC2 = 0x00000006, + TextureCompressionASTC = 0x00000007, + TextureCompressionASTCSliced3D = 0x00000008, + TimestampQuery = 0x00000009, + IndirectFirstInstance = 0x0000000A, + ShaderF16 = 0x0000000B, + RG11B10UfloatRenderable = 0x0000000C, + BGRA8UnormStorage = 0x0000000D, + Float32Filterable = 0x0000000E, + Float32Blendable = 0x0000000F, + ClipDistances = 0x00000010, + DualSourceBlending = 0x00000011, + Subgroups = 0x00000012, + TextureFormatsTier1 = 0x00000013, + TextureFormatsTier2 = 0x00000014, + PrimitiveIndex = 0x00000015, + TextureComponentSwizzle = 0x00000016, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUFilterMode + { + Undefined = 0x00000000, + Nearest = 0x00000001, + Linear = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUFrontFace + { + Undefined = 0x00000000, + CCW = 0x00000001, + CW = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUIndexFormat + { + Undefined = 0x00000000, + Uint16 = 0x00000001, + Uint32 = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUInstanceFeatureName + { + TimedWaitAny = 0x00000001, + ShaderSourceSPIRV = 0x00000002, + MultipleDevicesPerAdapter = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPULoadOp + { + Undefined = 0x00000000, + Load = 0x00000001, + Clear = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUMapAsyncStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Error = 0x00000003, + Aborted = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUMipmapFilterMode + { + Undefined = 0x00000000, + Nearest = 0x00000001, + Linear = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUOptionalBool + { + False = 0x00000000, + True = 0x00000001, + Undefined = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPopErrorScopeStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Error = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPowerPreference + { + Undefined = 0x00000000, + LowPower = 0x00000001, + HighPerformance = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPredefinedColorSpace + { + SRGB = 0x00000001, + DisplayP3 = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPresentMode + { + Undefined = 0x00000000, + Fifo = 0x00000001, + FifoRelaxed = 0x00000002, + Immediate = 0x00000003, + Mailbox = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUPrimitiveTopology + { + Undefined = 0x00000000, + PointList = 0x00000001, + LineList = 0x00000002, + LineStrip = 0x00000003, + TriangleList = 0x00000004, + TriangleStrip = 0x00000005, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUQueryType + { + Occlusion = 0x00000001, + Timestamp = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUQueueWorkDoneStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Error = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPURequestAdapterStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Unavailable = 0x00000003, + Error = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPURequestDeviceStatus + { + Success = 0x00000001, + CallbackCancelled = 0x00000002, + Error = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUSamplerBindingType + { + BindingNotUsed = 0x00000000, + Undefined = 0x00000001, + Filtering = 0x00000002, + NonFiltering = 0x00000003, + Comparison = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUStatus + { + Success = 0x00000001, + Error = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUStencilOperation + { + Undefined = 0x00000000, + Keep = 0x00000001, + Zero = 0x00000002, + Replace = 0x00000003, + Invert = 0x00000004, + IncrementClamp = 0x00000005, + DecrementClamp = 0x00000006, + IncrementWrap = 0x00000007, + DecrementWrap = 0x00000008, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUStorageTextureAccess + { + BindingNotUsed = 0x00000000, + Undefined = 0x00000001, + WriteOnly = 0x00000002, + ReadOnly = 0x00000003, + ReadWrite = 0x00000004, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUStoreOp + { + Undefined = 0x00000000, + Store = 0x00000001, + Discard = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUSType + { + ShaderSourceSPIRV = 0x00000001, + ShaderSourceWGSL = 0x00000002, + RenderPassMaxDrawCount = 0x00000003, + SurfaceSourceMetalLayer = 0x00000004, + SurfaceSourceWindowsHWND = 0x00000005, + SurfaceSourceXlibWindow = 0x00000006, + SurfaceSourceWaylandSurface = 0x00000007, + SurfaceSourceAndroidNativeWindow = 0x00000008, + SurfaceSourceXCBWindow = 0x00000009, + SurfaceColorManagement = 0x0000000A, + RequestAdapterWebXROptions = 0x0000000B, + TextureComponentSwizzleDescriptor = 0x0000000C, + ExternalTextureBindingLayout = 0x0000000D, + ExternalTextureBindingEntry = 0x0000000E, + CompatibilityModeLimits = 0x0000000F, + TextureBindingViewDimension = 0x00000010, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUSurfaceGetCurrentTextureStatus + { + SuccessOptimal = 0x00000001, + SuccessSuboptimal = 0x00000002, + Timeout = 0x00000003, + Outdated = 0x00000004, + Lost = 0x00000005, + Error = 0x00000006, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUTextureAspect + { + Undefined = 0x00000000, + All = 0x00000001, + StencilOnly = 0x00000002, + DepthOnly = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUTextureDimension + { + Undefined = 0x00000000, + _1D = 0x00000001, + _2D = 0x00000002, + _3D = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUTextureFormat + { + Undefined = 0x00000000, + R8Unorm = 0x00000001, + R8Snorm = 0x00000002, + R8Uint = 0x00000003, + R8Sint = 0x00000004, + R16Unorm = 0x00000005, + R16Snorm = 0x00000006, + R16Uint = 0x00000007, + R16Sint = 0x00000008, + R16Float = 0x00000009, + RG8Unorm = 0x0000000A, + RG8Snorm = 0x0000000B, + RG8Uint = 0x0000000C, + RG8Sint = 0x0000000D, + R32Float = 0x0000000E, + R32Uint = 0x0000000F, + R32Sint = 0x00000010, + RG16Unorm = 0x00000011, + RG16Snorm = 0x00000012, + RG16Uint = 0x00000013, + RG16Sint = 0x00000014, + RG16Float = 0x00000015, + RGBA8Unorm = 0x00000016, + RGBA8UnormSrgb = 0x00000017, + RGBA8Snorm = 0x00000018, + RGBA8Uint = 0x00000019, + RGBA8Sint = 0x0000001A, + BGRA8Unorm = 0x0000001B, + BGRA8UnormSrgb = 0x0000001C, + RGB10A2Uint = 0x0000001D, + RGB10A2Unorm = 0x0000001E, + RG11B10Ufloat = 0x0000001F, + RGB9E5Ufloat = 0x00000020, + RG32Float = 0x00000021, + RG32Uint = 0x00000022, + RG32Sint = 0x00000023, + RGBA16Unorm = 0x00000024, + RGBA16Snorm = 0x00000025, + RGBA16Uint = 0x00000026, + RGBA16Sint = 0x00000027, + RGBA16Float = 0x00000028, + RGBA32Float = 0x00000029, + RGBA32Uint = 0x0000002A, + RGBA32Sint = 0x0000002B, + Stencil8 = 0x0000002C, + Depth16Unorm = 0x0000002D, + Depth24Plus = 0x0000002E, + Depth24PlusStencil8 = 0x0000002F, + Depth32Float = 0x00000030, + Depth32FloatStencil8 = 0x00000031, + BC1RGBAUnorm = 0x00000032, + BC1RGBAUnormSrgb = 0x00000033, + BC2RGBAUnorm = 0x00000034, + BC2RGBAUnormSrgb = 0x00000035, + BC3RGBAUnorm = 0x00000036, + BC3RGBAUnormSrgb = 0x00000037, + BC4RUnorm = 0x00000038, + BC4RSnorm = 0x00000039, + BC5RGUnorm = 0x0000003A, + BC5RGSnorm = 0x0000003B, + BC6HRGBUfloat = 0x0000003C, + BC6HRGBFloat = 0x0000003D, + BC7RGBAUnorm = 0x0000003E, + BC7RGBAUnormSrgb = 0x0000003F, + ETC2RGB8Unorm = 0x00000040, + ETC2RGB8UnormSrgb = 0x00000041, + ETC2RGB8A1Unorm = 0x00000042, + ETC2RGB8A1UnormSrgb = 0x00000043, + ETC2RGBA8Unorm = 0x00000044, + ETC2RGBA8UnormSrgb = 0x00000045, + EACR11Unorm = 0x00000046, + EACR11Snorm = 0x00000047, + EACRG11Unorm = 0x00000048, + EACRG11Snorm = 0x00000049, + ASTC4x4Unorm = 0x0000004A, + ASTC4x4UnormSrgb = 0x0000004B, + ASTC5x4Unorm = 0x0000004C, + ASTC5x4UnormSrgb = 0x0000004D, + ASTC5x5Unorm = 0x0000004E, + ASTC5x5UnormSrgb = 0x0000004F, + ASTC6x5Unorm = 0x00000050, + ASTC6x5UnormSrgb = 0x00000051, + ASTC6x6Unorm = 0x00000052, + ASTC6x6UnormSrgb = 0x00000053, + ASTC8x5Unorm = 0x00000054, + ASTC8x5UnormSrgb = 0x00000055, + ASTC8x6Unorm = 0x00000056, + ASTC8x6UnormSrgb = 0x00000057, + ASTC8x8Unorm = 0x00000058, + ASTC8x8UnormSrgb = 0x00000059, + ASTC10x5Unorm = 0x0000005A, + ASTC10x5UnormSrgb = 0x0000005B, + ASTC10x6Unorm = 0x0000005C, + ASTC10x6UnormSrgb = 0x0000005D, + ASTC10x8Unorm = 0x0000005E, + ASTC10x8UnormSrgb = 0x0000005F, + ASTC10x10Unorm = 0x00000060, + ASTC10x10UnormSrgb = 0x00000061, + ASTC12x10Unorm = 0x00000062, + ASTC12x10UnormSrgb = 0x00000063, + ASTC12x12Unorm = 0x00000064, + ASTC12x12UnormSrgb = 0x00000065, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUTextureSampleType + { + BindingNotUsed = 0x00000000, + Undefined = 0x00000001, + Float = 0x00000002, + UnfilterableFloat = 0x00000003, + Depth = 0x00000004, + Sint = 0x00000005, + Uint = 0x00000006, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUTextureViewDimension + { + Undefined = 0x00000000, + _1D = 0x00000001, + _2D = 0x00000002, + _2DArray = 0x00000003, + Cube = 0x00000004, + CubeArray = 0x00000005, + _3D = 0x00000006, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUToneMappingMode + { + Standard = 0x00000001, + Extended = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUVertexFormat + { + Uint8 = 0x00000001, + Uint8x2 = 0x00000002, + Uint8x4 = 0x00000003, + Sint8 = 0x00000004, + Sint8x2 = 0x00000005, + Sint8x4 = 0x00000006, + Unorm8 = 0x00000007, + Unorm8x2 = 0x00000008, + Unorm8x4 = 0x00000009, + Snorm8 = 0x0000000A, + Snorm8x2 = 0x0000000B, + Snorm8x4 = 0x0000000C, + Uint16 = 0x0000000D, + Uint16x2 = 0x0000000E, + Uint16x4 = 0x0000000F, + Sint16 = 0x00000010, + Sint16x2 = 0x00000011, + Sint16x4 = 0x00000012, + Unorm16 = 0x00000013, + Unorm16x2 = 0x00000014, + Unorm16x4 = 0x00000015, + Snorm16 = 0x00000016, + Snorm16x2 = 0x00000017, + Snorm16x4 = 0x00000018, + Float16 = 0x00000019, + Float16x2 = 0x0000001A, + Float16x4 = 0x0000001B, + Float32 = 0x0000001C, + Float32x2 = 0x0000001D, + Float32x3 = 0x0000001E, + Float32x4 = 0x0000001F, + Uint32 = 0x00000020, + Uint32x2 = 0x00000021, + Uint32x3 = 0x00000022, + Uint32x4 = 0x00000023, + Sint32 = 0x00000024, + Sint32x2 = 0x00000025, + Sint32x3 = 0x00000026, + Sint32x4 = 0x00000027, + Unorm10_10_10_2 = 0x00000028, + Unorm8x4BGRA = 0x00000029, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUVertexStepMode + { + Undefined = 0x00000000, + Vertex = 0x00000001, + Instance = 0x00000002, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUWaitStatus + { + Success = 0x00000001, + TimedOut = 0x00000002, + Error = 0x00000003, + Force32 = 0x7FFFFFFF, + } + + internal enum WGPUWGSLLanguageFeatureName + { + ReadonlyAndReadwriteStorageTextures = 0x00000001, + Packed4x8IntegerDotProduct = 0x00000002, + UnrestrictedPointerParameters = 0x00000003, + PointerCompositeAccess = 0x00000004, + UniformBufferStandardLayout = 0x00000005, + SubgroupId = 0x00000006, + TextureAndSamplerLet = 0x00000007, + SubgroupUniformity = 0x00000008, + TextureFormatsTier1 = 0x00000009, + LinearIndexing = 0x0000000A, + Force32 = 0x7FFFFFFF, + } + + internal unsafe partial struct WGPUChainedStruct + { + [NativeTypeName("struct WGPUChainedStruct *")] + public WGPUChainedStruct* next; + + public WGPUSType sType; + } + + internal unsafe partial struct WGPUBufferMapCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUBufferMapCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUCompilationInfoCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUCompilationInfoCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUCreateComputePipelineAsyncCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUCreateComputePipelineAsyncCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUCreateRenderPipelineAsyncCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUCreateRenderPipelineAsyncCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUDeviceLostCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUDeviceLostCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUPopErrorScopeCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUPopErrorScopeCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUQueueWorkDoneCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPUQueueWorkDoneCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPURequestAdapterCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPURequestAdapterCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPURequestDeviceCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUCallbackMode mode; + + [NativeTypeName("WGPURequestDeviceCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUUncapturedErrorCallbackInfo + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUUncapturedErrorCallback")] + public delegate* unmanaged[Cdecl] callback; + + public void* userdata1; + + public void* userdata2; + } + + internal unsafe partial struct WGPUAdapterInfo + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView vendor; + + public WGPUStringView architecture; + + public WGPUStringView device; + + public WGPUStringView description; + + public WGPUBackendType backendType; + + public WGPUAdapterType adapterType; + + [NativeTypeName("uint32_t")] + public uint vendorID; + + [NativeTypeName("uint32_t")] + public uint deviceID; + + [NativeTypeName("uint32_t")] + public uint subgroupMinSize; + + [NativeTypeName("uint32_t")] + public uint subgroupMaxSize; + } + + internal partial struct WGPUBlendComponent + { + public WGPUBlendOperation operation; + + public WGPUBlendFactor srcFactor; + + public WGPUBlendFactor dstFactor; + } + + internal unsafe partial struct WGPUBufferBindingLayout + { + public WGPUChainedStruct* nextInChain; + + public WGPUBufferBindingType type; + + [NativeTypeName("WGPUBool")] + public uint hasDynamicOffset; + + [NativeTypeName("uint64_t")] + public ulong minBindingSize; + } + + internal unsafe partial struct WGPUBufferDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("WGPUBufferUsage")] + public ulong usage; + + [NativeTypeName("uint64_t")] + public ulong size; + + [NativeTypeName("WGPUBool")] + public uint mappedAtCreation; + } + + internal partial struct WGPUColor + { + public double r; + + public double g; + + public double b; + + public double a; + } + + internal unsafe partial struct WGPUCommandBufferDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal unsafe partial struct WGPUCommandEncoderDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal partial struct WGPUCompatibilityModeLimits + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint32_t")] + public uint maxStorageBuffersInVertexStage; + + [NativeTypeName("uint32_t")] + public uint maxStorageTexturesInVertexStage; + + [NativeTypeName("uint32_t")] + public uint maxStorageBuffersInFragmentStage; + + [NativeTypeName("uint32_t")] + public uint maxStorageTexturesInFragmentStage; + } + + internal unsafe partial struct WGPUCompilationMessage + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView message; + + public WGPUCompilationMessageType type; + + [NativeTypeName("uint64_t")] + public ulong lineNum; + + [NativeTypeName("uint64_t")] + public ulong linePos; + + [NativeTypeName("uint64_t")] + public ulong offset; + + [NativeTypeName("uint64_t")] + public ulong length; + } + + internal unsafe partial struct WGPUConstantEntry + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView key; + + public double value; + } + + internal partial struct WGPUExtent3D + { + [NativeTypeName("uint32_t")] + public uint width; + + [NativeTypeName("uint32_t")] + public uint height; + + [NativeTypeName("uint32_t")] + public uint depthOrArrayLayers; + } + + internal unsafe partial struct WGPUExternalTextureBindingEntry + { + public WGPUChainedStruct chain; + + [NativeTypeName("WGPUExternalTexture")] + public WGPUExternalTextureImpl* externalTexture; + } + + internal partial struct WGPUExternalTextureBindingLayout + { + public WGPUChainedStruct chain; + } + + internal partial struct WGPUFuture + { + [NativeTypeName("uint64_t")] + public ulong id; + } + + internal unsafe partial struct WGPUInstanceLimits + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("size_t")] + public nuint timedWaitAnyMaxCount; + } + + internal unsafe partial struct WGPUMultisampleState + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("uint32_t")] + public uint count; + + [NativeTypeName("uint32_t")] + public uint mask; + + [NativeTypeName("WGPUBool")] + public uint alphaToCoverageEnabled; + } + + internal partial struct WGPUOrigin3D + { + [NativeTypeName("uint32_t")] + public uint x; + + [NativeTypeName("uint32_t")] + public uint y; + + [NativeTypeName("uint32_t")] + public uint z; + } + + internal unsafe partial struct WGPUPassTimestampWrites + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUQuerySet")] + public WGPUQuerySetImpl* querySet; + + [NativeTypeName("uint32_t")] + public uint beginningOfPassWriteIndex; + + [NativeTypeName("uint32_t")] + public uint endOfPassWriteIndex; + } + + internal unsafe partial struct WGPUPipelineLayoutDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("size_t")] + public nuint bindGroupLayoutCount; + + [NativeTypeName("const WGPUBindGroupLayout *")] + public WGPUBindGroupLayoutImpl** bindGroupLayouts; + + [NativeTypeName("uint32_t")] + public uint immediateSize; + } + + internal unsafe partial struct WGPUPrimitiveState + { + public WGPUChainedStruct* nextInChain; + + public WGPUPrimitiveTopology topology; + + public WGPUIndexFormat stripIndexFormat; + + public WGPUFrontFace frontFace; + + public WGPUCullMode cullMode; + + [NativeTypeName("WGPUBool")] + public uint unclippedDepth; + } + + internal unsafe partial struct WGPUQuerySetDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + public WGPUQueryType type; + + [NativeTypeName("uint32_t")] + public uint count; + } + + internal unsafe partial struct WGPUQueueDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal unsafe partial struct WGPURenderBundleDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal unsafe partial struct WGPURenderBundleEncoderDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("size_t")] + public nuint colorFormatCount; + + [NativeTypeName("const WGPUTextureFormat *")] + public WGPUTextureFormat* colorFormats; + + public WGPUTextureFormat depthStencilFormat; + + [NativeTypeName("uint32_t")] + public uint sampleCount; + + [NativeTypeName("WGPUBool")] + public uint depthReadOnly; + + [NativeTypeName("WGPUBool")] + public uint stencilReadOnly; + } + + internal unsafe partial struct WGPURenderPassDepthStencilAttachment + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUTextureView")] + public WGPUTextureViewImpl* view; + + public WGPULoadOp depthLoadOp; + + public WGPUStoreOp depthStoreOp; + + public float depthClearValue; + + [NativeTypeName("WGPUBool")] + public uint depthReadOnly; + + public WGPULoadOp stencilLoadOp; + + public WGPUStoreOp stencilStoreOp; + + [NativeTypeName("uint32_t")] + public uint stencilClearValue; + + [NativeTypeName("WGPUBool")] + public uint stencilReadOnly; + } + + internal partial struct WGPURenderPassMaxDrawCount + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint64_t")] + public ulong maxDrawCount; + } + + internal partial struct WGPURequestAdapterWebXROptions + { + public WGPUChainedStruct chain; + + [NativeTypeName("WGPUBool")] + public uint xrCompatible; + } + + internal unsafe partial struct WGPUSamplerBindingLayout + { + public WGPUChainedStruct* nextInChain; + + public WGPUSamplerBindingType type; + } + + internal unsafe partial struct WGPUSamplerDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + public WGPUAddressMode addressModeU; + + public WGPUAddressMode addressModeV; + + public WGPUAddressMode addressModeW; + + public WGPUFilterMode magFilter; + + public WGPUFilterMode minFilter; + + public WGPUMipmapFilterMode mipmapFilter; + + public float lodMinClamp; + + public float lodMaxClamp; + + public WGPUCompareFunction compare; + + [NativeTypeName("uint16_t")] + public ushort maxAnisotropy; + } + + internal unsafe partial struct WGPUShaderSourceSPIRV + { + public WGPUChainedStruct chain; + + [NativeTypeName("uint32_t")] + public uint codeSize; + + [NativeTypeName("const uint32_t *")] + public uint* code; + } + + internal partial struct WGPUShaderSourceWGSL + { + public WGPUChainedStruct chain; + + public WGPUStringView code; + } + + internal partial struct WGPUStencilFaceState + { + public WGPUCompareFunction compare; + + public WGPUStencilOperation failOp; + + public WGPUStencilOperation depthFailOp; + + public WGPUStencilOperation passOp; + } + + internal unsafe partial struct WGPUStorageTextureBindingLayout + { + public WGPUChainedStruct* nextInChain; + + public WGPUStorageTextureAccess access; + + public WGPUTextureFormat format; + + public WGPUTextureViewDimension viewDimension; + } + + internal unsafe partial struct WGPUSupportedFeatures + { + [NativeTypeName("size_t")] + public nuint featureCount; + + [NativeTypeName("const WGPUFeatureName *")] + public WGPUFeatureName* features; + } + + internal unsafe partial struct WGPUSupportedInstanceFeatures + { + [NativeTypeName("size_t")] + public nuint featureCount; + + [NativeTypeName("const WGPUInstanceFeatureName *")] + public WGPUInstanceFeatureName* features; + } + + internal unsafe partial struct WGPUSupportedWGSLLanguageFeatures + { + [NativeTypeName("size_t")] + public nuint featureCount; + + [NativeTypeName("const WGPUWGSLLanguageFeatureName *")] + public WGPUWGSLLanguageFeatureName* features; + } + + internal unsafe partial struct WGPUSurfaceCapabilities + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUTextureUsage")] + public ulong usages; + + [NativeTypeName("size_t")] + public nuint formatCount; + + [NativeTypeName("const WGPUTextureFormat *")] + public WGPUTextureFormat* formats; + + [NativeTypeName("size_t")] + public nuint presentModeCount; + + [NativeTypeName("const WGPUPresentMode *")] + public WGPUPresentMode* presentModes; + + [NativeTypeName("size_t")] + public nuint alphaModeCount; + + [NativeTypeName("const WGPUCompositeAlphaMode *")] + public WGPUCompositeAlphaMode* alphaModes; + } + + internal partial struct WGPUSurfaceColorManagement + { + public WGPUChainedStruct chain; + + public WGPUPredefinedColorSpace colorSpace; + + public WGPUToneMappingMode toneMappingMode; + } + + internal unsafe partial struct WGPUSurfaceConfiguration + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUDevice")] + public WGPUDeviceImpl* device; + + public WGPUTextureFormat format; + + [NativeTypeName("WGPUTextureUsage")] + public ulong usage; + + [NativeTypeName("uint32_t")] + public uint width; + + [NativeTypeName("uint32_t")] + public uint height; + + [NativeTypeName("size_t")] + public nuint viewFormatCount; + + [NativeTypeName("const WGPUTextureFormat *")] + public WGPUTextureFormat* viewFormats; + + public WGPUCompositeAlphaMode alphaMode; + + public WGPUPresentMode presentMode; + } + + internal unsafe partial struct WGPUSurfaceSourceAndroidNativeWindow + { + public WGPUChainedStruct chain; + + public void* window; + } + + internal unsafe partial struct WGPUSurfaceSourceMetalLayer + { + public WGPUChainedStruct chain; + + public void* layer; + } + + internal unsafe partial struct WGPUSurfaceSourceWaylandSurface + { + public WGPUChainedStruct chain; + + public void* display; + + public void* surface; + } + + internal unsafe partial struct WGPUSurfaceSourceWindowsHWND + { + public WGPUChainedStruct chain; + + public void* hinstance; + + public void* hwnd; + } + + internal unsafe partial struct WGPUSurfaceSourceXCBWindow + { + public WGPUChainedStruct chain; + + public void* connection; + + [NativeTypeName("uint32_t")] + public uint window; + } + + internal unsafe partial struct WGPUSurfaceSourceXlibWindow + { + public WGPUChainedStruct chain; + + public void* display; + + [NativeTypeName("uint64_t")] + public ulong window; + } + + internal unsafe partial struct WGPUSurfaceTexture + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUTexture")] + public WGPUTextureImpl* texture; + + public WGPUSurfaceGetCurrentTextureStatus status; + } + + internal partial struct WGPUTexelCopyBufferLayout + { + [NativeTypeName("uint64_t")] + public ulong offset; + + [NativeTypeName("uint32_t")] + public uint bytesPerRow; + + [NativeTypeName("uint32_t")] + public uint rowsPerImage; + } + + internal unsafe partial struct WGPUTextureBindingLayout + { + public WGPUChainedStruct* nextInChain; + + public WGPUTextureSampleType sampleType; + + public WGPUTextureViewDimension viewDimension; + + [NativeTypeName("WGPUBool")] + public uint multisampled; + } + + internal partial struct WGPUTextureBindingViewDimension + { + public WGPUChainedStruct chain; + + public WGPUTextureViewDimension textureBindingViewDimension; + } + + internal partial struct WGPUTextureComponentSwizzle + { + public WGPUComponentSwizzle r; + + public WGPUComponentSwizzle g; + + public WGPUComponentSwizzle b; + + public WGPUComponentSwizzle a; + } + + internal unsafe partial struct WGPUVertexAttribute + { + public WGPUChainedStruct* nextInChain; + + public WGPUVertexFormat format; + + [NativeTypeName("uint64_t")] + public ulong offset; + + [NativeTypeName("uint32_t")] + public uint shaderLocation; + } + + internal unsafe partial struct WGPUBindGroupEntry + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("uint32_t")] + public uint binding; + + [NativeTypeName("WGPUBuffer")] + public WGPUBufferImpl* buffer; + + [NativeTypeName("uint64_t")] + public ulong offset; + + [NativeTypeName("uint64_t")] + public ulong size; + + [NativeTypeName("WGPUSampler")] + public WGPUSamplerImpl* sampler; + + [NativeTypeName("WGPUTextureView")] + public WGPUTextureViewImpl* textureView; + } + + internal unsafe partial struct WGPUBindGroupLayoutEntry + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("uint32_t")] + public uint binding; + + [NativeTypeName("WGPUShaderStage")] + public ulong visibility; + + [NativeTypeName("uint32_t")] + public uint bindingArraySize; + + public WGPUBufferBindingLayout buffer; + + public WGPUSamplerBindingLayout sampler; + + public WGPUTextureBindingLayout texture; + + public WGPUStorageTextureBindingLayout storageTexture; + } + + internal partial struct WGPUBlendState + { + public WGPUBlendComponent color; + + public WGPUBlendComponent alpha; + } + + internal unsafe partial struct WGPUCompilationInfo + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("size_t")] + public nuint messageCount; + + [NativeTypeName("const WGPUCompilationMessage *")] + public WGPUCompilationMessage* messages; + } + + internal unsafe partial struct WGPUComputePassDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("const WGPUPassTimestampWrites *")] + public WGPUPassTimestampWrites* timestampWrites; + } + + internal unsafe partial struct WGPUComputeState + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUShaderModule")] + public WGPUShaderModuleImpl* module; + + public WGPUStringView entryPoint; + + [NativeTypeName("size_t")] + public nuint constantCount; + + [NativeTypeName("const WGPUConstantEntry *")] + public WGPUConstantEntry* constants; + } + + internal unsafe partial struct WGPUDepthStencilState + { + public WGPUChainedStruct* nextInChain; + + public WGPUTextureFormat format; + + public WGPUOptionalBool depthWriteEnabled; + + public WGPUCompareFunction depthCompare; + + public WGPUStencilFaceState stencilFront; + + public WGPUStencilFaceState stencilBack; + + [NativeTypeName("uint32_t")] + public uint stencilReadMask; + + [NativeTypeName("uint32_t")] + public uint stencilWriteMask; + + [NativeTypeName("int32_t")] + public int depthBias; + + public float depthBiasSlopeScale; + + public float depthBiasClamp; + } + + internal partial struct WGPUFutureWaitInfo + { + public WGPUFuture future; + + [NativeTypeName("WGPUBool")] + public uint completed; + } + + internal unsafe partial struct WGPUInstanceDescriptor + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("size_t")] + public nuint requiredFeatureCount; + + [NativeTypeName("const WGPUInstanceFeatureName *")] + public WGPUInstanceFeatureName* requiredFeatures; + + [NativeTypeName("const WGPUInstanceLimits *")] + public WGPUInstanceLimits* requiredLimits; + } + + internal unsafe partial struct WGPULimits + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("uint32_t")] + public uint maxTextureDimension1D; + + [NativeTypeName("uint32_t")] + public uint maxTextureDimension2D; + + [NativeTypeName("uint32_t")] + public uint maxTextureDimension3D; + + [NativeTypeName("uint32_t")] + public uint maxTextureArrayLayers; + + [NativeTypeName("uint32_t")] + public uint maxBindGroups; + + [NativeTypeName("uint32_t")] + public uint maxBindGroupsPlusVertexBuffers; + + [NativeTypeName("uint32_t")] + public uint maxBindingsPerBindGroup; + + [NativeTypeName("uint32_t")] + public uint maxDynamicUniformBuffersPerPipelineLayout; + + [NativeTypeName("uint32_t")] + public uint maxDynamicStorageBuffersPerPipelineLayout; + + [NativeTypeName("uint32_t")] + public uint maxSampledTexturesPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxSamplersPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxStorageBuffersPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxStorageTexturesPerShaderStage; + + [NativeTypeName("uint32_t")] + public uint maxUniformBuffersPerShaderStage; + + [NativeTypeName("uint64_t")] + public ulong maxUniformBufferBindingSize; + + [NativeTypeName("uint64_t")] + public ulong maxStorageBufferBindingSize; + + [NativeTypeName("uint32_t")] + public uint minUniformBufferOffsetAlignment; + + [NativeTypeName("uint32_t")] + public uint minStorageBufferOffsetAlignment; + + [NativeTypeName("uint32_t")] + public uint maxVertexBuffers; + + [NativeTypeName("uint64_t")] + public ulong maxBufferSize; + + [NativeTypeName("uint32_t")] + public uint maxVertexAttributes; + + [NativeTypeName("uint32_t")] + public uint maxVertexBufferArrayStride; + + [NativeTypeName("uint32_t")] + public uint maxInterStageShaderVariables; + + [NativeTypeName("uint32_t")] + public uint maxColorAttachments; + + [NativeTypeName("uint32_t")] + public uint maxColorAttachmentBytesPerSample; + + [NativeTypeName("uint32_t")] + public uint maxComputeWorkgroupStorageSize; + + [NativeTypeName("uint32_t")] + public uint maxComputeInvocationsPerWorkgroup; + + [NativeTypeName("uint32_t")] + public uint maxComputeWorkgroupSizeX; + + [NativeTypeName("uint32_t")] + public uint maxComputeWorkgroupSizeY; + + [NativeTypeName("uint32_t")] + public uint maxComputeWorkgroupSizeZ; + + [NativeTypeName("uint32_t")] + public uint maxComputeWorkgroupsPerDimension; + + [NativeTypeName("uint32_t")] + public uint maxImmediateSize; + } + + internal unsafe partial struct WGPURenderPassColorAttachment + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUTextureView")] + public WGPUTextureViewImpl* view; + + [NativeTypeName("uint32_t")] + public uint depthSlice; + + [NativeTypeName("WGPUTextureView")] + public WGPUTextureViewImpl* resolveTarget; + + public WGPULoadOp loadOp; + + public WGPUStoreOp storeOp; + + public WGPUColor clearValue; + } + + internal unsafe partial struct WGPURequestAdapterOptions + { + public WGPUChainedStruct* nextInChain; + + public WGPUFeatureLevel featureLevel; + + public WGPUPowerPreference powerPreference; + + [NativeTypeName("WGPUBool")] + public uint forceFallbackAdapter; + + public WGPUBackendType backendType; + + [NativeTypeName("WGPUSurface")] + public WGPUSurfaceImpl* compatibleSurface; + } + + internal unsafe partial struct WGPUShaderModuleDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal unsafe partial struct WGPUSurfaceDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + } + + internal unsafe partial struct WGPUTexelCopyBufferInfo + { + public WGPUTexelCopyBufferLayout layout; + + [NativeTypeName("WGPUBuffer")] + public WGPUBufferImpl* buffer; + } + + internal unsafe partial struct WGPUTexelCopyTextureInfo + { + [NativeTypeName("WGPUTexture")] + public WGPUTextureImpl* texture; + + [NativeTypeName("uint32_t")] + public uint mipLevel; + + public WGPUOrigin3D origin; + + public WGPUTextureAspect aspect; + } + + internal partial struct WGPUTextureComponentSwizzleDescriptor + { + public WGPUChainedStruct chain; + + public WGPUTextureComponentSwizzle swizzle; + } + + internal unsafe partial struct WGPUTextureDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("WGPUTextureUsage")] + public ulong usage; + + public WGPUTextureDimension dimension; + + public WGPUExtent3D size; + + public WGPUTextureFormat format; + + [NativeTypeName("uint32_t")] + public uint mipLevelCount; + + [NativeTypeName("uint32_t")] + public uint sampleCount; + + [NativeTypeName("size_t")] + public nuint viewFormatCount; + + [NativeTypeName("const WGPUTextureFormat *")] + public WGPUTextureFormat* viewFormats; + } + + internal unsafe partial struct WGPUVertexBufferLayout + { + public WGPUChainedStruct* nextInChain; + + public WGPUVertexStepMode stepMode; + + [NativeTypeName("uint64_t")] + public ulong arrayStride; + + [NativeTypeName("size_t")] + public nuint attributeCount; + + [NativeTypeName("const WGPUVertexAttribute *")] + public WGPUVertexAttribute* attributes; + } + + internal unsafe partial struct WGPUBindGroupDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("WGPUBindGroupLayout")] + public WGPUBindGroupLayoutImpl* layout; + + [NativeTypeName("size_t")] + public nuint entryCount; + + [NativeTypeName("const WGPUBindGroupEntry *")] + public WGPUBindGroupEntry* entries; + } + + internal unsafe partial struct WGPUBindGroupLayoutDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("size_t")] + public nuint entryCount; + + [NativeTypeName("const WGPUBindGroupLayoutEntry *")] + public WGPUBindGroupLayoutEntry* entries; + } + + internal unsafe partial struct WGPUColorTargetState + { + public WGPUChainedStruct* nextInChain; + + public WGPUTextureFormat format; + + [NativeTypeName("const WGPUBlendState *")] + public WGPUBlendState* blend; + + [NativeTypeName("WGPUColorWriteMask")] + public ulong writeMask; + } + + internal unsafe partial struct WGPUComputePipelineDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("WGPUPipelineLayout")] + public WGPUPipelineLayoutImpl* layout; + + public WGPUComputeState compute; + } + + internal unsafe partial struct WGPUDeviceDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("size_t")] + public nuint requiredFeatureCount; + + [NativeTypeName("const WGPUFeatureName *")] + public WGPUFeatureName* requiredFeatures; + + [NativeTypeName("const WGPULimits *")] + public WGPULimits* requiredLimits; + + public WGPUQueueDescriptor defaultQueue; + + public WGPUDeviceLostCallbackInfo deviceLostCallbackInfo; + + public WGPUUncapturedErrorCallbackInfo uncapturedErrorCallbackInfo; + } + + internal unsafe partial struct WGPURenderPassDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("size_t")] + public nuint colorAttachmentCount; + + [NativeTypeName("const WGPURenderPassColorAttachment *")] + public WGPURenderPassColorAttachment* colorAttachments; + + [NativeTypeName("const WGPURenderPassDepthStencilAttachment *")] + public WGPURenderPassDepthStencilAttachment* depthStencilAttachment; + + [NativeTypeName("WGPUQuerySet")] + public WGPUQuerySetImpl* occlusionQuerySet; + + [NativeTypeName("const WGPUPassTimestampWrites *")] + public WGPUPassTimestampWrites* timestampWrites; + } + + internal unsafe partial struct WGPUTextureViewDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + public WGPUTextureFormat format; + + public WGPUTextureViewDimension dimension; + + [NativeTypeName("uint32_t")] + public uint baseMipLevel; + + [NativeTypeName("uint32_t")] + public uint mipLevelCount; + + [NativeTypeName("uint32_t")] + public uint baseArrayLayer; + + [NativeTypeName("uint32_t")] + public uint arrayLayerCount; + + public WGPUTextureAspect aspect; + + [NativeTypeName("WGPUTextureUsage")] + public ulong usage; + } + + internal unsafe partial struct WGPUVertexState + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUShaderModule")] + public WGPUShaderModuleImpl* module; + + public WGPUStringView entryPoint; + + [NativeTypeName("size_t")] + public nuint constantCount; + + [NativeTypeName("const WGPUConstantEntry *")] + public WGPUConstantEntry* constants; + + [NativeTypeName("size_t")] + public nuint bufferCount; + + [NativeTypeName("const WGPUVertexBufferLayout *")] + public WGPUVertexBufferLayout* buffers; + } + + internal unsafe partial struct WGPUFragmentState + { + public WGPUChainedStruct* nextInChain; + + [NativeTypeName("WGPUShaderModule")] + public WGPUShaderModuleImpl* module; + + public WGPUStringView entryPoint; + + [NativeTypeName("size_t")] + public nuint constantCount; + + [NativeTypeName("const WGPUConstantEntry *")] + public WGPUConstantEntry* constants; + + [NativeTypeName("size_t")] + public nuint targetCount; + + [NativeTypeName("const WGPUColorTargetState *")] + public WGPUColorTargetState* targets; + } + + internal unsafe partial struct WGPURenderPipelineDescriptor + { + public WGPUChainedStruct* nextInChain; + + public WGPUStringView label; + + [NativeTypeName("WGPUPipelineLayout")] + public WGPUPipelineLayoutImpl* layout; + + public WGPUVertexState vertex; + + public WGPUPrimitiveState primitive; + + [NativeTypeName("const WGPUDepthStencilState *")] + public WGPUDepthStencilState* depthStencil; + + public WGPUMultisampleState multisample; + + [NativeTypeName("const WGPUFragmentState *")] + public WGPUFragmentState* fragment; + } + + internal static unsafe partial class WebGPUNative + { + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_All = 0x00000000; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_Vulkan = 1 << 0; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_GL = 1 << 1; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_Metal = 1 << 2; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_DX12 = 1 << 3; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_BrowserWebGPU = 1 << 5; + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_Primary = (1 << 0) | (1 << 2) | (1 << 3) | (1 << 5); + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_Secondary = (1 << 1); + + [NativeTypeName("const WGPUInstanceBackend")] + public const ulong WGPUInstanceBackend_Force32 = 0x7FFFFFFF; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Empty = 0x00000000; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Debug = 1 << 0; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Validation = 1 << 1; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_DiscardHalLabels = 1 << 2; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_AllowUnderlyingNoncompliantAdapter = 1 << 3; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_GPUBasedValidation = 1 << 4; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_ValidationIndirectCall = 1 << 5; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_AutomaticTimestampNormalization = 1 << 6; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Default = 1 << 24; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Debugging = 1 << 25; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_AdvancedDebugging = 1 << 26; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_WithEnv = 1 << 27; + + [NativeTypeName("const WGPUInstanceFlag")] + public const ulong WGPUInstanceFlag_Force32 = 0x7FFFFFFF; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_None = 0x0000000000000000; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_BoundsChecks = 0x0000000000000001; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_ForceLoopBounding = 0x0000000000000002; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_RayQueryInitializationTracking = 0x0000000000000004; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_TaskShaderDispatchTracking = 0x0000000000000008; + + [NativeTypeName("const WGPUShaderRuntimeChecks")] + public const ulong WGPUShaderRuntimeChecks_MeshShaderPrimitiveIndicesClamp = 0x0000000000000010; + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuGenerateReport([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, WGPUGlobalReport* report); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("size_t")] + public static extern nuint wgpuInstanceEnumerateAdapters([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, [NativeTypeName("const WGPUInstanceEnumerateAdapterOptions *")] WGPUInstanceEnumerateAdapterOptions* options, [NativeTypeName("WGPUAdapter *")] WGPUAdapterImpl** adapters); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUSubmissionIndex")] + public static extern ulong wgpuQueueSubmitForIndex([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, [NativeTypeName("size_t")] nuint commandCount, [NativeTypeName("const WGPUCommandBuffer *")] WGPUCommandBufferImpl** commands); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern float wgpuQueueGetTimestampPeriod([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuDevicePoll([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("WGPUBool")] uint wait, [NativeTypeName("const WGPUSubmissionIndex *")] ulong* submissionIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUShaderModule")] + public static extern WGPUShaderModuleImpl* wgpuDeviceCreateShaderModuleSpirV([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUShaderModuleDescriptorSpirV *")] WGPUShaderModuleDescriptorSpirV* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSetLogCallback([NativeTypeName("WGPULogCallback")] delegate* unmanaged[Cdecl] callback, void* userdata); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSetLogLevel(WGPULogLevel level); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuGetVersion(); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void* wgpuDeviceGetNativeMetalDevice([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void* wgpuQueueGetNativeMetalCommandQueue([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void* wgpuTextureGetNativeMetalTexture([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderMultiDrawIndirect([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* encoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint32_t")] uint count); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderMultiDrawIndexedIndirect([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* encoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint32_t")] uint count); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderMultiDrawIndirectCount([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* encoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* count_buffer, [NativeTypeName("uint64_t")] ulong count_buffer_offset, [NativeTypeName("uint32_t")] uint max_count); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderMultiDrawIndexedIndirectCount([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* encoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* count_buffer, [NativeTypeName("uint64_t")] ulong count_buffer_offset, [NativeTypeName("uint32_t")] uint max_count); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderBeginPipelineStatisticsQuery([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderEndPipelineStatisticsQuery([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderBeginPipelineStatisticsQuery([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderEndPipelineStatisticsQuery([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderWriteTimestamp([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderWriteTimestamp([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuDeviceStartGraphicsDebuggerCapture([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceStopGraphicsDebuggerCapture([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderClearTexture([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture, [NativeTypeName("const WGPUImageSubresourceRange *")] WGPUImageSubresourceRange* range); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUShaderModule")] + public static extern WGPUShaderModuleImpl* wgpuDeviceCreateShaderModuleTrusted([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUShaderModuleDescriptor *")] WGPUShaderModuleDescriptor* descriptor, [NativeTypeName("WGPUShaderRuntimeChecks")] ulong runtimeChecks); + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_None = 0x0000000000000000; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_MapRead = 0x0000000000000001; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_MapWrite = 0x0000000000000002; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_CopySrc = 0x0000000000000004; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_CopyDst = 0x0000000000000008; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_Index = 0x0000000000000010; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_Vertex = 0x0000000000000020; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_Uniform = 0x0000000000000040; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_Storage = 0x0000000000000080; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_Indirect = 0x0000000000000100; + + [NativeTypeName("const WGPUBufferUsage")] + public const ulong WGPUBufferUsage_QueryResolve = 0x0000000000000200; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_None = 0x0000000000000000; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_Red = 0x0000000000000001; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_Green = 0x0000000000000002; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_Blue = 0x0000000000000004; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_Alpha = 0x0000000000000008; + + [NativeTypeName("const WGPUColorWriteMask")] + public const ulong WGPUColorWriteMask_All = 0x000000000000000F; + + [NativeTypeName("const WGPUMapMode")] + public const ulong WGPUMapMode_None = 0x0000000000000000; + + [NativeTypeName("const WGPUMapMode")] + public const ulong WGPUMapMode_Read = 0x0000000000000001; + + [NativeTypeName("const WGPUMapMode")] + public const ulong WGPUMapMode_Write = 0x0000000000000002; + + [NativeTypeName("const WGPUShaderStage")] + public const ulong WGPUShaderStage_None = 0x0000000000000000; + + [NativeTypeName("const WGPUShaderStage")] + public const ulong WGPUShaderStage_Vertex = 0x0000000000000001; + + [NativeTypeName("const WGPUShaderStage")] + public const ulong WGPUShaderStage_Fragment = 0x0000000000000002; + + [NativeTypeName("const WGPUShaderStage")] + public const ulong WGPUShaderStage_Compute = 0x0000000000000004; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_None = 0x0000000000000000; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_CopySrc = 0x0000000000000001; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_CopyDst = 0x0000000000000002; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_TextureBinding = 0x0000000000000004; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_StorageBinding = 0x0000000000000008; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_RenderAttachment = 0x0000000000000010; + + [NativeTypeName("const WGPUTextureUsage")] + public const ulong WGPUTextureUsage_TransientAttachment = 0x0000000000000020; + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUInstance")] + public static extern WGPUInstanceImpl* wgpuCreateInstance([NativeTypeName("const WGPUInstanceDescriptor *")] WGPUInstanceDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuGetInstanceFeatures(WGPUSupportedInstanceFeatures* features); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuGetInstanceLimits(WGPUInstanceLimits* limits); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuHasInstanceFeature(WGPUInstanceFeatureName feature); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUProc")] + public static extern delegate* unmanaged[Cdecl] wgpuGetProcAddress(WGPUStringView procName); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuAdapterGetFeatures([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, WGPUSupportedFeatures* features); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuAdapterGetInfo([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, WGPUAdapterInfo* info); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuAdapterGetLimits([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, WGPULimits* limits); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuAdapterHasFeature([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, WGPUFeatureName feature); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuAdapterRequestDevice([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, [NativeTypeName("const WGPUDeviceDescriptor *")] WGPUDeviceDescriptor* descriptor, WGPURequestDeviceCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuAdapterAddRef([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuAdapterRelease([NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuAdapterInfoFreeMembers(WGPUAdapterInfo adapterInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupSetLabel([NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* bindGroup, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupAddRef([NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* bindGroup); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupRelease([NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* bindGroup); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupLayoutSetLabel([NativeTypeName("WGPUBindGroupLayout")] WGPUBindGroupLayoutImpl* bindGroupLayout, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupLayoutAddRef([NativeTypeName("WGPUBindGroupLayout")] WGPUBindGroupLayoutImpl* bindGroupLayout); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBindGroupLayoutRelease([NativeTypeName("WGPUBindGroupLayout")] WGPUBindGroupLayoutImpl* bindGroupLayout); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBufferDestroy([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("const void *")] + public static extern void* wgpuBufferGetConstMappedRange([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("size_t")] nuint offset, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void* wgpuBufferGetMappedRange([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("size_t")] nuint offset, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUBufferMapState wgpuBufferGetMapState([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint64_t")] + public static extern ulong wgpuBufferGetSize([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBufferUsage")] + public static extern ulong wgpuBufferGetUsage([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuBufferMapAsync([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("WGPUMapMode")] ulong mode, [NativeTypeName("size_t")] nuint offset, [NativeTypeName("size_t")] nuint size, WGPUBufferMapCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuBufferReadMappedRange([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("size_t")] nuint offset, void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBufferSetLabel([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBufferUnmap([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuBufferWriteMappedRange([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("size_t")] nuint offset, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBufferAddRef([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuBufferRelease([NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandBufferSetLabel([NativeTypeName("WGPUCommandBuffer")] WGPUCommandBufferImpl* commandBuffer, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandBufferAddRef([NativeTypeName("WGPUCommandBuffer")] WGPUCommandBufferImpl* commandBuffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandBufferRelease([NativeTypeName("WGPUCommandBuffer")] WGPUCommandBufferImpl* commandBuffer); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUComputePassEncoder")] + public static extern WGPUComputePassEncoderImpl* wgpuCommandEncoderBeginComputePass([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPUComputePassDescriptor *")] WGPUComputePassDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPURenderPassEncoder")] + public static extern WGPURenderPassEncoderImpl* wgpuCommandEncoderBeginRenderPass([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPURenderPassDescriptor *")] WGPURenderPassDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderClearBuffer([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderCopyBufferToBuffer([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* source, [NativeTypeName("uint64_t")] ulong sourceOffset, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* destination, [NativeTypeName("uint64_t")] ulong destinationOffset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderCopyBufferToTexture([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPUTexelCopyBufferInfo *")] WGPUTexelCopyBufferInfo* source, [NativeTypeName("const WGPUTexelCopyTextureInfo *")] WGPUTexelCopyTextureInfo* destination, [NativeTypeName("const WGPUExtent3D *")] WGPUExtent3D* copySize); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderCopyTextureToBuffer([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPUTexelCopyTextureInfo *")] WGPUTexelCopyTextureInfo* source, [NativeTypeName("const WGPUTexelCopyBufferInfo *")] WGPUTexelCopyBufferInfo* destination, [NativeTypeName("const WGPUExtent3D *")] WGPUExtent3D* copySize); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderCopyTextureToTexture([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPUTexelCopyTextureInfo *")] WGPUTexelCopyTextureInfo* source, [NativeTypeName("const WGPUTexelCopyTextureInfo *")] WGPUTexelCopyTextureInfo* destination, [NativeTypeName("const WGPUExtent3D *")] WGPUExtent3D* copySize); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUCommandBuffer")] + public static extern WGPUCommandBufferImpl* wgpuCommandEncoderFinish([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("const WGPUCommandBufferDescriptor *")] WGPUCommandBufferDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderInsertDebugMarker([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, WGPUStringView markerLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderPopDebugGroup([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderPushDebugGroup([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, WGPUStringView groupLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderResolveQuerySet([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint firstQuery, [NativeTypeName("uint32_t")] uint queryCount, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* destination, [NativeTypeName("uint64_t")] ulong destinationOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderSetLabel([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderWriteTimestamp([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder, [NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderAddRef([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuCommandEncoderRelease([NativeTypeName("WGPUCommandEncoder")] WGPUCommandEncoderImpl* commandEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderDispatchWorkgroups([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("uint32_t")] uint workgroupCountX, [NativeTypeName("uint32_t")] uint workgroupCountY, [NativeTypeName("uint32_t")] uint workgroupCountZ); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderDispatchWorkgroupsIndirect([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* indirectBuffer, [NativeTypeName("uint64_t")] ulong indirectOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderEnd([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderInsertDebugMarker([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, WGPUStringView markerLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderPopDebugGroup([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderPushDebugGroup([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, WGPUStringView groupLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderSetBindGroup([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("uint32_t")] uint groupIndex, [NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* group, [NativeTypeName("size_t")] nuint dynamicOffsetCount, [NativeTypeName("const uint32_t *")] uint* dynamicOffsets); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderSetImmediates([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("uint32_t")] uint offset, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderSetLabel([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderSetPipeline([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder, [NativeTypeName("WGPUComputePipeline")] WGPUComputePipelineImpl* pipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderAddRef([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePassEncoderRelease([NativeTypeName("WGPUComputePassEncoder")] WGPUComputePassEncoderImpl* computePassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBindGroupLayout")] + public static extern WGPUBindGroupLayoutImpl* wgpuComputePipelineGetBindGroupLayout([NativeTypeName("WGPUComputePipeline")] WGPUComputePipelineImpl* computePipeline, [NativeTypeName("uint32_t")] uint groupIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePipelineSetLabel([NativeTypeName("WGPUComputePipeline")] WGPUComputePipelineImpl* computePipeline, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePipelineAddRef([NativeTypeName("WGPUComputePipeline")] WGPUComputePipelineImpl* computePipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuComputePipelineRelease([NativeTypeName("WGPUComputePipeline")] WGPUComputePipelineImpl* computePipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBindGroup")] + public static extern WGPUBindGroupImpl* wgpuDeviceCreateBindGroup([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUBindGroupDescriptor *")] WGPUBindGroupDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBindGroupLayout")] + public static extern WGPUBindGroupLayoutImpl* wgpuDeviceCreateBindGroupLayout([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUBindGroupLayoutDescriptor *")] WGPUBindGroupLayoutDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBuffer")] + public static extern WGPUBufferImpl* wgpuDeviceCreateBuffer([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUBufferDescriptor *")] WGPUBufferDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUCommandEncoder")] + public static extern WGPUCommandEncoderImpl* wgpuDeviceCreateCommandEncoder([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUCommandEncoderDescriptor *")] WGPUCommandEncoderDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUComputePipeline")] + public static extern WGPUComputePipelineImpl* wgpuDeviceCreateComputePipeline([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUComputePipelineDescriptor *")] WGPUComputePipelineDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuDeviceCreateComputePipelineAsync([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUComputePipelineDescriptor *")] WGPUComputePipelineDescriptor* descriptor, WGPUCreateComputePipelineAsyncCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUPipelineLayout")] + public static extern WGPUPipelineLayoutImpl* wgpuDeviceCreatePipelineLayout([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUPipelineLayoutDescriptor *")] WGPUPipelineLayoutDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUQuerySet")] + public static extern WGPUQuerySetImpl* wgpuDeviceCreateQuerySet([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUQuerySetDescriptor *")] WGPUQuerySetDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPURenderBundleEncoder")] + public static extern WGPURenderBundleEncoderImpl* wgpuDeviceCreateRenderBundleEncoder([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPURenderBundleEncoderDescriptor *")] WGPURenderBundleEncoderDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPURenderPipeline")] + public static extern WGPURenderPipelineImpl* wgpuDeviceCreateRenderPipeline([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPURenderPipelineDescriptor *")] WGPURenderPipelineDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuDeviceCreateRenderPipelineAsync([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPURenderPipelineDescriptor *")] WGPURenderPipelineDescriptor* descriptor, WGPUCreateRenderPipelineAsyncCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUSampler")] + public static extern WGPUSamplerImpl* wgpuDeviceCreateSampler([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUSamplerDescriptor *")] WGPUSamplerDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUShaderModule")] + public static extern WGPUShaderModuleImpl* wgpuDeviceCreateShaderModule([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUShaderModuleDescriptor *")] WGPUShaderModuleDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUTexture")] + public static extern WGPUTextureImpl* wgpuDeviceCreateTexture([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, [NativeTypeName("const WGPUTextureDescriptor *")] WGPUTextureDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceDestroy([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuDeviceGetAdapterInfo([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUAdapterInfo* adapterInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceGetFeatures([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUSupportedFeatures* features); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuDeviceGetLimits([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPULimits* limits); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuDeviceGetLostFuture([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUQueue")] + public static extern WGPUQueueImpl* wgpuDeviceGetQueue([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuDeviceHasFeature([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUFeatureName feature); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuDevicePopErrorScope([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUPopErrorScopeCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDevicePushErrorScope([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUErrorFilter filter); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceSetLabel([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceAddRef([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuDeviceRelease([NativeTypeName("WGPUDevice")] WGPUDeviceImpl* device); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuExternalTextureSetLabel([NativeTypeName("WGPUExternalTexture")] WGPUExternalTextureImpl* externalTexture, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuExternalTextureAddRef([NativeTypeName("WGPUExternalTexture")] WGPUExternalTextureImpl* externalTexture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuExternalTextureRelease([NativeTypeName("WGPUExternalTexture")] WGPUExternalTextureImpl* externalTexture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUSurface")] + public static extern WGPUSurfaceImpl* wgpuInstanceCreateSurface([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, [NativeTypeName("const WGPUSurfaceDescriptor *")] WGPUSurfaceDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuInstanceGetWGSLLanguageFeatures([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, WGPUSupportedWGSLLanguageFeatures* features); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBool")] + public static extern uint wgpuInstanceHasWGSLLanguageFeature([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, WGPUWGSLLanguageFeatureName feature); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuInstanceProcessEvents([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuInstanceRequestAdapter([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, [NativeTypeName("const WGPURequestAdapterOptions *")] WGPURequestAdapterOptions* options, WGPURequestAdapterCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUWaitStatus wgpuInstanceWaitAny([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance, [NativeTypeName("size_t")] nuint futureCount, WGPUFutureWaitInfo* futures, [NativeTypeName("uint64_t")] ulong timeoutNS); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuInstanceAddRef([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuInstanceRelease([NativeTypeName("WGPUInstance")] WGPUInstanceImpl* instance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuPipelineLayoutSetLabel([NativeTypeName("WGPUPipelineLayout")] WGPUPipelineLayoutImpl* pipelineLayout, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuPipelineLayoutAddRef([NativeTypeName("WGPUPipelineLayout")] WGPUPipelineLayoutImpl* pipelineLayout); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuPipelineLayoutRelease([NativeTypeName("WGPUPipelineLayout")] WGPUPipelineLayoutImpl* pipelineLayout); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQuerySetDestroy([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuQuerySetGetCount([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUQueryType wgpuQuerySetGetType([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQuerySetSetLabel([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQuerySetAddRef([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQuerySetRelease([NativeTypeName("WGPUQuerySet")] WGPUQuerySetImpl* querySet); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuQueueOnSubmittedWorkDone([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, WGPUQueueWorkDoneCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueSetLabel([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueSubmit([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, [NativeTypeName("size_t")] nuint commandCount, [NativeTypeName("const WGPUCommandBuffer *")] WGPUCommandBufferImpl** commands); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueWriteBuffer([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong bufferOffset, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueWriteTexture([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue, [NativeTypeName("const WGPUTexelCopyTextureInfo *")] WGPUTexelCopyTextureInfo* destination, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint dataSize, [NativeTypeName("const WGPUTexelCopyBufferLayout *")] WGPUTexelCopyBufferLayout* dataLayout, [NativeTypeName("const WGPUExtent3D *")] WGPUExtent3D* writeSize); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueAddRef([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuQueueRelease([NativeTypeName("WGPUQueue")] WGPUQueueImpl* queue); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleSetLabel([NativeTypeName("WGPURenderBundle")] WGPURenderBundleImpl* renderBundle, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleAddRef([NativeTypeName("WGPURenderBundle")] WGPURenderBundleImpl* renderBundle); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleRelease([NativeTypeName("WGPURenderBundle")] WGPURenderBundleImpl* renderBundle); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderDraw([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("uint32_t")] uint vertexCount, [NativeTypeName("uint32_t")] uint instanceCount, [NativeTypeName("uint32_t")] uint firstVertex, [NativeTypeName("uint32_t")] uint firstInstance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderDrawIndexed([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("uint32_t")] uint indexCount, [NativeTypeName("uint32_t")] uint instanceCount, [NativeTypeName("uint32_t")] uint firstIndex, [NativeTypeName("int32_t")] int baseVertex, [NativeTypeName("uint32_t")] uint firstInstance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderDrawIndexedIndirect([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* indirectBuffer, [NativeTypeName("uint64_t")] ulong indirectOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderDrawIndirect([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* indirectBuffer, [NativeTypeName("uint64_t")] ulong indirectOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPURenderBundle")] + public static extern WGPURenderBundleImpl* wgpuRenderBundleEncoderFinish([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("const WGPURenderBundleDescriptor *")] WGPURenderBundleDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderInsertDebugMarker([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, WGPUStringView markerLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderPopDebugGroup([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderPushDebugGroup([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, WGPUStringView groupLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetBindGroup([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("uint32_t")] uint groupIndex, [NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* group, [NativeTypeName("size_t")] nuint dynamicOffsetCount, [NativeTypeName("const uint32_t *")] uint* dynamicOffsets); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetImmediates([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("uint32_t")] uint offset, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetIndexBuffer([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, WGPUIndexFormat format, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetLabel([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetPipeline([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* pipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderSetVertexBuffer([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder, [NativeTypeName("uint32_t")] uint slot, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderAddRef([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderBundleEncoderRelease([NativeTypeName("WGPURenderBundleEncoder")] WGPURenderBundleEncoderImpl* renderBundleEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderBeginOcclusionQuery([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint queryIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderDraw([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint vertexCount, [NativeTypeName("uint32_t")] uint instanceCount, [NativeTypeName("uint32_t")] uint firstVertex, [NativeTypeName("uint32_t")] uint firstInstance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderDrawIndexed([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint indexCount, [NativeTypeName("uint32_t")] uint instanceCount, [NativeTypeName("uint32_t")] uint firstIndex, [NativeTypeName("int32_t")] int baseVertex, [NativeTypeName("uint32_t")] uint firstInstance); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderDrawIndexedIndirect([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* indirectBuffer, [NativeTypeName("uint64_t")] ulong indirectOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderDrawIndirect([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* indirectBuffer, [NativeTypeName("uint64_t")] ulong indirectOffset); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderEnd([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderEndOcclusionQuery([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderExecuteBundles([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("size_t")] nuint bundleCount, [NativeTypeName("const WGPURenderBundle *")] WGPURenderBundleImpl** bundles); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderInsertDebugMarker([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, WGPUStringView markerLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderPopDebugGroup([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderPushDebugGroup([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, WGPUStringView groupLabel); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetBindGroup([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint groupIndex, [NativeTypeName("WGPUBindGroup")] WGPUBindGroupImpl* group, [NativeTypeName("size_t")] nuint dynamicOffsetCount, [NativeTypeName("const uint32_t *")] uint* dynamicOffsets); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetBlendConstant([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("const WGPUColor *")] WGPUColor* color); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetImmediates([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint offset, [NativeTypeName("const void *")] void* data, [NativeTypeName("size_t")] nuint size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetIndexBuffer([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, WGPUIndexFormat format, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetLabel([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetPipeline([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* pipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetScissorRect([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint x, [NativeTypeName("uint32_t")] uint y, [NativeTypeName("uint32_t")] uint width, [NativeTypeName("uint32_t")] uint height); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetStencilReference([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint reference); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetVertexBuffer([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, [NativeTypeName("uint32_t")] uint slot, [NativeTypeName("WGPUBuffer")] WGPUBufferImpl* buffer, [NativeTypeName("uint64_t")] ulong offset, [NativeTypeName("uint64_t")] ulong size); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderSetViewport([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder, float x, float y, float width, float height, float minDepth, float maxDepth); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderAddRef([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPassEncoderRelease([NativeTypeName("WGPURenderPassEncoder")] WGPURenderPassEncoderImpl* renderPassEncoder); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUBindGroupLayout")] + public static extern WGPUBindGroupLayoutImpl* wgpuRenderPipelineGetBindGroupLayout([NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* renderPipeline, [NativeTypeName("uint32_t")] uint groupIndex); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPipelineSetLabel([NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* renderPipeline, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPipelineAddRef([NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* renderPipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuRenderPipelineRelease([NativeTypeName("WGPURenderPipeline")] WGPURenderPipelineImpl* renderPipeline); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSamplerSetLabel([NativeTypeName("WGPUSampler")] WGPUSamplerImpl* sampler, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSamplerAddRef([NativeTypeName("WGPUSampler")] WGPUSamplerImpl* sampler); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSamplerRelease([NativeTypeName("WGPUSampler")] WGPUSamplerImpl* sampler); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUFuture wgpuShaderModuleGetCompilationInfo([NativeTypeName("WGPUShaderModule")] WGPUShaderModuleImpl* shaderModule, WGPUCompilationInfoCallbackInfo callbackInfo); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuShaderModuleSetLabel([NativeTypeName("WGPUShaderModule")] WGPUShaderModuleImpl* shaderModule, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuShaderModuleAddRef([NativeTypeName("WGPUShaderModule")] WGPUShaderModuleImpl* shaderModule); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuShaderModuleRelease([NativeTypeName("WGPUShaderModule")] WGPUShaderModuleImpl* shaderModule); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSupportedFeaturesFreeMembers(WGPUSupportedFeatures supportedFeatures); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSupportedInstanceFeaturesFreeMembers(WGPUSupportedInstanceFeatures supportedInstanceFeatures); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSupportedWGSLLanguageFeaturesFreeMembers(WGPUSupportedWGSLLanguageFeatures supportedWGSLLanguageFeatures); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceConfigure([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface, [NativeTypeName("const WGPUSurfaceConfiguration *")] WGPUSurfaceConfiguration* config); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuSurfaceGetCapabilities([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface, [NativeTypeName("WGPUAdapter")] WGPUAdapterImpl* adapter, WGPUSurfaceCapabilities* capabilities); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceGetCurrentTexture([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface, WGPUSurfaceTexture* surfaceTexture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUStatus wgpuSurfacePresent([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceSetLabel([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceUnconfigure([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceAddRef([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceRelease([NativeTypeName("WGPUSurface")] WGPUSurfaceImpl* surface); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuSurfaceCapabilitiesFreeMembers(WGPUSurfaceCapabilities surfaceCapabilities); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUTextureView")] + public static extern WGPUTextureViewImpl* wgpuTextureCreateView([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture, [NativeTypeName("const WGPUTextureViewDescriptor *")] WGPUTextureViewDescriptor* descriptor); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureDestroy([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuTextureGetDepthOrArrayLayers([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUTextureDimension wgpuTextureGetDimension([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUTextureFormat wgpuTextureGetFormat([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuTextureGetHeight([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuTextureGetMipLevelCount([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuTextureGetSampleCount([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern WGPUTextureViewDimension wgpuTextureGetTextureBindingViewDimension([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("WGPUTextureUsage")] + public static extern ulong wgpuTextureGetUsage([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + [return: NativeTypeName("uint32_t")] + public static extern uint wgpuTextureGetWidth([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureSetLabel([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureAddRef([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureRelease([NativeTypeName("WGPUTexture")] WGPUTextureImpl* texture); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureViewSetLabel([NativeTypeName("WGPUTextureView")] WGPUTextureViewImpl* textureView, WGPUStringView label); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureViewAddRef([NativeTypeName("WGPUTextureView")] WGPUTextureViewImpl* textureView); + + [DllImport("wgpu_native", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + public static extern void wgpuTextureViewRelease([NativeTypeName("WGPUTextureView")] WGPUTextureViewImpl* textureView); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/GeneratedHeader.txt b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/GeneratedHeader.txt new file mode 100644 index 000000000..5dfce7720 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/GeneratedHeader.txt @@ -0,0 +1,3 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +// diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/webgpu.h b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/webgpu.h new file mode 100644 index 000000000..289fd6bb2 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/webgpu.h @@ -0,0 +1,6766 @@ +/** + * Copyright 2019-2023 WebGPU-Native developers + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +/** @file */ + +/** + * \mainpage + * + * **Important:** *This documentation is a Work In Progress.* + * + * This is the home of WebGPU C API specification. We define here the standard + * `webgpu.h` header that all implementations should provide. + * + * For all details where behavior is not otherwise specified, `webgpu.h` has + * the same behavior as the WebGPU specification for JavaScript on the Web. + * The WebIDL-based Web specification is mapped into C as faithfully (and + * bidirectionally) as practical/possible. + * The working draft of WebGPU can be found at . + * + * The standard include directive for this header is `#include ` + * (if it is provided in a system-wide or toolchain-wide include directory). + */ + +#ifndef WEBGPU_H_ +#define WEBGPU_H_ + +#if defined(WGPU_SHARED_LIBRARY) +# if defined(_WIN32) +# if defined(WGPU_IMPLEMENTATION) +# define WGPU_EXPORT __declspec(dllexport) +# else +# define WGPU_EXPORT __declspec(dllimport) +# endif +# else // defined(_WIN32) +# if defined(WGPU_IMPLEMENTATION) +# define WGPU_EXPORT __attribute__((visibility("default"))) +# else +# define WGPU_EXPORT +# endif +# endif // defined(_WIN32) +#else // defined(WGPU_SHARED_LIBRARY) +# define WGPU_EXPORT +#endif // defined(WGPU_SHARED_LIBRARY) + +#if !defined(WGPU_OBJECT_ATTRIBUTE) +#define WGPU_OBJECT_ATTRIBUTE +#endif +#if !defined(WGPU_ENUM_ATTRIBUTE) +#define WGPU_ENUM_ATTRIBUTE +#endif +#if !defined(WGPU_STRUCTURE_ATTRIBUTE) +#define WGPU_STRUCTURE_ATTRIBUTE +#endif +#if !defined(WGPU_FUNCTION_ATTRIBUTE) +#define WGPU_FUNCTION_ATTRIBUTE +#endif +#if !defined(WGPU_NULLABLE) +#define WGPU_NULLABLE +#endif + +#include +#include +#include + +#define _wgpu_COMMA , +#if defined(__cplusplus) +# define _wgpu_ENUM_ZERO_INIT(type) type(0) +# define _wgpu_STRUCT_ZERO_INIT {} +# if __cplusplus >= 201103L +# define _wgpu_MAKE_INIT_STRUCT(type, value) (type value) +# else +# define _wgpu_MAKE_INIT_STRUCT(type, value) value +# endif +#else +# define _wgpu_ENUM_ZERO_INIT(type) (type)0 +# define _wgpu_STRUCT_ZERO_INIT {0} +# if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L +# define _wgpu_MAKE_INIT_STRUCT(type, value) ((type) value) +# else +# define _wgpu_MAKE_INIT_STRUCT(type, value) value +# endif +#endif + +/** + * \defgroup Constants Constants + * \brief Constants. + * + * @{ + */ + +/** + * 'True' value of @ref WGPUBool. + * + * @remark It's not usually necessary to use this, as `true` (from + * `stdbool.h` or C++) casts to the same value. + */ +#define WGPU_TRUE (UINT32_C(1)) +/** + * 'False' value of @ref WGPUBool. + * + * @remark It's not usually necessary to use this, as `false` (from + * `stdbool.h` or C++) casts to the same value. + */ +#define WGPU_FALSE (UINT32_C(0)) +/** + * Indicates no array layer count is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_ARRAY_LAYER_COUNT_UNDEFINED (UINT32_MAX) +/** + * Indicates no copy stride is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_COPY_STRIDE_UNDEFINED (UINT32_MAX) +/** + * Indicates no depth clear value is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_DEPTH_CLEAR_VALUE_UNDEFINED (NAN) +/** + * Indicates no depth slice is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_DEPTH_SLICE_UNDEFINED (UINT32_MAX) +/** + * For `uint32_t` limits, indicates no limit value is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_LIMIT_U32_UNDEFINED (UINT32_MAX) +/** + * For `uint64_t` limits, indicates no limit value is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_LIMIT_U64_UNDEFINED (UINT64_MAX) +/** + * Indicates no mip level count is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_MIP_LEVEL_COUNT_UNDEFINED (UINT32_MAX) +/** + * Indicates no query set index is specified. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_QUERY_SET_INDEX_UNDEFINED (UINT32_MAX) +/** + * Sentinel value used in @ref WGPUStringView to indicate that the pointer + * is to a null-terminated string, rather than an explicitly-sized string. + */ +#define WGPU_STRLEN (SIZE_MAX) +/** + * Indicates a size extending to the end of the buffer. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_WHOLE_MAP_SIZE (SIZE_MAX) +/** + * Indicates a size extending to the end of the buffer. For more info, + * see @ref SentinelValues and the places that use this sentinel value. + */ +#define WGPU_WHOLE_SIZE (UINT64_MAX) + +/** @} */ + +/** + * \defgroup UtilityTypes Utility Types + * + * @{ + */ + +/** + * Nullable value defining a pointer+length view into a UTF-8 encoded string. + * + * Values passed into the API may use the special length value @ref WGPU_STRLEN + * to indicate a null-terminated string. + * Non-null values passed out of the API (for example as callback arguments) + * always provide an explicit length and **may or may not be null-terminated**. + * + * Some inputs to the API accept null values. Those which do not accept null + * values "default" to the empty string when null values are passed. + * + * Values are encoded as follows: + * - `{NULL, WGPU_STRLEN}`: the null value. + * - `{non_null_pointer, WGPU_STRLEN}`: a null-terminated string view. + * - `{any, 0}`: the empty string. + * - `{NULL, non_zero_length}`: not allowed (null dereference). + * - `{non_null_pointer, non_zero_length}`: an explictly-sized string view with + * size `non_zero_length` (in bytes). + * + * For info on how this is used in various places, see \ref Strings. + */ +typedef struct WGPUStringView { + WGPU_NULLABLE char const * data; + size_t length; +} WGPUStringView WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUStringView. + */ +#define WGPU_STRING_VIEW_INIT _wgpu_MAKE_INIT_STRUCT(WGPUStringView, { \ + /*.data=*/NULL _wgpu_COMMA \ + /*.length=*/WGPU_STRLEN _wgpu_COMMA \ +}) + +typedef uint64_t WGPUFlags; +typedef uint32_t WGPUBool; + +/** @} */ + +/** + * \defgroup Objects Objects + * \brief Opaque, non-dispatchable handles to WebGPU objects. + * + * @{ + */ +typedef struct WGPUAdapterImpl* WGPUAdapter WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUBindGroupImpl* WGPUBindGroup WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUBindGroupLayoutImpl* WGPUBindGroupLayout WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUBufferImpl* WGPUBuffer WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUCommandBufferImpl* WGPUCommandBuffer WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUCommandEncoderImpl* WGPUCommandEncoder WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUComputePassEncoderImpl* WGPUComputePassEncoder WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUComputePipelineImpl* WGPUComputePipeline WGPU_OBJECT_ATTRIBUTE; +/** + * TODO + * + * Releasing the last ref to a `WGPUDevice` also calls @ref wgpuDeviceDestroy. + * For more info, see @ref DeviceRelease. + */ +typedef struct WGPUDeviceImpl* WGPUDevice WGPU_OBJECT_ATTRIBUTE; +/** + * A sampleable 2D texture that may perform 0-copy YUV sampling internally. Creation of @ref WGPUExternalTexture is extremely implementation-dependent and not defined in this header. + */ +typedef struct WGPUExternalTextureImpl* WGPUExternalTexture WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUInstanceImpl* WGPUInstance WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUPipelineLayoutImpl* WGPUPipelineLayout WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUQuerySetImpl* WGPUQuerySet WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUQueueImpl* WGPUQueue WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPURenderBundleImpl* WGPURenderBundle WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPURenderBundleEncoderImpl* WGPURenderBundleEncoder WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPURenderPassEncoderImpl* WGPURenderPassEncoder WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPURenderPipelineImpl* WGPURenderPipeline WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUSamplerImpl* WGPUSampler WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUShaderModuleImpl* WGPUShaderModule WGPU_OBJECT_ATTRIBUTE; +/** + * An object used to continuously present image data to the user, see @ref Surfaces for more details. + */ +typedef struct WGPUSurfaceImpl* WGPUSurface WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUTextureImpl* WGPUTexture WGPU_OBJECT_ATTRIBUTE; +typedef struct WGPUTextureViewImpl* WGPUTextureView WGPU_OBJECT_ATTRIBUTE; + +/** @} */ + +// Structure forward declarations +struct WGPUAdapterInfo; +struct WGPUBlendComponent; +struct WGPUBufferBindingLayout; +struct WGPUBufferDescriptor; +struct WGPUColor; +struct WGPUCommandBufferDescriptor; +struct WGPUCommandEncoderDescriptor; +struct WGPUCompatibilityModeLimits; +struct WGPUCompilationMessage; +struct WGPUConstantEntry; +struct WGPUExtent3D; +struct WGPUExternalTextureBindingEntry; +struct WGPUExternalTextureBindingLayout; +struct WGPUFuture; +struct WGPUInstanceLimits; +struct WGPUMultisampleState; +struct WGPUOrigin3D; +struct WGPUPassTimestampWrites; +struct WGPUPipelineLayoutDescriptor; +struct WGPUPrimitiveState; +struct WGPUQuerySetDescriptor; +struct WGPUQueueDescriptor; +struct WGPURenderBundleDescriptor; +struct WGPURenderBundleEncoderDescriptor; +struct WGPURenderPassDepthStencilAttachment; +struct WGPURenderPassMaxDrawCount; +struct WGPURequestAdapterWebXROptions; +struct WGPUSamplerBindingLayout; +struct WGPUSamplerDescriptor; +struct WGPUShaderSourceSPIRV; +struct WGPUShaderSourceWGSL; +struct WGPUStencilFaceState; +struct WGPUStorageTextureBindingLayout; +struct WGPUSupportedFeatures; +struct WGPUSupportedInstanceFeatures; +struct WGPUSupportedWGSLLanguageFeatures; +struct WGPUSurfaceCapabilities; +struct WGPUSurfaceColorManagement; +struct WGPUSurfaceConfiguration; +struct WGPUSurfaceSourceAndroidNativeWindow; +struct WGPUSurfaceSourceMetalLayer; +struct WGPUSurfaceSourceWaylandSurface; +struct WGPUSurfaceSourceWindowsHWND; +struct WGPUSurfaceSourceXCBWindow; +struct WGPUSurfaceSourceXlibWindow; +struct WGPUSurfaceTexture; +struct WGPUTexelCopyBufferLayout; +struct WGPUTextureBindingLayout; +struct WGPUTextureBindingViewDimension; +struct WGPUTextureComponentSwizzle; +struct WGPUVertexAttribute; +struct WGPUBindGroupEntry; +struct WGPUBindGroupLayoutEntry; +struct WGPUBlendState; +struct WGPUCompilationInfo; +struct WGPUComputePassDescriptor; +struct WGPUComputeState; +struct WGPUDepthStencilState; +struct WGPUFutureWaitInfo; +struct WGPUInstanceDescriptor; +struct WGPULimits; +struct WGPURenderPassColorAttachment; +struct WGPURequestAdapterOptions; +struct WGPUShaderModuleDescriptor; +struct WGPUSurfaceDescriptor; +struct WGPUTexelCopyBufferInfo; +struct WGPUTexelCopyTextureInfo; +struct WGPUTextureComponentSwizzleDescriptor; +struct WGPUTextureDescriptor; +struct WGPUVertexBufferLayout; +struct WGPUBindGroupDescriptor; +struct WGPUBindGroupLayoutDescriptor; +struct WGPUColorTargetState; +struct WGPUComputePipelineDescriptor; +struct WGPUDeviceDescriptor; +struct WGPURenderPassDescriptor; +struct WGPUTextureViewDescriptor; +struct WGPUVertexState; +struct WGPUFragmentState; +struct WGPURenderPipelineDescriptor; + +// Callback info structure forward declarations +struct WGPUBufferMapCallbackInfo; +struct WGPUCompilationInfoCallbackInfo; +struct WGPUCreateComputePipelineAsyncCallbackInfo; +struct WGPUCreateRenderPipelineAsyncCallbackInfo; +struct WGPUDeviceLostCallbackInfo; +struct WGPUPopErrorScopeCallbackInfo; +struct WGPUQueueWorkDoneCallbackInfo; +struct WGPURequestAdapterCallbackInfo; +struct WGPURequestDeviceCallbackInfo; +struct WGPUUncapturedErrorCallbackInfo; + +/** + * \defgroup Enumerations Enumerations + * \brief Enums. + * + * @{ + */ + +typedef enum WGPUAdapterType { + WGPUAdapterType_DiscreteGPU = 0x00000001, + WGPUAdapterType_IntegratedGPU = 0x00000002, + WGPUAdapterType_CPU = 0x00000003, + WGPUAdapterType_Unknown = 0x00000004, + WGPUAdapterType_Force32 = 0x7FFFFFFF +} WGPUAdapterType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUAddressMode { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUAddressMode_Undefined = 0x00000000, + WGPUAddressMode_ClampToEdge = 0x00000001, + WGPUAddressMode_Repeat = 0x00000002, + WGPUAddressMode_MirrorRepeat = 0x00000003, + WGPUAddressMode_Force32 = 0x7FFFFFFF +} WGPUAddressMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUBackendType { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUBackendType_Undefined = 0x00000000, + WGPUBackendType_Null = 0x00000001, + WGPUBackendType_WebGPU = 0x00000002, + WGPUBackendType_D3D11 = 0x00000003, + WGPUBackendType_D3D12 = 0x00000004, + WGPUBackendType_Metal = 0x00000005, + WGPUBackendType_Vulkan = 0x00000006, + WGPUBackendType_OpenGL = 0x00000007, + WGPUBackendType_OpenGLES = 0x00000008, + WGPUBackendType_Force32 = 0x7FFFFFFF +} WGPUBackendType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUBlendFactor { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUBlendFactor_Undefined = 0x00000000, + WGPUBlendFactor_Zero = 0x00000001, + WGPUBlendFactor_One = 0x00000002, + WGPUBlendFactor_Src = 0x00000003, + WGPUBlendFactor_OneMinusSrc = 0x00000004, + WGPUBlendFactor_SrcAlpha = 0x00000005, + WGPUBlendFactor_OneMinusSrcAlpha = 0x00000006, + WGPUBlendFactor_Dst = 0x00000007, + WGPUBlendFactor_OneMinusDst = 0x00000008, + WGPUBlendFactor_DstAlpha = 0x00000009, + WGPUBlendFactor_OneMinusDstAlpha = 0x0000000A, + WGPUBlendFactor_SrcAlphaSaturated = 0x0000000B, + WGPUBlendFactor_Constant = 0x0000000C, + WGPUBlendFactor_OneMinusConstant = 0x0000000D, + WGPUBlendFactor_Src1 = 0x0000000E, + WGPUBlendFactor_OneMinusSrc1 = 0x0000000F, + WGPUBlendFactor_Src1Alpha = 0x00000010, + WGPUBlendFactor_OneMinusSrc1Alpha = 0x00000011, + WGPUBlendFactor_Force32 = 0x7FFFFFFF +} WGPUBlendFactor WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUBlendOperation { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUBlendOperation_Undefined = 0x00000000, + WGPUBlendOperation_Add = 0x00000001, + WGPUBlendOperation_Subtract = 0x00000002, + WGPUBlendOperation_ReverseSubtract = 0x00000003, + WGPUBlendOperation_Min = 0x00000004, + WGPUBlendOperation_Max = 0x00000005, + WGPUBlendOperation_Force32 = 0x7FFFFFFF +} WGPUBlendOperation WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUBufferBindingType { + /** + * `0`. Indicates that this @ref WGPUBufferBindingLayout member of + * its parent @ref WGPUBindGroupLayoutEntry is not used. + * (See also @ref SentinelValues.) + */ + WGPUBufferBindingType_BindingNotUsed = 0x00000000, + /** + * `1`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUBufferBindingType_Undefined = 0x00000001, + WGPUBufferBindingType_Uniform = 0x00000002, + WGPUBufferBindingType_Storage = 0x00000003, + WGPUBufferBindingType_ReadOnlyStorage = 0x00000004, + WGPUBufferBindingType_Force32 = 0x7FFFFFFF +} WGPUBufferBindingType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUBufferMapState { + WGPUBufferMapState_Unmapped = 0x00000001, + WGPUBufferMapState_Pending = 0x00000002, + WGPUBufferMapState_Mapped = 0x00000003, + WGPUBufferMapState_Force32 = 0x7FFFFFFF +} WGPUBufferMapState WGPU_ENUM_ATTRIBUTE; + +/** + * The callback mode controls how a callback for an asynchronous operation may be fired. See @ref Asynchronous-Operations for how these are used. + */ +typedef enum WGPUCallbackMode { + /** + * Callbacks created with `WGPUCallbackMode_WaitAnyOnly`: + * - fire when the asynchronous operation's future is passed to a call to @ref wgpuInstanceWaitAny + * AND the operation has already completed or it completes inside the call to @ref wgpuInstanceWaitAny. + */ + WGPUCallbackMode_WaitAnyOnly = 0x00000001, + /** + * Callbacks created with `WGPUCallbackMode_AllowProcessEvents`: + * - fire for the same reasons as callbacks created with `WGPUCallbackMode_WaitAnyOnly` + * - fire inside a call to @ref wgpuInstanceProcessEvents if the asynchronous operation is complete. + */ + WGPUCallbackMode_AllowProcessEvents = 0x00000002, + /** + * Callbacks created with `WGPUCallbackMode_AllowSpontaneous`: + * - fire for the same reasons as callbacks created with `WGPUCallbackMode_AllowProcessEvents` + * - **may** fire spontaneously on an arbitrary or application thread, when the WebGPU implementations discovers that the asynchronous operation is complete. + * + * Implementations _should_ fire spontaneous callbacks as soon as possible. + * + * @note Because spontaneous callbacks may fire at an arbitrary time on an arbitrary thread, applications should take extra care when acquiring locks or mutating state inside the callback. It undefined behavior to re-entrantly call into the webgpu.h API if the callback fires while inside the callstack of another webgpu.h function that is not `wgpuInstanceWaitAny` or `wgpuInstanceProcessEvents`. + */ + WGPUCallbackMode_AllowSpontaneous = 0x00000003, + WGPUCallbackMode_Force32 = 0x7FFFFFFF +} WGPUCallbackMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUCompareFunction { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUCompareFunction_Undefined = 0x00000000, + WGPUCompareFunction_Never = 0x00000001, + WGPUCompareFunction_Less = 0x00000002, + WGPUCompareFunction_Equal = 0x00000003, + WGPUCompareFunction_LessEqual = 0x00000004, + WGPUCompareFunction_Greater = 0x00000005, + WGPUCompareFunction_NotEqual = 0x00000006, + WGPUCompareFunction_GreaterEqual = 0x00000007, + WGPUCompareFunction_Always = 0x00000008, + WGPUCompareFunction_Force32 = 0x7FFFFFFF +} WGPUCompareFunction WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUCompilationInfoRequestStatus { + WGPUCompilationInfoRequestStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPUCompilationInfoRequestStatus_CallbackCancelled = 0x00000002, + WGPUCompilationInfoRequestStatus_Force32 = 0x7FFFFFFF +} WGPUCompilationInfoRequestStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUCompilationMessageType { + WGPUCompilationMessageType_Error = 0x00000001, + WGPUCompilationMessageType_Warning = 0x00000002, + WGPUCompilationMessageType_Info = 0x00000003, + WGPUCompilationMessageType_Force32 = 0x7FFFFFFF +} WGPUCompilationMessageType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUComponentSwizzle { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUComponentSwizzle_Undefined = 0x00000000, + /** + * Force its value to 0. + */ + WGPUComponentSwizzle_Zero = 0x00000001, + /** + * Force its value to 1. + */ + WGPUComponentSwizzle_One = 0x00000002, + /** + * Take its value from the red channel of the texture. + */ + WGPUComponentSwizzle_R = 0x00000003, + /** + * Take its value from the green channel of the texture. + */ + WGPUComponentSwizzle_G = 0x00000004, + /** + * Take its value from the blue channel of the texture. + */ + WGPUComponentSwizzle_B = 0x00000005, + /** + * Take its value from the alpha channel of the texture. + */ + WGPUComponentSwizzle_A = 0x00000006, + WGPUComponentSwizzle_Force32 = 0x7FFFFFFF +} WGPUComponentSwizzle WGPU_ENUM_ATTRIBUTE; + +/** + * Describes how frames are composited with other contents on the screen when @ref wgpuSurfacePresent is called. + */ +typedef enum WGPUCompositeAlphaMode { + /** + * `0`. Lets the WebGPU implementation choose the best mode (supported, and with the best performance) between @ref WGPUCompositeAlphaMode_Opaque or @ref WGPUCompositeAlphaMode_Inherit. + */ + WGPUCompositeAlphaMode_Auto = 0x00000000, + /** + * The alpha component of the image is ignored and teated as if it is always 1.0. + */ + WGPUCompositeAlphaMode_Opaque = 0x00000001, + /** + * The alpha component is respected and non-alpha components are assumed to be already multiplied with the alpha component. For example, (0.5, 0, 0, 0.5) is semi-transparent bright red. + */ + WGPUCompositeAlphaMode_Premultiplied = 0x00000002, + /** + * The alpha component is respected and non-alpha components are assumed to NOT be already multiplied with the alpha component. For example, (1.0, 0, 0, 0.5) is semi-transparent bright red. + */ + WGPUCompositeAlphaMode_Unpremultiplied = 0x00000003, + /** + * The handling of the alpha component is unknown to WebGPU and should be handled by the application using system-specific APIs. This mode may be unavailable (for example on Wasm). + */ + WGPUCompositeAlphaMode_Inherit = 0x00000004, + WGPUCompositeAlphaMode_Force32 = 0x7FFFFFFF +} WGPUCompositeAlphaMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUCreatePipelineAsyncStatus { + WGPUCreatePipelineAsyncStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPUCreatePipelineAsyncStatus_CallbackCancelled = 0x00000002, + WGPUCreatePipelineAsyncStatus_ValidationError = 0x00000003, + WGPUCreatePipelineAsyncStatus_InternalError = 0x00000004, + WGPUCreatePipelineAsyncStatus_Force32 = 0x7FFFFFFF +} WGPUCreatePipelineAsyncStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUCullMode { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUCullMode_Undefined = 0x00000000, + WGPUCullMode_None = 0x00000001, + WGPUCullMode_Front = 0x00000002, + WGPUCullMode_Back = 0x00000003, + WGPUCullMode_Force32 = 0x7FFFFFFF +} WGPUCullMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUDeviceLostReason { + WGPUDeviceLostReason_Unknown = 0x00000001, + WGPUDeviceLostReason_Destroyed = 0x00000002, + /** + * See @ref CallbackStatuses. + */ + WGPUDeviceLostReason_CallbackCancelled = 0x00000003, + WGPUDeviceLostReason_FailedCreation = 0x00000004, + WGPUDeviceLostReason_Force32 = 0x7FFFFFFF +} WGPUDeviceLostReason WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUErrorFilter { + WGPUErrorFilter_Validation = 0x00000001, + WGPUErrorFilter_OutOfMemory = 0x00000002, + WGPUErrorFilter_Internal = 0x00000003, + WGPUErrorFilter_Force32 = 0x7FFFFFFF +} WGPUErrorFilter WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUErrorType { + WGPUErrorType_NoError = 0x00000001, + WGPUErrorType_Validation = 0x00000002, + WGPUErrorType_OutOfMemory = 0x00000003, + WGPUErrorType_Internal = 0x00000004, + WGPUErrorType_Unknown = 0x00000005, + WGPUErrorType_Force32 = 0x7FFFFFFF +} WGPUErrorType WGPU_ENUM_ATTRIBUTE; + +/** + * See @ref WGPURequestAdapterOptions::featureLevel. + */ +typedef enum WGPUFeatureLevel { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUFeatureLevel_Undefined = 0x00000000, + /** + * "Compatibility" profile which can be supported on OpenGL ES 3.1 and D3D11. + */ + WGPUFeatureLevel_Compatibility = 0x00000001, + /** + * "Core" profile which can be supported on Vulkan/Metal/D3D12 (at least). + */ + WGPUFeatureLevel_Core = 0x00000002, + WGPUFeatureLevel_Force32 = 0x7FFFFFFF +} WGPUFeatureLevel WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUFeatureName { + WGPUFeatureName_CoreFeaturesAndLimits = 0x00000001, + WGPUFeatureName_DepthClipControl = 0x00000002, + WGPUFeatureName_Depth32FloatStencil8 = 0x00000003, + WGPUFeatureName_TextureCompressionBC = 0x00000004, + WGPUFeatureName_TextureCompressionBCSliced3D = 0x00000005, + WGPUFeatureName_TextureCompressionETC2 = 0x00000006, + WGPUFeatureName_TextureCompressionASTC = 0x00000007, + WGPUFeatureName_TextureCompressionASTCSliced3D = 0x00000008, + WGPUFeatureName_TimestampQuery = 0x00000009, + WGPUFeatureName_IndirectFirstInstance = 0x0000000A, + WGPUFeatureName_ShaderF16 = 0x0000000B, + WGPUFeatureName_RG11B10UfloatRenderable = 0x0000000C, + WGPUFeatureName_BGRA8UnormStorage = 0x0000000D, + WGPUFeatureName_Float32Filterable = 0x0000000E, + WGPUFeatureName_Float32Blendable = 0x0000000F, + WGPUFeatureName_ClipDistances = 0x00000010, + WGPUFeatureName_DualSourceBlending = 0x00000011, + WGPUFeatureName_Subgroups = 0x00000012, + WGPUFeatureName_TextureFormatsTier1 = 0x00000013, + WGPUFeatureName_TextureFormatsTier2 = 0x00000014, + WGPUFeatureName_PrimitiveIndex = 0x00000015, + WGPUFeatureName_TextureComponentSwizzle = 0x00000016, + WGPUFeatureName_Force32 = 0x7FFFFFFF +} WGPUFeatureName WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUFilterMode { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUFilterMode_Undefined = 0x00000000, + WGPUFilterMode_Nearest = 0x00000001, + WGPUFilterMode_Linear = 0x00000002, + WGPUFilterMode_Force32 = 0x7FFFFFFF +} WGPUFilterMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUFrontFace { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUFrontFace_Undefined = 0x00000000, + WGPUFrontFace_CCW = 0x00000001, + WGPUFrontFace_CW = 0x00000002, + WGPUFrontFace_Force32 = 0x7FFFFFFF +} WGPUFrontFace WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUIndexFormat { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUIndexFormat_Undefined = 0x00000000, + WGPUIndexFormat_Uint16 = 0x00000001, + WGPUIndexFormat_Uint32 = 0x00000002, + WGPUIndexFormat_Force32 = 0x7FFFFFFF +} WGPUIndexFormat WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUInstanceFeatureName { + /** + * Enable use of ::wgpuInstanceWaitAny with `timeoutNS > 0`. + */ + WGPUInstanceFeatureName_TimedWaitAny = 0x00000001, + /** + * Enable passing SPIR-V shaders to @ref wgpuDeviceCreateShaderModule, + * via @ref WGPUShaderSourceSPIRV. + */ + WGPUInstanceFeatureName_ShaderSourceSPIRV = 0x00000002, + /** + * Normally, a @ref WGPUAdapter can only create a single device. If this is + * available and enabled, then adapters won't immediately expire when they + * create a device, so can be reused to make multiple devices. They may + * still expire for other reasons. + */ + WGPUInstanceFeatureName_MultipleDevicesPerAdapter = 0x00000003, + WGPUInstanceFeatureName_Force32 = 0x7FFFFFFF +} WGPUInstanceFeatureName WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPULoadOp { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPULoadOp_Undefined = 0x00000000, + WGPULoadOp_Load = 0x00000001, + WGPULoadOp_Clear = 0x00000002, + WGPULoadOp_Force32 = 0x7FFFFFFF +} WGPULoadOp WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUMapAsyncStatus { + WGPUMapAsyncStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPUMapAsyncStatus_CallbackCancelled = 0x00000002, + WGPUMapAsyncStatus_Error = 0x00000003, + WGPUMapAsyncStatus_Aborted = 0x00000004, + WGPUMapAsyncStatus_Force32 = 0x7FFFFFFF +} WGPUMapAsyncStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUMipmapFilterMode { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUMipmapFilterMode_Undefined = 0x00000000, + WGPUMipmapFilterMode_Nearest = 0x00000001, + WGPUMipmapFilterMode_Linear = 0x00000002, + WGPUMipmapFilterMode_Force32 = 0x7FFFFFFF +} WGPUMipmapFilterMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUOptionalBool { + /** + * `0`. + */ + WGPUOptionalBool_False = 0x00000000, + WGPUOptionalBool_True = 0x00000001, + WGPUOptionalBool_Undefined = 0x00000002, + WGPUOptionalBool_Force32 = 0x7FFFFFFF +} WGPUOptionalBool WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUPopErrorScopeStatus { + /** + * The error scope stack was successfully popped and a result was reported. + */ + WGPUPopErrorScopeStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPUPopErrorScopeStatus_CallbackCancelled = 0x00000002, + /** + * The error scope stack could not be popped, because it was empty. + */ + WGPUPopErrorScopeStatus_Error = 0x00000003, + WGPUPopErrorScopeStatus_Force32 = 0x7FFFFFFF +} WGPUPopErrorScopeStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUPowerPreference { + /** + * `0`. No preference. (See also @ref SentinelValues.) + */ + WGPUPowerPreference_Undefined = 0x00000000, + WGPUPowerPreference_LowPower = 0x00000001, + WGPUPowerPreference_HighPerformance = 0x00000002, + WGPUPowerPreference_Force32 = 0x7FFFFFFF +} WGPUPowerPreference WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUPredefinedColorSpace { + WGPUPredefinedColorSpace_SRGB = 0x00000001, + WGPUPredefinedColorSpace_DisplayP3 = 0x00000002, + WGPUPredefinedColorSpace_Force32 = 0x7FFFFFFF +} WGPUPredefinedColorSpace WGPU_ENUM_ATTRIBUTE; + +/** + * Describes when and in which order frames are presented on the screen when @ref wgpuSurfacePresent is called. + */ +typedef enum WGPUPresentMode { + /** + * `0`. Present mode is not specified. Use the default. + */ + WGPUPresentMode_Undefined = 0x00000000, + /** + * The presentation of the image to the user waits for the next vertical blanking period to update in a first-in, first-out manner. + * Tearing cannot be observed and frame-loop will be limited to the display's refresh rate. + * This is the only mode that's always available. + */ + WGPUPresentMode_Fifo = 0x00000001, + /** + * The presentation of the image to the user tries to wait for the next vertical blanking period but may decide to not wait if a frame is presented late. + * Tearing can sometimes be observed but late-frame don't produce a full-frame stutter in the presentation. + * This is still a first-in, first-out mechanism so a frame-loop will be limited to the display's refresh rate. + */ + WGPUPresentMode_FifoRelaxed = 0x00000002, + /** + * The presentation of the image to the user is updated immediately without waiting for a vertical blank. + * Tearing can be observed but latency is minimized. + */ + WGPUPresentMode_Immediate = 0x00000003, + /** + * The presentation of the image to the user waits for the next vertical blanking period to update to the latest provided image. + * Tearing cannot be observed and a frame-loop is not limited to the display's refresh rate. + */ + WGPUPresentMode_Mailbox = 0x00000004, + WGPUPresentMode_Force32 = 0x7FFFFFFF +} WGPUPresentMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUPrimitiveTopology { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUPrimitiveTopology_Undefined = 0x00000000, + WGPUPrimitiveTopology_PointList = 0x00000001, + WGPUPrimitiveTopology_LineList = 0x00000002, + WGPUPrimitiveTopology_LineStrip = 0x00000003, + WGPUPrimitiveTopology_TriangleList = 0x00000004, + WGPUPrimitiveTopology_TriangleStrip = 0x00000005, + WGPUPrimitiveTopology_Force32 = 0x7FFFFFFF +} WGPUPrimitiveTopology WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUQueryType { + WGPUQueryType_Occlusion = 0x00000001, + WGPUQueryType_Timestamp = 0x00000002, + WGPUQueryType_Force32 = 0x7FFFFFFF +} WGPUQueryType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUQueueWorkDoneStatus { + WGPUQueueWorkDoneStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPUQueueWorkDoneStatus_CallbackCancelled = 0x00000002, + /** + * There was some deterministic error. (Note this is currently never used, + * but it will be relevant when it's possible to create a queue object.) + */ + WGPUQueueWorkDoneStatus_Error = 0x00000003, + WGPUQueueWorkDoneStatus_Force32 = 0x7FFFFFFF +} WGPUQueueWorkDoneStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPURequestAdapterStatus { + WGPURequestAdapterStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPURequestAdapterStatus_CallbackCancelled = 0x00000002, + WGPURequestAdapterStatus_Unavailable = 0x00000003, + WGPURequestAdapterStatus_Error = 0x00000004, + WGPURequestAdapterStatus_Force32 = 0x7FFFFFFF +} WGPURequestAdapterStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPURequestDeviceStatus { + WGPURequestDeviceStatus_Success = 0x00000001, + /** + * See @ref CallbackStatuses. + */ + WGPURequestDeviceStatus_CallbackCancelled = 0x00000002, + WGPURequestDeviceStatus_Error = 0x00000003, + WGPURequestDeviceStatus_Force32 = 0x7FFFFFFF +} WGPURequestDeviceStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUSamplerBindingType { + /** + * `0`. Indicates that this @ref WGPUSamplerBindingLayout member of + * its parent @ref WGPUBindGroupLayoutEntry is not used. + * (See also @ref SentinelValues.) + */ + WGPUSamplerBindingType_BindingNotUsed = 0x00000000, + /** + * `1`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUSamplerBindingType_Undefined = 0x00000001, + WGPUSamplerBindingType_Filtering = 0x00000002, + WGPUSamplerBindingType_NonFiltering = 0x00000003, + WGPUSamplerBindingType_Comparison = 0x00000004, + WGPUSamplerBindingType_Force32 = 0x7FFFFFFF +} WGPUSamplerBindingType WGPU_ENUM_ATTRIBUTE; + +/** + * Status code returned (synchronously) from many operations. Generally + * indicates an invalid input like an unknown enum value or @ref OutStructChainError. + * Read the function's documentation for specific error conditions. + */ +typedef enum WGPUStatus { + WGPUStatus_Success = 0x00000001, + WGPUStatus_Error = 0x00000002, + WGPUStatus_Force32 = 0x7FFFFFFF +} WGPUStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUStencilOperation { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUStencilOperation_Undefined = 0x00000000, + WGPUStencilOperation_Keep = 0x00000001, + WGPUStencilOperation_Zero = 0x00000002, + WGPUStencilOperation_Replace = 0x00000003, + WGPUStencilOperation_Invert = 0x00000004, + WGPUStencilOperation_IncrementClamp = 0x00000005, + WGPUStencilOperation_DecrementClamp = 0x00000006, + WGPUStencilOperation_IncrementWrap = 0x00000007, + WGPUStencilOperation_DecrementWrap = 0x00000008, + WGPUStencilOperation_Force32 = 0x7FFFFFFF +} WGPUStencilOperation WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUStorageTextureAccess { + /** + * `0`. Indicates that this @ref WGPUStorageTextureBindingLayout member of + * its parent @ref WGPUBindGroupLayoutEntry is not used. + * (See also @ref SentinelValues.) + */ + WGPUStorageTextureAccess_BindingNotUsed = 0x00000000, + /** + * `1`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUStorageTextureAccess_Undefined = 0x00000001, + WGPUStorageTextureAccess_WriteOnly = 0x00000002, + WGPUStorageTextureAccess_ReadOnly = 0x00000003, + WGPUStorageTextureAccess_ReadWrite = 0x00000004, + WGPUStorageTextureAccess_Force32 = 0x7FFFFFFF +} WGPUStorageTextureAccess WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUStoreOp { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUStoreOp_Undefined = 0x00000000, + WGPUStoreOp_Store = 0x00000001, + WGPUStoreOp_Discard = 0x00000002, + WGPUStoreOp_Force32 = 0x7FFFFFFF +} WGPUStoreOp WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUSType { + WGPUSType_ShaderSourceSPIRV = 0x00000001, + WGPUSType_ShaderSourceWGSL = 0x00000002, + WGPUSType_RenderPassMaxDrawCount = 0x00000003, + WGPUSType_SurfaceSourceMetalLayer = 0x00000004, + WGPUSType_SurfaceSourceWindowsHWND = 0x00000005, + WGPUSType_SurfaceSourceXlibWindow = 0x00000006, + WGPUSType_SurfaceSourceWaylandSurface = 0x00000007, + WGPUSType_SurfaceSourceAndroidNativeWindow = 0x00000008, + WGPUSType_SurfaceSourceXCBWindow = 0x00000009, + WGPUSType_SurfaceColorManagement = 0x0000000A, + WGPUSType_RequestAdapterWebXROptions = 0x0000000B, + WGPUSType_TextureComponentSwizzleDescriptor = 0x0000000C, + WGPUSType_ExternalTextureBindingLayout = 0x0000000D, + WGPUSType_ExternalTextureBindingEntry = 0x0000000E, + WGPUSType_CompatibilityModeLimits = 0x0000000F, + WGPUSType_TextureBindingViewDimension = 0x00000010, + WGPUSType_Force32 = 0x7FFFFFFF +} WGPUSType WGPU_ENUM_ATTRIBUTE; + +/** + * The status enum for @ref wgpuSurfaceGetCurrentTexture. + */ +typedef enum WGPUSurfaceGetCurrentTextureStatus { + /** + * Yay! Everything is good and we can render this frame. + */ + WGPUSurfaceGetCurrentTextureStatus_SuccessOptimal = 0x00000001, + /** + * Still OK - the surface can present the frame, but in a suboptimal way. The surface may need reconfiguration. + */ + WGPUSurfaceGetCurrentTextureStatus_SuccessSuboptimal = 0x00000002, + /** + * Some operation timed out while trying to acquire the frame. + */ + WGPUSurfaceGetCurrentTextureStatus_Timeout = 0x00000003, + /** + * The surface is too different to be used, compared to when it was originally created. + */ + WGPUSurfaceGetCurrentTextureStatus_Outdated = 0x00000004, + /** + * The connection to whatever owns the surface was lost, or generally needs to be fully reinitialized. + */ + WGPUSurfaceGetCurrentTextureStatus_Lost = 0x00000005, + /** + * There was some deterministic error (for example, the surface is not configured, or there was an @ref OutStructChainError). Should produce @ref ImplementationDefinedLogging containing details. + */ + WGPUSurfaceGetCurrentTextureStatus_Error = 0x00000006, + WGPUSurfaceGetCurrentTextureStatus_Force32 = 0x7FFFFFFF +} WGPUSurfaceGetCurrentTextureStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUTextureAspect { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUTextureAspect_Undefined = 0x00000000, + WGPUTextureAspect_All = 0x00000001, + WGPUTextureAspect_StencilOnly = 0x00000002, + WGPUTextureAspect_DepthOnly = 0x00000003, + WGPUTextureAspect_Force32 = 0x7FFFFFFF +} WGPUTextureAspect WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUTextureDimension { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUTextureDimension_Undefined = 0x00000000, + WGPUTextureDimension_1D = 0x00000001, + WGPUTextureDimension_2D = 0x00000002, + WGPUTextureDimension_3D = 0x00000003, + WGPUTextureDimension_Force32 = 0x7FFFFFFF +} WGPUTextureDimension WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUTextureFormat { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUTextureFormat_Undefined = 0x00000000, + WGPUTextureFormat_R8Unorm = 0x00000001, + WGPUTextureFormat_R8Snorm = 0x00000002, + WGPUTextureFormat_R8Uint = 0x00000003, + WGPUTextureFormat_R8Sint = 0x00000004, + WGPUTextureFormat_R16Unorm = 0x00000005, + WGPUTextureFormat_R16Snorm = 0x00000006, + WGPUTextureFormat_R16Uint = 0x00000007, + WGPUTextureFormat_R16Sint = 0x00000008, + WGPUTextureFormat_R16Float = 0x00000009, + WGPUTextureFormat_RG8Unorm = 0x0000000A, + WGPUTextureFormat_RG8Snorm = 0x0000000B, + WGPUTextureFormat_RG8Uint = 0x0000000C, + WGPUTextureFormat_RG8Sint = 0x0000000D, + WGPUTextureFormat_R32Float = 0x0000000E, + WGPUTextureFormat_R32Uint = 0x0000000F, + WGPUTextureFormat_R32Sint = 0x00000010, + WGPUTextureFormat_RG16Unorm = 0x00000011, + WGPUTextureFormat_RG16Snorm = 0x00000012, + WGPUTextureFormat_RG16Uint = 0x00000013, + WGPUTextureFormat_RG16Sint = 0x00000014, + WGPUTextureFormat_RG16Float = 0x00000015, + WGPUTextureFormat_RGBA8Unorm = 0x00000016, + WGPUTextureFormat_RGBA8UnormSrgb = 0x00000017, + WGPUTextureFormat_RGBA8Snorm = 0x00000018, + WGPUTextureFormat_RGBA8Uint = 0x00000019, + WGPUTextureFormat_RGBA8Sint = 0x0000001A, + WGPUTextureFormat_BGRA8Unorm = 0x0000001B, + WGPUTextureFormat_BGRA8UnormSrgb = 0x0000001C, + WGPUTextureFormat_RGB10A2Uint = 0x0000001D, + WGPUTextureFormat_RGB10A2Unorm = 0x0000001E, + WGPUTextureFormat_RG11B10Ufloat = 0x0000001F, + WGPUTextureFormat_RGB9E5Ufloat = 0x00000020, + WGPUTextureFormat_RG32Float = 0x00000021, + WGPUTextureFormat_RG32Uint = 0x00000022, + WGPUTextureFormat_RG32Sint = 0x00000023, + WGPUTextureFormat_RGBA16Unorm = 0x00000024, + WGPUTextureFormat_RGBA16Snorm = 0x00000025, + WGPUTextureFormat_RGBA16Uint = 0x00000026, + WGPUTextureFormat_RGBA16Sint = 0x00000027, + WGPUTextureFormat_RGBA16Float = 0x00000028, + WGPUTextureFormat_RGBA32Float = 0x00000029, + WGPUTextureFormat_RGBA32Uint = 0x0000002A, + WGPUTextureFormat_RGBA32Sint = 0x0000002B, + WGPUTextureFormat_Stencil8 = 0x0000002C, + WGPUTextureFormat_Depth16Unorm = 0x0000002D, + WGPUTextureFormat_Depth24Plus = 0x0000002E, + WGPUTextureFormat_Depth24PlusStencil8 = 0x0000002F, + WGPUTextureFormat_Depth32Float = 0x00000030, + WGPUTextureFormat_Depth32FloatStencil8 = 0x00000031, + WGPUTextureFormat_BC1RGBAUnorm = 0x00000032, + WGPUTextureFormat_BC1RGBAUnormSrgb = 0x00000033, + WGPUTextureFormat_BC2RGBAUnorm = 0x00000034, + WGPUTextureFormat_BC2RGBAUnormSrgb = 0x00000035, + WGPUTextureFormat_BC3RGBAUnorm = 0x00000036, + WGPUTextureFormat_BC3RGBAUnormSrgb = 0x00000037, + WGPUTextureFormat_BC4RUnorm = 0x00000038, + WGPUTextureFormat_BC4RSnorm = 0x00000039, + WGPUTextureFormat_BC5RGUnorm = 0x0000003A, + WGPUTextureFormat_BC5RGSnorm = 0x0000003B, + WGPUTextureFormat_BC6HRGBUfloat = 0x0000003C, + WGPUTextureFormat_BC6HRGBFloat = 0x0000003D, + WGPUTextureFormat_BC7RGBAUnorm = 0x0000003E, + WGPUTextureFormat_BC7RGBAUnormSrgb = 0x0000003F, + WGPUTextureFormat_ETC2RGB8Unorm = 0x00000040, + WGPUTextureFormat_ETC2RGB8UnormSrgb = 0x00000041, + WGPUTextureFormat_ETC2RGB8A1Unorm = 0x00000042, + WGPUTextureFormat_ETC2RGB8A1UnormSrgb = 0x00000043, + WGPUTextureFormat_ETC2RGBA8Unorm = 0x00000044, + WGPUTextureFormat_ETC2RGBA8UnormSrgb = 0x00000045, + WGPUTextureFormat_EACR11Unorm = 0x00000046, + WGPUTextureFormat_EACR11Snorm = 0x00000047, + WGPUTextureFormat_EACRG11Unorm = 0x00000048, + WGPUTextureFormat_EACRG11Snorm = 0x00000049, + WGPUTextureFormat_ASTC4x4Unorm = 0x0000004A, + WGPUTextureFormat_ASTC4x4UnormSrgb = 0x0000004B, + WGPUTextureFormat_ASTC5x4Unorm = 0x0000004C, + WGPUTextureFormat_ASTC5x4UnormSrgb = 0x0000004D, + WGPUTextureFormat_ASTC5x5Unorm = 0x0000004E, + WGPUTextureFormat_ASTC5x5UnormSrgb = 0x0000004F, + WGPUTextureFormat_ASTC6x5Unorm = 0x00000050, + WGPUTextureFormat_ASTC6x5UnormSrgb = 0x00000051, + WGPUTextureFormat_ASTC6x6Unorm = 0x00000052, + WGPUTextureFormat_ASTC6x6UnormSrgb = 0x00000053, + WGPUTextureFormat_ASTC8x5Unorm = 0x00000054, + WGPUTextureFormat_ASTC8x5UnormSrgb = 0x00000055, + WGPUTextureFormat_ASTC8x6Unorm = 0x00000056, + WGPUTextureFormat_ASTC8x6UnormSrgb = 0x00000057, + WGPUTextureFormat_ASTC8x8Unorm = 0x00000058, + WGPUTextureFormat_ASTC8x8UnormSrgb = 0x00000059, + WGPUTextureFormat_ASTC10x5Unorm = 0x0000005A, + WGPUTextureFormat_ASTC10x5UnormSrgb = 0x0000005B, + WGPUTextureFormat_ASTC10x6Unorm = 0x0000005C, + WGPUTextureFormat_ASTC10x6UnormSrgb = 0x0000005D, + WGPUTextureFormat_ASTC10x8Unorm = 0x0000005E, + WGPUTextureFormat_ASTC10x8UnormSrgb = 0x0000005F, + WGPUTextureFormat_ASTC10x10Unorm = 0x00000060, + WGPUTextureFormat_ASTC10x10UnormSrgb = 0x00000061, + WGPUTextureFormat_ASTC12x10Unorm = 0x00000062, + WGPUTextureFormat_ASTC12x10UnormSrgb = 0x00000063, + WGPUTextureFormat_ASTC12x12Unorm = 0x00000064, + WGPUTextureFormat_ASTC12x12UnormSrgb = 0x00000065, + WGPUTextureFormat_Force32 = 0x7FFFFFFF +} WGPUTextureFormat WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUTextureSampleType { + /** + * `0`. Indicates that this @ref WGPUTextureBindingLayout member of + * its parent @ref WGPUBindGroupLayoutEntry is not used. + * (See also @ref SentinelValues.) + */ + WGPUTextureSampleType_BindingNotUsed = 0x00000000, + /** + * `1`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUTextureSampleType_Undefined = 0x00000001, + WGPUTextureSampleType_Float = 0x00000002, + WGPUTextureSampleType_UnfilterableFloat = 0x00000003, + WGPUTextureSampleType_Depth = 0x00000004, + WGPUTextureSampleType_Sint = 0x00000005, + WGPUTextureSampleType_Uint = 0x00000006, + WGPUTextureSampleType_Force32 = 0x7FFFFFFF +} WGPUTextureSampleType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUTextureViewDimension { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUTextureViewDimension_Undefined = 0x00000000, + WGPUTextureViewDimension_1D = 0x00000001, + WGPUTextureViewDimension_2D = 0x00000002, + WGPUTextureViewDimension_2DArray = 0x00000003, + WGPUTextureViewDimension_Cube = 0x00000004, + WGPUTextureViewDimension_CubeArray = 0x00000005, + WGPUTextureViewDimension_3D = 0x00000006, + WGPUTextureViewDimension_Force32 = 0x7FFFFFFF +} WGPUTextureViewDimension WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUToneMappingMode { + WGPUToneMappingMode_Standard = 0x00000001, + WGPUToneMappingMode_Extended = 0x00000002, + WGPUToneMappingMode_Force32 = 0x7FFFFFFF +} WGPUToneMappingMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUVertexFormat { + WGPUVertexFormat_Uint8 = 0x00000001, + WGPUVertexFormat_Uint8x2 = 0x00000002, + WGPUVertexFormat_Uint8x4 = 0x00000003, + WGPUVertexFormat_Sint8 = 0x00000004, + WGPUVertexFormat_Sint8x2 = 0x00000005, + WGPUVertexFormat_Sint8x4 = 0x00000006, + WGPUVertexFormat_Unorm8 = 0x00000007, + WGPUVertexFormat_Unorm8x2 = 0x00000008, + WGPUVertexFormat_Unorm8x4 = 0x00000009, + WGPUVertexFormat_Snorm8 = 0x0000000A, + WGPUVertexFormat_Snorm8x2 = 0x0000000B, + WGPUVertexFormat_Snorm8x4 = 0x0000000C, + WGPUVertexFormat_Uint16 = 0x0000000D, + WGPUVertexFormat_Uint16x2 = 0x0000000E, + WGPUVertexFormat_Uint16x4 = 0x0000000F, + WGPUVertexFormat_Sint16 = 0x00000010, + WGPUVertexFormat_Sint16x2 = 0x00000011, + WGPUVertexFormat_Sint16x4 = 0x00000012, + WGPUVertexFormat_Unorm16 = 0x00000013, + WGPUVertexFormat_Unorm16x2 = 0x00000014, + WGPUVertexFormat_Unorm16x4 = 0x00000015, + WGPUVertexFormat_Snorm16 = 0x00000016, + WGPUVertexFormat_Snorm16x2 = 0x00000017, + WGPUVertexFormat_Snorm16x4 = 0x00000018, + WGPUVertexFormat_Float16 = 0x00000019, + WGPUVertexFormat_Float16x2 = 0x0000001A, + WGPUVertexFormat_Float16x4 = 0x0000001B, + WGPUVertexFormat_Float32 = 0x0000001C, + WGPUVertexFormat_Float32x2 = 0x0000001D, + WGPUVertexFormat_Float32x3 = 0x0000001E, + WGPUVertexFormat_Float32x4 = 0x0000001F, + WGPUVertexFormat_Uint32 = 0x00000020, + WGPUVertexFormat_Uint32x2 = 0x00000021, + WGPUVertexFormat_Uint32x3 = 0x00000022, + WGPUVertexFormat_Uint32x4 = 0x00000023, + WGPUVertexFormat_Sint32 = 0x00000024, + WGPUVertexFormat_Sint32x2 = 0x00000025, + WGPUVertexFormat_Sint32x3 = 0x00000026, + WGPUVertexFormat_Sint32x4 = 0x00000027, + WGPUVertexFormat_Unorm10_10_10_2 = 0x00000028, + WGPUVertexFormat_Unorm8x4BGRA = 0x00000029, + WGPUVertexFormat_Force32 = 0x7FFFFFFF +} WGPUVertexFormat WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUVertexStepMode { + /** + * `0`. Indicates no value is passed for this argument. See @ref SentinelValues. + */ + WGPUVertexStepMode_Undefined = 0x00000000, + WGPUVertexStepMode_Vertex = 0x00000001, + WGPUVertexStepMode_Instance = 0x00000002, + WGPUVertexStepMode_Force32 = 0x7FFFFFFF +} WGPUVertexStepMode WGPU_ENUM_ATTRIBUTE; + +/** + * Status returned from a call to ::wgpuInstanceWaitAny. + */ +typedef enum WGPUWaitStatus { + /** + * At least one WGPUFuture completed successfully. + */ + WGPUWaitStatus_Success = 0x00000001, + /** + * The wait operation succeeded, but no WGPUFutures completed within the timeout. + */ + WGPUWaitStatus_TimedOut = 0x00000002, + /** + * The call was invalid for some reason (see @ref Wait-Any). + * Should produce @ref ImplementationDefinedLogging containing details. + */ + WGPUWaitStatus_Error = 0x00000003, + WGPUWaitStatus_Force32 = 0x7FFFFFFF +} WGPUWaitStatus WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUWGSLLanguageFeatureName { + WGPUWGSLLanguageFeatureName_ReadonlyAndReadwriteStorageTextures = 0x00000001, + WGPUWGSLLanguageFeatureName_Packed4x8IntegerDotProduct = 0x00000002, + WGPUWGSLLanguageFeatureName_UnrestrictedPointerParameters = 0x00000003, + WGPUWGSLLanguageFeatureName_PointerCompositeAccess = 0x00000004, + WGPUWGSLLanguageFeatureName_UniformBufferStandardLayout = 0x00000005, + WGPUWGSLLanguageFeatureName_SubgroupId = 0x00000006, + WGPUWGSLLanguageFeatureName_TextureAndSamplerLet = 0x00000007, + WGPUWGSLLanguageFeatureName_SubgroupUniformity = 0x00000008, + WGPUWGSLLanguageFeatureName_TextureFormatsTier1 = 0x00000009, + WGPUWGSLLanguageFeatureName_LinearIndexing = 0x0000000A, + WGPUWGSLLanguageFeatureName_Force32 = 0x7FFFFFFF +} WGPUWGSLLanguageFeatureName WGPU_ENUM_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup Bitflags Bitflags + * \brief Type and constant definitions for bitflag types. + * + * @{ + */ + +/** + * For reserved non-standard bitflag values, see @ref BitflagRegistry. + */ +typedef WGPUFlags WGPUBufferUsage; +/** + * `0`. + */ +static const WGPUBufferUsage WGPUBufferUsage_None = 0x0000000000000000; +/** + * The buffer can be *mapped* on the CPU side in *read* mode (using @ref WGPUMapMode_Read). + */ +static const WGPUBufferUsage WGPUBufferUsage_MapRead = 0x0000000000000001; +/** + * The buffer can be *mapped* on the CPU side in *write* mode (using @ref WGPUMapMode_Write). + * + * @note This usage is **not** required to set `mappedAtCreation` to `true` in @ref WGPUBufferDescriptor. + */ +static const WGPUBufferUsage WGPUBufferUsage_MapWrite = 0x0000000000000002; +/** + * The buffer can be used as the *source* of a GPU-side copy operation. + */ +static const WGPUBufferUsage WGPUBufferUsage_CopySrc = 0x0000000000000004; +/** + * The buffer can be used as the *destination* of a GPU-side copy operation. + */ +static const WGPUBufferUsage WGPUBufferUsage_CopyDst = 0x0000000000000008; +/** + * The buffer can be used as an Index buffer when doing indexed drawing in a render pipeline. + */ +static const WGPUBufferUsage WGPUBufferUsage_Index = 0x0000000000000010; +/** + * The buffer can be used as a Vertex buffer when using a render pipeline. + */ +static const WGPUBufferUsage WGPUBufferUsage_Vertex = 0x0000000000000020; +/** + * The buffer can be bound to a shader as a uniform buffer. + */ +static const WGPUBufferUsage WGPUBufferUsage_Uniform = 0x0000000000000040; +/** + * The buffer can be bound to a shader as a storage buffer. + */ +static const WGPUBufferUsage WGPUBufferUsage_Storage = 0x0000000000000080; +/** + * The buffer can store arguments for an indirect draw call. + */ +static const WGPUBufferUsage WGPUBufferUsage_Indirect = 0x0000000000000100; +/** + * The buffer can store the result of a timestamp or occlusion query. + */ +static const WGPUBufferUsage WGPUBufferUsage_QueryResolve = 0x0000000000000200; + +/** + * For reserved non-standard bitflag values, see @ref BitflagRegistry. + */ +typedef WGPUFlags WGPUColorWriteMask; +/** + * `0`. + */ +static const WGPUColorWriteMask WGPUColorWriteMask_None = 0x0000000000000000; +static const WGPUColorWriteMask WGPUColorWriteMask_Red = 0x0000000000000001; +static const WGPUColorWriteMask WGPUColorWriteMask_Green = 0x0000000000000002; +static const WGPUColorWriteMask WGPUColorWriteMask_Blue = 0x0000000000000004; +static const WGPUColorWriteMask WGPUColorWriteMask_Alpha = 0x0000000000000008; +/** + * `Red | Green | Blue | Alpha`. + */ +static const WGPUColorWriteMask WGPUColorWriteMask_All = 0x000000000000000F; + +/** + * For reserved non-standard bitflag values, see @ref BitflagRegistry. + */ +typedef WGPUFlags WGPUMapMode; +/** + * `0`. + */ +static const WGPUMapMode WGPUMapMode_None = 0x0000000000000000; +static const WGPUMapMode WGPUMapMode_Read = 0x0000000000000001; +static const WGPUMapMode WGPUMapMode_Write = 0x0000000000000002; + +/** + * For reserved non-standard bitflag values, see @ref BitflagRegistry. + */ +typedef WGPUFlags WGPUShaderStage; +/** + * `0`. + */ +static const WGPUShaderStage WGPUShaderStage_None = 0x0000000000000000; +static const WGPUShaderStage WGPUShaderStage_Vertex = 0x0000000000000001; +static const WGPUShaderStage WGPUShaderStage_Fragment = 0x0000000000000002; +static const WGPUShaderStage WGPUShaderStage_Compute = 0x0000000000000004; + +/** + * For reserved non-standard bitflag values, see @ref BitflagRegistry. + */ +typedef WGPUFlags WGPUTextureUsage; +/** + * `0`. + */ +static const WGPUTextureUsage WGPUTextureUsage_None = 0x0000000000000000; +static const WGPUTextureUsage WGPUTextureUsage_CopySrc = 0x0000000000000001; +static const WGPUTextureUsage WGPUTextureUsage_CopyDst = 0x0000000000000002; +static const WGPUTextureUsage WGPUTextureUsage_TextureBinding = 0x0000000000000004; +static const WGPUTextureUsage WGPUTextureUsage_StorageBinding = 0x0000000000000008; +static const WGPUTextureUsage WGPUTextureUsage_RenderAttachment = 0x0000000000000010; +static const WGPUTextureUsage WGPUTextureUsage_TransientAttachment = 0x0000000000000020; + +/** @} */ + +typedef void (*WGPUProc)(void) WGPU_FUNCTION_ATTRIBUTE; + +/** + * \defgroup Callbacks Callbacks + * \brief Callbacks through which asynchronous functions return. + * + * @{ + */ + +/** + * See also @ref CallbackError. + * + * @param message + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUBufferMapCallback)(WGPUMapAsyncStatus status, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param compilationInfo + * This argument contains multiple @ref ImplementationAllocatedStructChain roots. + * Arbitrary chains must be handled gracefully by the application! + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUCompilationInfoCallback)(WGPUCompilationInfoRequestStatus status, struct WGPUCompilationInfo const * compilationInfo, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param pipeline + * This parameter is @ref PassedWithOwnership. + */ +typedef void (*WGPUCreateComputePipelineAsyncCallback)(WGPUCreatePipelineAsyncStatus status, WGPUComputePipeline pipeline, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param pipeline + * This parameter is @ref PassedWithOwnership. + */ +typedef void (*WGPUCreateRenderPipelineAsyncCallback)(WGPUCreatePipelineAsyncStatus status, WGPURenderPipeline pipeline, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param device + * Pointer to the device which was lost. This is always a non-null pointer. + * The pointed-to @ref WGPUDevice will be null if, and only if, either: + * (1) The `reason` is @ref WGPUDeviceLostReason_FailedCreation. + * (2) The last ref of the device has been (or is being) released: see @ref DeviceRelease. + * This parameter is @ref PassedWithoutOwnership. + * + * @param reason + * An error code explaining why the device was lost. + * + * @param message + * A @ref LocalizableHumanReadableMessageString describing why the device was lost. + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUDeviceLostCallback)(WGPUDevice const * device, WGPUDeviceLostReason reason, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param status + * See @ref WGPUPopErrorScopeStatus. + * + * @param type + * The type of the error caught by the scope, or @ref WGPUErrorType_NoError if there was none. + * If the `status` is not @ref WGPUPopErrorScopeStatus_Success, this is @ref WGPUErrorType_NoError. + * + * @param message + * If the `status` is not @ref WGPUPopErrorScopeStatus_Success **or** + * the `type` is not @ref WGPUErrorType_NoError, this is a non-empty + * @ref LocalizableHumanReadableMessageString; + * otherwise, this is an empty string. + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUPopErrorScopeCallback)(WGPUPopErrorScopeStatus status, WGPUErrorType type, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param status + * See @ref WGPUQueueWorkDoneStatus. + * + * @param message + * If the `status` is not @ref WGPUQueueWorkDoneStatus_Success, + * this is a non-empty @ref LocalizableHumanReadableMessageString; + * otherwise, this is an empty string. + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUQueueWorkDoneCallback)(WGPUQueueWorkDoneStatus status, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param adapter + * This parameter is @ref PassedWithOwnership. + * + * @param message + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPURequestAdapterCallback)(WGPURequestAdapterStatus status, WGPUAdapter adapter, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param device + * This parameter is @ref PassedWithOwnership. + * + * @param message + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPURequestDeviceCallback)(WGPURequestDeviceStatus status, WGPUDevice device, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** + * See also @ref CallbackError. + * + * @param device + * This parameter is @ref PassedWithoutOwnership. + * + * @param message + * This parameter is @ref PassedWithoutOwnership. + */ +typedef void (*WGPUUncapturedErrorCallback)(WGPUDevice const * device, WGPUErrorType type, WGPUStringView message, WGPU_NULLABLE void* userdata1, WGPU_NULLABLE void* userdata2) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ +/** + * \defgroup ChainedStructures Chained Structures + * \brief Structures used to extend descriptors. + * + * @{ + */ + +typedef struct WGPUChainedStruct { + struct WGPUChainedStruct * next; + WGPUSType sType; +} WGPUChainedStruct WGPU_STRUCTURE_ATTRIBUTE; + +/** @} */ + + +/** + * \defgroup Structures Structures + * \brief Descriptors and other transparent structures. + * + * @{ + */ + +/** + * \defgroup CallbackInfoStructs Callback Info Structs + * \brief Callback info structures that are used in asynchronous functions. + * + * @{ + */ + +typedef struct WGPUBufferMapCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUBufferMapCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUBufferMapCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBufferMapCallbackInfo. + */ +#define WGPU_BUFFER_MAP_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBufferMapCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUCompilationInfoCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUCompilationInfoCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUCompilationInfoCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCompilationInfoCallbackInfo. + */ +#define WGPU_COMPILATION_INFO_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCompilationInfoCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUCreateComputePipelineAsyncCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUCreateComputePipelineAsyncCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUCreateComputePipelineAsyncCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCreateComputePipelineAsyncCallbackInfo. + */ +#define WGPU_CREATE_COMPUTE_PIPELINE_ASYNC_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCreateComputePipelineAsyncCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUCreateRenderPipelineAsyncCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUCreateRenderPipelineAsyncCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUCreateRenderPipelineAsyncCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCreateRenderPipelineAsyncCallbackInfo. + */ +#define WGPU_CREATE_RENDER_PIPELINE_ASYNC_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCreateRenderPipelineAsyncCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUDeviceLostCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUDeviceLostCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUDeviceLostCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUDeviceLostCallbackInfo. + */ +#define WGPU_DEVICE_LOST_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUDeviceLostCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUPopErrorScopeCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUPopErrorScopeCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUPopErrorScopeCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUPopErrorScopeCallbackInfo. + */ +#define WGPU_POP_ERROR_SCOPE_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUPopErrorScopeCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUQueueWorkDoneCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPUQueueWorkDoneCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUQueueWorkDoneCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUQueueWorkDoneCallbackInfo. + */ +#define WGPU_QUEUE_WORK_DONE_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUQueueWorkDoneCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPURequestAdapterCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPURequestAdapterCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPURequestAdapterCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURequestAdapterCallbackInfo. + */ +#define WGPU_REQUEST_ADAPTER_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPURequestAdapterCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPURequestDeviceCallbackInfo { + WGPUChainedStruct * nextInChain; + /** + * Controls when the callback may be called. + * + * Has no default. The `INIT` macro sets this to (@ref WGPUCallbackMode)0. + */ + WGPUCallbackMode mode; + WGPURequestDeviceCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPURequestDeviceCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURequestDeviceCallbackInfo. + */ +#define WGPU_REQUEST_DEVICE_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPURequestDeviceCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.mode=*/_wgpu_ENUM_ZERO_INIT(WGPUCallbackMode) _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +typedef struct WGPUUncapturedErrorCallbackInfo { + WGPUChainedStruct * nextInChain; + WGPUUncapturedErrorCallback callback; + WGPU_NULLABLE void* userdata1; + WGPU_NULLABLE void* userdata2; +} WGPUUncapturedErrorCallbackInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUUncapturedErrorCallbackInfo. + */ +#define WGPU_UNCAPTURED_ERROR_CALLBACK_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUUncapturedErrorCallbackInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.callback=*/NULL _wgpu_COMMA \ + /*.userdata1=*/NULL _wgpu_COMMA \ + /*.userdata2=*/NULL _wgpu_COMMA \ +}) + +/** @} */ + +/** + * Default values can be set using @ref WGPU_ADAPTER_INFO_INIT as initializer. + */ +typedef struct WGPUAdapterInfo { + WGPUChainedStruct * nextInChain; + /** + * This is an \ref OutputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView vendor; + /** + * This is an \ref OutputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView architecture; + /** + * This is an \ref OutputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView device; + /** + * This is an \ref OutputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView description; + /** + * The `INIT` macro sets this to @ref WGPUBackendType_Undefined. + */ + WGPUBackendType backendType; + /** + * The `INIT` macro sets this to (@ref WGPUAdapterType)0. + */ + WGPUAdapterType adapterType; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t vendorID; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t deviceID; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t subgroupMinSize; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t subgroupMaxSize; +} WGPUAdapterInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUAdapterInfo. + */ +#define WGPU_ADAPTER_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUAdapterInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.vendor=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.architecture=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.device=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.description=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.backendType=*/WGPUBackendType_Undefined _wgpu_COMMA \ + /*.adapterType=*/_wgpu_ENUM_ZERO_INIT(WGPUAdapterType) _wgpu_COMMA \ + /*.vendorID=*/0 _wgpu_COMMA \ + /*.deviceID=*/0 _wgpu_COMMA \ + /*.subgroupMinSize=*/0 _wgpu_COMMA \ + /*.subgroupMaxSize=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BLEND_COMPONENT_INIT as initializer. + */ +typedef struct WGPUBlendComponent { + /** + * If set to @ref WGPUBlendOperation_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUBlendOperation_Add. + * + * The `INIT` macro sets this to @ref WGPUBlendOperation_Undefined. + */ + WGPUBlendOperation operation; + /** + * If set to @ref WGPUBlendFactor_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUBlendFactor_One. + * + * The `INIT` macro sets this to @ref WGPUBlendFactor_Undefined. + */ + WGPUBlendFactor srcFactor; + /** + * If set to @ref WGPUBlendFactor_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUBlendFactor_Zero. + * + * The `INIT` macro sets this to @ref WGPUBlendFactor_Undefined. + */ + WGPUBlendFactor dstFactor; +} WGPUBlendComponent WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBlendComponent. + */ +#define WGPU_BLEND_COMPONENT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBlendComponent, { \ + /*.operation=*/WGPUBlendOperation_Undefined _wgpu_COMMA \ + /*.srcFactor=*/WGPUBlendFactor_Undefined _wgpu_COMMA \ + /*.dstFactor=*/WGPUBlendFactor_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BUFFER_BINDING_LAYOUT_INIT as initializer. + */ +typedef struct WGPUBufferBindingLayout { + WGPUChainedStruct * nextInChain; + /** + * If set to @ref WGPUBufferBindingType_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUBufferBindingType_Uniform. + * + * The `INIT` macro sets this to @ref WGPUBufferBindingType_Undefined. + */ + WGPUBufferBindingType type; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool hasDynamicOffset; + /** + * The `INIT` macro sets this to `0`. + */ + uint64_t minBindingSize; +} WGPUBufferBindingLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBufferBindingLayout. + */ +#define WGPU_BUFFER_BINDING_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBufferBindingLayout, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.type=*/WGPUBufferBindingType_Undefined _wgpu_COMMA \ + /*.hasDynamicOffset=*/WGPU_FALSE _wgpu_COMMA \ + /*.minBindingSize=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BUFFER_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUBufferDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to @ref WGPUBufferUsage_None. + */ + WGPUBufferUsage usage; + /** + * The `INIT` macro sets this to `0`. + */ + uint64_t size; + /** + * When true, the buffer is mapped in write mode at creation. It should thus be unmapped once its initial data has been written. + * + * @note Mapping at creation does **not** require the usage @ref WGPUBufferUsage_MapWrite. + * + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool mappedAtCreation; +} WGPUBufferDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBufferDescriptor. + */ +#define WGPU_BUFFER_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBufferDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.usage=*/WGPUBufferUsage_None _wgpu_COMMA \ + /*.size=*/0 _wgpu_COMMA \ + /*.mappedAtCreation=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * An RGBA color. Represents a `f32`, `i32`, or `u32` color using @ref DoubleAsSupertype. + * + * If any channel is non-finite, produces a @ref NonFiniteFloatValueError. + * + * Default values can be set using @ref WGPU_COLOR_INIT as initializer. + */ +typedef struct WGPUColor { + /** + * The `INIT` macro sets this to `0.`. + */ + double r; + /** + * The `INIT` macro sets this to `0.`. + */ + double g; + /** + * The `INIT` macro sets this to `0.`. + */ + double b; + /** + * The `INIT` macro sets this to `0.`. + */ + double a; +} WGPUColor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUColor. + */ +#define WGPU_COLOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUColor, { \ + /*.r=*/0. _wgpu_COMMA \ + /*.g=*/0. _wgpu_COMMA \ + /*.b=*/0. _wgpu_COMMA \ + /*.a=*/0. _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COMMAND_BUFFER_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUCommandBufferDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPUCommandBufferDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCommandBufferDescriptor. + */ +#define WGPU_COMMAND_BUFFER_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCommandBufferDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COMMAND_ENCODER_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUCommandEncoderDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPUCommandEncoderDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCommandEncoderDescriptor. + */ +#define WGPU_COMMAND_ENCODER_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCommandEncoderDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Note: While Compatibility Mode is optional to implement, this extension struct + * is required to be supported (for both queries and requests) and behave as + * defined in the WebGPU spec. + * + * Default values can be set using @ref WGPU_COMPATIBILITY_MODE_LIMITS_INIT as initializer. + */ +typedef struct WGPUCompatibilityModeLimits { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageBuffersInVertexStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageTexturesInVertexStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageBuffersInFragmentStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageTexturesInFragmentStage; +} WGPUCompatibilityModeLimits WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCompatibilityModeLimits. + */ +#define WGPU_COMPATIBILITY_MODE_LIMITS_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCompatibilityModeLimits, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_CompatibilityModeLimits _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.maxStorageBuffersInVertexStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxStorageTexturesInVertexStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxStorageBuffersInFragmentStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxStorageTexturesInFragmentStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ +}) + +/** + * This is an @ref ImplementationAllocatedStructChain root. + * Arbitrary chains must be handled gracefully by the application! + * + * Default values can be set using @ref WGPU_COMPILATION_MESSAGE_INIT as initializer. + */ +typedef struct WGPUCompilationMessage { + WGPUChainedStruct * nextInChain; + /** + * A @ref LocalizableHumanReadableMessageString. + * + * This is an \ref OutputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView message; + /** + * Severity level of the message. + * + * The `INIT` macro sets this to (@ref WGPUCompilationMessageType)0. + */ + WGPUCompilationMessageType type; + /** + * Line number where the message is attached, starting at 1. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t lineNum; + /** + * Offset in UTF-8 code units (bytes) from the beginning of the line, starting at 1. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t linePos; + /** + * Offset in UTF-8 code units (bytes) from the beginning of the shader code, starting at 0. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t offset; + /** + * Length in UTF-8 code units (bytes) of the span the message corresponds to. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t length; +} WGPUCompilationMessage WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCompilationMessage. + */ +#define WGPU_COMPILATION_MESSAGE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCompilationMessage, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.message=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.type=*/_wgpu_ENUM_ZERO_INIT(WGPUCompilationMessageType) _wgpu_COMMA \ + /*.lineNum=*/0 _wgpu_COMMA \ + /*.linePos=*/0 _wgpu_COMMA \ + /*.offset=*/0 _wgpu_COMMA \ + /*.length=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_CONSTANT_ENTRY_INIT as initializer. + */ +typedef struct WGPUConstantEntry { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView key; + /** + * Represents a WGSL numeric or boolean value using @ref DoubleAsSupertype. + * + * If non-finite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to `0.`. + */ + double value; +} WGPUConstantEntry WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUConstantEntry. + */ +#define WGPU_CONSTANT_ENTRY_INIT _wgpu_MAKE_INIT_STRUCT(WGPUConstantEntry, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.key=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.value=*/0. _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_EXTENT_3D_INIT as initializer. + */ +typedef struct WGPUExtent3D { + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t width; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t height; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t depthOrArrayLayers; +} WGPUExtent3D WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUExtent3D. + */ +#define WGPU_EXTENT_3D_INIT _wgpu_MAKE_INIT_STRUCT(WGPUExtent3D, { \ + /*.width=*/0 _wgpu_COMMA \ + /*.height=*/1 _wgpu_COMMA \ + /*.depthOrArrayLayers=*/1 _wgpu_COMMA \ +}) + +/** + * Chained in an @ref WGPUBindGroupEntry to set it to an @ref WGPUExternalTexture. This must have a corresponding @ref WGPUExternalTextureBindingLayout in the @ref WGPUBindGroupLayout. + * + * Default values can be set using @ref WGPU_EXTERNAL_TEXTURE_BINDING_ENTRY_INIT as initializer. + */ +typedef struct WGPUExternalTextureBindingEntry { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUExternalTexture externalTexture; +} WGPUExternalTextureBindingEntry WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUExternalTextureBindingEntry. + */ +#define WGPU_EXTERNAL_TEXTURE_BINDING_ENTRY_INIT _wgpu_MAKE_INIT_STRUCT(WGPUExternalTextureBindingEntry, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_ExternalTextureBindingEntry _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.externalTexture=*/NULL _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUBindGroupLayoutEntry to specify that the corresponding entries in an @ref WGPUBindGroup will contain an @ref WGPUExternalTexture. + * + * Default values can be set using @ref WGPU_EXTERNAL_TEXTURE_BINDING_LAYOUT_INIT as initializer. + */ +typedef struct WGPUExternalTextureBindingLayout { + WGPUChainedStruct chain; +} WGPUExternalTextureBindingLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUExternalTextureBindingLayout. + */ +#define WGPU_EXTERNAL_TEXTURE_BINDING_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUExternalTextureBindingLayout, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_ExternalTextureBindingLayout _wgpu_COMMA \ + }) _wgpu_COMMA \ +}) + +/** + * Opaque handle to an asynchronous operation. See @ref Asynchronous-Operations for more information. + * + * Default values can be set using @ref WGPU_FUTURE_INIT as initializer. + */ +typedef struct WGPUFuture { + /** + * Opaque id of the @ref WGPUFuture + * + * The `INIT` macro sets this to `0`. + */ + uint64_t id; +} WGPUFuture WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUFuture. + */ +#define WGPU_FUTURE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUFuture, { \ + /*.id=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_INSTANCE_LIMITS_INIT as initializer. + */ +typedef struct WGPUInstanceLimits { + WGPUChainedStruct * nextInChain; + /** + * The maximum number @ref WGPUFutureWaitInfo supported in a call to ::wgpuInstanceWaitAny with `timeoutNS > 0`. + * + * The `INIT` macro sets this to `0`. + */ + size_t timedWaitAnyMaxCount; +} WGPUInstanceLimits WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUInstanceLimits. + */ +#define WGPU_INSTANCE_LIMITS_INIT _wgpu_MAKE_INIT_STRUCT(WGPUInstanceLimits, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.timedWaitAnyMaxCount=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_MULTISAMPLE_STATE_INIT as initializer. + */ +typedef struct WGPUMultisampleState { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t count; + /** + * The `INIT` macro sets this to `0xFFFFFFFF`. + */ + uint32_t mask; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool alphaToCoverageEnabled; +} WGPUMultisampleState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUMultisampleState. + */ +#define WGPU_MULTISAMPLE_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUMultisampleState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.count=*/1 _wgpu_COMMA \ + /*.mask=*/0xFFFFFFFF _wgpu_COMMA \ + /*.alphaToCoverageEnabled=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_ORIGIN_3D_INIT as initializer. + */ +typedef struct WGPUOrigin3D { + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t x; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t y; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t z; +} WGPUOrigin3D WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUOrigin3D. + */ +#define WGPU_ORIGIN_3D_INIT _wgpu_MAKE_INIT_STRUCT(WGPUOrigin3D, { \ + /*.x=*/0 _wgpu_COMMA \ + /*.y=*/0 _wgpu_COMMA \ + /*.z=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_PASS_TIMESTAMP_WRITES_INIT as initializer. + */ +typedef struct WGPUPassTimestampWrites { + WGPUChainedStruct * nextInChain; + /** + * Query set to write timestamps to. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUQuerySet querySet; + /** + * The `INIT` macro sets this to @ref WGPU_QUERY_SET_INDEX_UNDEFINED. + */ + uint32_t beginningOfPassWriteIndex; + /** + * The `INIT` macro sets this to @ref WGPU_QUERY_SET_INDEX_UNDEFINED. + */ + uint32_t endOfPassWriteIndex; +} WGPUPassTimestampWrites WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUPassTimestampWrites. + */ +#define WGPU_PASS_TIMESTAMP_WRITES_INIT _wgpu_MAKE_INIT_STRUCT(WGPUPassTimestampWrites, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.querySet=*/NULL _wgpu_COMMA \ + /*.beginningOfPassWriteIndex=*/WGPU_QUERY_SET_INDEX_UNDEFINED _wgpu_COMMA \ + /*.endOfPassWriteIndex=*/WGPU_QUERY_SET_INDEX_UNDEFINED _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_PIPELINE_LAYOUT_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUPipelineLayoutDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * Array count for `bindGroupLayouts`. The `INIT` macro sets this to 0. + */ + size_t bindGroupLayoutCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUBindGroupLayout const * bindGroupLayouts; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t immediateSize; +} WGPUPipelineLayoutDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUPipelineLayoutDescriptor. + */ +#define WGPU_PIPELINE_LAYOUT_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUPipelineLayoutDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.bindGroupLayoutCount=*/0 _wgpu_COMMA \ + /*.bindGroupLayouts=*/NULL _wgpu_COMMA \ + /*.immediateSize=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_PRIMITIVE_STATE_INIT as initializer. + */ +typedef struct WGPUPrimitiveState { + WGPUChainedStruct * nextInChain; + /** + * If set to @ref WGPUPrimitiveTopology_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUPrimitiveTopology_TriangleList. + * + * The `INIT` macro sets this to @ref WGPUPrimitiveTopology_Undefined. + */ + WGPUPrimitiveTopology topology; + /** + * The `INIT` macro sets this to @ref WGPUIndexFormat_Undefined. + */ + WGPUIndexFormat stripIndexFormat; + /** + * If set to @ref WGPUFrontFace_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUFrontFace_CCW. + * + * The `INIT` macro sets this to @ref WGPUFrontFace_Undefined. + */ + WGPUFrontFace frontFace; + /** + * If set to @ref WGPUCullMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUCullMode_None. + * + * The `INIT` macro sets this to @ref WGPUCullMode_Undefined. + */ + WGPUCullMode cullMode; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool unclippedDepth; +} WGPUPrimitiveState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUPrimitiveState. + */ +#define WGPU_PRIMITIVE_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUPrimitiveState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.topology=*/WGPUPrimitiveTopology_Undefined _wgpu_COMMA \ + /*.stripIndexFormat=*/WGPUIndexFormat_Undefined _wgpu_COMMA \ + /*.frontFace=*/WGPUFrontFace_Undefined _wgpu_COMMA \ + /*.cullMode=*/WGPUCullMode_Undefined _wgpu_COMMA \ + /*.unclippedDepth=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_QUERY_SET_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUQuerySetDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to (@ref WGPUQueryType)0. + */ + WGPUQueryType type; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t count; +} WGPUQuerySetDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUQuerySetDescriptor. + */ +#define WGPU_QUERY_SET_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUQuerySetDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.type=*/_wgpu_ENUM_ZERO_INIT(WGPUQueryType) _wgpu_COMMA \ + /*.count=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_QUEUE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUQueueDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPUQueueDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUQueueDescriptor. + */ +#define WGPU_QUEUE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUQueueDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_BUNDLE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPURenderBundleDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPURenderBundleDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderBundleDescriptor. + */ +#define WGPU_RENDER_BUNDLE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderBundleDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_BUNDLE_ENCODER_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPURenderBundleEncoderDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * Array count for `colorFormats`. The `INIT` macro sets this to 0. + */ + size_t colorFormatCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUTextureFormat const * colorFormats; + /** + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat depthStencilFormat; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t sampleCount; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool depthReadOnly; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool stencilReadOnly; +} WGPURenderBundleEncoderDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderBundleEncoderDescriptor. + */ +#define WGPU_RENDER_BUNDLE_ENCODER_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderBundleEncoderDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.colorFormatCount=*/0 _wgpu_COMMA \ + /*.colorFormats=*/NULL _wgpu_COMMA \ + /*.depthStencilFormat=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.sampleCount=*/1 _wgpu_COMMA \ + /*.depthReadOnly=*/WGPU_FALSE _wgpu_COMMA \ + /*.stencilReadOnly=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_INIT as initializer. + */ +typedef struct WGPURenderPassDepthStencilAttachment { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUTextureView view; + /** + * The `INIT` macro sets this to @ref WGPULoadOp_Undefined. + */ + WGPULoadOp depthLoadOp; + /** + * The `INIT` macro sets this to @ref WGPUStoreOp_Undefined. + */ + WGPUStoreOp depthStoreOp; + /** + * This is a @ref NullableFloatingPointType. + * + * If `NaN`, indicates an `undefined` value (as defined by the JS spec). + * Use @ref WGPU_DEPTH_CLEAR_VALUE_UNDEFINED to indicate this semantically. + * + * If infinite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to @ref WGPU_DEPTH_CLEAR_VALUE_UNDEFINED. + */ + float depthClearValue; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool depthReadOnly; + /** + * The `INIT` macro sets this to @ref WGPULoadOp_Undefined. + */ + WGPULoadOp stencilLoadOp; + /** + * The `INIT` macro sets this to @ref WGPUStoreOp_Undefined. + */ + WGPUStoreOp stencilStoreOp; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t stencilClearValue; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool stencilReadOnly; +} WGPURenderPassDepthStencilAttachment WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderPassDepthStencilAttachment. + */ +#define WGPU_RENDER_PASS_DEPTH_STENCIL_ATTACHMENT_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderPassDepthStencilAttachment, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.view=*/NULL _wgpu_COMMA \ + /*.depthLoadOp=*/WGPULoadOp_Undefined _wgpu_COMMA \ + /*.depthStoreOp=*/WGPUStoreOp_Undefined _wgpu_COMMA \ + /*.depthClearValue=*/WGPU_DEPTH_CLEAR_VALUE_UNDEFINED _wgpu_COMMA \ + /*.depthReadOnly=*/WGPU_FALSE _wgpu_COMMA \ + /*.stencilLoadOp=*/WGPULoadOp_Undefined _wgpu_COMMA \ + /*.stencilStoreOp=*/WGPUStoreOp_Undefined _wgpu_COMMA \ + /*.stencilClearValue=*/0 _wgpu_COMMA \ + /*.stencilReadOnly=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_PASS_MAX_DRAW_COUNT_INIT as initializer. + */ +typedef struct WGPURenderPassMaxDrawCount { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to `50000000`. + */ + uint64_t maxDrawCount; +} WGPURenderPassMaxDrawCount WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderPassMaxDrawCount. + */ +#define WGPU_RENDER_PASS_MAX_DRAW_COUNT_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderPassMaxDrawCount, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_RenderPassMaxDrawCount _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.maxDrawCount=*/50000000 _wgpu_COMMA \ +}) + +/** + * Extension providing requestAdapter options for implementations with WebXR interop (i.e. Wasm). + * + * Default values can be set using @ref WGPU_REQUEST_ADAPTER_WEBXR_OPTIONS_INIT as initializer. + */ +typedef struct WGPURequestAdapterWebXROptions { + WGPUChainedStruct chain; + /** + * Sets the `xrCompatible` option in the JS API. + * + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool xrCompatible; +} WGPURequestAdapterWebXROptions WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURequestAdapterWebXROptions. + */ +#define WGPU_REQUEST_ADAPTER_WEBXR_OPTIONS_INIT _wgpu_MAKE_INIT_STRUCT(WGPURequestAdapterWebXROptions, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_RequestAdapterWebXROptions _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.xrCompatible=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SAMPLER_BINDING_LAYOUT_INIT as initializer. + */ +typedef struct WGPUSamplerBindingLayout { + WGPUChainedStruct * nextInChain; + /** + * If set to @ref WGPUSamplerBindingType_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUSamplerBindingType_Filtering. + * + * The `INIT` macro sets this to @ref WGPUSamplerBindingType_Undefined. + */ + WGPUSamplerBindingType type; +} WGPUSamplerBindingLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSamplerBindingLayout. + */ +#define WGPU_SAMPLER_BINDING_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSamplerBindingLayout, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.type=*/WGPUSamplerBindingType_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SAMPLER_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUSamplerDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * If set to @ref WGPUAddressMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUAddressMode_ClampToEdge. + * + * The `INIT` macro sets this to @ref WGPUAddressMode_Undefined. + */ + WGPUAddressMode addressModeU; + /** + * If set to @ref WGPUAddressMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUAddressMode_ClampToEdge. + * + * The `INIT` macro sets this to @ref WGPUAddressMode_Undefined. + */ + WGPUAddressMode addressModeV; + /** + * If set to @ref WGPUAddressMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUAddressMode_ClampToEdge. + * + * The `INIT` macro sets this to @ref WGPUAddressMode_Undefined. + */ + WGPUAddressMode addressModeW; + /** + * If set to @ref WGPUFilterMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUFilterMode_Nearest. + * + * The `INIT` macro sets this to @ref WGPUFilterMode_Undefined. + */ + WGPUFilterMode magFilter; + /** + * If set to @ref WGPUFilterMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUFilterMode_Nearest. + * + * The `INIT` macro sets this to @ref WGPUFilterMode_Undefined. + */ + WGPUFilterMode minFilter; + /** + * If set to @ref WGPUFilterMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUMipmapFilterMode_Nearest. + * + * The `INIT` macro sets this to @ref WGPUMipmapFilterMode_Undefined. + */ + WGPUMipmapFilterMode mipmapFilter; + /** + * TODO + * + * If non-finite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to `0.f`. + */ + float lodMinClamp; + /** + * TODO + * + * If non-finite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to `32.f`. + */ + float lodMaxClamp; + /** + * The `INIT` macro sets this to @ref WGPUCompareFunction_Undefined. + */ + WGPUCompareFunction compare; + /** + * The `INIT` macro sets this to `1`. + */ + uint16_t maxAnisotropy; +} WGPUSamplerDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSamplerDescriptor. + */ +#define WGPU_SAMPLER_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSamplerDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.addressModeU=*/WGPUAddressMode_Undefined _wgpu_COMMA \ + /*.addressModeV=*/WGPUAddressMode_Undefined _wgpu_COMMA \ + /*.addressModeW=*/WGPUAddressMode_Undefined _wgpu_COMMA \ + /*.magFilter=*/WGPUFilterMode_Undefined _wgpu_COMMA \ + /*.minFilter=*/WGPUFilterMode_Undefined _wgpu_COMMA \ + /*.mipmapFilter=*/WGPUMipmapFilterMode_Undefined _wgpu_COMMA \ + /*.lodMinClamp=*/0.f _wgpu_COMMA \ + /*.lodMaxClamp=*/32.f _wgpu_COMMA \ + /*.compare=*/WGPUCompareFunction_Undefined _wgpu_COMMA \ + /*.maxAnisotropy=*/1 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SHADER_SOURCE_SPIRV_INIT as initializer. + */ +typedef struct WGPUShaderSourceSPIRV { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t codeSize; + /** + * The `INIT` macro sets this to `NULL`. + */ + uint32_t const * code; +} WGPUShaderSourceSPIRV WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUShaderSourceSPIRV. + */ +#define WGPU_SHADER_SOURCE_SPIRV_INIT _wgpu_MAKE_INIT_STRUCT(WGPUShaderSourceSPIRV, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_ShaderSourceSPIRV _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.codeSize=*/0 _wgpu_COMMA \ + /*.code=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SHADER_SOURCE_WGSL_INIT as initializer. + */ +typedef struct WGPUShaderSourceWGSL { + WGPUChainedStruct chain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView code; +} WGPUShaderSourceWGSL WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUShaderSourceWGSL. + */ +#define WGPU_SHADER_SOURCE_WGSL_INIT _wgpu_MAKE_INIT_STRUCT(WGPUShaderSourceWGSL, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_ShaderSourceWGSL _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.code=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_STENCIL_FACE_STATE_INIT as initializer. + */ +typedef struct WGPUStencilFaceState { + /** + * If set to @ref WGPUCompareFunction_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUCompareFunction_Always. + * + * The `INIT` macro sets this to @ref WGPUCompareFunction_Undefined. + */ + WGPUCompareFunction compare; + /** + * If set to @ref WGPUStencilOperation_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUStencilOperation_Keep. + * + * The `INIT` macro sets this to @ref WGPUStencilOperation_Undefined. + */ + WGPUStencilOperation failOp; + /** + * If set to @ref WGPUStencilOperation_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUStencilOperation_Keep. + * + * The `INIT` macro sets this to @ref WGPUStencilOperation_Undefined. + */ + WGPUStencilOperation depthFailOp; + /** + * If set to @ref WGPUStencilOperation_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUStencilOperation_Keep. + * + * The `INIT` macro sets this to @ref WGPUStencilOperation_Undefined. + */ + WGPUStencilOperation passOp; +} WGPUStencilFaceState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUStencilFaceState. + */ +#define WGPU_STENCIL_FACE_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUStencilFaceState, { \ + /*.compare=*/WGPUCompareFunction_Undefined _wgpu_COMMA \ + /*.failOp=*/WGPUStencilOperation_Undefined _wgpu_COMMA \ + /*.depthFailOp=*/WGPUStencilOperation_Undefined _wgpu_COMMA \ + /*.passOp=*/WGPUStencilOperation_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_STORAGE_TEXTURE_BINDING_LAYOUT_INIT as initializer. + */ +typedef struct WGPUStorageTextureBindingLayout { + WGPUChainedStruct * nextInChain; + /** + * If set to @ref WGPUStorageTextureAccess_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUStorageTextureAccess_WriteOnly. + * + * The `INIT` macro sets this to @ref WGPUStorageTextureAccess_Undefined. + */ + WGPUStorageTextureAccess access; + /** + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * If set to @ref WGPUTextureViewDimension_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureViewDimension_2D. + * + * The `INIT` macro sets this to @ref WGPUTextureViewDimension_Undefined. + */ + WGPUTextureViewDimension viewDimension; +} WGPUStorageTextureBindingLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUStorageTextureBindingLayout. + */ +#define WGPU_STORAGE_TEXTURE_BINDING_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUStorageTextureBindingLayout, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.access=*/WGPUStorageTextureAccess_Undefined _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.viewDimension=*/WGPUTextureViewDimension_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SUPPORTED_FEATURES_INIT as initializer. + */ +typedef struct WGPUSupportedFeatures { + /** + * Array count for `features`. The `INIT` macro sets this to 0. + */ + size_t featureCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUFeatureName const * features; +} WGPUSupportedFeatures WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSupportedFeatures. + */ +#define WGPU_SUPPORTED_FEATURES_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSupportedFeatures, { \ + /*.featureCount=*/0 _wgpu_COMMA \ + /*.features=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SUPPORTED_INSTANCE_FEATURES_INIT as initializer. + */ +typedef struct WGPUSupportedInstanceFeatures { + /** + * Array count for `features`. The `INIT` macro sets this to 0. + */ + size_t featureCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUInstanceFeatureName const * features; +} WGPUSupportedInstanceFeatures WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSupportedInstanceFeatures. + */ +#define WGPU_SUPPORTED_INSTANCE_FEATURES_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSupportedInstanceFeatures, { \ + /*.featureCount=*/0 _wgpu_COMMA \ + /*.features=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SUPPORTED_WGSL_LANGUAGE_FEATURES_INIT as initializer. + */ +typedef struct WGPUSupportedWGSLLanguageFeatures { + /** + * Array count for `features`. The `INIT` macro sets this to 0. + */ + size_t featureCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUWGSLLanguageFeatureName const * features; +} WGPUSupportedWGSLLanguageFeatures WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSupportedWGSLLanguageFeatures. + */ +#define WGPU_SUPPORTED_WGSL_LANGUAGE_FEATURES_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSupportedWGSLLanguageFeatures, { \ + /*.featureCount=*/0 _wgpu_COMMA \ + /*.features=*/NULL _wgpu_COMMA \ +}) + +/** + * Filled by @ref wgpuSurfaceGetCapabilities with what's supported for @ref wgpuSurfaceConfigure for a pair of @ref WGPUSurface and @ref WGPUAdapter. + * + * Default values can be set using @ref WGPU_SURFACE_CAPABILITIES_INIT as initializer. + */ +typedef struct WGPUSurfaceCapabilities { + WGPUChainedStruct * nextInChain; + /** + * The bit set of supported @ref WGPUTextureUsage bits. + * Guaranteed to contain @ref WGPUTextureUsage_RenderAttachment. + * + * The `INIT` macro sets this to @ref WGPUTextureUsage_None. + */ + WGPUTextureUsage usages; + /** + * Array count for `formats`. The `INIT` macro sets this to 0. + */ + size_t formatCount; + /** + * A list of supported @ref WGPUTextureFormat values, in order of preference. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUTextureFormat const * formats; + /** + * Array count for `presentModes`. The `INIT` macro sets this to 0. + */ + size_t presentModeCount; + /** + * A list of supported @ref WGPUPresentMode values. + * Guaranteed to contain @ref WGPUPresentMode_Fifo. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUPresentMode const * presentModes; + /** + * Array count for `alphaModes`. The `INIT` macro sets this to 0. + */ + size_t alphaModeCount; + /** + * A list of supported @ref WGPUCompositeAlphaMode values. + * @ref WGPUCompositeAlphaMode_Auto will be an alias for the first element and will never be present in this array. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUCompositeAlphaMode const * alphaModes; +} WGPUSurfaceCapabilities WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceCapabilities. + */ +#define WGPU_SURFACE_CAPABILITIES_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceCapabilities, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.usages=*/WGPUTextureUsage_None _wgpu_COMMA \ + /*.formatCount=*/0 _wgpu_COMMA \ + /*.formats=*/NULL _wgpu_COMMA \ + /*.presentModeCount=*/0 _wgpu_COMMA \ + /*.presentModes=*/NULL _wgpu_COMMA \ + /*.alphaModeCount=*/0 _wgpu_COMMA \ + /*.alphaModes=*/NULL _wgpu_COMMA \ +}) + +/** + * Extension of @ref WGPUSurfaceConfiguration for color spaces and HDR. + * + * Default values can be set using @ref WGPU_SURFACE_COLOR_MANAGEMENT_INIT as initializer. + */ +typedef struct WGPUSurfaceColorManagement { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to (@ref WGPUPredefinedColorSpace)0. + */ + WGPUPredefinedColorSpace colorSpace; + /** + * The `INIT` macro sets this to (@ref WGPUToneMappingMode)0. + */ + WGPUToneMappingMode toneMappingMode; +} WGPUSurfaceColorManagement WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceColorManagement. + */ +#define WGPU_SURFACE_COLOR_MANAGEMENT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceColorManagement, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceColorManagement _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.colorSpace=*/_wgpu_ENUM_ZERO_INIT(WGPUPredefinedColorSpace) _wgpu_COMMA \ + /*.toneMappingMode=*/_wgpu_ENUM_ZERO_INIT(WGPUToneMappingMode) _wgpu_COMMA \ +}) + +/** + * Options to @ref wgpuSurfaceConfigure for defining how a @ref WGPUSurface will be rendered to and presented to the user. + * See @ref Surface-Configuration for more details. + * + * Default values can be set using @ref WGPU_SURFACE_CONFIGURATION_INIT as initializer. + */ +typedef struct WGPUSurfaceConfiguration { + WGPUChainedStruct * nextInChain; + /** + * The @ref WGPUDevice to use to render to surface's textures. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUDevice device; + /** + * The @ref WGPUTextureFormat of the surface's textures. + * + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * The @ref WGPUTextureUsage of the surface's textures. + * + * The `INIT` macro sets this to @ref WGPUTextureUsage_RenderAttachment. + */ + WGPUTextureUsage usage; + /** + * The width of the surface's textures. + * + * The `INIT` macro sets this to `0`. + */ + uint32_t width; + /** + * The height of the surface's textures. + * + * The `INIT` macro sets this to `0`. + */ + uint32_t height; + /** + * Array count for `viewFormats`. The `INIT` macro sets this to 0. + */ + size_t viewFormatCount; + /** + * The additional @ref WGPUTextureFormat for @ref WGPUTextureView format reinterpretation of the surface's textures. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUTextureFormat const * viewFormats; + /** + * How the surface's frames will be composited on the screen. + * + * If set to @ref WGPUCompositeAlphaMode_Auto, + * [defaults] to @ref WGPUCompositeAlphaMode_Inherit in native (allowing the mode + * to be configured externally), and to @ref WGPUCompositeAlphaMode_Opaque in Wasm. + * + * The `INIT` macro sets this to @ref WGPUCompositeAlphaMode_Auto. + */ + WGPUCompositeAlphaMode alphaMode; + /** + * When and in which order the surface's frames will be shown on the screen. + * + * If set to @ref WGPUPresentMode_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUPresentMode_Fifo. + * + * The `INIT` macro sets this to @ref WGPUPresentMode_Undefined. + */ + WGPUPresentMode presentMode; +} WGPUSurfaceConfiguration WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceConfiguration. + */ +#define WGPU_SURFACE_CONFIGURATION_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceConfiguration, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.device=*/NULL _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.usage=*/WGPUTextureUsage_RenderAttachment _wgpu_COMMA \ + /*.width=*/0 _wgpu_COMMA \ + /*.height=*/0 _wgpu_COMMA \ + /*.viewFormatCount=*/0 _wgpu_COMMA \ + /*.viewFormats=*/NULL _wgpu_COMMA \ + /*.alphaMode=*/WGPUCompositeAlphaMode_Auto _wgpu_COMMA \ + /*.presentMode=*/WGPUPresentMode_Undefined _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping an Android [`ANativeWindow`](https://developer.android.com/ndk/reference/group/a-native-window). + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_ANDROID_NATIVE_WINDOW_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceAndroidNativeWindow { + WGPUChainedStruct chain; + /** + * The pointer to the [`ANativeWindow`](https://developer.android.com/ndk/reference/group/a-native-window) that will be wrapped by the @ref WGPUSurface. + * + * The `INIT` macro sets this to `NULL`. + */ + void * window; +} WGPUSurfaceSourceAndroidNativeWindow WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceAndroidNativeWindow. + */ +#define WGPU_SURFACE_SOURCE_ANDROID_NATIVE_WINDOW_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceAndroidNativeWindow, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceAndroidNativeWindow _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.window=*/NULL _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping a [`CAMetalLayer`](https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc). + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_METAL_LAYER_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceMetalLayer { + WGPUChainedStruct chain; + /** + * The pointer to the [`CAMetalLayer`](https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc) that will be wrapped by the @ref WGPUSurface. + * + * The `INIT` macro sets this to `NULL`. + */ + void * layer; +} WGPUSurfaceSourceMetalLayer WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceMetalLayer. + */ +#define WGPU_SURFACE_SOURCE_METAL_LAYER_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceMetalLayer, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceMetalLayer _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.layer=*/NULL _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping a [Wayland](https://wayland.freedesktop.org/) [`wl_surface`](https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_surface). + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_WAYLAND_SURFACE_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceWaylandSurface { + WGPUChainedStruct chain; + /** + * A [`wl_display`](https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_display) for this Wayland instance. + * + * The `INIT` macro sets this to `NULL`. + */ + void * display; + /** + * A [`wl_surface`](https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_surface) that will be wrapped by the @ref WGPUSurface + * + * The `INIT` macro sets this to `NULL`. + */ + void * surface; +} WGPUSurfaceSourceWaylandSurface WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceWaylandSurface. + */ +#define WGPU_SURFACE_SOURCE_WAYLAND_SURFACE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceWaylandSurface, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceWaylandSurface _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.display=*/NULL _wgpu_COMMA \ + /*.surface=*/NULL _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping a Windows [`HWND`](https://learn.microsoft.com/en-us/windows/apps/develop/ui-input/retrieve-hwnd). + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_WINDOWS_HWND_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceWindowsHWND { + WGPUChainedStruct chain; + /** + * The [`HINSTANCE`](https://learn.microsoft.com/en-us/windows/win32/learnwin32/winmain--the-application-entry-point) for this application. + * Most commonly `GetModuleHandle(nullptr)`. + * + * The `INIT` macro sets this to `NULL`. + */ + void * hinstance; + /** + * The [`HWND`](https://learn.microsoft.com/en-us/windows/apps/develop/ui-input/retrieve-hwnd) that will be wrapped by the @ref WGPUSurface. + * + * The `INIT` macro sets this to `NULL`. + */ + void * hwnd; +} WGPUSurfaceSourceWindowsHWND WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceWindowsHWND. + */ +#define WGPU_SURFACE_SOURCE_WINDOWS_HWND_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceWindowsHWND, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceWindowsHWND _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.hinstance=*/NULL _wgpu_COMMA \ + /*.hwnd=*/NULL _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping an [XCB](https://xcb.freedesktop.org/) `xcb_window_t`. + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_XCB_WINDOW_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceXCBWindow { + WGPUChainedStruct chain; + /** + * The `xcb_connection_t` for the connection to the X server. + * + * The `INIT` macro sets this to `NULL`. + */ + void * connection; + /** + * The `xcb_window_t` for the window that will be wrapped by the @ref WGPUSurface. + * + * The `INIT` macro sets this to `0`. + */ + uint32_t window; +} WGPUSurfaceSourceXCBWindow WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceXCBWindow. + */ +#define WGPU_SURFACE_SOURCE_XCB_WINDOW_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceXCBWindow, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceXCBWindow _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.connection=*/NULL _wgpu_COMMA \ + /*.window=*/0 _wgpu_COMMA \ +}) + +/** + * Chained in @ref WGPUSurfaceDescriptor to make an @ref WGPUSurface wrapping an [Xlib](https://www.x.org/releases/current/doc/libX11/libX11/libX11.html) `Window`. + * + * Default values can be set using @ref WGPU_SURFACE_SOURCE_XLIB_WINDOW_INIT as initializer. + */ +typedef struct WGPUSurfaceSourceXlibWindow { + WGPUChainedStruct chain; + /** + * A pointer to the [`Display`](https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Opening_the_Display) connected to the X server. + * + * The `INIT` macro sets this to `NULL`. + */ + void * display; + /** + * The [`Window`](https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Creating_Windows) that will be wrapped by the @ref WGPUSurface. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t window; +} WGPUSurfaceSourceXlibWindow WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceSourceXlibWindow. + */ +#define WGPU_SURFACE_SOURCE_XLIB_WINDOW_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceSourceXlibWindow, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_SurfaceSourceXlibWindow _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.display=*/NULL _wgpu_COMMA \ + /*.window=*/0 _wgpu_COMMA \ +}) + +/** + * Queried each frame from a @ref WGPUSurface to get a @ref WGPUTexture to render to along with some metadata. + * See @ref Surface-Presenting for more details. + * + * Default values can be set using @ref WGPU_SURFACE_TEXTURE_INIT as initializer. + */ +typedef struct WGPUSurfaceTexture { + WGPUChainedStruct * nextInChain; + /** + * The @ref WGPUTexture representing the frame that will be shown on the surface. + * It is @ref ReturnedWithOwnership from @ref wgpuSurfaceGetCurrentTexture. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPUTexture texture; + /** + * Whether the call to @ref wgpuSurfaceGetCurrentTexture succeeded and a hint as to why it might not have. + * + * The `INIT` macro sets this to (@ref WGPUSurfaceGetCurrentTextureStatus)0. + */ + WGPUSurfaceGetCurrentTextureStatus status; +} WGPUSurfaceTexture WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceTexture. + */ +#define WGPU_SURFACE_TEXTURE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceTexture, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.texture=*/NULL _wgpu_COMMA \ + /*.status=*/_wgpu_ENUM_ZERO_INIT(WGPUSurfaceGetCurrentTextureStatus) _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXEL_COPY_BUFFER_LAYOUT_INIT as initializer. + */ +typedef struct WGPUTexelCopyBufferLayout { + /** + * The `INIT` macro sets this to `0`. + */ + uint64_t offset; + /** + * The `INIT` macro sets this to @ref WGPU_COPY_STRIDE_UNDEFINED. + */ + uint32_t bytesPerRow; + /** + * The `INIT` macro sets this to @ref WGPU_COPY_STRIDE_UNDEFINED. + */ + uint32_t rowsPerImage; +} WGPUTexelCopyBufferLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTexelCopyBufferLayout. + */ +#define WGPU_TEXEL_COPY_BUFFER_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTexelCopyBufferLayout, { \ + /*.offset=*/0 _wgpu_COMMA \ + /*.bytesPerRow=*/WGPU_COPY_STRIDE_UNDEFINED _wgpu_COMMA \ + /*.rowsPerImage=*/WGPU_COPY_STRIDE_UNDEFINED _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXTURE_BINDING_LAYOUT_INIT as initializer. + */ +typedef struct WGPUTextureBindingLayout { + WGPUChainedStruct * nextInChain; + /** + * If set to @ref WGPUTextureSampleType_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureSampleType_Float. + * + * The `INIT` macro sets this to @ref WGPUTextureSampleType_Undefined. + */ + WGPUTextureSampleType sampleType; + /** + * If set to @ref WGPUTextureViewDimension_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureViewDimension_2D. + * + * The `INIT` macro sets this to @ref WGPUTextureViewDimension_Undefined. + */ + WGPUTextureViewDimension viewDimension; + /** + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool multisampled; +} WGPUTextureBindingLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureBindingLayout. + */ +#define WGPU_TEXTURE_BINDING_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureBindingLayout, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.sampleType=*/WGPUTextureSampleType_Undefined _wgpu_COMMA \ + /*.viewDimension=*/WGPUTextureViewDimension_Undefined _wgpu_COMMA \ + /*.multisampled=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Note: While Compatibility Mode is optional to implement, this extension struct + * is required to be accepted (but per the WebGPU spec, its contents are ignored + * on devices that have the @ref WGPUFeatureName_CoreFeaturesAndLimits feature). + * + * Default values can be set using @ref WGPU_TEXTURE_BINDING_VIEW_DIMENSION_INIT as initializer. + */ +typedef struct WGPUTextureBindingViewDimension { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to @ref WGPUTextureViewDimension_Undefined. + */ + WGPUTextureViewDimension textureBindingViewDimension; +} WGPUTextureBindingViewDimension WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureBindingViewDimension. + */ +#define WGPU_TEXTURE_BINDING_VIEW_DIMENSION_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureBindingViewDimension, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_TextureBindingViewDimension _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.textureBindingViewDimension=*/WGPUTextureViewDimension_Undefined _wgpu_COMMA \ +}) + +/** + * When accessed by a shader, the red/green/blue/alpha channels are replaced + * by the value corresponding to the component specified in r, g, b, and a, + * respectively unlike the JS API which uses a string of length four, with + * each character mapping to the texture view's red/green/blue/alpha channels. + * + * Default values can be set using @ref WGPU_TEXTURE_COMPONENT_SWIZZLE_INIT as initializer. + */ +typedef struct WGPUTextureComponentSwizzle { + /** + * The value that replaces the red channel in the shader. + * + * If set to @ref WGPUComponentSwizzle_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUComponentSwizzle_R. + * + * The `INIT` macro sets this to @ref WGPUComponentSwizzle_Undefined. + */ + WGPUComponentSwizzle r; + /** + * The value that replaces the green channel in the shader. + * + * If set to @ref WGPUComponentSwizzle_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUComponentSwizzle_G. + * + * The `INIT` macro sets this to @ref WGPUComponentSwizzle_Undefined. + */ + WGPUComponentSwizzle g; + /** + * The value that replaces the blue channel in the shader. + * + * If set to @ref WGPUComponentSwizzle_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUComponentSwizzle_B. + * + * The `INIT` macro sets this to @ref WGPUComponentSwizzle_Undefined. + */ + WGPUComponentSwizzle b; + /** + * The value that replaces the alpha channel in the shader. + * + * If set to @ref WGPUComponentSwizzle_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUComponentSwizzle_A. + * + * The `INIT` macro sets this to @ref WGPUComponentSwizzle_Undefined. + */ + WGPUComponentSwizzle a; +} WGPUTextureComponentSwizzle WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureComponentSwizzle. + */ +#define WGPU_TEXTURE_COMPONENT_SWIZZLE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureComponentSwizzle, { \ + /*.r=*/WGPUComponentSwizzle_Undefined _wgpu_COMMA \ + /*.g=*/WGPUComponentSwizzle_Undefined _wgpu_COMMA \ + /*.b=*/WGPUComponentSwizzle_Undefined _wgpu_COMMA \ + /*.a=*/WGPUComponentSwizzle_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_VERTEX_ATTRIBUTE_INIT as initializer. + */ +typedef struct WGPUVertexAttribute { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to (@ref WGPUVertexFormat)0. + */ + WGPUVertexFormat format; + /** + * The `INIT` macro sets this to `0`. + */ + uint64_t offset; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t shaderLocation; +} WGPUVertexAttribute WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUVertexAttribute. + */ +#define WGPU_VERTEX_ATTRIBUTE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUVertexAttribute, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.format=*/_wgpu_ENUM_ZERO_INIT(WGPUVertexFormat) _wgpu_COMMA \ + /*.offset=*/0 _wgpu_COMMA \ + /*.shaderLocation=*/0 _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BIND_GROUP_ENTRY_INIT as initializer. + */ +typedef struct WGPUBindGroupEntry { + WGPUChainedStruct * nextInChain; + /** + * Binding index in the bind group. + * + * The `INIT` macro sets this to `0`. + */ + uint32_t binding; + /** + * Set this if the binding is a buffer object. + * Otherwise must be null. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUBuffer buffer; + /** + * If the binding is a buffer, this is the byte offset of the binding range. + * Otherwise ignored. + * + * The `INIT` macro sets this to `0`. + */ + uint64_t offset; + /** + * If the binding is a buffer, this is the byte size of the binding range + * (@ref WGPU_WHOLE_SIZE means the binding ends at the end of the buffer). + * Otherwise ignored. + * + * The `INIT` macro sets this to @ref WGPU_WHOLE_SIZE. + */ + uint64_t size; + /** + * Set this if the binding is a sampler object. + * Otherwise must be null. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUSampler sampler; + /** + * Set this if the binding is a texture view object. + * Otherwise must be null. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUTextureView textureView; +} WGPUBindGroupEntry WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBindGroupEntry. + */ +#define WGPU_BIND_GROUP_ENTRY_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBindGroupEntry, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.binding=*/0 _wgpu_COMMA \ + /*.buffer=*/NULL _wgpu_COMMA \ + /*.offset=*/0 _wgpu_COMMA \ + /*.size=*/WGPU_WHOLE_SIZE _wgpu_COMMA \ + /*.sampler=*/NULL _wgpu_COMMA \ + /*.textureView=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BIND_GROUP_LAYOUT_ENTRY_INIT as initializer. + */ +typedef struct WGPUBindGroupLayoutEntry { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t binding; + /** + * The `INIT` macro sets this to @ref WGPUShaderStage_None. + */ + WGPUShaderStage visibility; + /** + * If non-zero, this entry defines a binding array with this size. + * + * The `INIT` macro sets this to `0`. + */ + uint32_t bindingArraySize; + /** + * The `INIT` macro sets this to zero (which sets the entry to `BindingNotUsed`). + */ + WGPUBufferBindingLayout buffer; + /** + * The `INIT` macro sets this to zero (which sets the entry to `BindingNotUsed`). + */ + WGPUSamplerBindingLayout sampler; + /** + * The `INIT` macro sets this to zero (which sets the entry to `BindingNotUsed`). + */ + WGPUTextureBindingLayout texture; + /** + * The `INIT` macro sets this to zero (which sets the entry to `BindingNotUsed`). + */ + WGPUStorageTextureBindingLayout storageTexture; +} WGPUBindGroupLayoutEntry WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBindGroupLayoutEntry. + */ +#define WGPU_BIND_GROUP_LAYOUT_ENTRY_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBindGroupLayoutEntry, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.binding=*/0 _wgpu_COMMA \ + /*.visibility=*/WGPUShaderStage_None _wgpu_COMMA \ + /*.bindingArraySize=*/0 _wgpu_COMMA \ + /*.buffer=*/_wgpu_STRUCT_ZERO_INIT _wgpu_COMMA \ + /*.sampler=*/_wgpu_STRUCT_ZERO_INIT _wgpu_COMMA \ + /*.texture=*/_wgpu_STRUCT_ZERO_INIT _wgpu_COMMA \ + /*.storageTexture=*/_wgpu_STRUCT_ZERO_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BLEND_STATE_INIT as initializer. + */ +typedef struct WGPUBlendState { + /** + * The `INIT` macro sets this to @ref WGPU_BLEND_COMPONENT_INIT. + */ + WGPUBlendComponent color; + /** + * The `INIT` macro sets this to @ref WGPU_BLEND_COMPONENT_INIT. + */ + WGPUBlendComponent alpha; +} WGPUBlendState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBlendState. + */ +#define WGPU_BLEND_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBlendState, { \ + /*.color=*/WGPU_BLEND_COMPONENT_INIT _wgpu_COMMA \ + /*.alpha=*/WGPU_BLEND_COMPONENT_INIT _wgpu_COMMA \ +}) + +/** + * This is an @ref ImplementationAllocatedStructChain root. + * Arbitrary chains must be handled gracefully by the application! + * + * Default values can be set using @ref WGPU_COMPILATION_INFO_INIT as initializer. + */ +typedef struct WGPUCompilationInfo { + WGPUChainedStruct * nextInChain; + /** + * Array count for `messages`. The `INIT` macro sets this to 0. + */ + size_t messageCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUCompilationMessage const * messages; +} WGPUCompilationInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUCompilationInfo. + */ +#define WGPU_COMPILATION_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUCompilationInfo, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.messageCount=*/0 _wgpu_COMMA \ + /*.messages=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COMPUTE_PASS_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUComputePassDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUPassTimestampWrites const * timestampWrites; +} WGPUComputePassDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUComputePassDescriptor. + */ +#define WGPU_COMPUTE_PASS_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUComputePassDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.timestampWrites=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COMPUTE_STATE_INIT as initializer. + */ +typedef struct WGPUComputeState { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUShaderModule module; + /** + * This is a \ref NullableInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView entryPoint; + /** + * Array count for `constants`. The `INIT` macro sets this to 0. + */ + size_t constantCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUConstantEntry const * constants; +} WGPUComputeState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUComputeState. + */ +#define WGPU_COMPUTE_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUComputeState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.module=*/NULL _wgpu_COMMA \ + /*.entryPoint=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.constantCount=*/0 _wgpu_COMMA \ + /*.constants=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_DEPTH_STENCIL_STATE_INIT as initializer. + */ +typedef struct WGPUDepthStencilState { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * The `INIT` macro sets this to @ref WGPUOptionalBool_Undefined. + */ + WGPUOptionalBool depthWriteEnabled; + /** + * The `INIT` macro sets this to @ref WGPUCompareFunction_Undefined. + */ + WGPUCompareFunction depthCompare; + /** + * The `INIT` macro sets this to @ref WGPU_STENCIL_FACE_STATE_INIT. + */ + WGPUStencilFaceState stencilFront; + /** + * The `INIT` macro sets this to @ref WGPU_STENCIL_FACE_STATE_INIT. + */ + WGPUStencilFaceState stencilBack; + /** + * The `INIT` macro sets this to `0xFFFFFFFF`. + */ + uint32_t stencilReadMask; + /** + * The `INIT` macro sets this to `0xFFFFFFFF`. + */ + uint32_t stencilWriteMask; + /** + * The `INIT` macro sets this to `0`. + */ + int32_t depthBias; + /** + * TODO + * + * If non-finite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to `0.f`. + */ + float depthBiasSlopeScale; + /** + * TODO + * + * If non-finite, produces a @ref NonFiniteFloatValueError. + * + * The `INIT` macro sets this to `0.f`. + */ + float depthBiasClamp; +} WGPUDepthStencilState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUDepthStencilState. + */ +#define WGPU_DEPTH_STENCIL_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUDepthStencilState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.depthWriteEnabled=*/WGPUOptionalBool_Undefined _wgpu_COMMA \ + /*.depthCompare=*/WGPUCompareFunction_Undefined _wgpu_COMMA \ + /*.stencilFront=*/WGPU_STENCIL_FACE_STATE_INIT _wgpu_COMMA \ + /*.stencilBack=*/WGPU_STENCIL_FACE_STATE_INIT _wgpu_COMMA \ + /*.stencilReadMask=*/0xFFFFFFFF _wgpu_COMMA \ + /*.stencilWriteMask=*/0xFFFFFFFF _wgpu_COMMA \ + /*.depthBias=*/0 _wgpu_COMMA \ + /*.depthBiasSlopeScale=*/0.f _wgpu_COMMA \ + /*.depthBiasClamp=*/0.f _wgpu_COMMA \ +}) + +/** + * Struct holding a future to wait on, and a `completed` boolean flag. + * + * Default values can be set using @ref WGPU_FUTURE_WAIT_INFO_INIT as initializer. + */ +typedef struct WGPUFutureWaitInfo { + /** + * The future to wait on. + * + * The `INIT` macro sets this to @ref WGPU_FUTURE_INIT. + */ + WGPUFuture future; + /** + * Whether or not the future completed. + * + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool completed; +} WGPUFutureWaitInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUFutureWaitInfo. + */ +#define WGPU_FUTURE_WAIT_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUFutureWaitInfo, { \ + /*.future=*/WGPU_FUTURE_INIT _wgpu_COMMA \ + /*.completed=*/WGPU_FALSE _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_INSTANCE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUInstanceDescriptor { + WGPUChainedStruct * nextInChain; + /** + * Array count for `requiredFeatures`. The `INIT` macro sets this to 0. + */ + size_t requiredFeatureCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUInstanceFeatureName const * requiredFeatures; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUInstanceLimits const * requiredLimits; +} WGPUInstanceDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUInstanceDescriptor. + */ +#define WGPU_INSTANCE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUInstanceDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.requiredFeatureCount=*/0 _wgpu_COMMA \ + /*.requiredFeatures=*/NULL _wgpu_COMMA \ + /*.requiredLimits=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_LIMITS_INIT as initializer. + */ +typedef struct WGPULimits { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxTextureDimension1D; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxTextureDimension2D; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxTextureDimension3D; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxTextureArrayLayers; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxBindGroups; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxBindGroupsPlusVertexBuffers; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxBindingsPerBindGroup; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxDynamicUniformBuffersPerPipelineLayout; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxDynamicStorageBuffersPerPipelineLayout; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxSampledTexturesPerShaderStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxSamplersPerShaderStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageBuffersPerShaderStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxStorageTexturesPerShaderStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxUniformBuffersPerShaderStage; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U64_UNDEFINED. + */ + uint64_t maxUniformBufferBindingSize; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U64_UNDEFINED. + */ + uint64_t maxStorageBufferBindingSize; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t minUniformBufferOffsetAlignment; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t minStorageBufferOffsetAlignment; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxVertexBuffers; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U64_UNDEFINED. + */ + uint64_t maxBufferSize; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxVertexAttributes; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxVertexBufferArrayStride; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxInterStageShaderVariables; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxColorAttachments; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxColorAttachmentBytesPerSample; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeWorkgroupStorageSize; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeInvocationsPerWorkgroup; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeWorkgroupSizeX; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeWorkgroupSizeY; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeWorkgroupSizeZ; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxComputeWorkgroupsPerDimension; + /** + * The `INIT` macro sets this to @ref WGPU_LIMIT_U32_UNDEFINED. + */ + uint32_t maxImmediateSize; +} WGPULimits WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPULimits. + */ +#define WGPU_LIMITS_INIT _wgpu_MAKE_INIT_STRUCT(WGPULimits, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.maxTextureDimension1D=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxTextureDimension2D=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxTextureDimension3D=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxTextureArrayLayers=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBindGroups=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBindGroupsPlusVertexBuffers=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBindingsPerBindGroup=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxDynamicUniformBuffersPerPipelineLayout=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxDynamicStorageBuffersPerPipelineLayout=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxSampledTexturesPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxSamplersPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxStorageBuffersPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxStorageTexturesPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxUniformBuffersPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxUniformBufferBindingSize=*/WGPU_LIMIT_U64_UNDEFINED _wgpu_COMMA \ + /*.maxStorageBufferBindingSize=*/WGPU_LIMIT_U64_UNDEFINED _wgpu_COMMA \ + /*.minUniformBufferOffsetAlignment=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.minStorageBufferOffsetAlignment=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxVertexBuffers=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBufferSize=*/WGPU_LIMIT_U64_UNDEFINED _wgpu_COMMA \ + /*.maxVertexAttributes=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxVertexBufferArrayStride=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxInterStageShaderVariables=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxColorAttachments=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxColorAttachmentBytesPerSample=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeWorkgroupStorageSize=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeInvocationsPerWorkgroup=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeWorkgroupSizeX=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeWorkgroupSizeY=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeWorkgroupSizeZ=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxComputeWorkgroupsPerDimension=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxImmediateSize=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT as initializer. + */ +typedef struct WGPURenderPassColorAttachment { + WGPUChainedStruct * nextInChain; + /** + * If `NULL`, indicates a hole in the parent + * @ref WGPURenderPassDescriptor::colorAttachments array. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUTextureView view; + /** + * The `INIT` macro sets this to @ref WGPU_DEPTH_SLICE_UNDEFINED. + */ + uint32_t depthSlice; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUTextureView resolveTarget; + /** + * The `INIT` macro sets this to @ref WGPULoadOp_Undefined. + */ + WGPULoadOp loadOp; + /** + * The `INIT` macro sets this to @ref WGPUStoreOp_Undefined. + */ + WGPUStoreOp storeOp; + /** + * The `INIT` macro sets this to @ref WGPU_COLOR_INIT. + */ + WGPUColor clearValue; +} WGPURenderPassColorAttachment WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderPassColorAttachment. + */ +#define WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderPassColorAttachment, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.view=*/NULL _wgpu_COMMA \ + /*.depthSlice=*/WGPU_DEPTH_SLICE_UNDEFINED _wgpu_COMMA \ + /*.resolveTarget=*/NULL _wgpu_COMMA \ + /*.loadOp=*/WGPULoadOp_Undefined _wgpu_COMMA \ + /*.storeOp=*/WGPUStoreOp_Undefined _wgpu_COMMA \ + /*.clearValue=*/WGPU_COLOR_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_REQUEST_ADAPTER_OPTIONS_INIT as initializer. + */ +typedef struct WGPURequestAdapterOptions { + WGPUChainedStruct * nextInChain; + /** + * "Feature level" for the adapter request. If an adapter is returned, it must support the features and limits in the requested feature level. + * + * If set to @ref WGPUFeatureLevel_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUFeatureLevel_Core. + * Additionally, implementations may ignore @ref WGPUFeatureLevel_Compatibility + * and provide @ref WGPUFeatureLevel_Core instead. + * + * The `INIT` macro sets this to @ref WGPUFeatureLevel_Undefined. + */ + WGPUFeatureLevel featureLevel; + /** + * The `INIT` macro sets this to @ref WGPUPowerPreference_Undefined. + */ + WGPUPowerPreference powerPreference; + /** + * If true, requires the adapter to be a "fallback" adapter as defined by the JS spec. + * If this is not possible, the request returns null. + * + * The `INIT` macro sets this to `WGPU_FALSE`. + */ + WGPUBool forceFallbackAdapter; + /** + * If set, requires the adapter to have a particular backend type. + * If this is not possible, the request returns null. + * + * The `INIT` macro sets this to @ref WGPUBackendType_Undefined. + */ + WGPUBackendType backendType; + /** + * If set, requires the adapter to be able to output to a particular surface. + * If this is not possible, the request returns null. + * + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUSurface compatibleSurface; +} WGPURequestAdapterOptions WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURequestAdapterOptions. + */ +#define WGPU_REQUEST_ADAPTER_OPTIONS_INIT _wgpu_MAKE_INIT_STRUCT(WGPURequestAdapterOptions, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.featureLevel=*/WGPUFeatureLevel_Undefined _wgpu_COMMA \ + /*.powerPreference=*/WGPUPowerPreference_Undefined _wgpu_COMMA \ + /*.forceFallbackAdapter=*/WGPU_FALSE _wgpu_COMMA \ + /*.backendType=*/WGPUBackendType_Undefined _wgpu_COMMA \ + /*.compatibleSurface=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_SHADER_MODULE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUShaderModuleDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPUShaderModuleDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUShaderModuleDescriptor. + */ +#define WGPU_SHADER_MODULE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUShaderModuleDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * The root descriptor for the creation of an @ref WGPUSurface with @ref wgpuInstanceCreateSurface. + * It isn't sufficient by itself and must have one of the `WGPUSurfaceSource*` in its chain. + * See @ref Surface-Creation for more details. + * + * Default values can be set using @ref WGPU_SURFACE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUSurfaceDescriptor { + WGPUChainedStruct * nextInChain; + /** + * Label used to refer to the object. + * + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; +} WGPUSurfaceDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUSurfaceDescriptor. + */ +#define WGPU_SURFACE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUSurfaceDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXEL_COPY_BUFFER_INFO_INIT as initializer. + */ +typedef struct WGPUTexelCopyBufferInfo { + /** + * The `INIT` macro sets this to @ref WGPU_TEXEL_COPY_BUFFER_LAYOUT_INIT. + */ + WGPUTexelCopyBufferLayout layout; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUBuffer buffer; +} WGPUTexelCopyBufferInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTexelCopyBufferInfo. + */ +#define WGPU_TEXEL_COPY_BUFFER_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTexelCopyBufferInfo, { \ + /*.layout=*/WGPU_TEXEL_COPY_BUFFER_LAYOUT_INIT _wgpu_COMMA \ + /*.buffer=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXEL_COPY_TEXTURE_INFO_INIT as initializer. + */ +typedef struct WGPUTexelCopyTextureInfo { + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUTexture texture; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t mipLevel; + /** + * The `INIT` macro sets this to @ref WGPU_ORIGIN_3D_INIT. + */ + WGPUOrigin3D origin; + /** + * If set to @ref WGPUTextureAspect_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureAspect_All. + * + * The `INIT` macro sets this to @ref WGPUTextureAspect_Undefined. + */ + WGPUTextureAspect aspect; +} WGPUTexelCopyTextureInfo WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTexelCopyTextureInfo. + */ +#define WGPU_TEXEL_COPY_TEXTURE_INFO_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTexelCopyTextureInfo, { \ + /*.texture=*/NULL _wgpu_COMMA \ + /*.mipLevel=*/0 _wgpu_COMMA \ + /*.origin=*/WGPU_ORIGIN_3D_INIT _wgpu_COMMA \ + /*.aspect=*/WGPUTextureAspect_Undefined _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXTURE_COMPONENT_SWIZZLE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUTextureComponentSwizzleDescriptor { + WGPUChainedStruct chain; + /** + * The `INIT` macro sets this to @ref WGPU_TEXTURE_COMPONENT_SWIZZLE_INIT. + */ + WGPUTextureComponentSwizzle swizzle; +} WGPUTextureComponentSwizzleDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureComponentSwizzleDescriptor. + */ +#define WGPU_TEXTURE_COMPONENT_SWIZZLE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureComponentSwizzleDescriptor, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/WGPUSType_TextureComponentSwizzleDescriptor _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.swizzle=*/WGPU_TEXTURE_COMPONENT_SWIZZLE_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXTURE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUTextureDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to @ref WGPUTextureUsage_None. + */ + WGPUTextureUsage usage; + /** + * If set to @ref WGPUTextureDimension_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureDimension_2D. + * + * The `INIT` macro sets this to @ref WGPUTextureDimension_Undefined. + */ + WGPUTextureDimension dimension; + /** + * The `INIT` macro sets this to @ref WGPU_EXTENT_3D_INIT. + */ + WGPUExtent3D size; + /** + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t mipLevelCount; + /** + * The `INIT` macro sets this to `1`. + */ + uint32_t sampleCount; + /** + * Array count for `viewFormats`. The `INIT` macro sets this to 0. + */ + size_t viewFormatCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUTextureFormat const * viewFormats; +} WGPUTextureDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureDescriptor. + */ +#define WGPU_TEXTURE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.usage=*/WGPUTextureUsage_None _wgpu_COMMA \ + /*.dimension=*/WGPUTextureDimension_Undefined _wgpu_COMMA \ + /*.size=*/WGPU_EXTENT_3D_INIT _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.mipLevelCount=*/1 _wgpu_COMMA \ + /*.sampleCount=*/1 _wgpu_COMMA \ + /*.viewFormatCount=*/0 _wgpu_COMMA \ + /*.viewFormats=*/NULL _wgpu_COMMA \ +}) + +/** + * If `attributes` is empty *and* `stepMode` is @ref WGPUVertexStepMode_Undefined, + * indicates a "hole" in the parent @ref WGPUVertexState `buffers` array, + * with behavior equivalent to `null` in the JS API. + * + * If `attributes` is empty but `stepMode` is *not* @ref WGPUVertexStepMode_Undefined, + * indicates a vertex buffer with no attributes, with behavior equivalent to + * `{ attributes: [] }` in the JS API. (TODO: If the JS API changes not to + * distinguish these cases, then this distinction doesn't matter and we can + * remove this documentation.) + * + * If `stepMode` is @ref WGPUVertexStepMode_Undefined but `attributes` is *not* empty, + * `stepMode` [defaults](@ref SentinelValues) to @ref WGPUVertexStepMode_Vertex. + * + * Default values can be set using @ref WGPU_VERTEX_BUFFER_LAYOUT_INIT as initializer. + */ +typedef struct WGPUVertexBufferLayout { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to @ref WGPUVertexStepMode_Undefined. + */ + WGPUVertexStepMode stepMode; + /** + * The `INIT` macro sets this to `0`. + */ + uint64_t arrayStride; + /** + * Array count for `attributes`. The `INIT` macro sets this to 0. + */ + size_t attributeCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUVertexAttribute const * attributes; +} WGPUVertexBufferLayout WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUVertexBufferLayout. + */ +#define WGPU_VERTEX_BUFFER_LAYOUT_INIT _wgpu_MAKE_INIT_STRUCT(WGPUVertexBufferLayout, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.stepMode=*/WGPUVertexStepMode_Undefined _wgpu_COMMA \ + /*.arrayStride=*/0 _wgpu_COMMA \ + /*.attributeCount=*/0 _wgpu_COMMA \ + /*.attributes=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BIND_GROUP_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUBindGroupDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUBindGroupLayout layout; + /** + * Array count for `entries`. The `INIT` macro sets this to 0. + */ + size_t entryCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUBindGroupEntry const * entries; +} WGPUBindGroupDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBindGroupDescriptor. + */ +#define WGPU_BIND_GROUP_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBindGroupDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.layout=*/NULL _wgpu_COMMA \ + /*.entryCount=*/0 _wgpu_COMMA \ + /*.entries=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_BIND_GROUP_LAYOUT_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUBindGroupLayoutDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * Array count for `entries`. The `INIT` macro sets this to 0. + */ + size_t entryCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUBindGroupLayoutEntry const * entries; +} WGPUBindGroupLayoutDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUBindGroupLayoutDescriptor. + */ +#define WGPU_BIND_GROUP_LAYOUT_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUBindGroupLayoutDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.entryCount=*/0 _wgpu_COMMA \ + /*.entries=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COLOR_TARGET_STATE_INIT as initializer. + */ +typedef struct WGPUColorTargetState { + WGPUChainedStruct * nextInChain; + /** + * The texture format of the target. If @ref WGPUTextureFormat_Undefined, + * indicates a "hole" in the parent @ref WGPUFragmentState `targets` array: + * the pipeline does not output a value at this `location`. + * + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUBlendState const * blend; + /** + * The `INIT` macro sets this to @ref WGPUColorWriteMask_All. + */ + WGPUColorWriteMask writeMask; +} WGPUColorTargetState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUColorTargetState. + */ +#define WGPU_COLOR_TARGET_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUColorTargetState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.blend=*/NULL _wgpu_COMMA \ + /*.writeMask=*/WGPUColorWriteMask_All _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_COMPUTE_PIPELINE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUComputePipelineDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUPipelineLayout layout; + /** + * The `INIT` macro sets this to @ref WGPU_COMPUTE_STATE_INIT. + */ + WGPUComputeState compute; +} WGPUComputePipelineDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUComputePipelineDescriptor. + */ +#define WGPU_COMPUTE_PIPELINE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUComputePipelineDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.layout=*/NULL _wgpu_COMMA \ + /*.compute=*/WGPU_COMPUTE_STATE_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_DEVICE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUDeviceDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * Array count for `requiredFeatures`. The `INIT` macro sets this to 0. + */ + size_t requiredFeatureCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUFeatureName const * requiredFeatures; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPULimits const * requiredLimits; + /** + * The `INIT` macro sets this to @ref WGPU_QUEUE_DESCRIPTOR_INIT. + */ + WGPUQueueDescriptor defaultQueue; + /** + * The `INIT` macro sets this to @ref WGPU_DEVICE_LOST_CALLBACK_INFO_INIT. + */ + WGPUDeviceLostCallbackInfo deviceLostCallbackInfo; + /** + * Called when there is an uncaptured error on this device, from any thread. + * See @ref ErrorScopes. + * + * **Important:** This callback does not have a configurable @ref WGPUCallbackMode; it may be called at any time (like @ref WGPUCallbackMode_AllowSpontaneous). As such, calls into the `webgpu.h` API from this callback are unsafe. See @ref CallbackReentrancy. + * + * The `INIT` macro sets this to @ref WGPU_UNCAPTURED_ERROR_CALLBACK_INFO_INIT. + */ + WGPUUncapturedErrorCallbackInfo uncapturedErrorCallbackInfo; +} WGPUDeviceDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUDeviceDescriptor. + */ +#define WGPU_DEVICE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUDeviceDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.requiredFeatureCount=*/0 _wgpu_COMMA \ + /*.requiredFeatures=*/NULL _wgpu_COMMA \ + /*.requiredLimits=*/NULL _wgpu_COMMA \ + /*.defaultQueue=*/WGPU_QUEUE_DESCRIPTOR_INIT _wgpu_COMMA \ + /*.deviceLostCallbackInfo=*/WGPU_DEVICE_LOST_CALLBACK_INFO_INIT _wgpu_COMMA \ + /*.uncapturedErrorCallbackInfo=*/WGPU_UNCAPTURED_ERROR_CALLBACK_INFO_INIT _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_PASS_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPURenderPassDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * Array count for `colorAttachments`. The `INIT` macro sets this to 0. + */ + size_t colorAttachmentCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPURenderPassColorAttachment const * colorAttachments; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPURenderPassDepthStencilAttachment const * depthStencilAttachment; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUQuerySet occlusionQuerySet; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUPassTimestampWrites const * timestampWrites; +} WGPURenderPassDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderPassDescriptor. + */ +#define WGPU_RENDER_PASS_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderPassDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.colorAttachmentCount=*/0 _wgpu_COMMA \ + /*.colorAttachments=*/NULL _wgpu_COMMA \ + /*.depthStencilAttachment=*/NULL _wgpu_COMMA \ + /*.occlusionQuerySet=*/NULL _wgpu_COMMA \ + /*.timestampWrites=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPUTextureViewDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to @ref WGPUTextureFormat_Undefined. + */ + WGPUTextureFormat format; + /** + * The `INIT` macro sets this to @ref WGPUTextureViewDimension_Undefined. + */ + WGPUTextureViewDimension dimension; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t baseMipLevel; + /** + * The `INIT` macro sets this to @ref WGPU_MIP_LEVEL_COUNT_UNDEFINED. + */ + uint32_t mipLevelCount; + /** + * The `INIT` macro sets this to `0`. + */ + uint32_t baseArrayLayer; + /** + * The `INIT` macro sets this to @ref WGPU_ARRAY_LAYER_COUNT_UNDEFINED. + */ + uint32_t arrayLayerCount; + /** + * If set to @ref WGPUTextureAspect_Undefined, + * [defaults](@ref SentinelValues) to @ref WGPUTextureAspect_All. + * + * The `INIT` macro sets this to @ref WGPUTextureAspect_Undefined. + */ + WGPUTextureAspect aspect; + /** + * The `INIT` macro sets this to @ref WGPUTextureUsage_None. + */ + WGPUTextureUsage usage; +} WGPUTextureViewDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUTextureViewDescriptor. + */ +#define WGPU_TEXTURE_VIEW_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPUTextureViewDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.format=*/WGPUTextureFormat_Undefined _wgpu_COMMA \ + /*.dimension=*/WGPUTextureViewDimension_Undefined _wgpu_COMMA \ + /*.baseMipLevel=*/0 _wgpu_COMMA \ + /*.mipLevelCount=*/WGPU_MIP_LEVEL_COUNT_UNDEFINED _wgpu_COMMA \ + /*.baseArrayLayer=*/0 _wgpu_COMMA \ + /*.arrayLayerCount=*/WGPU_ARRAY_LAYER_COUNT_UNDEFINED _wgpu_COMMA \ + /*.aspect=*/WGPUTextureAspect_Undefined _wgpu_COMMA \ + /*.usage=*/WGPUTextureUsage_None _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_VERTEX_STATE_INIT as initializer. + */ +typedef struct WGPUVertexState { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUShaderModule module; + /** + * This is a \ref NullableInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView entryPoint; + /** + * Array count for `constants`. The `INIT` macro sets this to 0. + */ + size_t constantCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUConstantEntry const * constants; + /** + * Array count for `buffers`. The `INIT` macro sets this to 0. + */ + size_t bufferCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUVertexBufferLayout const * buffers; +} WGPUVertexState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUVertexState. + */ +#define WGPU_VERTEX_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUVertexState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.module=*/NULL _wgpu_COMMA \ + /*.entryPoint=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.constantCount=*/0 _wgpu_COMMA \ + /*.constants=*/NULL _wgpu_COMMA \ + /*.bufferCount=*/0 _wgpu_COMMA \ + /*.buffers=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_FRAGMENT_STATE_INIT as initializer. + */ +typedef struct WGPUFragmentState { + WGPUChainedStruct * nextInChain; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUShaderModule module; + /** + * This is a \ref NullableInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView entryPoint; + /** + * Array count for `constants`. The `INIT` macro sets this to 0. + */ + size_t constantCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUConstantEntry const * constants; + /** + * Array count for `targets`. The `INIT` macro sets this to 0. + */ + size_t targetCount; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPUColorTargetState const * targets; +} WGPUFragmentState WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPUFragmentState. + */ +#define WGPU_FRAGMENT_STATE_INIT _wgpu_MAKE_INIT_STRUCT(WGPUFragmentState, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.module=*/NULL _wgpu_COMMA \ + /*.entryPoint=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.constantCount=*/0 _wgpu_COMMA \ + /*.constants=*/NULL _wgpu_COMMA \ + /*.targetCount=*/0 _wgpu_COMMA \ + /*.targets=*/NULL _wgpu_COMMA \ +}) + +/** + * Default values can be set using @ref WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT as initializer. + */ +typedef struct WGPURenderPipelineDescriptor { + WGPUChainedStruct * nextInChain; + /** + * This is a \ref NonNullInputString. + * + * The `INIT` macro sets this to @ref WGPU_STRING_VIEW_INIT. + */ + WGPUStringView label; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUPipelineLayout layout; + /** + * The `INIT` macro sets this to @ref WGPU_VERTEX_STATE_INIT. + */ + WGPUVertexState vertex; + /** + * The `INIT` macro sets this to @ref WGPU_PRIMITIVE_STATE_INIT. + */ + WGPUPrimitiveState primitive; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUDepthStencilState const * depthStencil; + /** + * The `INIT` macro sets this to @ref WGPU_MULTISAMPLE_STATE_INIT. + */ + WGPUMultisampleState multisample; + /** + * The `INIT` macro sets this to `NULL`. + */ + WGPU_NULLABLE WGPUFragmentState const * fragment; +} WGPURenderPipelineDescriptor WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Initializer for @ref WGPURenderPipelineDescriptor. + */ +#define WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT _wgpu_MAKE_INIT_STRUCT(WGPURenderPipelineDescriptor, { \ + /*.nextInChain=*/NULL _wgpu_COMMA \ + /*.label=*/WGPU_STRING_VIEW_INIT _wgpu_COMMA \ + /*.layout=*/NULL _wgpu_COMMA \ + /*.vertex=*/WGPU_VERTEX_STATE_INIT _wgpu_COMMA \ + /*.primitive=*/WGPU_PRIMITIVE_STATE_INIT _wgpu_COMMA \ + /*.depthStencil=*/NULL _wgpu_COMMA \ + /*.multisample=*/WGPU_MULTISAMPLE_STATE_INIT _wgpu_COMMA \ + /*.fragment=*/NULL _wgpu_COMMA \ +}) + +/** @} */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(WGPU_SKIP_PROCS) +// Global procs +/** + * Proc pointer type for @ref wgpuCreateInstance: + * > @copydoc wgpuCreateInstance + */ +typedef WGPUInstance (*WGPUProcCreateInstance)(WGPU_NULLABLE WGPUInstanceDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuGetInstanceFeatures: + * > @copydoc wgpuGetInstanceFeatures + */ +typedef void (*WGPUProcGetInstanceFeatures)(WGPUSupportedInstanceFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuGetInstanceLimits: + * > @copydoc wgpuGetInstanceLimits + */ +typedef WGPUStatus (*WGPUProcGetInstanceLimits)(WGPUInstanceLimits * limits) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuHasInstanceFeature: + * > @copydoc wgpuHasInstanceFeature + */ +typedef WGPUBool (*WGPUProcHasInstanceFeature)(WGPUInstanceFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuGetProcAddress: + * > @copydoc wgpuGetProcAddress + */ +typedef WGPUProc (*WGPUProcGetProcAddress)(WGPUStringView procName) WGPU_FUNCTION_ATTRIBUTE; + + +// Procs of Adapter +/** + * Proc pointer type for @ref wgpuAdapterGetFeatures: + * > @copydoc wgpuAdapterGetFeatures + */ +typedef void (*WGPUProcAdapterGetFeatures)(WGPUAdapter adapter, WGPUSupportedFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterGetInfo: + * > @copydoc wgpuAdapterGetInfo + */ +typedef WGPUStatus (*WGPUProcAdapterGetInfo)(WGPUAdapter adapter, WGPUAdapterInfo * info) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterGetLimits: + * > @copydoc wgpuAdapterGetLimits + */ +typedef WGPUStatus (*WGPUProcAdapterGetLimits)(WGPUAdapter adapter, WGPULimits * limits) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterHasFeature: + * > @copydoc wgpuAdapterHasFeature + */ +typedef WGPUBool (*WGPUProcAdapterHasFeature)(WGPUAdapter adapter, WGPUFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterRequestDevice: + * > @copydoc wgpuAdapterRequestDevice + */ +typedef WGPUFuture (*WGPUProcAdapterRequestDevice)(WGPUAdapter adapter, WGPU_NULLABLE WGPUDeviceDescriptor const * descriptor, WGPURequestDeviceCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterAddRef: + * > @copydoc wgpuAdapterAddRef + */ +typedef void (*WGPUProcAdapterAddRef)(WGPUAdapter adapter) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuAdapterRelease: + * > @copydoc wgpuAdapterRelease + */ +typedef void (*WGPUProcAdapterRelease)(WGPUAdapter adapter) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of AdapterInfo +/** + * Proc pointer type for @ref wgpuAdapterInfoFreeMembers: + * > @copydoc wgpuAdapterInfoFreeMembers + */ +typedef void (*WGPUProcAdapterInfoFreeMembers)(WGPUAdapterInfo adapterInfo) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of BindGroup +/** + * Proc pointer type for @ref wgpuBindGroupSetLabel: + * > @copydoc wgpuBindGroupSetLabel + */ +typedef void (*WGPUProcBindGroupSetLabel)(WGPUBindGroup bindGroup, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBindGroupAddRef: + * > @copydoc wgpuBindGroupAddRef + */ +typedef void (*WGPUProcBindGroupAddRef)(WGPUBindGroup bindGroup) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBindGroupRelease: + * > @copydoc wgpuBindGroupRelease + */ +typedef void (*WGPUProcBindGroupRelease)(WGPUBindGroup bindGroup) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of BindGroupLayout +/** + * Proc pointer type for @ref wgpuBindGroupLayoutSetLabel: + * > @copydoc wgpuBindGroupLayoutSetLabel + */ +typedef void (*WGPUProcBindGroupLayoutSetLabel)(WGPUBindGroupLayout bindGroupLayout, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBindGroupLayoutAddRef: + * > @copydoc wgpuBindGroupLayoutAddRef + */ +typedef void (*WGPUProcBindGroupLayoutAddRef)(WGPUBindGroupLayout bindGroupLayout) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBindGroupLayoutRelease: + * > @copydoc wgpuBindGroupLayoutRelease + */ +typedef void (*WGPUProcBindGroupLayoutRelease)(WGPUBindGroupLayout bindGroupLayout) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Buffer +/** + * Proc pointer type for @ref wgpuBufferDestroy: + * > @copydoc wgpuBufferDestroy + */ +typedef void (*WGPUProcBufferDestroy)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferGetConstMappedRange: + * > @copydoc wgpuBufferGetConstMappedRange + */ +typedef void const * (*WGPUProcBufferGetConstMappedRange)(WGPUBuffer buffer, size_t offset, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferGetMappedRange: + * > @copydoc wgpuBufferGetMappedRange + */ +typedef void * (*WGPUProcBufferGetMappedRange)(WGPUBuffer buffer, size_t offset, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferGetMapState: + * > @copydoc wgpuBufferGetMapState + */ +typedef WGPUBufferMapState (*WGPUProcBufferGetMapState)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferGetSize: + * > @copydoc wgpuBufferGetSize + */ +typedef uint64_t (*WGPUProcBufferGetSize)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferGetUsage: + * > @copydoc wgpuBufferGetUsage + */ +typedef WGPUBufferUsage (*WGPUProcBufferGetUsage)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferMapAsync: + * > @copydoc wgpuBufferMapAsync + */ +typedef WGPUFuture (*WGPUProcBufferMapAsync)(WGPUBuffer buffer, WGPUMapMode mode, size_t offset, size_t size, WGPUBufferMapCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferReadMappedRange: + * > @copydoc wgpuBufferReadMappedRange + */ +typedef WGPUStatus (*WGPUProcBufferReadMappedRange)(WGPUBuffer buffer, size_t offset, void * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferSetLabel: + * > @copydoc wgpuBufferSetLabel + */ +typedef void (*WGPUProcBufferSetLabel)(WGPUBuffer buffer, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferUnmap: + * > @copydoc wgpuBufferUnmap + */ +typedef void (*WGPUProcBufferUnmap)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferWriteMappedRange: + * > @copydoc wgpuBufferWriteMappedRange + */ +typedef WGPUStatus (*WGPUProcBufferWriteMappedRange)(WGPUBuffer buffer, size_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferAddRef: + * > @copydoc wgpuBufferAddRef + */ +typedef void (*WGPUProcBufferAddRef)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuBufferRelease: + * > @copydoc wgpuBufferRelease + */ +typedef void (*WGPUProcBufferRelease)(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of CommandBuffer +/** + * Proc pointer type for @ref wgpuCommandBufferSetLabel: + * > @copydoc wgpuCommandBufferSetLabel + */ +typedef void (*WGPUProcCommandBufferSetLabel)(WGPUCommandBuffer commandBuffer, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandBufferAddRef: + * > @copydoc wgpuCommandBufferAddRef + */ +typedef void (*WGPUProcCommandBufferAddRef)(WGPUCommandBuffer commandBuffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandBufferRelease: + * > @copydoc wgpuCommandBufferRelease + */ +typedef void (*WGPUProcCommandBufferRelease)(WGPUCommandBuffer commandBuffer) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of CommandEncoder +/** + * Proc pointer type for @ref wgpuCommandEncoderBeginComputePass: + * > @copydoc wgpuCommandEncoderBeginComputePass + */ +typedef WGPUComputePassEncoder (*WGPUProcCommandEncoderBeginComputePass)(WGPUCommandEncoder commandEncoder, WGPU_NULLABLE WGPUComputePassDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderBeginRenderPass: + * > @copydoc wgpuCommandEncoderBeginRenderPass + */ +typedef WGPURenderPassEncoder (*WGPUProcCommandEncoderBeginRenderPass)(WGPUCommandEncoder commandEncoder, WGPURenderPassDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderClearBuffer: + * > @copydoc wgpuCommandEncoderClearBuffer + */ +typedef void (*WGPUProcCommandEncoderClearBuffer)(WGPUCommandEncoder commandEncoder, WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderCopyBufferToBuffer: + * > @copydoc wgpuCommandEncoderCopyBufferToBuffer + */ +typedef void (*WGPUProcCommandEncoderCopyBufferToBuffer)(WGPUCommandEncoder commandEncoder, WGPUBuffer source, uint64_t sourceOffset, WGPUBuffer destination, uint64_t destinationOffset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderCopyBufferToTexture: + * > @copydoc wgpuCommandEncoderCopyBufferToTexture + */ +typedef void (*WGPUProcCommandEncoderCopyBufferToTexture)(WGPUCommandEncoder commandEncoder, WGPUTexelCopyBufferInfo const * source, WGPUTexelCopyTextureInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderCopyTextureToBuffer: + * > @copydoc wgpuCommandEncoderCopyTextureToBuffer + */ +typedef void (*WGPUProcCommandEncoderCopyTextureToBuffer)(WGPUCommandEncoder commandEncoder, WGPUTexelCopyTextureInfo const * source, WGPUTexelCopyBufferInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderCopyTextureToTexture: + * > @copydoc wgpuCommandEncoderCopyTextureToTexture + */ +typedef void (*WGPUProcCommandEncoderCopyTextureToTexture)(WGPUCommandEncoder commandEncoder, WGPUTexelCopyTextureInfo const * source, WGPUTexelCopyTextureInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderFinish: + * > @copydoc wgpuCommandEncoderFinish + */ +typedef WGPUCommandBuffer (*WGPUProcCommandEncoderFinish)(WGPUCommandEncoder commandEncoder, WGPU_NULLABLE WGPUCommandBufferDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderInsertDebugMarker: + * > @copydoc wgpuCommandEncoderInsertDebugMarker + */ +typedef void (*WGPUProcCommandEncoderInsertDebugMarker)(WGPUCommandEncoder commandEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderPopDebugGroup: + * > @copydoc wgpuCommandEncoderPopDebugGroup + */ +typedef void (*WGPUProcCommandEncoderPopDebugGroup)(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderPushDebugGroup: + * > @copydoc wgpuCommandEncoderPushDebugGroup + */ +typedef void (*WGPUProcCommandEncoderPushDebugGroup)(WGPUCommandEncoder commandEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderResolveQuerySet: + * > @copydoc wgpuCommandEncoderResolveQuerySet + */ +typedef void (*WGPUProcCommandEncoderResolveQuerySet)(WGPUCommandEncoder commandEncoder, WGPUQuerySet querySet, uint32_t firstQuery, uint32_t queryCount, WGPUBuffer destination, uint64_t destinationOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderSetLabel: + * > @copydoc wgpuCommandEncoderSetLabel + */ +typedef void (*WGPUProcCommandEncoderSetLabel)(WGPUCommandEncoder commandEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderWriteTimestamp: + * > @copydoc wgpuCommandEncoderWriteTimestamp + */ +typedef void (*WGPUProcCommandEncoderWriteTimestamp)(WGPUCommandEncoder commandEncoder, WGPUQuerySet querySet, uint32_t queryIndex) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderAddRef: + * > @copydoc wgpuCommandEncoderAddRef + */ +typedef void (*WGPUProcCommandEncoderAddRef)(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuCommandEncoderRelease: + * > @copydoc wgpuCommandEncoderRelease + */ +typedef void (*WGPUProcCommandEncoderRelease)(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of ComputePassEncoder +/** + * Proc pointer type for @ref wgpuComputePassEncoderDispatchWorkgroups: + * > @copydoc wgpuComputePassEncoderDispatchWorkgroups + */ +typedef void (*WGPUProcComputePassEncoderDispatchWorkgroups)(WGPUComputePassEncoder computePassEncoder, uint32_t workgroupCountX, uint32_t workgroupCountY, uint32_t workgroupCountZ) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderDispatchWorkgroupsIndirect: + * > @copydoc wgpuComputePassEncoderDispatchWorkgroupsIndirect + */ +typedef void (*WGPUProcComputePassEncoderDispatchWorkgroupsIndirect)(WGPUComputePassEncoder computePassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderEnd: + * > @copydoc wgpuComputePassEncoderEnd + */ +typedef void (*WGPUProcComputePassEncoderEnd)(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderInsertDebugMarker: + * > @copydoc wgpuComputePassEncoderInsertDebugMarker + */ +typedef void (*WGPUProcComputePassEncoderInsertDebugMarker)(WGPUComputePassEncoder computePassEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderPopDebugGroup: + * > @copydoc wgpuComputePassEncoderPopDebugGroup + */ +typedef void (*WGPUProcComputePassEncoderPopDebugGroup)(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderPushDebugGroup: + * > @copydoc wgpuComputePassEncoderPushDebugGroup + */ +typedef void (*WGPUProcComputePassEncoderPushDebugGroup)(WGPUComputePassEncoder computePassEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderSetBindGroup: + * > @copydoc wgpuComputePassEncoderSetBindGroup + */ +typedef void (*WGPUProcComputePassEncoderSetBindGroup)(WGPUComputePassEncoder computePassEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderSetImmediates: + * > @copydoc wgpuComputePassEncoderSetImmediates + */ +typedef void (*WGPUProcComputePassEncoderSetImmediates)(WGPUComputePassEncoder computePassEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderSetLabel: + * > @copydoc wgpuComputePassEncoderSetLabel + */ +typedef void (*WGPUProcComputePassEncoderSetLabel)(WGPUComputePassEncoder computePassEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderSetPipeline: + * > @copydoc wgpuComputePassEncoderSetPipeline + */ +typedef void (*WGPUProcComputePassEncoderSetPipeline)(WGPUComputePassEncoder computePassEncoder, WGPUComputePipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderAddRef: + * > @copydoc wgpuComputePassEncoderAddRef + */ +typedef void (*WGPUProcComputePassEncoderAddRef)(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePassEncoderRelease: + * > @copydoc wgpuComputePassEncoderRelease + */ +typedef void (*WGPUProcComputePassEncoderRelease)(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of ComputePipeline +/** + * Proc pointer type for @ref wgpuComputePipelineGetBindGroupLayout: + * > @copydoc wgpuComputePipelineGetBindGroupLayout + */ +typedef WGPUBindGroupLayout (*WGPUProcComputePipelineGetBindGroupLayout)(WGPUComputePipeline computePipeline, uint32_t groupIndex) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePipelineSetLabel: + * > @copydoc wgpuComputePipelineSetLabel + */ +typedef void (*WGPUProcComputePipelineSetLabel)(WGPUComputePipeline computePipeline, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePipelineAddRef: + * > @copydoc wgpuComputePipelineAddRef + */ +typedef void (*WGPUProcComputePipelineAddRef)(WGPUComputePipeline computePipeline) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuComputePipelineRelease: + * > @copydoc wgpuComputePipelineRelease + */ +typedef void (*WGPUProcComputePipelineRelease)(WGPUComputePipeline computePipeline) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Device +/** + * Proc pointer type for @ref wgpuDeviceCreateBindGroup: + * > @copydoc wgpuDeviceCreateBindGroup + */ +typedef WGPUBindGroup (*WGPUProcDeviceCreateBindGroup)(WGPUDevice device, WGPUBindGroupDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateBindGroupLayout: + * > @copydoc wgpuDeviceCreateBindGroupLayout + */ +typedef WGPUBindGroupLayout (*WGPUProcDeviceCreateBindGroupLayout)(WGPUDevice device, WGPUBindGroupLayoutDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateBuffer: + * > @copydoc wgpuDeviceCreateBuffer + */ +typedef WGPU_NULLABLE WGPUBuffer (*WGPUProcDeviceCreateBuffer)(WGPUDevice device, WGPUBufferDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateCommandEncoder: + * > @copydoc wgpuDeviceCreateCommandEncoder + */ +typedef WGPUCommandEncoder (*WGPUProcDeviceCreateCommandEncoder)(WGPUDevice device, WGPU_NULLABLE WGPUCommandEncoderDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateComputePipeline: + * > @copydoc wgpuDeviceCreateComputePipeline + */ +typedef WGPUComputePipeline (*WGPUProcDeviceCreateComputePipeline)(WGPUDevice device, WGPUComputePipelineDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateComputePipelineAsync: + * > @copydoc wgpuDeviceCreateComputePipelineAsync + */ +typedef WGPUFuture (*WGPUProcDeviceCreateComputePipelineAsync)(WGPUDevice device, WGPUComputePipelineDescriptor const * descriptor, WGPUCreateComputePipelineAsyncCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreatePipelineLayout: + * > @copydoc wgpuDeviceCreatePipelineLayout + */ +typedef WGPUPipelineLayout (*WGPUProcDeviceCreatePipelineLayout)(WGPUDevice device, WGPUPipelineLayoutDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateQuerySet: + * > @copydoc wgpuDeviceCreateQuerySet + */ +typedef WGPUQuerySet (*WGPUProcDeviceCreateQuerySet)(WGPUDevice device, WGPUQuerySetDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateRenderBundleEncoder: + * > @copydoc wgpuDeviceCreateRenderBundleEncoder + */ +typedef WGPURenderBundleEncoder (*WGPUProcDeviceCreateRenderBundleEncoder)(WGPUDevice device, WGPURenderBundleEncoderDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateRenderPipeline: + * > @copydoc wgpuDeviceCreateRenderPipeline + */ +typedef WGPURenderPipeline (*WGPUProcDeviceCreateRenderPipeline)(WGPUDevice device, WGPURenderPipelineDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateRenderPipelineAsync: + * > @copydoc wgpuDeviceCreateRenderPipelineAsync + */ +typedef WGPUFuture (*WGPUProcDeviceCreateRenderPipelineAsync)(WGPUDevice device, WGPURenderPipelineDescriptor const * descriptor, WGPUCreateRenderPipelineAsyncCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateSampler: + * > @copydoc wgpuDeviceCreateSampler + */ +typedef WGPUSampler (*WGPUProcDeviceCreateSampler)(WGPUDevice device, WGPU_NULLABLE WGPUSamplerDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateShaderModule: + * > @copydoc wgpuDeviceCreateShaderModule + */ +typedef WGPUShaderModule (*WGPUProcDeviceCreateShaderModule)(WGPUDevice device, WGPUShaderModuleDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceCreateTexture: + * > @copydoc wgpuDeviceCreateTexture + */ +typedef WGPUTexture (*WGPUProcDeviceCreateTexture)(WGPUDevice device, WGPUTextureDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceDestroy: + * > @copydoc wgpuDeviceDestroy + */ +typedef void (*WGPUProcDeviceDestroy)(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceGetAdapterInfo: + * > @copydoc wgpuDeviceGetAdapterInfo + */ +typedef WGPUStatus (*WGPUProcDeviceGetAdapterInfo)(WGPUDevice device, WGPUAdapterInfo * adapterInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceGetFeatures: + * > @copydoc wgpuDeviceGetFeatures + */ +typedef void (*WGPUProcDeviceGetFeatures)(WGPUDevice device, WGPUSupportedFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceGetLimits: + * > @copydoc wgpuDeviceGetLimits + */ +typedef WGPUStatus (*WGPUProcDeviceGetLimits)(WGPUDevice device, WGPULimits * limits) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceGetLostFuture: + * > @copydoc wgpuDeviceGetLostFuture + */ +typedef WGPUFuture (*WGPUProcDeviceGetLostFuture)(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceGetQueue: + * > @copydoc wgpuDeviceGetQueue + */ +typedef WGPUQueue (*WGPUProcDeviceGetQueue)(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceHasFeature: + * > @copydoc wgpuDeviceHasFeature + */ +typedef WGPUBool (*WGPUProcDeviceHasFeature)(WGPUDevice device, WGPUFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDevicePopErrorScope: + * > @copydoc wgpuDevicePopErrorScope + */ +typedef WGPUFuture (*WGPUProcDevicePopErrorScope)(WGPUDevice device, WGPUPopErrorScopeCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDevicePushErrorScope: + * > @copydoc wgpuDevicePushErrorScope + */ +typedef void (*WGPUProcDevicePushErrorScope)(WGPUDevice device, WGPUErrorFilter filter) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceSetLabel: + * > @copydoc wgpuDeviceSetLabel + */ +typedef void (*WGPUProcDeviceSetLabel)(WGPUDevice device, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceAddRef: + * > @copydoc wgpuDeviceAddRef + */ +typedef void (*WGPUProcDeviceAddRef)(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuDeviceRelease: + * > @copydoc wgpuDeviceRelease + */ +typedef void (*WGPUProcDeviceRelease)(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of ExternalTexture +/** + * Proc pointer type for @ref wgpuExternalTextureSetLabel: + * > @copydoc wgpuExternalTextureSetLabel + */ +typedef void (*WGPUProcExternalTextureSetLabel)(WGPUExternalTexture externalTexture, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuExternalTextureAddRef: + * > @copydoc wgpuExternalTextureAddRef + */ +typedef void (*WGPUProcExternalTextureAddRef)(WGPUExternalTexture externalTexture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuExternalTextureRelease: + * > @copydoc wgpuExternalTextureRelease + */ +typedef void (*WGPUProcExternalTextureRelease)(WGPUExternalTexture externalTexture) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Instance +/** + * Proc pointer type for @ref wgpuInstanceCreateSurface: + * > @copydoc wgpuInstanceCreateSurface + */ +typedef WGPUSurface (*WGPUProcInstanceCreateSurface)(WGPUInstance instance, WGPUSurfaceDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceGetWGSLLanguageFeatures: + * > @copydoc wgpuInstanceGetWGSLLanguageFeatures + */ +typedef void (*WGPUProcInstanceGetWGSLLanguageFeatures)(WGPUInstance instance, WGPUSupportedWGSLLanguageFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceHasWGSLLanguageFeature: + * > @copydoc wgpuInstanceHasWGSLLanguageFeature + */ +typedef WGPUBool (*WGPUProcInstanceHasWGSLLanguageFeature)(WGPUInstance instance, WGPUWGSLLanguageFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceProcessEvents: + * > @copydoc wgpuInstanceProcessEvents + */ +typedef void (*WGPUProcInstanceProcessEvents)(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceRequestAdapter: + * > @copydoc wgpuInstanceRequestAdapter + */ +typedef WGPUFuture (*WGPUProcInstanceRequestAdapter)(WGPUInstance instance, WGPU_NULLABLE WGPURequestAdapterOptions const * options, WGPURequestAdapterCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceWaitAny: + * > @copydoc wgpuInstanceWaitAny + */ +typedef WGPUWaitStatus (*WGPUProcInstanceWaitAny)(WGPUInstance instance, size_t futureCount, WGPU_NULLABLE WGPUFutureWaitInfo * futures, uint64_t timeoutNS) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceAddRef: + * > @copydoc wgpuInstanceAddRef + */ +typedef void (*WGPUProcInstanceAddRef)(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuInstanceRelease: + * > @copydoc wgpuInstanceRelease + */ +typedef void (*WGPUProcInstanceRelease)(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of PipelineLayout +/** + * Proc pointer type for @ref wgpuPipelineLayoutSetLabel: + * > @copydoc wgpuPipelineLayoutSetLabel + */ +typedef void (*WGPUProcPipelineLayoutSetLabel)(WGPUPipelineLayout pipelineLayout, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuPipelineLayoutAddRef: + * > @copydoc wgpuPipelineLayoutAddRef + */ +typedef void (*WGPUProcPipelineLayoutAddRef)(WGPUPipelineLayout pipelineLayout) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuPipelineLayoutRelease: + * > @copydoc wgpuPipelineLayoutRelease + */ +typedef void (*WGPUProcPipelineLayoutRelease)(WGPUPipelineLayout pipelineLayout) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of QuerySet +/** + * Proc pointer type for @ref wgpuQuerySetDestroy: + * > @copydoc wgpuQuerySetDestroy + */ +typedef void (*WGPUProcQuerySetDestroy)(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQuerySetGetCount: + * > @copydoc wgpuQuerySetGetCount + */ +typedef uint32_t (*WGPUProcQuerySetGetCount)(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQuerySetGetType: + * > @copydoc wgpuQuerySetGetType + */ +typedef WGPUQueryType (*WGPUProcQuerySetGetType)(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQuerySetSetLabel: + * > @copydoc wgpuQuerySetSetLabel + */ +typedef void (*WGPUProcQuerySetSetLabel)(WGPUQuerySet querySet, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQuerySetAddRef: + * > @copydoc wgpuQuerySetAddRef + */ +typedef void (*WGPUProcQuerySetAddRef)(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQuerySetRelease: + * > @copydoc wgpuQuerySetRelease + */ +typedef void (*WGPUProcQuerySetRelease)(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Queue +/** + * Proc pointer type for @ref wgpuQueueOnSubmittedWorkDone: + * > @copydoc wgpuQueueOnSubmittedWorkDone + */ +typedef WGPUFuture (*WGPUProcQueueOnSubmittedWorkDone)(WGPUQueue queue, WGPUQueueWorkDoneCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueSetLabel: + * > @copydoc wgpuQueueSetLabel + */ +typedef void (*WGPUProcQueueSetLabel)(WGPUQueue queue, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueSubmit: + * > @copydoc wgpuQueueSubmit + */ +typedef void (*WGPUProcQueueSubmit)(WGPUQueue queue, size_t commandCount, WGPUCommandBuffer const * commands) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueWriteBuffer: + * > @copydoc wgpuQueueWriteBuffer + */ +typedef void (*WGPUProcQueueWriteBuffer)(WGPUQueue queue, WGPUBuffer buffer, uint64_t bufferOffset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueWriteTexture: + * > @copydoc wgpuQueueWriteTexture + */ +typedef void (*WGPUProcQueueWriteTexture)(WGPUQueue queue, WGPUTexelCopyTextureInfo const * destination, void const * data, size_t dataSize, WGPUTexelCopyBufferLayout const * dataLayout, WGPUExtent3D const * writeSize) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueAddRef: + * > @copydoc wgpuQueueAddRef + */ +typedef void (*WGPUProcQueueAddRef)(WGPUQueue queue) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuQueueRelease: + * > @copydoc wgpuQueueRelease + */ +typedef void (*WGPUProcQueueRelease)(WGPUQueue queue) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of RenderBundle +/** + * Proc pointer type for @ref wgpuRenderBundleSetLabel: + * > @copydoc wgpuRenderBundleSetLabel + */ +typedef void (*WGPUProcRenderBundleSetLabel)(WGPURenderBundle renderBundle, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleAddRef: + * > @copydoc wgpuRenderBundleAddRef + */ +typedef void (*WGPUProcRenderBundleAddRef)(WGPURenderBundle renderBundle) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleRelease: + * > @copydoc wgpuRenderBundleRelease + */ +typedef void (*WGPUProcRenderBundleRelease)(WGPURenderBundle renderBundle) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of RenderBundleEncoder +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderDraw: + * > @copydoc wgpuRenderBundleEncoderDraw + */ +typedef void (*WGPUProcRenderBundleEncoderDraw)(WGPURenderBundleEncoder renderBundleEncoder, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderDrawIndexed: + * > @copydoc wgpuRenderBundleEncoderDrawIndexed + */ +typedef void (*WGPUProcRenderBundleEncoderDrawIndexed)(WGPURenderBundleEncoder renderBundleEncoder, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t baseVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderDrawIndexedIndirect: + * > @copydoc wgpuRenderBundleEncoderDrawIndexedIndirect + */ +typedef void (*WGPUProcRenderBundleEncoderDrawIndexedIndirect)(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderDrawIndirect: + * > @copydoc wgpuRenderBundleEncoderDrawIndirect + */ +typedef void (*WGPUProcRenderBundleEncoderDrawIndirect)(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderFinish: + * > @copydoc wgpuRenderBundleEncoderFinish + */ +typedef WGPURenderBundle (*WGPUProcRenderBundleEncoderFinish)(WGPURenderBundleEncoder renderBundleEncoder, WGPU_NULLABLE WGPURenderBundleDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderInsertDebugMarker: + * > @copydoc wgpuRenderBundleEncoderInsertDebugMarker + */ +typedef void (*WGPUProcRenderBundleEncoderInsertDebugMarker)(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderPopDebugGroup: + * > @copydoc wgpuRenderBundleEncoderPopDebugGroup + */ +typedef void (*WGPUProcRenderBundleEncoderPopDebugGroup)(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderPushDebugGroup: + * > @copydoc wgpuRenderBundleEncoderPushDebugGroup + */ +typedef void (*WGPUProcRenderBundleEncoderPushDebugGroup)(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetBindGroup: + * > @copydoc wgpuRenderBundleEncoderSetBindGroup + */ +typedef void (*WGPUProcRenderBundleEncoderSetBindGroup)(WGPURenderBundleEncoder renderBundleEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetImmediates: + * > @copydoc wgpuRenderBundleEncoderSetImmediates + */ +typedef void (*WGPUProcRenderBundleEncoderSetImmediates)(WGPURenderBundleEncoder renderBundleEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetIndexBuffer: + * > @copydoc wgpuRenderBundleEncoderSetIndexBuffer + */ +typedef void (*WGPUProcRenderBundleEncoderSetIndexBuffer)(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer buffer, WGPUIndexFormat format, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetLabel: + * > @copydoc wgpuRenderBundleEncoderSetLabel + */ +typedef void (*WGPUProcRenderBundleEncoderSetLabel)(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetPipeline: + * > @copydoc wgpuRenderBundleEncoderSetPipeline + */ +typedef void (*WGPUProcRenderBundleEncoderSetPipeline)(WGPURenderBundleEncoder renderBundleEncoder, WGPURenderPipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderSetVertexBuffer: + * > @copydoc wgpuRenderBundleEncoderSetVertexBuffer + */ +typedef void (*WGPUProcRenderBundleEncoderSetVertexBuffer)(WGPURenderBundleEncoder renderBundleEncoder, uint32_t slot, WGPU_NULLABLE WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderAddRef: + * > @copydoc wgpuRenderBundleEncoderAddRef + */ +typedef void (*WGPUProcRenderBundleEncoderAddRef)(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderBundleEncoderRelease: + * > @copydoc wgpuRenderBundleEncoderRelease + */ +typedef void (*WGPUProcRenderBundleEncoderRelease)(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of RenderPassEncoder +/** + * Proc pointer type for @ref wgpuRenderPassEncoderBeginOcclusionQuery: + * > @copydoc wgpuRenderPassEncoderBeginOcclusionQuery + */ +typedef void (*WGPUProcRenderPassEncoderBeginOcclusionQuery)(WGPURenderPassEncoder renderPassEncoder, uint32_t queryIndex) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderDraw: + * > @copydoc wgpuRenderPassEncoderDraw + */ +typedef void (*WGPUProcRenderPassEncoderDraw)(WGPURenderPassEncoder renderPassEncoder, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderDrawIndexed: + * > @copydoc wgpuRenderPassEncoderDrawIndexed + */ +typedef void (*WGPUProcRenderPassEncoderDrawIndexed)(WGPURenderPassEncoder renderPassEncoder, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t baseVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderDrawIndexedIndirect: + * > @copydoc wgpuRenderPassEncoderDrawIndexedIndirect + */ +typedef void (*WGPUProcRenderPassEncoderDrawIndexedIndirect)(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderDrawIndirect: + * > @copydoc wgpuRenderPassEncoderDrawIndirect + */ +typedef void (*WGPUProcRenderPassEncoderDrawIndirect)(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderEnd: + * > @copydoc wgpuRenderPassEncoderEnd + */ +typedef void (*WGPUProcRenderPassEncoderEnd)(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderEndOcclusionQuery: + * > @copydoc wgpuRenderPassEncoderEndOcclusionQuery + */ +typedef void (*WGPUProcRenderPassEncoderEndOcclusionQuery)(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderExecuteBundles: + * > @copydoc wgpuRenderPassEncoderExecuteBundles + */ +typedef void (*WGPUProcRenderPassEncoderExecuteBundles)(WGPURenderPassEncoder renderPassEncoder, size_t bundleCount, WGPURenderBundle const * bundles) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderInsertDebugMarker: + * > @copydoc wgpuRenderPassEncoderInsertDebugMarker + */ +typedef void (*WGPUProcRenderPassEncoderInsertDebugMarker)(WGPURenderPassEncoder renderPassEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderPopDebugGroup: + * > @copydoc wgpuRenderPassEncoderPopDebugGroup + */ +typedef void (*WGPUProcRenderPassEncoderPopDebugGroup)(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderPushDebugGroup: + * > @copydoc wgpuRenderPassEncoderPushDebugGroup + */ +typedef void (*WGPUProcRenderPassEncoderPushDebugGroup)(WGPURenderPassEncoder renderPassEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetBindGroup: + * > @copydoc wgpuRenderPassEncoderSetBindGroup + */ +typedef void (*WGPUProcRenderPassEncoderSetBindGroup)(WGPURenderPassEncoder renderPassEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetBlendConstant: + * > @copydoc wgpuRenderPassEncoderSetBlendConstant + */ +typedef void (*WGPUProcRenderPassEncoderSetBlendConstant)(WGPURenderPassEncoder renderPassEncoder, WGPUColor const * color) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetImmediates: + * > @copydoc wgpuRenderPassEncoderSetImmediates + */ +typedef void (*WGPUProcRenderPassEncoderSetImmediates)(WGPURenderPassEncoder renderPassEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetIndexBuffer: + * > @copydoc wgpuRenderPassEncoderSetIndexBuffer + */ +typedef void (*WGPUProcRenderPassEncoderSetIndexBuffer)(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer buffer, WGPUIndexFormat format, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetLabel: + * > @copydoc wgpuRenderPassEncoderSetLabel + */ +typedef void (*WGPUProcRenderPassEncoderSetLabel)(WGPURenderPassEncoder renderPassEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetPipeline: + * > @copydoc wgpuRenderPassEncoderSetPipeline + */ +typedef void (*WGPUProcRenderPassEncoderSetPipeline)(WGPURenderPassEncoder renderPassEncoder, WGPURenderPipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetScissorRect: + * > @copydoc wgpuRenderPassEncoderSetScissorRect + */ +typedef void (*WGPUProcRenderPassEncoderSetScissorRect)(WGPURenderPassEncoder renderPassEncoder, uint32_t x, uint32_t y, uint32_t width, uint32_t height) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetStencilReference: + * > @copydoc wgpuRenderPassEncoderSetStencilReference + */ +typedef void (*WGPUProcRenderPassEncoderSetStencilReference)(WGPURenderPassEncoder renderPassEncoder, uint32_t reference) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetVertexBuffer: + * > @copydoc wgpuRenderPassEncoderSetVertexBuffer + */ +typedef void (*WGPUProcRenderPassEncoderSetVertexBuffer)(WGPURenderPassEncoder renderPassEncoder, uint32_t slot, WGPU_NULLABLE WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderSetViewport: + * > @copydoc wgpuRenderPassEncoderSetViewport + */ +typedef void (*WGPUProcRenderPassEncoderSetViewport)(WGPURenderPassEncoder renderPassEncoder, float x, float y, float width, float height, float minDepth, float maxDepth) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderAddRef: + * > @copydoc wgpuRenderPassEncoderAddRef + */ +typedef void (*WGPUProcRenderPassEncoderAddRef)(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPassEncoderRelease: + * > @copydoc wgpuRenderPassEncoderRelease + */ +typedef void (*WGPUProcRenderPassEncoderRelease)(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of RenderPipeline +/** + * Proc pointer type for @ref wgpuRenderPipelineGetBindGroupLayout: + * > @copydoc wgpuRenderPipelineGetBindGroupLayout + */ +typedef WGPUBindGroupLayout (*WGPUProcRenderPipelineGetBindGroupLayout)(WGPURenderPipeline renderPipeline, uint32_t groupIndex) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPipelineSetLabel: + * > @copydoc wgpuRenderPipelineSetLabel + */ +typedef void (*WGPUProcRenderPipelineSetLabel)(WGPURenderPipeline renderPipeline, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPipelineAddRef: + * > @copydoc wgpuRenderPipelineAddRef + */ +typedef void (*WGPUProcRenderPipelineAddRef)(WGPURenderPipeline renderPipeline) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuRenderPipelineRelease: + * > @copydoc wgpuRenderPipelineRelease + */ +typedef void (*WGPUProcRenderPipelineRelease)(WGPURenderPipeline renderPipeline) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Sampler +/** + * Proc pointer type for @ref wgpuSamplerSetLabel: + * > @copydoc wgpuSamplerSetLabel + */ +typedef void (*WGPUProcSamplerSetLabel)(WGPUSampler sampler, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSamplerAddRef: + * > @copydoc wgpuSamplerAddRef + */ +typedef void (*WGPUProcSamplerAddRef)(WGPUSampler sampler) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSamplerRelease: + * > @copydoc wgpuSamplerRelease + */ +typedef void (*WGPUProcSamplerRelease)(WGPUSampler sampler) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of ShaderModule +/** + * Proc pointer type for @ref wgpuShaderModuleGetCompilationInfo: + * > @copydoc wgpuShaderModuleGetCompilationInfo + */ +typedef WGPUFuture (*WGPUProcShaderModuleGetCompilationInfo)(WGPUShaderModule shaderModule, WGPUCompilationInfoCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuShaderModuleSetLabel: + * > @copydoc wgpuShaderModuleSetLabel + */ +typedef void (*WGPUProcShaderModuleSetLabel)(WGPUShaderModule shaderModule, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuShaderModuleAddRef: + * > @copydoc wgpuShaderModuleAddRef + */ +typedef void (*WGPUProcShaderModuleAddRef)(WGPUShaderModule shaderModule) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuShaderModuleRelease: + * > @copydoc wgpuShaderModuleRelease + */ +typedef void (*WGPUProcShaderModuleRelease)(WGPUShaderModule shaderModule) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of SupportedFeatures +/** + * Proc pointer type for @ref wgpuSupportedFeaturesFreeMembers: + * > @copydoc wgpuSupportedFeaturesFreeMembers + */ +typedef void (*WGPUProcSupportedFeaturesFreeMembers)(WGPUSupportedFeatures supportedFeatures) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of SupportedInstanceFeatures +/** + * Proc pointer type for @ref wgpuSupportedInstanceFeaturesFreeMembers: + * > @copydoc wgpuSupportedInstanceFeaturesFreeMembers + */ +typedef void (*WGPUProcSupportedInstanceFeaturesFreeMembers)(WGPUSupportedInstanceFeatures supportedInstanceFeatures) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of SupportedWGSLLanguageFeatures +/** + * Proc pointer type for @ref wgpuSupportedWGSLLanguageFeaturesFreeMembers: + * > @copydoc wgpuSupportedWGSLLanguageFeaturesFreeMembers + */ +typedef void (*WGPUProcSupportedWGSLLanguageFeaturesFreeMembers)(WGPUSupportedWGSLLanguageFeatures supportedWGSLLanguageFeatures) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Surface +/** + * Proc pointer type for @ref wgpuSurfaceConfigure: + * > @copydoc wgpuSurfaceConfigure + */ +typedef void (*WGPUProcSurfaceConfigure)(WGPUSurface surface, WGPUSurfaceConfiguration const * config) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceGetCapabilities: + * > @copydoc wgpuSurfaceGetCapabilities + */ +typedef WGPUStatus (*WGPUProcSurfaceGetCapabilities)(WGPUSurface surface, WGPUAdapter adapter, WGPUSurfaceCapabilities * capabilities) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceGetCurrentTexture: + * > @copydoc wgpuSurfaceGetCurrentTexture + */ +typedef void (*WGPUProcSurfaceGetCurrentTexture)(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfacePresent: + * > @copydoc wgpuSurfacePresent + */ +typedef WGPUStatus (*WGPUProcSurfacePresent)(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceSetLabel: + * > @copydoc wgpuSurfaceSetLabel + */ +typedef void (*WGPUProcSurfaceSetLabel)(WGPUSurface surface, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceUnconfigure: + * > @copydoc wgpuSurfaceUnconfigure + */ +typedef void (*WGPUProcSurfaceUnconfigure)(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceAddRef: + * > @copydoc wgpuSurfaceAddRef + */ +typedef void (*WGPUProcSurfaceAddRef)(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuSurfaceRelease: + * > @copydoc wgpuSurfaceRelease + */ +typedef void (*WGPUProcSurfaceRelease)(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of SurfaceCapabilities +/** + * Proc pointer type for @ref wgpuSurfaceCapabilitiesFreeMembers: + * > @copydoc wgpuSurfaceCapabilitiesFreeMembers + */ +typedef void (*WGPUProcSurfaceCapabilitiesFreeMembers)(WGPUSurfaceCapabilities surfaceCapabilities) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of Texture +/** + * Proc pointer type for @ref wgpuTextureCreateView: + * > @copydoc wgpuTextureCreateView + */ +typedef WGPUTextureView (*WGPUProcTextureCreateView)(WGPUTexture texture, WGPU_NULLABLE WGPUTextureViewDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureDestroy: + * > @copydoc wgpuTextureDestroy + */ +typedef void (*WGPUProcTextureDestroy)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetDepthOrArrayLayers: + * > @copydoc wgpuTextureGetDepthOrArrayLayers + */ +typedef uint32_t (*WGPUProcTextureGetDepthOrArrayLayers)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetDimension: + * > @copydoc wgpuTextureGetDimension + */ +typedef WGPUTextureDimension (*WGPUProcTextureGetDimension)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetFormat: + * > @copydoc wgpuTextureGetFormat + */ +typedef WGPUTextureFormat (*WGPUProcTextureGetFormat)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetHeight: + * > @copydoc wgpuTextureGetHeight + */ +typedef uint32_t (*WGPUProcTextureGetHeight)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetMipLevelCount: + * > @copydoc wgpuTextureGetMipLevelCount + */ +typedef uint32_t (*WGPUProcTextureGetMipLevelCount)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetSampleCount: + * > @copydoc wgpuTextureGetSampleCount + */ +typedef uint32_t (*WGPUProcTextureGetSampleCount)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetTextureBindingViewDimension: + * > @copydoc wgpuTextureGetTextureBindingViewDimension + */ +typedef WGPUTextureViewDimension (*WGPUProcTextureGetTextureBindingViewDimension)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetUsage: + * > @copydoc wgpuTextureGetUsage + */ +typedef WGPUTextureUsage (*WGPUProcTextureGetUsage)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureGetWidth: + * > @copydoc wgpuTextureGetWidth + */ +typedef uint32_t (*WGPUProcTextureGetWidth)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureSetLabel: + * > @copydoc wgpuTextureSetLabel + */ +typedef void (*WGPUProcTextureSetLabel)(WGPUTexture texture, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureAddRef: + * > @copydoc wgpuTextureAddRef + */ +typedef void (*WGPUProcTextureAddRef)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureRelease: + * > @copydoc wgpuTextureRelease + */ +typedef void (*WGPUProcTextureRelease)(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; + +// Procs of TextureView +/** + * Proc pointer type for @ref wgpuTextureViewSetLabel: + * > @copydoc wgpuTextureViewSetLabel + */ +typedef void (*WGPUProcTextureViewSetLabel)(WGPUTextureView textureView, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureViewAddRef: + * > @copydoc wgpuTextureViewAddRef + */ +typedef void (*WGPUProcTextureViewAddRef)(WGPUTextureView textureView) WGPU_FUNCTION_ATTRIBUTE; +/** + * Proc pointer type for @ref wgpuTextureViewRelease: + * > @copydoc wgpuTextureViewRelease + */ +typedef void (*WGPUProcTextureViewRelease)(WGPUTextureView textureView) WGPU_FUNCTION_ATTRIBUTE; + +#endif // !defined(WGPU_SKIP_PROCS) + +#if !defined(WGPU_SKIP_DECLARATIONS) +/** + * \defgroup GlobalFunctions Global Functions + * \brief Functions that are not specific to an object. + * + * @{ + */ +/** + * Create a WGPUInstance + * + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUInstance wgpuCreateInstance(WGPU_NULLABLE WGPUInstanceDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Get the list of @ref WGPUInstanceFeatureName values supported by the instance. + * + * @param features + * This parameter is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT void wgpuGetInstanceFeatures(WGPUSupportedInstanceFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * Get the limits supported by the instance. + * + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuGetInstanceLimits(WGPUInstanceLimits * limits) WGPU_FUNCTION_ATTRIBUTE; +/** + * Check whether a particular @ref WGPUInstanceFeatureName is supported by the instance. + */ +WGPU_EXPORT WGPUBool wgpuHasInstanceFeature(WGPUInstanceFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Returns the "procedure address" (function pointer) of the named function. + * The result must be cast to the appropriate proc pointer type. + */ +WGPU_EXPORT WGPUProc wgpuGetProcAddress(WGPUStringView procName) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup Methods Methods + * \brief Functions that are relative to a specific object. + * + * @{ + */ + +/** + * \defgroup WGPUAdapterMethods WGPUAdapter methods + * \brief Functions whose first argument has type WGPUAdapter. + * + * @{ + */ +/** + * Get the list of @ref WGPUFeatureName values supported by the adapter. + * + * @param features + * This parameter is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT void wgpuAdapterGetFeatures(WGPUAdapter adapter, WGPUSupportedFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * @param info + * This parameter is @ref ReturnedWithOwnership. + * + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuAdapterGetInfo(WGPUAdapter adapter, WGPUAdapterInfo * info) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuAdapterGetLimits(WGPUAdapter adapter, WGPULimits * limits) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUBool wgpuAdapterHasFeature(WGPUAdapter adapter, WGPUFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUFuture wgpuAdapterRequestDevice(WGPUAdapter adapter, WGPU_NULLABLE WGPUDeviceDescriptor const * descriptor, WGPURequestDeviceCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuAdapterAddRef(WGPUAdapter adapter) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuAdapterRelease(WGPUAdapter adapter) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUAdapterInfoMethods WGPUAdapterInfo methods + * \brief Functions whose first argument has type WGPUAdapterInfo. + * + * @{ + */ +/** + * Frees members which were allocated by the API. + */ +WGPU_EXPORT void wgpuAdapterInfoFreeMembers(WGPUAdapterInfo adapterInfo) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUBindGroupMethods WGPUBindGroup methods + * \brief Functions whose first argument has type WGPUBindGroup. + * + * @{ + */ +WGPU_EXPORT void wgpuBindGroupSetLabel(WGPUBindGroup bindGroup, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBindGroupAddRef(WGPUBindGroup bindGroup) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBindGroupRelease(WGPUBindGroup bindGroup) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUBindGroupLayoutMethods WGPUBindGroupLayout methods + * \brief Functions whose first argument has type WGPUBindGroupLayout. + * + * @{ + */ +WGPU_EXPORT void wgpuBindGroupLayoutSetLabel(WGPUBindGroupLayout bindGroupLayout, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBindGroupLayoutAddRef(WGPUBindGroupLayout bindGroupLayout) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBindGroupLayoutRelease(WGPUBindGroupLayout bindGroupLayout) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUBufferMethods WGPUBuffer methods + * \brief Functions whose first argument has type WGPUBuffer. + * + * @{ + */ +WGPU_EXPORT void wgpuBufferDestroy(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Returns a const pointer to beginning of the mapped range. + * It must not be written; writing to this range causes undefined behavior. + * See @ref MappedRangeBehavior for error conditions and guarantees. + * This function is safe to call inside spontaneous callbacks (see @ref CallbackReentrancy). + * + * In Wasm, if `memcpy`ing from this range, prefer using @ref wgpuBufferReadMappedRange + * instead for better performance. + * + * @param offset + * Byte offset relative to the beginning of the buffer. + * + * @param size + * Byte size of the range to get. + * If this is @ref WGPU_WHOLE_MAP_SIZE, it defaults to `buffer.size - offset`. + * The returned pointer is valid for exactly this many bytes. + */ +WGPU_EXPORT void const * wgpuBufferGetConstMappedRange(WGPUBuffer buffer, size_t offset, size_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * Returns a mutable pointer to beginning of the mapped range. + * See @ref MappedRangeBehavior for error conditions and guarantees. + * This function is safe to call inside spontaneous callbacks (see @ref CallbackReentrancy). + * + * In Wasm, if `memcpy`ing into this range, prefer using @ref wgpuBufferWriteMappedRange + * instead for better performance. + * + * @param offset + * Byte offset relative to the beginning of the buffer. + * + * @param size + * Byte size of the range to get. + * If this is @ref WGPU_WHOLE_MAP_SIZE, it defaults to `buffer.size - offset`. + * The returned pointer is valid for exactly this many bytes. + */ +WGPU_EXPORT void * wgpuBufferGetMappedRange(WGPUBuffer buffer, size_t offset, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUBufferMapState wgpuBufferGetMapState(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint64_t wgpuBufferGetSize(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUBufferUsage wgpuBufferGetUsage(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * @param mode + * The mapping mode (read or write). + * + * @param offset + * Byte offset relative to beginning of the buffer. + * + * @param size + * Byte size of the region to map. + * If this is @ref WGPU_WHOLE_MAP_SIZE, it defaults to `buffer.size - offset`. + */ +WGPU_EXPORT WGPUFuture wgpuBufferMapAsync(WGPUBuffer buffer, WGPUMapMode mode, size_t offset, size_t size, WGPUBufferMapCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Copies a range of data from the buffer mapping into the provided destination pointer. + * See @ref MappedRangeBehavior for error conditions and guarantees. + * This function is safe to call inside spontaneous callbacks (see @ref CallbackReentrancy). + * + * In Wasm, this is more efficient than copying from a mapped range into a `malloc`'d range. + * + * @param offset + * Byte offset relative to the beginning of the buffer. + * + * @param data + * Destination, to read buffer data into. + * + * @param size + * Number of bytes of data to read from the buffer. + * (Note @ref WGPU_WHOLE_MAP_SIZE is *not* accepted here.) + * + * @returns + * @ref WGPUStatus_Error if the copy did not occur. + */ +WGPU_EXPORT WGPUStatus wgpuBufferReadMappedRange(WGPUBuffer buffer, size_t offset, void * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBufferSetLabel(WGPUBuffer buffer, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBufferUnmap(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +/** + * Copies a range of data from the provided source pointer into the buffer mapping. + * See @ref MappedRangeBehavior for error conditions and guarantees. + * This function is safe to call inside spontaneous callbacks (see @ref CallbackReentrancy). + * + * In Wasm, this is more efficient than copying from a `malloc`'d range into a mapped range. + * + * @param offset + * Byte offset relative to the beginning of the buffer. + * + * @param data + * Source, to write buffer data from. + * + * @param size + * Number of bytes of data to write to the buffer. + * (Note @ref WGPU_WHOLE_MAP_SIZE is *not* accepted here.) + * + * @returns + * @ref WGPUStatus_Error if the copy did not occur. + */ +WGPU_EXPORT WGPUStatus wgpuBufferWriteMappedRange(WGPUBuffer buffer, size_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBufferAddRef(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuBufferRelease(WGPUBuffer buffer) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUCommandBufferMethods WGPUCommandBuffer methods + * \brief Functions whose first argument has type WGPUCommandBuffer. + * + * @{ + */ +WGPU_EXPORT void wgpuCommandBufferSetLabel(WGPUCommandBuffer commandBuffer, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandBufferAddRef(WGPUCommandBuffer commandBuffer) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandBufferRelease(WGPUCommandBuffer commandBuffer) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUCommandEncoderMethods WGPUCommandEncoder methods + * \brief Functions whose first argument has type WGPUCommandEncoder. + * + * @{ + */ +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUComputePassEncoder wgpuCommandEncoderBeginComputePass(WGPUCommandEncoder commandEncoder, WGPU_NULLABLE WGPUComputePassDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPURenderPassEncoder wgpuCommandEncoderBeginRenderPass(WGPUCommandEncoder commandEncoder, WGPURenderPassDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderClearBuffer(WGPUCommandEncoder commandEncoder, WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderCopyBufferToBuffer(WGPUCommandEncoder commandEncoder, WGPUBuffer source, uint64_t sourceOffset, WGPUBuffer destination, uint64_t destinationOffset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderCopyBufferToTexture(WGPUCommandEncoder commandEncoder, WGPUTexelCopyBufferInfo const * source, WGPUTexelCopyTextureInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderCopyTextureToBuffer(WGPUCommandEncoder commandEncoder, WGPUTexelCopyTextureInfo const * source, WGPUTexelCopyBufferInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderCopyTextureToTexture(WGPUCommandEncoder commandEncoder, WGPUTexelCopyTextureInfo const * source, WGPUTexelCopyTextureInfo const * destination, WGPUExtent3D const * copySize) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUCommandBuffer wgpuCommandEncoderFinish(WGPUCommandEncoder commandEncoder, WGPU_NULLABLE WGPUCommandBufferDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderInsertDebugMarker(WGPUCommandEncoder commandEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderPopDebugGroup(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderPushDebugGroup(WGPUCommandEncoder commandEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderResolveQuerySet(WGPUCommandEncoder commandEncoder, WGPUQuerySet querySet, uint32_t firstQuery, uint32_t queryCount, WGPUBuffer destination, uint64_t destinationOffset) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderSetLabel(WGPUCommandEncoder commandEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderWriteTimestamp(WGPUCommandEncoder commandEncoder, WGPUQuerySet querySet, uint32_t queryIndex) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderAddRef(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuCommandEncoderRelease(WGPUCommandEncoder commandEncoder) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUComputePassEncoderMethods WGPUComputePassEncoder methods + * \brief Functions whose first argument has type WGPUComputePassEncoder. + * + * @{ + */ +WGPU_EXPORT void wgpuComputePassEncoderDispatchWorkgroups(WGPUComputePassEncoder computePassEncoder, uint32_t workgroupCountX, uint32_t workgroupCountY, uint32_t workgroupCountZ) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderDispatchWorkgroupsIndirect(WGPUComputePassEncoder computePassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderEnd(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderInsertDebugMarker(WGPUComputePassEncoder computePassEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderPopDebugGroup(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderPushDebugGroup(WGPUComputePassEncoder computePassEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderSetBindGroup(WGPUComputePassEncoder computePassEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderSetImmediates(WGPUComputePassEncoder computePassEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderSetLabel(WGPUComputePassEncoder computePassEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderSetPipeline(WGPUComputePassEncoder computePassEncoder, WGPUComputePipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderAddRef(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePassEncoderRelease(WGPUComputePassEncoder computePassEncoder) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUComputePipelineMethods WGPUComputePipeline methods + * \brief Functions whose first argument has type WGPUComputePipeline. + * + * @{ + */ +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUBindGroupLayout wgpuComputePipelineGetBindGroupLayout(WGPUComputePipeline computePipeline, uint32_t groupIndex) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePipelineSetLabel(WGPUComputePipeline computePipeline, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePipelineAddRef(WGPUComputePipeline computePipeline) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuComputePipelineRelease(WGPUComputePipeline computePipeline) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUDeviceMethods WGPUDevice methods + * \brief Functions whose first argument has type WGPUDevice. + * + * @{ + */ +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUBindGroup wgpuDeviceCreateBindGroup(WGPUDevice device, WGPUBindGroupDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUBindGroupLayout wgpuDeviceCreateBindGroupLayout(WGPUDevice device, WGPUBindGroupLayoutDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * TODO + * + * If @ref WGPUBufferDescriptor::mappedAtCreation is `true` and the mapping allocation fails, + * returns `NULL`. + * + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPU_NULLABLE WGPUBuffer wgpuDeviceCreateBuffer(WGPUDevice device, WGPUBufferDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUCommandEncoder wgpuDeviceCreateCommandEncoder(WGPUDevice device, WGPU_NULLABLE WGPUCommandEncoderDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUComputePipeline wgpuDeviceCreateComputePipeline(WGPUDevice device, WGPUComputePipelineDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUFuture wgpuDeviceCreateComputePipelineAsync(WGPUDevice device, WGPUComputePipelineDescriptor const * descriptor, WGPUCreateComputePipelineAsyncCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUPipelineLayout wgpuDeviceCreatePipelineLayout(WGPUDevice device, WGPUPipelineLayoutDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUQuerySet wgpuDeviceCreateQuerySet(WGPUDevice device, WGPUQuerySetDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPURenderBundleEncoder wgpuDeviceCreateRenderBundleEncoder(WGPUDevice device, WGPURenderBundleEncoderDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPURenderPipeline wgpuDeviceCreateRenderPipeline(WGPUDevice device, WGPURenderPipelineDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUFuture wgpuDeviceCreateRenderPipelineAsync(WGPUDevice device, WGPURenderPipelineDescriptor const * descriptor, WGPUCreateRenderPipelineAsyncCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUSampler wgpuDeviceCreateSampler(WGPUDevice device, WGPU_NULLABLE WGPUSamplerDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUShaderModule wgpuDeviceCreateShaderModule(WGPUDevice device, WGPUShaderModuleDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUTexture wgpuDeviceCreateTexture(WGPUDevice device, WGPUTextureDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuDeviceDestroy(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * @param adapterInfo + * This parameter is @ref ReturnedWithOwnership. + * + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuDeviceGetAdapterInfo(WGPUDevice device, WGPUAdapterInfo * adapterInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Get the list of @ref WGPUFeatureName values supported by the device. + * + * @param features + * This parameter is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT void wgpuDeviceGetFeatures(WGPUDevice device, WGPUSupportedFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuDeviceGetLimits(WGPUDevice device, WGPULimits * limits) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * The @ref WGPUFuture for the device-lost event of the device. + */ +WGPU_EXPORT WGPUFuture wgpuDeviceGetLostFuture(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUQueue wgpuDeviceGetQueue(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUBool wgpuDeviceHasFeature(WGPUDevice device, WGPUFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Pops an error scope to the current thread's error scope stack, + * asynchronously returning the result. See @ref ErrorScopes. + */ +WGPU_EXPORT WGPUFuture wgpuDevicePopErrorScope(WGPUDevice device, WGPUPopErrorScopeCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Pushes an error scope to the current thread's error scope stack. + * See @ref ErrorScopes. + */ +WGPU_EXPORT void wgpuDevicePushErrorScope(WGPUDevice device, WGPUErrorFilter filter) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuDeviceSetLabel(WGPUDevice device, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuDeviceAddRef(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuDeviceRelease(WGPUDevice device) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUExternalTextureMethods WGPUExternalTexture methods + * \brief Functions whose first argument has type WGPUExternalTexture. + * + * @{ + */ +WGPU_EXPORT void wgpuExternalTextureSetLabel(WGPUExternalTexture externalTexture, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuExternalTextureAddRef(WGPUExternalTexture externalTexture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuExternalTextureRelease(WGPUExternalTexture externalTexture) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUInstanceMethods WGPUInstance methods + * \brief Functions whose first argument has type WGPUInstance. + * + * @{ + */ +/** + * Creates a @ref WGPUSurface, see @ref Surface-Creation for more details. + * + * @param descriptor + * The description of the @ref WGPUSurface to create. + * + * @returns + * A new @ref WGPUSurface for this descriptor (or an error @ref WGPUSurface). + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUSurface wgpuInstanceCreateSurface(WGPUInstance instance, WGPUSurfaceDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +/** + * Get the list of @ref WGPUWGSLLanguageFeatureName values supported by the instance. + */ +WGPU_EXPORT void wgpuInstanceGetWGSLLanguageFeatures(WGPUInstance instance, WGPUSupportedWGSLLanguageFeatures * features) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUBool wgpuInstanceHasWGSLLanguageFeature(WGPUInstance instance, WGPUWGSLLanguageFeatureName feature) WGPU_FUNCTION_ATTRIBUTE; +/** + * Processes asynchronous events on this `WGPUInstance`, calling any callbacks for asynchronous operations created with @ref WGPUCallbackMode_AllowProcessEvents. + * + * See @ref Process-Events for more information. + */ +WGPU_EXPORT void wgpuInstanceProcessEvents(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUFuture wgpuInstanceRequestAdapter(WGPUInstance instance, WGPU_NULLABLE WGPURequestAdapterOptions const * options, WGPURequestAdapterCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +/** + * Wait for at least one WGPUFuture in `futures` to complete, and call callbacks of the respective completed asynchronous operations. + * + * See @ref Wait-Any for more information. + */ +WGPU_EXPORT WGPUWaitStatus wgpuInstanceWaitAny(WGPUInstance instance, size_t futureCount, WGPU_NULLABLE WGPUFutureWaitInfo * futures, uint64_t timeoutNS) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuInstanceAddRef(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuInstanceRelease(WGPUInstance instance) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUPipelineLayoutMethods WGPUPipelineLayout methods + * \brief Functions whose first argument has type WGPUPipelineLayout. + * + * @{ + */ +WGPU_EXPORT void wgpuPipelineLayoutSetLabel(WGPUPipelineLayout pipelineLayout, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuPipelineLayoutAddRef(WGPUPipelineLayout pipelineLayout) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuPipelineLayoutRelease(WGPUPipelineLayout pipelineLayout) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUQuerySetMethods WGPUQuerySet methods + * \brief Functions whose first argument has type WGPUQuerySet. + * + * @{ + */ +WGPU_EXPORT void wgpuQuerySetDestroy(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuQuerySetGetCount(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUQueryType wgpuQuerySetGetType(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQuerySetSetLabel(WGPUQuerySet querySet, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQuerySetAddRef(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQuerySetRelease(WGPUQuerySet querySet) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUQueueMethods WGPUQueue methods + * \brief Functions whose first argument has type WGPUQueue. + * + * @{ + */ +WGPU_EXPORT WGPUFuture wgpuQueueOnSubmittedWorkDone(WGPUQueue queue, WGPUQueueWorkDoneCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQueueSetLabel(WGPUQueue queue, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQueueSubmit(WGPUQueue queue, size_t commandCount, WGPUCommandBuffer const * commands) WGPU_FUNCTION_ATTRIBUTE; +/** + * Produces a @ref DeviceError both content-timeline (`size` alignment) and device-timeline + * errors defined by the WebGPU specification. + */ +WGPU_EXPORT void wgpuQueueWriteBuffer(WGPUQueue queue, WGPUBuffer buffer, uint64_t bufferOffset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQueueWriteTexture(WGPUQueue queue, WGPUTexelCopyTextureInfo const * destination, void const * data, size_t dataSize, WGPUTexelCopyBufferLayout const * dataLayout, WGPUExtent3D const * writeSize) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQueueAddRef(WGPUQueue queue) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuQueueRelease(WGPUQueue queue) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPURenderBundleMethods WGPURenderBundle methods + * \brief Functions whose first argument has type WGPURenderBundle. + * + * @{ + */ +WGPU_EXPORT void wgpuRenderBundleSetLabel(WGPURenderBundle renderBundle, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleAddRef(WGPURenderBundle renderBundle) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleRelease(WGPURenderBundle renderBundle) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPURenderBundleEncoderMethods WGPURenderBundleEncoder methods + * \brief Functions whose first argument has type WGPURenderBundleEncoder. + * + * @{ + */ +WGPU_EXPORT void wgpuRenderBundleEncoderDraw(WGPURenderBundleEncoder renderBundleEncoder, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderDrawIndexed(WGPURenderBundleEncoder renderBundleEncoder, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t baseVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderDrawIndexedIndirect(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderDrawIndirect(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPURenderBundle wgpuRenderBundleEncoderFinish(WGPURenderBundleEncoder renderBundleEncoder, WGPU_NULLABLE WGPURenderBundleDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderInsertDebugMarker(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderPopDebugGroup(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderPushDebugGroup(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetBindGroup(WGPURenderBundleEncoder renderBundleEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetImmediates(WGPURenderBundleEncoder renderBundleEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetIndexBuffer(WGPURenderBundleEncoder renderBundleEncoder, WGPUBuffer buffer, WGPUIndexFormat format, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetLabel(WGPURenderBundleEncoder renderBundleEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetPipeline(WGPURenderBundleEncoder renderBundleEncoder, WGPURenderPipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderSetVertexBuffer(WGPURenderBundleEncoder renderBundleEncoder, uint32_t slot, WGPU_NULLABLE WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderAddRef(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderBundleEncoderRelease(WGPURenderBundleEncoder renderBundleEncoder) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPURenderPassEncoderMethods WGPURenderPassEncoder methods + * \brief Functions whose first argument has type WGPURenderPassEncoder. + * + * @{ + */ +WGPU_EXPORT void wgpuRenderPassEncoderBeginOcclusionQuery(WGPURenderPassEncoder renderPassEncoder, uint32_t queryIndex) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderDraw(WGPURenderPassEncoder renderPassEncoder, uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderDrawIndexed(WGPURenderPassEncoder renderPassEncoder, uint32_t indexCount, uint32_t instanceCount, uint32_t firstIndex, int32_t baseVertex, uint32_t firstInstance) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderDrawIndexedIndirect(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderDrawIndirect(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer indirectBuffer, uint64_t indirectOffset) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderEnd(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderEndOcclusionQuery(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderExecuteBundles(WGPURenderPassEncoder renderPassEncoder, size_t bundleCount, WGPURenderBundle const * bundles) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderInsertDebugMarker(WGPURenderPassEncoder renderPassEncoder, WGPUStringView markerLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderPopDebugGroup(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderPushDebugGroup(WGPURenderPassEncoder renderPassEncoder, WGPUStringView groupLabel) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetBindGroup(WGPURenderPassEncoder renderPassEncoder, uint32_t groupIndex, WGPU_NULLABLE WGPUBindGroup group, size_t dynamicOffsetCount, uint32_t const * dynamicOffsets) WGPU_FUNCTION_ATTRIBUTE; +/** + * @param color + * The RGBA blend constant. Represents an `f32` color using @ref DoubleAsSupertype. + */ +WGPU_EXPORT void wgpuRenderPassEncoderSetBlendConstant(WGPURenderPassEncoder renderPassEncoder, WGPUColor const * color) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetImmediates(WGPURenderPassEncoder renderPassEncoder, uint32_t offset, void const * data, size_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetIndexBuffer(WGPURenderPassEncoder renderPassEncoder, WGPUBuffer buffer, WGPUIndexFormat format, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetLabel(WGPURenderPassEncoder renderPassEncoder, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetPipeline(WGPURenderPassEncoder renderPassEncoder, WGPURenderPipeline pipeline) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetScissorRect(WGPURenderPassEncoder renderPassEncoder, uint32_t x, uint32_t y, uint32_t width, uint32_t height) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetStencilReference(WGPURenderPassEncoder renderPassEncoder, uint32_t reference) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderSetVertexBuffer(WGPURenderPassEncoder renderPassEncoder, uint32_t slot, WGPU_NULLABLE WGPUBuffer buffer, uint64_t offset, uint64_t size) WGPU_FUNCTION_ATTRIBUTE; +/** + * TODO + * + * If any argument is non-finite, produces a @ref NonFiniteFloatValueError. + */ +WGPU_EXPORT void wgpuRenderPassEncoderSetViewport(WGPURenderPassEncoder renderPassEncoder, float x, float y, float width, float height, float minDepth, float maxDepth) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderAddRef(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPassEncoderRelease(WGPURenderPassEncoder renderPassEncoder) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPURenderPipelineMethods WGPURenderPipeline methods + * \brief Functions whose first argument has type WGPURenderPipeline. + * + * @{ + */ +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUBindGroupLayout wgpuRenderPipelineGetBindGroupLayout(WGPURenderPipeline renderPipeline, uint32_t groupIndex) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPipelineSetLabel(WGPURenderPipeline renderPipeline, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPipelineAddRef(WGPURenderPipeline renderPipeline) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuRenderPipelineRelease(WGPURenderPipeline renderPipeline) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSamplerMethods WGPUSampler methods + * \brief Functions whose first argument has type WGPUSampler. + * + * @{ + */ +WGPU_EXPORT void wgpuSamplerSetLabel(WGPUSampler sampler, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuSamplerAddRef(WGPUSampler sampler) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuSamplerRelease(WGPUSampler sampler) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUShaderModuleMethods WGPUShaderModule methods + * \brief Functions whose first argument has type WGPUShaderModule. + * + * @{ + */ +WGPU_EXPORT WGPUFuture wgpuShaderModuleGetCompilationInfo(WGPUShaderModule shaderModule, WGPUCompilationInfoCallbackInfo callbackInfo) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuShaderModuleSetLabel(WGPUShaderModule shaderModule, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuShaderModuleAddRef(WGPUShaderModule shaderModule) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuShaderModuleRelease(WGPUShaderModule shaderModule) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSupportedFeaturesMethods WGPUSupportedFeatures methods + * \brief Functions whose first argument has type WGPUSupportedFeatures. + * + * @{ + */ +/** + * Frees members which were allocated by the API. + */ +WGPU_EXPORT void wgpuSupportedFeaturesFreeMembers(WGPUSupportedFeatures supportedFeatures) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSupportedInstanceFeaturesMethods WGPUSupportedInstanceFeatures methods + * \brief Functions whose first argument has type WGPUSupportedInstanceFeatures. + * + * @{ + */ +/** + * Frees members which were allocated by the API. + */ +WGPU_EXPORT void wgpuSupportedInstanceFeaturesFreeMembers(WGPUSupportedInstanceFeatures supportedInstanceFeatures) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSupportedWGSLLanguageFeaturesMethods WGPUSupportedWGSLLanguageFeatures methods + * \brief Functions whose first argument has type WGPUSupportedWGSLLanguageFeatures. + * + * @{ + */ +/** + * Frees members which were allocated by the API. + */ +WGPU_EXPORT void wgpuSupportedWGSLLanguageFeaturesFreeMembers(WGPUSupportedWGSLLanguageFeatures supportedWGSLLanguageFeatures) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSurfaceMethods WGPUSurface methods + * \brief Functions whose first argument has type WGPUSurface. + * + * @{ + */ +/** + * Configures parameters for rendering to `surface`. + * Produces a @ref DeviceError for all content-timeline errors defined by the WebGPU specification. + * + * See @ref Surface-Configuration for more details. + * + * @param config + * The new configuration to use. + */ +WGPU_EXPORT void wgpuSurfaceConfigure(WGPUSurface surface, WGPUSurfaceConfiguration const * config) WGPU_FUNCTION_ATTRIBUTE; +/** + * Provides information on how `adapter` is able to use `surface`. + * See @ref Surface-Capabilities for more details. + * + * @param adapter + * The @ref WGPUAdapter to get capabilities for presenting to this @ref WGPUSurface. + * + * @param capabilities + * The structure to fill capabilities in. + * It may contain memory allocations so @ref wgpuSurfaceCapabilitiesFreeMembers must be called to avoid memory leaks. + * This parameter is @ref ReturnedWithOwnership. + * + * @returns + * Indicates if there was an @ref OutStructChainError. + */ +WGPU_EXPORT WGPUStatus wgpuSurfaceGetCapabilities(WGPUSurface surface, WGPUAdapter adapter, WGPUSurfaceCapabilities * capabilities) WGPU_FUNCTION_ATTRIBUTE; +/** + * Returns the @ref WGPUTexture to render to `surface` this frame along with metadata on the frame. + * Returns `NULL` and @ref WGPUSurfaceGetCurrentTextureStatus_Error if the surface is not configured. + * + * See @ref Surface-Presenting for more details. + * + * @param surfaceTexture + * The structure to fill the @ref WGPUTexture and metadata in. + */ +WGPU_EXPORT void wgpuSurfaceGetCurrentTexture(WGPUSurface surface, WGPUSurfaceTexture * surfaceTexture) WGPU_FUNCTION_ATTRIBUTE; +/** + * Shows `surface`'s current texture to the user. + * See @ref Surface-Presenting for more details. + * + * @returns + * Returns @ref WGPUStatus_Error if the surface doesn't have a current texture. + */ +WGPU_EXPORT WGPUStatus wgpuSurfacePresent(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +/** + * Modifies the label used to refer to `surface`. + * + * @param label + * The new label. + */ +WGPU_EXPORT void wgpuSurfaceSetLabel(WGPUSurface surface, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +/** + * Removes the configuration for `surface`. + * See @ref Surface-Configuration for more details. + */ +WGPU_EXPORT void wgpuSurfaceUnconfigure(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuSurfaceAddRef(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuSurfaceRelease(WGPUSurface surface) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUSurfaceCapabilitiesMethods WGPUSurfaceCapabilities methods + * \brief Functions whose first argument has type WGPUSurfaceCapabilities. + * + * @{ + */ +/** + * Frees members which were allocated by the API. + */ +WGPU_EXPORT void wgpuSurfaceCapabilitiesFreeMembers(WGPUSurfaceCapabilities surfaceCapabilities) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUTextureMethods WGPUTexture methods + * \brief Functions whose first argument has type WGPUTexture. + * + * @{ + */ +/** + * @returns + * This value is @ref ReturnedWithOwnership. + */ +WGPU_EXPORT WGPUTextureView wgpuTextureCreateView(WGPUTexture texture, WGPU_NULLABLE WGPUTextureViewDescriptor const * descriptor) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureDestroy(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuTextureGetDepthOrArrayLayers(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUTextureDimension wgpuTextureGetDimension(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUTextureFormat wgpuTextureGetFormat(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuTextureGetHeight(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuTextureGetMipLevelCount(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuTextureGetSampleCount(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUTextureViewDimension wgpuTextureGetTextureBindingViewDimension(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT WGPUTextureUsage wgpuTextureGetUsage(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT uint32_t wgpuTextureGetWidth(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureSetLabel(WGPUTexture texture, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureAddRef(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureRelease(WGPUTexture texture) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** + * \defgroup WGPUTextureViewMethods WGPUTextureView methods + * \brief Functions whose first argument has type WGPUTextureView. + * + * @{ + */ +WGPU_EXPORT void wgpuTextureViewSetLabel(WGPUTextureView textureView, WGPUStringView label) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureViewAddRef(WGPUTextureView textureView) WGPU_FUNCTION_ATTRIBUTE; +WGPU_EXPORT void wgpuTextureViewRelease(WGPUTextureView textureView) WGPU_FUNCTION_ATTRIBUTE; + +/** @} */ + +/** @} */ + +#endif // !defined(WGPU_SKIP_DECLARATIONS) + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // WEBGPU_H_ diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/wgpu.h b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/wgpu.h new file mode 100644 index 000000000..4f7d4e0d4 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/Headers/wgpu.h @@ -0,0 +1,1551 @@ +/** + * @file wgpu.h + * @brief wgpu-native specific extensions to the standard WebGPU C API. + * + * This header defines native-only types, enumerations, structures, and functions + * that extend the WebGPU specification defined in @c webgpu.h. All extension + * enum values and struct type identifiers (@ref WGPUNativeSType) are allocated + * within the @c 0x0003XXXX range reserved for wgpu-native. + * + * Include this header after @c webgpu.h (it is included automatically). + */ +#ifndef WGPU_H_ +#define WGPU_H_ + +#include "webgpu.h" + +typedef enum WGPUNativeSType +{ + // Start at 0003 since that's allocated range for wgpu-native + /** Identifies @ref WGPUDeviceExtras. */ + WGPUSType_DeviceExtras = 0x00030001, + /** Identifies @ref WGPUNativeLimits. */ + WGPUSType_NativeLimits = 0x00030002, + /** Identifies @ref WGPUShaderSourceGLSL. */ + WGPUSType_ShaderSourceGLSL = 0x00030003, + /** Identifies @ref WGPUInstanceExtras. */ + WGPUSType_InstanceExtras = 0x00030004, + /** Identifies @ref WGPUBindGroupEntryExtras. */ + WGPUSType_BindGroupEntryExtras = 0x00030005, + /** Identifies @ref WGPUBindGroupLayoutEntryExtras. */ + WGPUSType_BindGroupLayoutEntryExtras = 0x00030006, + /** Identifies @ref WGPUQuerySetDescriptorExtras. */ + WGPUSType_QuerySetDescriptorExtras = 0x00030007, + /** Identifies @ref WGPUSurfaceConfigurationExtras. */ + WGPUSType_SurfaceConfigurationExtras = 0x00030008, + /** Identifies @ref WGPUSurfaceSourceSwapChainPanel. */ + WGPUSType_SurfaceSourceSwapChainPanel = 0x00030009, + /** Identifies @ref WGPUPrimitiveStateExtras. */ + WGPUSType_PrimitiveStateExtras = 0x0003000A, + /** Identifies @ref WGPUSamplerDescriptorExtras. */ + WGPUSType_SamplerDescriptorExtras = 0x0003000B, + WGPUNativeSType_Force32 = 0x7FFFFFFF +} WGPUNativeSType; + +/** + * Additional surface-get-current-texture status codes defined by wgpu-native. + * + * These extend the standard @c WGPUSurfaceGetCurrentTextureStatus values. + */ +typedef enum WGPUNativeSurfaceGetCurrentTextureStatus +{ + /** + * The surface texture was not acquired because the window is occluded + * (e.g. minimized or fully covered by another window). + * + * No texture is returned and the @c texture field of + * @c WGPUSurfaceTexture will be NULL. The surface and swapchain remain + * valid -- there is no need to reconfigure or recreate the surface. + * + * Applications should skip rendering for the current frame and try + * again once the window is no longer occluded. If you are using a + * windowing library such as winit, listen for the window's "occluded" + * event and request a new redraw when the window becomes visible again. + * + * When does this occur? + * + * Currently this status is only produced by the Metal backend on macOS. + * When a window is not visible (checked via the @c NSWindow + * @c occlusionState property), acquiring the next drawable would block + * for up to one second waiting for vsync. wgpu-native returns + * @c Occluded instead to avoid that hang. + * + * Other backends (Vulkan, DX12, GL) do not currently report this + * status; an occluded window on those backends may produce + * @c WGPUSurfaceGetCurrentTextureStatus_Timeout or simply succeed + * normally. + */ + WGPUSurfaceGetCurrentTextureStatus_Occluded = 0x00030001, + WGPUNativeSurfaceGetCurrentTextureStatus_Force32 = 0x7FFFFFFF +} WGPUNativeSurfaceGetCurrentTextureStatus; + +/** + * Native-only device features. + * + * These extend the standard @c WGPUFeatureName values and can be passed to + * @c WGPUDeviceDescriptor::requiredFeatures to request additional + * capabilities when creating a device. + */ +typedef enum WGPUNativeFeature +{ + /** + * Allows the use of immediate data: small, fast blocks of memory + * that can be updated inside a render pass, compute pass, or render + * bundle encoder. + * + * Enables @ref wgpuRenderPassEncoderSetImmediates, + * @ref wgpuComputePassEncoderSetImmediates, + * @ref wgpuRenderBundleEncoderSetImmediates, + * non-zero @c immediateSize in @ref WGPUPipelineLayout, + * and non-zero @c maxImmediateSize in @ref WGPULimits. + * + * A block of immediate data can be declared in WGSL with + * @c var: + * @code + * struct Immediates { example: f32, } + * var c: Immediates; + * @endcode + * + * In GLSL, this corresponds to @c layout(immediates) @c uniform @c Name @c {..}. + * + * Supported platforms: + * - DX12 + * - Vulkan + * - Metal + * - OpenGL (emulated with uniforms) + * - WebGPU + * + * This is a web and native feature. + */ + WGPUNativeFeature_Immediates = 0x00030001, + /** + * Enables device-specific texture format features. + * + * By default only texture format properties as defined by the WebGPU + * specification are allowed. Enabling this feature flag extends the + * features of each format to the ones supported by the current device. + * Note that without this flag, read/write storage access is not allowed + * at all. + * + * This extension does not enable additional formats. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureAdapterSpecificFormatFeatures = 0x00030002, + /** + * Allows the use of a buffer containing the actual number of draw calls. + * + * Enables @ref wgpuRenderPassEncoderMultiDrawIndirectCount and + * @ref wgpuRenderPassEncoderMultiDrawIndexedIndirectCount. + * + * This feature being present also implies that all calls to + * @ref wgpuRenderPassEncoderMultiDrawIndirect and + * @ref wgpuRenderPassEncoderMultiDrawIndexedIndirect are not being + * emulated with a series of @c draw_indirect calls. + * + * Supported platforms: + * - DX12 + * - Vulkan 1.2+ (or VK_KHR_draw_indirect_count) + * + * This is a native only feature. + */ + WGPUNativeFeature_MultiDrawIndirectCount = 0x00030004, + /** + * Enables bindings of writable storage buffers and textures visible + * to vertex shaders. + * + * Note: some (tiled-based) platforms do not support vertex shaders + * with any side-effects. + * + * Supported platforms: + * - All + * + * This is a native only feature. + */ + WGPUNativeFeature_VertexWritableStorage = 0x00030005, + /** + * Allows the user to create uniform arrays of textures in shaders: + * + * - WGSL: @c var @c textures: @c binding_array, @c 10> + * - GLSL: @c uniform @c texture2D @c textures[10] + * + * If @ref WGPUNativeFeature_StorageResourceBindingArray is supported + * as well as this, the user may also create uniform arrays of storage + * textures. + * + * This capability allows them to exist and to be indexed by dynamically + * uniform values. + * + * Supported platforms: + * - DX12 + * - Metal (with MSL 2.0+ on macOS 10.13+) + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureBindingArray = 0x00030006, + /** + * Allows shaders to index sampled texture and storage buffer resource + * arrays with dynamically non-uniform values: + * + * e.g. @c texture_array[vertex_data] + * + * In order to use this capability, the corresponding GLSL extension must + * be enabled: + * + * @c \#extension @c GL_EXT_nonuniform_qualifier @c : @c require + * + * and then used either as @c nonuniformEXT qualifier in variable + * declaration or as @c nonuniformEXT constructor. + * + * WGSL and HLSL do not need any extension. + * + * Supported platforms: + * - DX12 + * - Metal (with MSL 2.0+ on macOS 10.13+) + * - Vulkan 1.2+ (or VK_EXT_descriptor_indexing) + * + * This is a native only feature. + */ + WGPUNativeFeature_SampledTextureAndStorageBufferArrayNonUniformIndexing = 0x00030007, + /** + * Enables use of Pipeline Statistics Queries. These queries report the + * count of various operations performed between the start and stop call. + * + * Use @ref wgpuRenderPassEncoderBeginPipelineStatisticsQuery / + * @ref wgpuRenderPassEncoderEndPipelineStatisticsQuery (or the compute + * pass equivalents) to start and stop a query. + * + * They must be resolved using @c wgpuCommandEncoderResolveQuerySet into + * a buffer. See @ref WGPUPipelineStatisticName for the list of available + * statistics. + * + * Supported platforms: + * - Vulkan + * - DX12 + * + * This is a native only feature. + */ + WGPUNativeFeature_PipelineStatisticsQuery = 0x00030008, + /** + * Allows the user to create uniform arrays of storage buffers or + * textures in shaders, if @ref WGPUNativeFeature_BufferBindingArray + * or @ref WGPUNativeFeature_TextureBindingArray (respectively) + * is also supported. + * + * This capability allows them to exist and to be indexed by dynamically + * uniform values. + * + * Supported platforms: + * - Metal (with MSL 2.2+ on macOS 10.13+) + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_StorageResourceBindingArray = 0x00030009, + /** + * Allows the user to create bind groups containing arrays with fewer + * bindings than the @c WGPUBindGroupLayout requires. + * + * Supported platforms: + * - Vulkan + * - DX12 + * + * This is a native only feature. + */ + WGPUNativeFeature_PartiallyBoundBindingArray = 0x0003000A, + /** + * Enables normalized 16-bit texture formats: + * @ref WGPUTextureFormat_R16Unorm, @ref WGPUTextureFormat_R16Snorm, + * @ref WGPUTextureFormat_RG16Unorm, @ref WGPUTextureFormat_RG16Snorm, + * @ref WGPUTextureFormat_RGBA16Unorm, @ref WGPUTextureFormat_RGBA16Snorm. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureFormat16bitNorm = 0x0003000B, + /** + * Enables ASTC HDR family of compressed textures. + * + * Compressed textures sacrifice some quality in exchange for + * significantly reduced bandwidth usage. + * + * Support for this feature guarantees availability of + * @c COPY_SRC | @c COPY_DST | @c TEXTURE_BINDING for ASTC formats + * with the HDR channel type. + * @ref WGPUNativeFeature_TextureAdapterSpecificFormatFeatures may + * enable additional usages. + * + * Supported platforms: + * - Metal + * - Vulkan + * - OpenGL + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureCompressionAstcHdr = 0x0003000C, + /** + * Removes the WebGPU restriction that @c MAP_READ and @c MAP_WRITE + * buffer usages must be paired exclusively with @c COPY_DST and + * @c COPY_SRC respectively. + * + * This is only beneficial on systems that share memory between CPU and + * GPU. If enabled on a system that doesn't, this can severely hinder + * performance. Only use if you understand the consequences. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_MappablePrimaryBuffers = 0x0003000E, + /** + * Allows the user to create arrays of buffers in shaders: + * + * - WGSL: @c var @c buffer_array: @c array + * - GLSL: @c uniform @c myBuffer @c { @c ... @c } @c buffer_array[10] + * + * This capability allows them to exist and to be indexed by dynamically + * uniform values. + * + * If @ref WGPUNativeFeature_StorageResourceBindingArray is supported as + * well as this, the user may also create arrays of storage buffers. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_BufferBindingArray = 0x0003000F, + /** + * Allows shaders to index storage texture resource + * arrays with dynamically non-uniform values. + * + * This is a native only feature. + */ + WGPUNativeFeature_StorageTextureArrayNonUniformIndexing = 0x00030010, + WGPUNativeFeature_AddressModeClampToZero = 0x00030011, + WGPUNativeFeature_AddressModeClampToBorder = 0x00030012, + /** + * Allows the user to set @ref WGPUPolygonMode_Line in + * @ref WGPUPrimitiveStateExtras::polygonMode. + * + * This allows drawing polygons/triangles as lines (wireframe) instead + * of filled. + * + * Supported platforms: + * - DX12 + * - Vulkan + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_PolygonModeLine = 0x00030013, + /** + * Allows the user to set @ref WGPUPolygonMode_Point in + * @ref WGPUPrimitiveStateExtras::polygonMode. + * + * This allows only drawing the vertices of polygons/triangles instead + * of filled. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_PolygonModePoint = 0x00030014, + /** + * Allows the user to enable overestimation conservative rasterization + * via @ref WGPUPrimitiveStateExtras::conservative. + * + * Processing of degenerate triangles/lines is hardware specific. + * Only triangles are supported. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_ConservativeRasterization = 0x00030015, + /** + * Enables clear to zero for textures. + * + * Supported platforms: + * - All + * + * This is a native only feature. + */ + WGPUNativeFeature_ClearTexture = 0x00030016, + /** + * Enables multiview render passes and `builtin(view_index)` in vertex/mesh shaders. + * + * Supported platforms: + * - Vulkan + * - Metal + * - DX12 + * - OpenGL (web only) + * + * This is a native only feature. + */ + WGPUNativeFeature_Multiview = 0x00030018, + /** + * Enables using 64-bit types for vertex attributes. + * + * Requires @ref WGPUNativeFeature_ShaderF64. + * + * This is a native only feature. + */ + WGPUNativeFeature_VertexAttribute64bit = 0x00030019, + /** + * Allows for creation of textures of format + * @ref WGPUNativeTextureFormat_NV12. + * + * Supported platforms: + * - DX12 + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureFormatNv12 = 0x0003001A, + /** + * Allows for the creation of ray-tracing queries within shaders. + * + * @b EXPERIMENTAL: Features enabled by this may have major bugs and are + * expected to be subject to breaking changes. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_RayQuery = 0x0003001C, + /** + * Enables 64-bit floating point types in SPIR-V shaders. + * + * Note: even when supported by GPU hardware, 64-bit floating point + * operations are frequently between 16 and 64 @e times slower than + * equivalent operations on 32-bit floats. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderF64 = 0x0003001D, + /** + * Allows shaders to use i16. Not currently supported in naga, only + * available through SPIR-V passthrough. + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderI16 = 0x0003001E, + /** + * Allows shaders to use the @c early_depth_test attribute. + * + * The attribute is applied to the fragment shader entry point and can be + * used in two ways: + * + * 1. Force early depth/stencil tests: + * - WGSL: @c \@early_depth_test(force) + * - GLSL: @c layout(early_fragment_tests) @c in; + * + * 2. Provide a conservative depth specifier that allows an additional + * early depth test under certain conditions: + * - WGSL: @c \@early_depth_test(greater_equal/less_equal/unchanged) + * - GLSL: @c layout(depth_) @c out @c float @c gl_FragDepth; + * + * Supported platforms: + * - Vulkan + * - GLES 3.1+ + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderEarlyDepthTest = 0x00030020, + /** + * Allows compute and fragment shaders to use the subgroup operation + * built-ins and perform subgroup operations (except barriers). + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_Subgroup = 0x00030021, + /** + * Allows vertex shaders to use the subgroup operation built-ins and + * perform subgroup operations (except barriers). + * + * Supported platforms: + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_SubgroupVertex = 0x00030022, + /** + * Allows compute shaders to use the subgroup barrier. + * + * Requires @ref WGPUNativeFeature_Subgroup. Without it, enables nothing. + * + * Supported platforms: + * - Vulkan + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_SubgroupBarrier = 0x00030023, + /** + * Allows for timestamp queries directly on command encoders. + * + * Implies @c WGPUFeatureName_TimestampQuery is supported. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal + * - OpenGL (with GL_ARB_timer_query) + * + * This is a native only feature. + */ + WGPUNativeFeature_TimestampQueryInsideEncoders = 0x00030024, + /** + * Allows for timestamp queries inside render and compute passes. + * + * Implies @c WGPUFeatureName_TimestampQuery and + * @ref WGPUNativeFeature_TimestampQueryInsideEncoders are supported. + * + * Enables @ref wgpuRenderPassEncoderWriteTimestamp and + * @ref wgpuComputePassEncoderWriteTimestamp. + * + * This is generally not available on tile-based rasterization GPUs. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal (AMD & Intel, not Apple GPUs) + * - OpenGL (with GL_ARB_timer_query) + * + * This is a native only feature. + */ + WGPUNativeFeature_TimestampQueryInsidePasses = 0x00030025, + /** + * Allows shaders to use i64 and u64. + * + * Supported platforms: + * - Vulkan + * - DX12 (DXC only) + * - Metal (with MSL 2.3+) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderInt64 = 0x00030026, + /** + * Allows shaders to use f32 atomic load, store, add, sub, and exchange. + * + * Supported platforms: + * - Metal (with MSL 3.0+ and Apple7+/Mac2) + * - Vulkan (with [VK_EXT_shader_atomic_float]) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderFloat32Atomic = 0x00030027, + /** + * Enables image atomic fetch add, and, xor, or, min, and max for R32Uint and R32Sint textures. + * + * Supported platforms: + * - Vulkan + * - DX12 + * - Metal (with MSL 3.1+) + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureAtomic = 0x00030028, + /** + * Allows for creation of textures of format + * @ref WGPUNativeTextureFormat_P010. + * + * Supported platforms: + * - DX12 + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureFormatP010 = 0x00030029, + // TODO: requires wgpu.h api change + // WGPUNativeFeature_ExternalTexture = 0x0003002A, + /** + * Allows the use of pipeline cache objects + * + * Supported platforms: + * - Vulkan + * + * Unimplemented Platforms: + * - DX12 + * - Metal + */ + WGPUNativeFeature_PipelineCache = 0x0003002B, + /** + * Allows shaders to use i64 and u64 atomic min and max. + * + * Supported platforms: + * - Vulkan (with VK_KHR_shader_atomic_int64) + * - DX12 (with SM 6.6+) + * - Metal (with MSL 2.4+) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderInt64AtomicMinMax = 0x0003002C, + /** + * Allows shaders to use all i64 and u64 atomic operations. + * + * Supported platforms: + * - Vulkan (with VK_KHR_shader_atomic_int64) + * - DX12 (with SM 6.6+) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderInt64AtomicAllOps = 0x0003002D, + // TODO: requires wgpu.h api change + // WGPUNativeFeature_VulkanGoogleDisplayTiming = 0x0003002E, + // WGPUNativeFeature_VulkanExternalMemoryWin32 = 0x0003002F, + /** + * Enables R64Uint image atomic min and max. + * + * Supported platforms: + * - Vulkan (with VK_EXT_shader_image_atomic_int64) + * - DX12 (with SM 6.6+) + * - Metal (with MSL 3.1+) + * + * This is a native only feature. + */ + WGPUNativeFeature_TextureInt64Atomic = 0x00030030, + // TODO: not implemented yet, see https://github.com/gfx-rs/wgpu/issues/7149 + // WGPUNativeFeature_UniformBufferBindingArrays = 0x00030031, + // TODO: requires wgpu.h api change + // WGPUNativeFeature_MeshShader = 0x00030032, + // WGPUNativeFeature_RayHitVertexReturn = 0x00030033, + // WGPUNativeFeature_MeshShaderMultiview = 0x00030034, + // WGPUNativeFeature_ExtendedAccelerationStructureVertexFormats = 0x00030035, + // WGPUNativeFeature_PassthroughShaders = 0x00030036, + /** + * Enables shader barycentric coordinates. + * + * Supported platforms: + * - Vulkan (with VK_KHR_fragment_shader_barycentric) + * - DX12 (with SM 6.1+) + * - Metal (with MSL 2.2+) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderBarycentrics = 0x00030037, + /** + * Enables using multiview where not all texture array layers are rendered to in a single render pass/render pipeline. Making + * use of this feature also requires enabling `Features::MULTIVIEW`. + * + * Supported platforms + * - Vulkan + * - DX12 + * + * While metal supports this in theory, the behavior of `view_index` differs from vulkan and dx12 so the feature isn't exposed. + */ + WGPUNativeFeature_SelectiveMultiview = 0x00030038, + // TODO: requires wgpu.h api change + // WGPUNativeFeature_MeshShaderPoints = 0x00030039, + WGPUNativeFeature_MultisampleArray = 0x0003003A, + /** + * Enables cooperative matrix operations (also known as tensor cores on NVIDIA GPUs + * or simdgroup matrix operations on Apple GPUs). + * + * Cooperative matrices allow a workgroup to collectively load, store, and perform + * matrix multiply-accumulate operations on small tiles of data, enabling + * hardware-accelerated matrix math. + * + * @b EXPERIMENTAL: Features enabled by this may have major bugs and are + * expected to be subject to breaking changes. + * + * **Current limitations:** The implementation currently only supports 8x8 f32 matrices. + * On Vulkan, support is determined by querying `vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR` + * for configurations matching 8x8x8 f32. Most Vulkan implementations (NVIDIA, AMD) primarily + * support f16 inputs at larger sizes (e.g., 16x16), so Vulkan support may be limited. + * + * Supported platforms: + * - Metal (with MSL 2.3+ and Apple7+/Mac2+, using simdgroup matrix operations) + * - Vulkan (with [VK_KHR_cooperative_matrix](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_cooperative_matrix.html), if 8x8 f32 is supported) + * + * This is a native only feature. + */ + WGPUNativeFeature_CooperativeMatrix = 0x0003003B, + /** + * Enables shader per-vertex attributes. + * + * Supported platforms: + * - Vulkan (with VK_KHR_fragment_shader_barycentric) + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderPerVertex = 0x0003003C, + /** + * Enables shader `draw_index` builtin. + * + * Supported platforms: + * - GLES + * - Vulkan + * + * Potential platforms: + * - DX12 + * - Metal + * + * This is a native only feature. + */ + WGPUNativeFeature_ShaderDrawIndex = 0x0003003D, + /** + * Allows the user to create arrays of acceleration structures in shaders: + * + * ex. + * - `var tlas: binding_array` (WGSL) + * + * This capability allows them to exist and to be indexed by dynamically uniform values. + * + * Supported platforms: + * - DX12 + * - Vulkan + * + * This is a native only feature. + */ + WGPUNativeFeature_AccelerationStructureBindingArray = 0x0003003E, + /** + * Enables the `@coherent` memory decoration on storage buffer variables. + * + * Backend mapping: + * - Vulkan + * - DX12 + * - Metal (3.2+) + * - GLES (ES 3.1+ / GL 4.3+) + * + * This is a native only feature. + */ + WGPUNativeFeature_MemoryDecorationCoherent = 0x0003003F, + /** + * Enables the `@volatile` memory decoration on storage buffer variables. + * + * Backend mapping: + * - Vulkan + * - GLES (ES 3.1+ / GL 4.3+) + * + * This is a native only feature. + */ + WGPUNativeFeature_MemoryDecorationVolatile = 0x00030040, + + WGPUNativeFeature_Force32 = 0x7FFFFFFF +} WGPUNativeFeature; + +typedef enum WGPULogLevel +{ + WGPULogLevel_Off = 0x00000000, + /** Only error messages. */ + WGPULogLevel_Error = 0x00000001, + /** Errors and warnings. */ + WGPULogLevel_Warn = 0x00000002, + /** Errors, warnings, and informational messages. */ + WGPULogLevel_Info = 0x00000003, + /** Errors, warnings, informational, and debug messages. */ + WGPULogLevel_Debug = 0x00000004, + /** All messages, including very verbose trace-level output. */ + WGPULogLevel_Trace = 0x00000005, + WGPULogLevel_Force32 = 0x7FFFFFFF +} WGPULogLevel; + +/** + * Bitflags selecting which graphics backends the @ref WGPUInstance should + * enable. + * + * Pass in the @c backends field of @ref WGPUInstanceExtras. + */ +typedef WGPUFlags WGPUInstanceBackend; +/** All backends (the default when zero-initialized). */ +static const WGPUInstanceBackend WGPUInstanceBackend_All = 0x00000000; +/** + * Vulkan backend. + * Supported on Windows, Linux/Android, and macOS/iOS via Vulkan Portability. + */ +static const WGPUInstanceBackend WGPUInstanceBackend_Vulkan = 1 << 0; +/** + * OpenGL / OpenGL ES backend. + * Supported on Linux/Android, the web via WebGL, and Windows/macOS via ANGLE. + */ +static const WGPUInstanceBackend WGPUInstanceBackend_GL = 1 << 1; +/** + * Metal backend. + * Supported on macOS and iOS. + */ +static const WGPUInstanceBackend WGPUInstanceBackend_Metal = 1 << 2; +/** + * Direct3D 12 backend. + * Supported on Windows 10 and later. + */ +static const WGPUInstanceBackend WGPUInstanceBackend_DX12 = 1 << 3; +/** + * Browser WebGPU backend. + * Supported when targeting the web through WebAssembly. + */ +static const WGPUInstanceBackend WGPUInstanceBackend_BrowserWebGPU = 1 << 5; +/** Primary (first-tier) backends: Vulkan, Metal, DX12, and BrowserWebGPU. */ +static const WGPUInstanceBackend WGPUInstanceBackend_Primary = (1 << 0) | (1 << 2) | (1 << 3) | (1 << 5); +/** Secondary (second-tier) backends: GL. */ +static const WGPUInstanceBackend WGPUInstanceBackend_Secondary = (1 << 1); +static const WGPUInstanceBackend WGPUInstanceBackend_Force32 = 0x7FFFFFFF; + +/** + * Bitflags controlling instance debugging and validation behavior. + * + * These are not part of the WebGPU standard. + * + * Pass in the @c flags field of @ref WGPUInstanceExtras. + */ +typedef WGPUFlags WGPUInstanceFlag; +/** No flags set. */ +static const WGPUInstanceFlag WGPUInstanceFlag_Empty = 0x00000000; +/** + * Generate debug information in shaders and objects. + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_DEBUG environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_Debug = 1 << 0; +/** + * Enable validation in the backend API, if possible: + * + * - On the DX12 backend, this calls @c ID3D12Debug::EnableDebugLayer. + * - On the Vulkan backend, this enables the Vulkan Validation Layers. + * - On the GLES backend (Windows), this enables debug output. + * - On the GLES backend (non-Windows), this calls @c eglDebugMessageControlKHR. + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_VALIDATION environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_Validation = 1 << 1; +/** + * Don't pass labels to the backend API (wgpu-hal). + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_DISCARD_HAL_LABELS environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_DiscardHalLabels = 1 << 2; +/** + * Whether wgpu should expose adapters that run on top of non-compliant + * adapters. + * + * Turning this on might mean that some of the functionality provided by the + * wgpu adapter/device is not working or broken. This mainly applies to a + * Vulkan driver's compliance version. If the major compliance version is 0, + * then the driver is ignored unless this flag is set. + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_ALLOW_UNDERLYING_NONCOMPLIANT_ADAPTER environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_AllowUnderlyingNoncompliantAdapter = 1 << 3; +/** + * Enable GPU-based validation. Implies @ref WGPUInstanceFlag_Validation. + * Currently only changes behavior on the DX12 and Vulkan backends. + * + * - D3D12: Called "GPU-based validation" (GBV). + * - Vulkan: Called "GPU-Assisted Validation" via VK_LAYER_KHRONOS_validation. + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_GPU_BASED_VALIDATION environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_GPUBasedValidation = 1 << 4; +/** + * Validate indirect buffer content prior to issuing indirect draws/dispatches. + * + * This validation will transform indirect calls into no-ops if they are not + * valid. For example, @c dispatch_workgroups_indirect arguments must be less + * than the @c max_compute_workgroups_per_dimension device limit. + * + * When using @ref WGPUInstanceFlag_WithEnv, takes value from the + * @c WGPU_VALIDATION_INDIRECT_CALL environment variable. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_ValidationIndirectCall = 1 << 5; +/** + * Enable automatic timestamp normalization. When enabled, + * @c wgpuCommandEncoderResolveQuerySet will automatically normalize timestamps + * to nanoseconds instead of returning raw timestamp values. + * + * This introduces a compute shader into the resolution of query sets. When + * enabled, the timestamp period returned by @ref wgpuQueueGetTimestampPeriod + * will always be @c 1.0. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_AutomaticTimestampNormalization = 1 << 6; +/** + * Use the default flags for the current build configuration. + * In debug builds, this typically enables @ref WGPUInstanceFlag_Debug and + * @ref WGPUInstanceFlag_Validation. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_Default = 1 << 24; +/** + * Convenience alias that enables @ref WGPUInstanceFlag_Debug and + * @ref WGPUInstanceFlag_Validation. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_Debugging = 1 << 25; +/** + * Convenience alias that enables @ref WGPUInstanceFlag_Debug, + * @ref WGPUInstanceFlag_Validation, and + * @ref WGPUInstanceFlag_GPUBasedValidation. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_AdvancedDebugging = 1 << 26; +/** + * Modify the flags based on environment variables. Flags with environment + * variable support (e.g. @c WGPU_DEBUG, @c WGPU_VALIDATION) will be read + * from the process environment and applied on top of the explicitly set flags. + */ +static const WGPUInstanceFlag WGPUInstanceFlag_WithEnv = 1 << 27; +static const WGPUInstanceFlag WGPUInstanceFlag_Force32 = 0x7FFFFFFF; + +typedef enum WGPUDx12Compiler +{ + WGPUDx12Compiler_Undefined = 0x00000000, + /** + * Use the FXC (D3DCompile) shader compiler. + * + * The FXC compiler is old, slow, and unmaintained. However, it doesn't + * require any additional DLLs to be shipped with the application. + */ + WGPUDx12Compiler_Fxc = 0x00000001, + /** + * Use the DXC (DirectX Shader Compiler). + * + * The DXC compiler is new, fast, and maintained. However, it requires + * @c dxcompiler.dll to be available. The path to this DLL can be + * specified via @ref WGPUInstanceExtras::dxcPath. + * + * Minimum supported version: v1.8.2502. + * Requires WDDM 2.1 (Windows 10 version 1607). + */ + WGPUDx12Compiler_Dxc = 0x00000002, + WGPUDx12Compiler_Force32 = 0x7FFFFFFF +} WGPUDx12Compiler; + +typedef enum WGPUGles3MinorVersion +{ + WGPUGles3MinorVersion_Automatic = 0x00000000, + /** Request an ES 3.0 context. */ + WGPUGles3MinorVersion_Version0 = 0x00000001, + /** Request an ES 3.1 context. */ + WGPUGles3MinorVersion_Version1 = 0x00000002, + /** Request an ES 3.2 context. */ + WGPUGles3MinorVersion_Version2 = 0x00000003, + WGPUGles3MinorVersion_Force32 = 0x7FFFFFFF +} WGPUGles3MinorVersion; + +typedef enum WGPUPipelineStatisticName +{ + WGPUPipelineStatisticName_VertexShaderInvocations = 0x00000000, + /** + * Number of times the clipper is invoked. This is also the number of + * triangles output by the vertex shader. + */ + WGPUPipelineStatisticName_ClipperInvocations = 0x00000001, + /** + * Number of primitives that are not culled by the clipper. This is the + * number of triangles that are actually on screen and will be rasterized + * and rendered. + */ + WGPUPipelineStatisticName_ClipperPrimitivesOut = 0x00000002, + /** + * Number of times the fragment shader is invoked. Accounts for fragment + * shaders running in 2x2 blocks in order to get derivatives. + */ + WGPUPipelineStatisticName_FragmentShaderInvocations = 0x00000003, + /** + * Number of times a compute shader is invoked. This will be equivalent + * to the dispatch count times the workgroup size. + */ + WGPUPipelineStatisticName_ComputeShaderInvocations = 0x00000004, + WGPUPipelineStatisticName_Force32 = 0x7FFFFFFF +} WGPUPipelineStatisticName WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUNativeQueryType +{ + WGPUNativeQueryType_PipelineStatistics = 0x00030000, + WGPUNativeQueryType_Force32 = 0x7FFFFFFF +} WGPUNativeQueryType WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUDxcMaxShaderModel +{ + WGPUDxcMaxShaderModel_V6_0 = 0x00000000, + /** Shader Model 6.1 */ + WGPUDxcMaxShaderModel_V6_1 = 0x00000001, + /** Shader Model 6.2 */ + WGPUDxcMaxShaderModel_V6_2 = 0x00000002, + /** Shader Model 6.3 */ + WGPUDxcMaxShaderModel_V6_3 = 0x00000003, + /** Shader Model 6.4 */ + WGPUDxcMaxShaderModel_V6_4 = 0x00000004, + /** Shader Model 6.5 */ + WGPUDxcMaxShaderModel_V6_5 = 0x00000005, + /** Shader Model 6.6 */ + WGPUDxcMaxShaderModel_V6_6 = 0x00000006, + /** Shader Model 6.7 */ + WGPUDxcMaxShaderModel_V6_7 = 0x00000007, + WGPUDxcMaxShaderModel_Force32 = 0x7FFFFFFF +} WGPUDxcMaxShaderModel; + +typedef enum WGPUGLFenceBehaviour +{ + WGPUGLFenceBehaviour_Normal = 0x00000000, + /** + * Fences are short-circuited to always report completion immediately. + * + * This solves a specific issue that arose due to a bug in wgpu-core that + * made many WebGL programs work when they shouldn't have. If you have + * code that calls @ref wgpuDevicePoll with @c wait=true on WebGL, you + * may need to enable this option for "wait" to behave how you expect. + * + * When this is set, @c wgpuQueueOnCompletedWorkDone callbacks will fire + * the next time the device is polled, not when work is actually done on + * the GPU. + */ + WGPUGLFenceBehaviour_AutoFinish = 0x00000001, + WGPUGLFenceBehaviour_Force32 = 0x7FFFFFFF +} WGPUGLFenceBehaviour; + +typedef enum WGPUDx12SwapchainKind +{ + WGPUDx12SwapchainKind_Undefined = 0x00000000, + /** + * Use a DXGI swapchain created directly from the window's HWND. + * + * This does not support transparency but has better support from + * developer tooling such as RenderDoc. + */ + WGPUDx12SwapchainKind_DxgiFromHwnd = 0x00000001, + /** + * Use a DXGI swapchain created from a DirectComposition visual made + * automatically from the window's HWND. + * + * This creates a single @c IDCompositionVisual over the entire window. + * Supports transparency. If you want to manage the composition tree + * yourself, create your own device and composition and pass the relevant + * visual via the surface target. + */ + WGPUDx12SwapchainKind_DxgiFromVisual = 0x00000002, + WGPUDx12SwapchainKind_Force32 = 0x7FFFFFFF +} WGPUDx12SwapchainKind; + +/** + * Discriminant for @ref WGPUNativeDisplayHandle. + * + * Identifies which platform's display connection is stored in the tagged union. + * Use @ref WGPUNativeDisplayHandleType_None (the default when zero-initialized) when + * no display handle is needed. Platforms with no display connection data (Windows, + * macOS, iOS, Android) should use @ref WGPUNativeDisplayHandleType_None. + */ +typedef enum WGPUNativeDisplayHandleType +{ + /** No display handle provided. */ + WGPUNativeDisplayHandleType_None = 0x00000000, + /** X11 display connection via Xlib. See @ref WGPUXlibDisplayHandle. */ + WGPUNativeDisplayHandleType_Xlib = 0x00000001, + /** X11 display connection via XCB. See @ref WGPUXcbDisplayHandle. */ + WGPUNativeDisplayHandleType_Xcb = 0x00000002, + /** Wayland display connection. See @ref WGPUWaylandDisplayHandle. */ + WGPUNativeDisplayHandleType_Wayland = 0x00000003, + WGPUNativeDisplayHandleType_Force32 = 0x7FFFFFFF +} WGPUNativeDisplayHandleType; + +/** + * Xlib display connection data for @ref WGPUNativeDisplayHandle. + */ +typedef struct WGPUXlibDisplayHandle +{ + /** Pointer to the X11 @c Display (i.e. @c Display*). Must not be NULL. */ + void *display; + /** X11 screen number. */ + int screen; +} WGPUXlibDisplayHandle; + +/** + * XCB display connection data for @ref WGPUNativeDisplayHandle. + */ +typedef struct WGPUXcbDisplayHandle +{ + /** Pointer to the XCB connection (i.e. @c xcb_connection_t*). Must not be NULL. */ + void *connection; + /** X11 screen number. */ + int screen; +} WGPUXcbDisplayHandle; + +/** + * Wayland display connection data for @ref WGPUNativeDisplayHandle. + */ +typedef struct WGPUWaylandDisplayHandle +{ + /** Pointer to the Wayland display (i.e. @c wl_display*). Must not be NULL. */ + void *display; +} WGPUWaylandDisplayHandle; + +/** + * Platform display connection, passed as a field of @ref WGPUInstanceExtras. + * + * This is a tagged union. Set @c type to indicate which variant is active, then + * populate the corresponding field in @c data. Zero-initialization yields + * @ref WGPUNativeDisplayHandleType_None, meaning no display handle is provided. + * + * Currently required by the GLES backend when presenting on Wayland. Other + * backends ignore this field. If the instance is created with a display handle, + * all surfaces created from it must use the same display connection. + */ +typedef struct WGPUNativeDisplayHandle +{ + WGPUNativeDisplayHandleType type; + union + { + WGPUXlibDisplayHandle xlib; + WGPUXcbDisplayHandle xcb; + WGPUWaylandDisplayHandle wayland; + } data; +} WGPUNativeDisplayHandle; + +typedef struct WGPUInstanceExtras +{ + WGPUChainedStruct chain; + /** + * Which backends to enable. + * Zero (@ref WGPUInstanceBackend_All) enables all backends. + */ + WGPUInstanceBackend backends; + /** + * Flags controlling debug/validation behavior. + * See @ref WGPUInstanceFlag for available flags. + */ + WGPUInstanceFlag flags; + /** + * Which DX12 shader compiler to use. + * See @ref WGPUDx12Compiler. Ignored on non-DX12 backends. + */ + WGPUDx12Compiler dx12ShaderCompiler; + /** + * Which OpenGL ES 3 minor version to request. + * See @ref WGPUGles3MinorVersion. Ignored on non-GL backends. + */ + WGPUGles3MinorVersion gles3MinorVersion; + /** + * Controls OpenGL fence synchronization behavior. + * See @ref WGPUGLFenceBehaviour. Ignored on non-GL backends. + */ + WGPUGLFenceBehaviour glFenceBehaviour; + /** + * File system path to @c dxcompiler.dll for dynamic DXC loading. + * Only used when @c dx12ShaderCompiler is @ref WGPUDx12Compiler_Dxc. + * An empty/undefined string view means the DLL will be searched for + * on the system PATH. + */ + WGPUStringView dxcPath; + /** + * Maximum HLSL shader model version that DXC should target. + * See @ref WGPUDxcMaxShaderModel. Only used with the DXC compiler. + */ + WGPUDxcMaxShaderModel dxcMaxShaderModel; + /** + * Which DX12 presentation system (swapchain kind) to use. + * See @ref WGPUDx12SwapchainKind. Ignored on non-DX12 backends. + */ + WGPUDx12SwapchainKind dx12PresentationSystem; + + WGPU_NULLABLE const uint8_t *budgetForDeviceCreation; + WGPU_NULLABLE const uint8_t *budgetForDeviceLoss; + + /** + * Platform display connection to associate with this instance. + * Zero-initialized yields @ref WGPUNativeDisplayHandleType_None (no handle). + */ + WGPUNativeDisplayHandle displayHandle; +} WGPUInstanceExtras; + +typedef struct WGPUDeviceExtras +{ + WGPUChainedStruct chain; + /** + * File system path for API trace output. + * + * When set to a non-empty path, wgpu will record all API calls to + * the given directory, which can later be replayed for debugging. + * An empty/undefined string view disables tracing. + */ + WGPUStringView tracePath; +} WGPUDeviceExtras; + +typedef struct WGPUNativeLimits +{ + /** This struct chain is used as mutable in some places and immutable in others. */ + WGPUChainedStruct chain; + /** + * Maximum number of live non-sampler bindings. + * + * Default is 1,000,000. Only meaningful on D3D12. + * + * @b Warning: On integrated GPUs, large values can cause significant + * system RAM consumption. + */ + uint32_t maxNonSamplerBindings; + /** + * Maximum number of individual resources within binding arrays that can be accessed + * in a single shader stage. Applies to all types of bindings except samplers. + */ + uint32_t maxBindingArrayElementsPerShaderStage; + /** + * Maximum number of individual samplers within binding arrays that + * can be accessed in a single shader stage. + */ + uint32_t maxBindingArraySamplerElementsPerShaderStage; + /** + * The maximum number of views that can be used in multiview rendering. + */ + uint32_t maxMultiviewViewCount; +} WGPUNativeLimits; + +#define WGPU_NATIVE_LIMITS_INIT _wgpu_MAKE_INIT_STRUCT(WGPUNativeLimits, { \ + /*.chain=*/_wgpu_MAKE_INIT_STRUCT(WGPUChainedStruct, { \ + /*.next=*/NULL _wgpu_COMMA \ + /*.sType=*/(WGPUSType)WGPUSType_NativeLimits _wgpu_COMMA \ + }) _wgpu_COMMA \ + /*.maxNonSamplerBindings=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBindingArrayElementsPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxBindingArraySamplerElementsPerShaderStage=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ + /*.maxMultiviewViewCount=*/WGPU_LIMIT_U32_UNDEFINED _wgpu_COMMA \ +}) + +/** + * Identifier for a particular call to @ref wgpuQueueSubmitForIndex. + * + * Can be passed to @ref wgpuDevicePoll to block until a particular + * submission has finished execution. + * + * This type is unique to wgpu-native; there is no analogue in the + * WebGPU specification. + */ +typedef uint64_t WGPUSubmissionIndex; + +typedef struct WGPUShaderDefine +{ + WGPUStringView name; + /** The value of the preprocessor macro (e.g. @c "1"). */ + WGPUStringView value; +} WGPUShaderDefine; + +typedef struct WGPUShaderSourceGLSL +{ + WGPUChainedStruct chain; + /** The shader stage this GLSL source targets. */ + WGPUShaderStage stage; + /** GLSL source code. */ + WGPUStringView code; + /** Number of entries in @c defines. */ + uint32_t defineCount; + WGPUShaderDefine const *defines; +} WGPUShaderSourceGLSL; + +typedef struct WGPUShaderModuleDescriptorSpirV +{ + WGPUStringView label; + /** Number of 32-bit words in @c source. */ + uint32_t sourceSize; + uint32_t const *source; +} WGPUShaderModuleDescriptorSpirV; + +typedef struct WGPURegistryReport +{ + size_t numAllocated; + size_t numKeptFromUser; + size_t numReleasedFromUser; + size_t elementSize; +} WGPURegistryReport; + +typedef struct WGPUHubReport +{ + WGPURegistryReport adapters; + WGPURegistryReport devices; + WGPURegistryReport queues; + WGPURegistryReport pipelineLayouts; + WGPURegistryReport shaderModules; + WGPURegistryReport bindGroupLayouts; + WGPURegistryReport bindGroups; + WGPURegistryReport commandBuffers; + WGPURegistryReport renderBundles; + WGPURegistryReport renderPipelines; + WGPURegistryReport computePipelines; + WGPURegistryReport pipelineCaches; + WGPURegistryReport querySets; + WGPURegistryReport buffers; + WGPURegistryReport textures; + WGPURegistryReport textureViews; + WGPURegistryReport samplers; +} WGPUHubReport; + +typedef struct WGPUGlobalReport +{ + WGPURegistryReport surfaces; + /** Statistics for all other resource types, grouped by backend hub. */ + WGPUHubReport hub; +} WGPUGlobalReport; + +typedef struct WGPUInstanceEnumerateAdapterOptions +{ + WGPUChainedStruct const *nextInChain; + WGPUInstanceBackend backends; +} WGPUInstanceEnumerateAdapterOptions; + +typedef struct WGPUBindGroupEntryExtras +{ + WGPUChainedStruct chain; + WGPUBuffer const *buffers; + size_t bufferCount; + WGPUSampler const *samplers; + size_t samplerCount; + WGPUTextureView const *textureViews; + size_t textureViewCount; +} WGPUBindGroupEntryExtras; + +typedef struct WGPUBindGroupLayoutEntryExtras +{ + WGPUChainedStruct chain; + /** + * Number of resources in this binding array slot. Corresponds to the + * array size in the shader (e.g. @c binding_array). + */ + uint32_t count; +} WGPUBindGroupLayoutEntryExtras; + +typedef struct WGPUQuerySetDescriptorExtras +{ + WGPUChainedStruct chain; + WGPUPipelineStatisticName const *pipelineStatistics; + size_t pipelineStatisticCount; +} WGPUQuerySetDescriptorExtras WGPU_STRUCTURE_ATTRIBUTE; + +typedef struct WGPUSurfaceConfigurationExtras +{ + WGPUChainedStruct chain; + /** + * Desired maximum number of frames in flight (i.e. the number of monitor + * refreshes between @c wgpuSurfaceGetCurrentTexture and presentation). + * + * - 1: Minimize latency (CPU and GPU cannot run in parallel). + * - 2: Balance between latency and throughput (the default). + * - 3+: Maximize throughput. + */ + uint32_t desiredMaximumFrameLatency; +} WGPUSurfaceConfigurationExtras WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Chained in @ref WGPUSurfaceDescriptor to make a @ref WGPUSurface wrapping a WinUI [`SwapChainPanel`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.controls.swapchainpanel). + */ +typedef struct WGPUSurfaceSourceSwapChainPanel +{ + WGPUChainedStruct chain; + /** + * A pointer to the [`ISwapChainPanelNative`](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/win32/microsoft.ui.xaml.media.dxinterop/nn-microsoft-ui-xaml-media-dxinterop-iswapchainpanelnative) + * interface of the SwapChainPanel that will be wrapped by the @ref WGPUSurface. + */ + void *panelNative; +} WGPUSurfaceSourceSwapChainPanel WGPU_STRUCTURE_ATTRIBUTE; + +typedef enum WGPUPolygonMode +{ + WGPUPolygonMode_Fill = 0, + /** + * Polygons are drawn as line segments (wireframe). + * Requires @ref WGPUNativeFeature_PolygonModeLine. + */ + WGPUPolygonMode_Line = 1, + /** + * Polygons are drawn as points (vertices only). + * Requires @ref WGPUNativeFeature_PolygonModePoint. + */ + WGPUPolygonMode_Point = 2, +} WGPUPolygonMode; + +typedef struct WGPUPrimitiveStateExtras +{ + WGPUChainedStruct chain; + /** + * Controls the way each polygon is rasterized. + * See @ref WGPUPolygonMode. Defaults to @ref WGPUPolygonMode_Fill. + */ + WGPUPolygonMode polygonMode; + /** + * If set to true, the primitives are rendered with conservative + * overestimation. Only valid when @c polygonMode is + * @ref WGPUPolygonMode_Fill. + * Requires @ref WGPUNativeFeature_ConservativeRasterization. + */ + WGPUBool conservative; +} WGPUPrimitiveStateExtras WGPU_STRUCTURE_ATTRIBUTE; + +typedef void (*WGPULogCallback)(WGPULogLevel level, WGPUStringView message, void *userdata); + +typedef enum WGPUNativeTextureFormat +{ + /** + * YUV 4:2:0 chroma subsampled format (NV12). + * Plane 0 contains R8Unorm luminance (Y), Plane 1 contains Rg8Unorm + * chrominance (UV) at half width and half height. + * Requires @ref WGPUNativeFeature_TextureFormatNv12. + */ + WGPUNativeTextureFormat_NV12 = 0x00030007, + /** + * YUV 4:2:0 with 10 bits used from 16-bit channels (P010). + * Plane 0 contains R16Unorm luminance (Y), Plane 1 contains Rg16Unorm + * chrominance (UV) at half width and half height. + */ + WGPUNativeTextureFormat_P010 = 0x00030008, +} WGPUNativeTextureFormat; + +typedef struct WGPUImageSubresourceRange { + WGPUTextureAspect aspect; + uint32_t baseMipLevel; + uint32_t mipLevelCount; + uint32_t baseArrayLayer; + uint32_t arrayLayerCount; +} WGPUImageSubresourceRange WGPU_STRUCTURE_ATTRIBUTE; + +/** + * Describes how shader bound checks should be performed. + */ +typedef WGPUFlags WGPUShaderRuntimeChecks; + +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_None = 0x0000000000000000; +/** + * Enforce bounds checks in shaders, even if the underlying driver doesn’t support doing so natively. + */ +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_BoundsChecks = 0x0000000000000001; +/** + * If not set, the caller MUST ensure that all passed shaders do not contain any infinite loops. + */ +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_ForceLoopBounding = 0x0000000000000002; +/** + * If not set, the caller MUST ensure that in all passed shaders every function operating on a ray + * query must obey these rules (functions using wgsl naming). + */ +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_RayQueryInitializationTracking = 0x0000000000000004; +/** + * If not set, task shaders will not validate that the mesh shader grid they dispatch is within legal limits. + */ +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_TaskShaderDispatchTracking = 0x0000000000000008; +/** + * If not set, mesh shaders won’t clamp the output primitives’ vertex indices, which can lead to + * undefined behavior and arbitrary memory access. + */ +static const WGPUShaderRuntimeChecks WGPUShaderRuntimeChecks_MeshShaderPrimitiveIndicesClamp = 0x0000000000000010; + +typedef enum WGPUNativeAddressMode +{ + WGPUNativeAddressMode_ClampToBorder = 0x00000004, + WGPUNativeAddressMode_Force32 = 0x7FFFFFFF +} WGPUNativeAddressMode WGPU_ENUM_ATTRIBUTE; + +typedef enum WGPUSamplerBorderColor +{ + WGPUSamplerBorderColor_Undefined = 0x00000000, + WGPUSamplerBorderColor_TransparentBlack = 0x00000001, + WGPUSamplerBorderColor_OpaqueBlack = 0x00000002, + WGPUSamplerBorderColor_OpaqueWhite = 0x00000003, + WGPUSamplerBorderColor_Zero = 0x00000004, + WGPUSamplerBorderColor_Force32 = 0x7FFFFFFF +} WGPUSamplerBorderColor; + +typedef struct WGPUSamplerDescriptorExtras { + WGPUChainedStruct chain; + WGPUSamplerBorderColor samplerBorderColor; +} WGPUSamplerDescriptorExtras WGPU_STRUCTURE_ATTRIBUTE; + +#ifdef __cplusplus +extern "C" +{ +#endif + + void wgpuGenerateReport(WGPUInstance instance, WGPUGlobalReport *report); + size_t wgpuInstanceEnumerateAdapters(WGPUInstance instance, WGPU_NULLABLE WGPUInstanceEnumerateAdapterOptions const *options, WGPUAdapter *adapters); + + WGPUSubmissionIndex wgpuQueueSubmitForIndex(WGPUQueue queue, size_t commandCount, WGPUCommandBuffer const *commands); + float wgpuQueueGetTimestampPeriod(WGPUQueue queue); + + // Returns true if the queue is empty, or false if there are more queue submissions still in flight. + WGPUBool wgpuDevicePoll(WGPUDevice device, WGPUBool wait, WGPU_NULLABLE WGPUSubmissionIndex const *submissionIndex); + WGPUShaderModule wgpuDeviceCreateShaderModuleSpirV(WGPUDevice device, WGPUShaderModuleDescriptorSpirV const *descriptor); + + void wgpuSetLogCallback(WGPULogCallback callback, void *userdata); + + void wgpuSetLogLevel(WGPULogLevel level); + + uint32_t wgpuGetVersion(void); + + /** + * Returns the backend-native `id` as an opaque pointer. + * + * The returned pointer is borrowed and remains valid only while `device` is alive. + * Ownership is retained by wgpu-native; callers must not release or destroy it. + * Returns NULL when the active backend is not Metal or when the handle is unavailable. + */ + void *wgpuDeviceGetNativeMetalDevice(WGPUDevice device); + + /** + * Returns the backend-native `id` as an opaque pointer. + * + * The returned pointer is borrowed and remains valid only while `queue` is alive. + * Ownership is retained by wgpu-native; callers must not release or destroy it. + * Returns NULL when the active backend is not Metal or when the handle is unavailable. + */ + void *wgpuQueueGetNativeMetalCommandQueue(WGPUQueue queue); + + /** + * Returns the backend-native `id` as an opaque pointer. + * + * The returned pointer is borrowed and remains valid only while `texture` is alive. + * Ownership is retained by wgpu-native; callers must not release or destroy it. + * Returns NULL when the active backend is not Metal or when the handle is unavailable. + */ + void *wgpuTextureGetNativeMetalTexture(WGPUTexture texture); + + void wgpuRenderPassEncoderMultiDrawIndirect(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, uint32_t count); + void wgpuRenderPassEncoderMultiDrawIndexedIndirect(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, uint32_t count); + + void wgpuRenderPassEncoderMultiDrawIndirectCount(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, WGPUBuffer count_buffer, uint64_t count_buffer_offset, uint32_t max_count); + void wgpuRenderPassEncoderMultiDrawIndexedIndirectCount(WGPURenderPassEncoder encoder, WGPUBuffer buffer, uint64_t offset, WGPUBuffer count_buffer, uint64_t count_buffer_offset, uint32_t max_count); + + void wgpuComputePassEncoderBeginPipelineStatisticsQuery(WGPUComputePassEncoder computePassEncoder, WGPUQuerySet querySet, uint32_t queryIndex); + void wgpuComputePassEncoderEndPipelineStatisticsQuery(WGPUComputePassEncoder computePassEncoder); + void wgpuRenderPassEncoderBeginPipelineStatisticsQuery(WGPURenderPassEncoder renderPassEncoder, WGPUQuerySet querySet, uint32_t queryIndex); + void wgpuRenderPassEncoderEndPipelineStatisticsQuery(WGPURenderPassEncoder renderPassEncoder); + + void wgpuComputePassEncoderWriteTimestamp(WGPUComputePassEncoder computePassEncoder, WGPUQuerySet querySet, uint32_t queryIndex); + void wgpuRenderPassEncoderWriteTimestamp(WGPURenderPassEncoder renderPassEncoder, WGPUQuerySet querySet, uint32_t queryIndex); + + // Returns true if the capture was successfully started, or false if it failed to start or is not supported on the current platform. + WGPUBool wgpuDeviceStartGraphicsDebuggerCapture(WGPUDevice device); + void wgpuDeviceStopGraphicsDebuggerCapture(WGPUDevice device); + + void wgpuCommandEncoderClearTexture(WGPUCommandEncoder commandEncoder, WGPUTexture texture, WGPUImageSubresourceRange const * range); + + WGPUShaderModule wgpuDeviceCreateShaderModuleTrusted(WGPUDevice device, WGPUShaderModuleDescriptor const * descriptor, WGPUShaderRuntimeChecks runtimeChecks); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/NativeTypeNameAttribute.cs b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/NativeTypeNameAttribute.cs new file mode 100644 index 000000000..51c01dbb6 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/NativeTypeNameAttribute.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Records the original C type name associated with a generated managed declaration. +/// +[AttributeUsage( + AttributeTargets.Struct | + AttributeTargets.Enum | + AttributeTargets.Property | + AttributeTargets.Field | + AttributeTargets.Parameter | + AttributeTargets.ReturnValue, + AllowMultiple = false, + Inherited = true)] +[Conditional("DEBUG")] +internal sealed class NativeTypeNameAttribute : Attribute +{ + private readonly string name; + + /// + /// Initializes a new instance of the class. + /// + /// The native C type name. + public NativeTypeNameAttribute(string name) => this.name = name; + + /// + /// Gets the native C type name. + /// + public string Name => this.name; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/Bindings/README.md b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/README.md new file mode 100644 index 000000000..33ffa38fb --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/Bindings/README.md @@ -0,0 +1,50 @@ +# WebGPU native bindings + +The checked-in bindings are generated from the `webgpu.h` and `wgpu.h` headers shipped with +the configured wgpu-native release. They are internal implementation details of the WebGPU backend. + +Regenerate them from the repository root with: + +```powershell +dotnet msbuild src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj ` + -t:GenerateWebGPUBindings ` + -p:Configuration=Release +``` + +Download the matching native release assets and refresh the headers with: + +```powershell +dotnet msbuild src/ImageSharp.Drawing.WebGPU/ImageSharp.Drawing.WebGPU.csproj ` + -t:DownloadWebGPUNative ` + -p:Configuration=Release +``` + +The target restores the project-scoped ClangSharp tool at its pinned version and writes +`Generated/WebGPUNative.g.cs`. Normal builds compile the checked-in output without restoring or +running ClangSharp. + +Generated imports use wgpu-native's upstream `wgpu_native` library name. Platform-specific native +assets retain their upstream filenames so the runtime's standard native-library resolution selects +`wgpu_native.dll`, `libwgpu_native.so`, or `libwgpu_native.dylib` as appropriate. + +The downloader verifies the configured release's published SHA-256 digest before copying each asset +into `runtimes//native`. Its archive cache lives under `obj/WebGPUNative` and is not packaged. + +Windows runtime directories also contain `dxcompiler.dll` and `dxil.dll` from the configured +Microsoft DirectX Shader Compiler package. The Windows backend explicitly selects DX12 and this +packaged compiler because wgpu-native's legacy compiler makes the staged fine shader prohibitively +slow to compile. Packaging both DLLs keeps compiler selection independent of the developer +machine's SDK. + +The iOS device and simulator assets are static libraries in the same standard runtime layout. The +.NET iOS SDK selects the library matching the application's runtime identifier and links it as a +static native library. A separate `NativeReference` must not be added because that would submit the +same archive to the native linker twice. + +On Windows, Visual Studio C++ Build Tools and a Windows SDK are required because libclang uses their +C standard-library headers while parsing the WebGPU headers. The generator script locates the +newest installed toolset and SDK; those paths do not become part of the generated source. + +Do not edit the generated file. When changing the wgpu-native version, replace both headers from +the same official release before regenerating so the managed structure layouts and function +signatures remain ABI-compatible with the native library. diff --git a/src/ImageSharp.Drawing.WebGPU/Native/NativeModuleLocator.cs b/src/ImageSharp.Drawing.WebGPU/Native/NativeModuleLocator.cs new file mode 100644 index 000000000..443f8a302 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/NativeModuleLocator.cs @@ -0,0 +1,175 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using FilePath = System.IO.Path; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Locates this library's own loaded module on disk. +/// +/// +/// +/// Under Native AOT this code is compiled into a shared library or executable image, and when +/// that image is a plugin loaded by a foreign host, +/// points at the host process rather than the image. Anchoring native-asset probing to the +/// image itself keeps resolution correct there. Under JIT hosting managed code does not live +/// inside a loadable image, so resolution reports failure and callers fall back to standard +/// probing. +/// +/// +/// The runtime provides no managed API for recovering a loaded image's path; the mechanism +/// here follows and the +/// address-anchored approach demonstrated in +/// . +/// +/// +internal static unsafe partial class NativeModuleLocator +{ + /// + /// GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT from libloaderapi.h. The + /// returned handle does not take a reference on the module, so it must not be freed. + /// + private const uint GetModuleHandleExFlagUnchangedRefCount = 0x00000002; + + /// + /// GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS from libloaderapi.h. The address + /// argument is any address inside the target module rather than a module name. + /// + private const uint GetModuleHandleExFlagFromAddress = 0x00000004; + + /// + /// Attempts to resolve the directory of the image containing this library's code. + /// + /// Receives the image directory. + /// when the image path could be resolved. + public static bool TryGetModuleDirectory(out string directory) + { + directory = string.Empty; + delegate* managed anchor = &Anchor; + + string modulePath; + if (OperatingSystem.IsWindows()) + { + if (GetModuleHandleExW(GetModuleHandleExFlagFromAddress | GetModuleHandleExFlagUnchangedRefCount, (nint)anchor, out nint module) == 0) + { + return false; + } + + modulePath = GetModuleFilePath(module); + } + else + { + // dladdr reports the image containing an address. Resolving it through the process + // exports avoids binding to a platform-specific libdl name. + if (!NativeLibrary.TryGetExport(NativeLibrary.GetMainProgramHandle(), "dladdr", out nint export)) + { + return false; + } + + delegate* unmanaged dladdr = (delegate* unmanaged)export; + DlInfo info; + if (dladdr(anchor, &info) == 0 || info.FileName is null) + { + return false; + } + + modulePath = Marshal.PtrToStringUTF8((nint)info.FileName) ?? string.Empty; + } + + if (modulePath.Length == 0) + { + return false; + } + + string? parent = FilePath.GetDirectoryName(modulePath); + if (parent is null || parent.Length == 0) + { + return false; + } + + directory = parent; + return true; + } + + /// + /// Reads the file path of a loaded native module. + /// + /// The loaded module handle. + /// The module's file path, or an empty string when it cannot be read. + public static string GetModuleFilePath(nint module) + { + Span buffer = stackalloc char[260]; + fixed (char* bufferPointer = buffer) + { + uint length = GetModuleFileNameW(module, bufferPointer, (uint)buffer.Length); + if (length > 0 && length < buffer.Length) + { + return new string(buffer[..(int)length]); + } + } + + // Cold fallback for paths beyond MAX_PATH up to the documented Windows maximum. + char[] largeBuffer = new char[short.MaxValue]; + fixed (char* largePointer = largeBuffer) + { + uint length = GetModuleFileNameW(module, largePointer, (uint)largeBuffer.Length); + return length > 0 && length < largeBuffer.Length ? new string(largeBuffer, 0, (int)length) : string.Empty; + } + } + + /// + /// Provides an address that lives inside this library's compiled image. + /// + private static void Anchor() + { + } + + /// + /// Reads the file path of a loaded module into the supplied buffer. + /// + /// The loaded module handle. + /// The buffer receiving the path. + /// The buffer length in characters. + /// The number of characters written, or zero on failure. + [LibraryImport("kernel32", EntryPoint = "GetModuleFileNameW", SetLastError = true)] + private static partial uint GetModuleFileNameW(nint module, char* filename, uint size); + + /// + /// Retrieves a module handle for the image containing the supplied address. + /// + /// The handle retrieval flags. + /// An address inside the target image. + /// Receives the module handle. + /// A non-zero value on success. + [LibraryImport("kernel32", EntryPoint = "GetModuleHandleExW", SetLastError = true)] + private static partial int GetModuleHandleExW(uint flags, nint address, out nint module); + + /// + /// Image information reported by dladdr. + /// + [StructLayout(LayoutKind.Sequential)] + private struct DlInfo + { + /// + /// The path of the image containing the queried address. + /// + public byte* FileName; + + /// + /// The base address of the image. + /// + public void* BaseAddress; + + /// + /// The name of the nearest symbol. + /// + public byte* SymbolName; + + /// + /// The address of the nearest symbol. + /// + public void* SymbolAddress; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WGPUExtent3D.cs b/src/ImageSharp.Drawing.WebGPU/Native/WGPUExtent3D.cs new file mode 100644 index 000000000..7a815c47a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WGPUExtent3D.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +internal partial struct WGPUExtent3D +{ + /// + /// Initializes a new instance of the struct. + /// + /// The texture width. + /// The texture height. + /// The texture depth or array-layer count. + public WGPUExtent3D(uint width, uint height, uint depthOrArrayLayers) + { + this.width = width; + this.height = height; + this.depthOrArrayLayers = depthOrArrayLayers; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WGPUOrigin3D.cs b/src/ImageSharp.Drawing.WebGPU/Native/WGPUOrigin3D.cs new file mode 100644 index 000000000..5f2323bd3 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WGPUOrigin3D.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +internal partial struct WGPUOrigin3D +{ + /// + /// Initializes a new instance of the struct. + /// + /// The X coordinate. + /// The Y coordinate. + /// The Z coordinate. + public WGPUOrigin3D(uint x, uint y, uint z) + { + this.x = x; + this.y = y; + this.z = z; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WGPUStringView.cs b/src/ImageSharp.Drawing.WebGPU/Native/WGPUStringView.cs new file mode 100644 index 000000000..d6fd6f0c8 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WGPUStringView.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Text; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +internal unsafe partial struct WGPUStringView +{ + /// + /// Initializes a new instance of the struct over a caller-owned UTF-8 byte sequence. + /// + /// The first UTF-8 byte, or for a null string view. + /// The byte length, or when is null-terminated. + public WGPUStringView(byte* data, nuint length) + { + this.data = (sbyte*)data; + this.length = length; + } + + /// + /// Creates a null-terminated string view over caller-owned UTF-8 bytes. + /// + /// The first UTF-8 byte, or for a null string view. + public static implicit operator WGPUStringView(byte* data) + => new(data, nuint.MaxValue); + + /// + /// Decodes the UTF-8 bytes represented by this view. + /// + /// The decoded text, or an empty string for a null view. + public readonly string ToManagedString() + { + if (this.data is null) + { + return string.Empty; + } + + // WebGPU uses SIZE_MAX to distinguish null-terminated input from an explicitly sized view. + return this.length == nuint.MaxValue + ? Marshal.PtrToStringUTF8((nint)this.data) ?? string.Empty + : Encoding.UTF8.GetString((byte*)this.data, checked((int)this.length)); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPUBufferMapCallback.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUBufferMapCallback.cs new file mode 100644 index 000000000..6b76b4dec --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUBufferMapCallback.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Keeps a managed buffer-map callback rooted for every invocation accepted by native WebGPU. +/// +internal sealed unsafe class WebGPUBufferMapCallback : WebGPUCallbackLifetime +{ + private readonly Callback callback; + private readonly NativeCallback nativeCallback; + private readonly nint pointer; + + /// + /// Initializes a new instance of the class. + /// + /// The managed callback to invoke while its owner remains active. + private WebGPUBufferMapCallback(Callback callback) + { + this.callback = callback; + + // The public backend callback predates WebGPU's diagnostic message and second userdata + // value. This thunk preserves that established callback contract while matching the ABI. + this.nativeCallback = this.Invoke; + this.pointer = Marshal.GetFunctionPointerForDelegate(this.nativeCallback); + } + + /// + /// Represents the managed buffer-map completion callback. + /// + /// The map completion status. + /// The caller-provided context pointer. + public delegate void Callback(WGPUMapAsyncStatus status, void* userData); + + /// + /// Matches the complete native WGPUBufferMapCallback ABI declared by webgpu.h. + /// + /// The map completion status. + /// The native diagnostic message. + /// The first caller-provided context pointer. + /// The second caller-provided context pointer. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeCallback( + WGPUMapAsyncStatus status, + WGPUStringView message, + void* userdata1, + void* userdata2); + + /// + /// Gets the unmanaged callback pointer stored in a WebGPU callback-info structure. + /// + public delegate* unmanaged[Cdecl] Pointer + => (delegate* unmanaged[Cdecl])this.pointer; + + /// + /// Creates a rooted callback thunk. + /// + /// The managed callback to invoke. + /// The rooted callback thunk. + public static WebGPUBufferMapCallback From(Callback callback) => new(callback); + + /// + /// Dispatches one native completion while preventing concurrent owner disposal. + /// + /// The map completion status. + /// The native diagnostic message. + /// The caller-provided context pointer. + /// The second native context pointer, which this wrapper does not use. + private void Invoke(WGPUMapAsyncStatus status, WGPUStringView message, void* userdata1, void* userdata2) + { + _ = message; + _ = userdata2; + bool invokeCallback = this.EnterInvocation(); + + try + { + // Once the managed owner has retired, its event and captured state may be disposed. + // The native callback must still run to retire its registered delegate lifetime, but + // it must not enter that released managed state. + if (invokeCallback) + { + this.callback(status, userdata1); + } + } + finally + { + this.ExitInvocation(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPUCallbackLifetime.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUCallbackLifetime.cs new file mode 100644 index 000000000..79c760848 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUCallbackLifetime.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Owns the managed lifetime shared by asynchronous WebGPU callback thunks. +/// +internal abstract class WebGPUCallbackLifetime : IDisposable +{ + private readonly object sync = new(); + private GCHandle selfRoot; + private int pendingInvocationCount; + private bool disposed; + + /// + /// Registers one invocation before its callback pointer is passed to native WebGPU. + /// + public void RegisterInvocation() + { + lock (this.sync) + { + // Marshal.GetFunctionPointerForDelegate does not root the delegate. Root the callback + // owner before native code can invoke the pointer, and retain it until disposal and + // every registered invocation have both completed. + if (!this.selfRoot.IsAllocated) + { + this.selfRoot = GCHandle.Alloc(this); + } + + this.pendingInvocationCount++; + } + } + + /// + /// Cancels a registration when native WebGPU did not receive the callback pointer. + /// + public void CancelInvocation() + { + lock (this.sync) + { + this.pendingInvocationCount--; + this.ReleaseRootIfRetired(); + } + } + + /// + /// Enters one native callback while excluding concurrent disposal. + /// + /// when the owning managed operation remains active. + protected bool EnterInvocation() + { + Monitor.Enter(this.sync); + return !this.disposed; + } + + /// + /// Completes one native callback and releases its self-root when the owner has retired. + /// + protected void ExitInvocation() + { + try + { + this.pendingInvocationCount--; + this.ReleaseRootIfRetired(); + } + finally + { + Monitor.Exit(this.sync); + } + } + + /// + /// Retires the managed callback without invalidating native invocations that are still pending. + /// + public void Dispose() + { + lock (this.sync) + { + if (this.disposed) + { + return; + } + + // Disposal prevents a late callback from touching state the owner is about to release. + // The GC root remains until native WebGPU invokes every callback it already accepted. + this.disposed = true; + this.ReleaseRootIfRetired(); + } + } + + /// + /// Releases the self-root once neither managed nor native code can require this callback. + /// + private void ReleaseRootIfRetired() + { + if (this.disposed && this.pendingInvocationCount == 0 && this.selfRoot.IsAllocated) + { + this.selfRoot.Free(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPUNative.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUNative.cs new file mode 100644 index 000000000..28d3fd536 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUNative.cs @@ -0,0 +1,48 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Reflection; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Selects the native library used by the generated WebGPU bindings. +/// +internal static partial class WebGPUNative +{ + private const string LibraryName = "wgpu_native"; + private static readonly nint LibraryHandle; + + static WebGPUNative() + { + string fileName = OperatingSystem.IsWindows() + ? "wgpu_native.dll" + : OperatingSystem.IsMacOS() + ? "libwgpu_native.dylib" + : "libwgpu_native.so"; + + string libraryPath = System.IO.Path.Combine(AppContext.BaseDirectory, fileName); + if (File.Exists(libraryPath)) + { + // Load the binary shipped with this package before .NET searches RID-native directories. + // This prevents an older same-named dependency from satisfying our generated v29 ABI. + LibraryHandle = NativeLibrary.Load(libraryPath); + } + else if (NativeModuleLocator.TryGetModuleDirectory(out string moduleDirectory)) + { + // A Native AOT shared library hosted by a foreign process probes relative to that + // host, so also look beside this library's own image. + string anchoredPath = System.IO.Path.Combine(moduleDirectory, fileName); + if (File.Exists(anchoredPath)) + { + LibraryHandle = NativeLibrary.Load(anchoredPath); + } + } + + NativeLibrary.SetDllImportResolver(typeof(WebGPUNative).Assembly, ResolveLibrary); + } + + private static nint ResolveLibrary(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) + => libraryName == LibraryName ? LibraryHandle : nint.Zero; +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPUPopErrorScopeCallback.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUPopErrorScopeCallback.cs new file mode 100644 index 000000000..1ded21dfd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUPopErrorScopeCallback.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Keeps a managed error-scope callback rooted for every invocation accepted by native WebGPU. +/// +internal sealed unsafe class WebGPUPopErrorScopeCallback : WebGPUCallbackLifetime +{ + private readonly Callback callback; + private readonly NativeCallback nativeCallback; + private readonly nint pointer; + + /// + /// Initializes a new instance of the class. + /// + /// The managed callback to invoke while its owner remains active. + private WebGPUPopErrorScopeCallback(Callback callback) + { + this.callback = callback; + this.nativeCallback = this.Invoke; + this.pointer = Marshal.GetFunctionPointerForDelegate(this.nativeCallback); + } + + /// + /// Represents the managed error-scope completion callback. + /// + /// The error-scope request status. + /// The captured error type, or . + /// The borrowed native diagnostic message. + /// The caller-provided context pointer. + public delegate void Callback(WGPUPopErrorScopeStatus status, WGPUErrorType errorType, WGPUStringView message, void* userData); + + /// + /// Matches the complete native WGPUPopErrorScopeCallback ABI declared by webgpu.h. + /// + /// The error-scope request status. + /// The captured error type. + /// The borrowed native diagnostic message. + /// The first caller-provided context pointer. + /// The second caller-provided context pointer. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeCallback( + WGPUPopErrorScopeStatus status, + WGPUErrorType errorType, + WGPUStringView message, + void* userdata1, + void* userdata2); + + /// + /// Gets the unmanaged callback pointer stored in a WebGPU callback-info structure. + /// + public delegate* unmanaged[Cdecl] Pointer + => (delegate* unmanaged[Cdecl])this.pointer; + + /// + /// Creates a rooted callback thunk. + /// + /// The managed callback to invoke. + /// The rooted callback thunk. + public static WebGPUPopErrorScopeCallback From(Callback callback) => new(callback); + + /// + /// Dispatches one native completion while preventing concurrent owner disposal. + /// + /// The error-scope request status. + /// The captured error type. + /// The borrowed native diagnostic message. + /// The caller-provided context pointer. + /// The second native context pointer, which this wrapper does not use. + private void Invoke( + WGPUPopErrorScopeStatus status, + WGPUErrorType errorType, + WGPUStringView message, + void* userdata1, + void* userdata2) + { + _ = userdata2; + bool invokeCallback = this.EnterInvocation(); + + try + { + // The message string is borrowed for this invocation, so the managed callback must + // copy it before returning to native WebGPU. + if (invokeCallback) + { + this.callback(status, errorType, message, userdata1); + } + } + finally + { + this.ExitInvocation(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPUQueueWorkDoneCallback.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUQueueWorkDoneCallback.cs new file mode 100644 index 000000000..5fc78d999 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPUQueueWorkDoneCallback.cs @@ -0,0 +1,87 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Keeps a managed queue-completion callback rooted for every invocation accepted by native WebGPU. +/// +internal sealed unsafe class WebGPUQueueWorkDoneCallback : WebGPUCallbackLifetime +{ + private readonly Callback callback; + private readonly NativeCallback nativeCallback; + private readonly nint pointer; + + /// + /// Initializes a new instance of the class. + /// + /// The managed callback to invoke while its owner remains active. + private WebGPUQueueWorkDoneCallback(Callback callback) + { + this.callback = callback; + this.nativeCallback = this.Invoke; + this.pointer = Marshal.GetFunctionPointerForDelegate(this.nativeCallback); + } + + /// + /// Represents the managed queue-completion callback. + /// + /// The queue completion status. + /// The caller-provided context pointer. + public delegate void Callback(WGPUQueueWorkDoneStatus status, void* userData); + + /// + /// Matches the complete native WGPUQueueWorkDoneCallback ABI declared by webgpu.h. + /// + /// The queue completion status. + /// The native diagnostic message. + /// The first caller-provided context pointer. + /// The second caller-provided context pointer. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeCallback( + WGPUQueueWorkDoneStatus status, + WGPUStringView message, + void* userdata1, + void* userdata2); + + /// + /// Gets the unmanaged callback pointer stored in a WebGPU callback-info structure. + /// + public delegate* unmanaged[Cdecl] Pointer + => (delegate* unmanaged[Cdecl])this.pointer; + + /// + /// Creates a rooted callback thunk. + /// + /// The managed callback to invoke. + /// The rooted callback thunk. + public static WebGPUQueueWorkDoneCallback From(Callback callback) => new(callback); + + /// + /// Dispatches one native completion while preventing concurrent owner disposal. + /// + /// The queue completion status. + /// The native diagnostic message. + /// The caller-provided context pointer. + /// The second native context pointer, which this wrapper does not use. + private void Invoke(WGPUQueueWorkDoneStatus status, WGPUStringView message, void* userdata1, void* userdata2) + { + _ = message; + _ = userdata2; + bool invokeCallback = this.EnterInvocation(); + + try + { + if (invokeCallback) + { + this.callback(status, userdata1); + } + } + finally + { + this.ExitInvocation(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestAdapterCallback.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestAdapterCallback.cs new file mode 100644 index 000000000..ea8b393b7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestAdapterCallback.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Keeps a managed adapter-request callback rooted for every invocation accepted by native WebGPU. +/// +internal sealed unsafe class WebGPURequestAdapterCallback : WebGPUCallbackLifetime +{ + private readonly Callback callback; + private readonly Callback abandonedCallback; + private readonly NativeCallback nativeCallback; + private readonly nint pointer; + + /// + /// Initializes a new instance of the class. + /// + /// The managed callback to invoke while its owner remains active. + /// The callback that releases a result arriving after owner retirement. + private WebGPURequestAdapterCallback(Callback callback, Callback abandonedCallback) + { + this.callback = callback; + this.abandonedCallback = abandonedCallback; + this.nativeCallback = this.Invoke; + this.pointer = Marshal.GetFunctionPointerForDelegate(this.nativeCallback); + } + + /// + /// Represents the managed adapter-request completion callback. + /// + /// The request status. + /// The requested adapter, or on failure. + /// The native diagnostic message. + /// The caller-provided context pointer. + public delegate void Callback( + WGPURequestAdapterStatus status, + WGPUAdapterImpl* adapter, + WGPUStringView message, + void* userData); + + /// + /// Matches the complete native WGPURequestAdapterCallback ABI declared by webgpu.h. + /// + /// The request status. + /// The returned adapter. + /// The native diagnostic message. + /// The first caller-provided context pointer. + /// The second caller-provided context pointer. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeCallback( + WGPURequestAdapterStatus status, + WGPUAdapterImpl* adapter, + WGPUStringView message, + void* userdata1, + void* userdata2); + + /// + /// Gets the unmanaged callback pointer stored in a WebGPU callback-info structure. + /// + public delegate* unmanaged[Cdecl] Pointer + => (delegate* unmanaged[Cdecl])this.pointer; + + /// + /// Creates a rooted callback thunk. + /// + /// The managed callback to invoke. + /// The callback that releases a result arriving after managed retirement. + /// The rooted callback thunk. + public static WebGPURequestAdapterCallback From(Callback callback, Callback abandonedCallback) => new(callback, abandonedCallback); + + /// + /// Dispatches one native completion while preventing concurrent owner disposal. + /// + /// The request status. + /// The returned adapter. + /// The native diagnostic message. + /// The caller-provided context pointer. + /// The second native context pointer, which this wrapper does not use. + private void Invoke( + WGPURequestAdapterStatus status, + WGPUAdapterImpl* adapter, + WGPUStringView message, + void* userdata1, + void* userdata2) + { + _ = userdata2; + bool invokeCallback = this.EnterInvocation(); + + try + { + if (invokeCallback) + { + this.callback(status, adapter, message, userdata1); + } + else + { + // Adapter results are returned with ownership. A request that completes after its + // managed timeout has no caller left to receive that ownership, so the dedicated + // abandonment path must release any returned handle. + this.abandonedCallback(status, adapter, message, userdata1); + } + } + finally + { + this.ExitInvocation(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestDeviceCallback.cs b/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestDeviceCallback.cs new file mode 100644 index 000000000..be4709e08 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Native/WebGPURequestDeviceCallback.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +/// +/// Keeps a managed device-request callback rooted for every invocation accepted by native WebGPU. +/// +internal sealed unsafe class WebGPURequestDeviceCallback : WebGPUCallbackLifetime +{ + private readonly Callback callback; + private readonly Callback abandonedCallback; + private readonly NativeCallback nativeCallback; + private readonly nint pointer; + + /// + /// Initializes a new instance of the class. + /// + /// The managed callback to invoke while its owner remains active. + /// The callback that releases a result arriving after owner retirement. + private WebGPURequestDeviceCallback(Callback callback, Callback abandonedCallback) + { + this.callback = callback; + this.abandonedCallback = abandonedCallback; + this.nativeCallback = this.Invoke; + this.pointer = Marshal.GetFunctionPointerForDelegate(this.nativeCallback); + } + + /// + /// Represents the managed device-request completion callback. + /// + /// The request status. + /// The requested device, or on failure. + /// The native diagnostic message. + /// The caller-provided context pointer. + public delegate void Callback( + WGPURequestDeviceStatus status, + WGPUDeviceImpl* device, + WGPUStringView message, + void* userData); + + /// + /// Matches the complete native WGPURequestDeviceCallback ABI declared by webgpu.h. + /// + /// The request status. + /// The returned device. + /// The native diagnostic message. + /// The first caller-provided context pointer. + /// The second caller-provided context pointer. + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate void NativeCallback( + WGPURequestDeviceStatus status, + WGPUDeviceImpl* device, + WGPUStringView message, + void* userdata1, + void* userdata2); + + /// + /// Gets the unmanaged callback pointer stored in a WebGPU callback-info structure. + /// + public delegate* unmanaged[Cdecl] Pointer + => (delegate* unmanaged[Cdecl])this.pointer; + + /// + /// Creates a rooted callback thunk. + /// + /// The managed callback to invoke. + /// The callback that releases a result arriving after managed retirement. + /// The rooted callback thunk. + public static WebGPURequestDeviceCallback From(Callback callback, Callback abandonedCallback) => new(callback, abandonedCallback); + + /// + /// Dispatches one native completion while preventing concurrent owner disposal. + /// + /// The request status. + /// The returned device. + /// The native diagnostic message. + /// The caller-provided context pointer. + /// The second native context pointer, which this wrapper does not use. + private void Invoke( + WGPURequestDeviceStatus status, + WGPUDeviceImpl* device, + WGPUStringView message, + void* userdata1, + void* userdata2) + { + _ = userdata2; + bool invokeCallback = this.EnterInvocation(); + + try + { + if (invokeCallback) + { + this.callback(status, device, message, userdata1); + } + else + { + // Device results are returned with ownership. A request that completes after its + // managed timeout has no caller left to receive that ownership, so the dedicated + // abandonment path must release any returned handle. + this.abandonedCallback(status, device, message, userdata1); + } + } + finally + { + this.ExitInvocation(); + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs index 7f91e2809..95113070d 100644 --- a/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs +++ b/src/ImageSharp.Drawing.WebGPU/RemoteExecutor/RemoteExecutor.cs @@ -76,7 +76,7 @@ static RemoteExecutor() /// /// Gets a value indicating whether this remote executor is supported on the current platform. /// - internal static bool IsSupported { get; } = + public static bool IsSupported { get; } = !RuntimeInformation.IsOSPlatform(OSPlatform.Create("IOS")) && !RuntimeInformation.IsOSPlatform(OSPlatform.Create("ANDROID")) && !RuntimeInformation.IsOSPlatform(OSPlatform.Create("BROWSER")) && @@ -91,7 +91,7 @@ static RemoteExecutor() /// A static method returning (the exit code). /// Maximum time to wait for the child process. /// The exit code from the child process, or -1 on failure. - internal static int Invoke(Func method, int timeoutMilliseconds = 30_000) + public static int Invoke(Func method, int timeoutMilliseconds = 30_000) { if (!IsSupported || AssemblyPath is null || HostRunner is null) { diff --git a/src/ImageSharp.Drawing.WebGPU/ShaderStage.cs b/src/ImageSharp.Drawing.WebGPU/ShaderStage.cs new file mode 100644 index 000000000..38a6f73e5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/ShaderStage.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies the shader stages that can access a binding. +/// +[Flags] +internal enum ShaderStage : ulong +{ + /// + /// No shader stage can access the binding. + /// + None = 0, + + /// + /// The vertex stage can access the binding. + /// + Vertex = 1, + + /// + /// The fragment stage can access the binding. + /// + Fragment = 2, + + /// + /// The compute stage can access the binding. + /// + Compute = 4 +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs index d78c37a48..0d495438c 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BackdropComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that prefix-sums dynamically allocated per-path tile backdrops. +/// GPU stage that converts per-tile backdrop deltas into absolute winding numbers by +/// prefix-summing each sparse path row left to right, seeded with the row's own backdrop. +/// Wraps backdrop_dyn.wgsl. /// internal static unsafe class BackdropComputeShader { @@ -23,7 +25,11 @@ internal static unsafe class BackdropComputeShader /// /// Gets the X workgroup count required to process every path in the scene. + /// The shader runs one thread per path at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of paths (draw objects) in the scene. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathCount) => (pathCount + 255U) / 256U; @@ -38,21 +44,27 @@ public static uint GetDispatchX(uint pathCount) /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + // Bindings match backdrop_dyn.wgsl: + // 0 config uniform + // 1 bump allocators (read-write because the buffer is atomic; this stage only reads the failure mask) + // 2 paths (read-only Path records) + // 3 rows (read-only sparse PathRow records) + // 4 tiles (read-write; backdrop rewritten in place as absolute winding) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[5]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, - Entries = entries + entryCount = 5, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs index 1aeeeb4dc..40360b7a7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BboxClearComputeShader.cs @@ -2,12 +2,13 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that clears path bounding boxes before flatten repopulates them. +/// GPU stage that initializes every per-path bounding box to an empty (inverted) box so +/// that flatten can accumulate extents with atomic min/max. Wraps bbox_clear.wgsl. /// internal static unsafe class BboxClearComputeShader { @@ -23,7 +24,11 @@ internal static unsafe class BboxClearComputeShader /// /// Gets the X workgroup count required to clear every path bounding box. + /// The shader resets one bounding box per thread at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of paths in the scene. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathCount) => (pathCount + 255U) / 256U; @@ -31,20 +36,28 @@ public static uint GetDispatchX(uint pathCount) /// /// Creates the bind-group layout required by the bbox-clear stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; + // Bindings match bbox_clear.wgsl: + // 0 config uniform + // 1 path_bboxes (read-write; min/max fields reset to their inverted extremes) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[2]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 2, - Entries = entries + entryCount = 2, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs index a631af22f..2b38bf33e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/BinningComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that bins draw objects into 16x16 tile bins using Vello's bitmap-compaction structure. +/// GPU stage that assigns each draw object to every 16x16-tile bin its clipped bounding box +/// touches, writing bin headers and element lists using Vello's bitmap-compaction structure. +/// Wraps binning.wgsl. /// internal static unsafe class BinningComputeShader { @@ -23,7 +25,12 @@ internal static unsafe class BinningComputeShader /// /// Gets the X workgroup count required to bin every draw object in the scene. + /// Each workgroup covers one 256-element draw partition (workgroup size 256, one thread + /// per draw object), so this is ceil( / 256). The Y axis + /// of the dispatch chunks the bin grid and is computed by the caller. /// + /// The number of draw objects in the scene. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint drawObjectCount) => (drawObjectCount + 255U) / 256U; @@ -31,25 +38,38 @@ public static uint GetDispatchX(uint drawObjectCount) /// /// Creates the bind-group layout required by the binning stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + // Bindings match binning.wgsl: + // 0 config uniform + // 1 draw_monoids (read-only) + // 2 path_bbox_buf (read-only path bboxes plus interest rects) + // 3 clip_bbox_buf (read-only clip-stack bboxes from clip_leaf) + // 4 intersected_bbox (read-write; clipped bbox written per draw object) + // 5 bump allocators (read-write; binning counter and failure mask) + // 6 info_bin_data (read-write; bin headers and element lists) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, - Entries = entries + entryCount = 7, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ChunkResetComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ChunkResetComputeShader.cs index 2878e9479..778228711 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/ChunkResetComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ChunkResetComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that preserves chunk-invariant scheduling state while clearing the per-chunk allocators before the next oversized-scene tile window. +/// GPU stage that preserves chunk-invariant scheduling state (the shared-stage counters and +/// failure bits) while clearing the per-chunk allocators before the next oversized-scene +/// tile-row window. Wraps chunk_reset.wgsl. /// internal static unsafe class ChunkResetComputeShader { @@ -23,7 +25,9 @@ internal static unsafe class ChunkResetComputeShader /// /// Gets the fixed X workgroup count required by the chunk-reset stage. + /// The stage runs as a single workgroup of one thread, so the count is always 1. /// + /// The X dispatch dimension in workgroups; always 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX() => 1; @@ -37,17 +41,19 @@ internal static unsafe class ChunkResetComputeShader /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[1]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + // Bindings match chunk_reset.wgsl: + // 0 bump allocators (read-write; chunk-local counters zeroed, shared-stage state retained) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[1]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 1, - Entries = entries + entryCount = 1, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs index 84665fe9f..a8fc7a09c 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipLeafComputeShader.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that resolves clip leaves into concrete clip bounding boxes. +/// GPU stage that runs the second pass of the clip-stack monoid scan: it resolves +/// BeginClip/EndClip nesting across the scene, producing a conservative clip bounding box per +/// clip record and rewriting each EndClip's draw monoid to reference its matching BeginClip. +/// Wraps clip_leaf.wgsl. /// internal static unsafe class ClipLeafComputeShader { @@ -23,25 +26,38 @@ internal static unsafe class ClipLeafComputeShader /// /// Creates the bind-group layout required by the clip-leaf stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + // Bindings match clip_leaf.wgsl: + // 0 config uniform + // 1 clip_inp (read-only ClipInp records from draw_leaf) + // 2 path_bboxes (read-only per-path bounds) + // 3 reduced (read-only per-workgroup Bic aggregates from clip_reduce) + // 4 clip_els (read-only open-clip stack elements from clip_reduce) + // 5 draw_monoids (read-write; EndClip entries rewritten in place) + // 6 clip_bboxes (read-write; conservative bbox per clip record, consumed by binning) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); - entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); - - BindGroupLayoutDescriptor descriptor = new() + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.ReadOnlyStorage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.Storage); + + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, - Entries = entries + entryCount = 7, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs index 9b184af72..96d03b45d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/ClipReduceComputeShader.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that reduces clip inputs into the binary-interval-combination structure consumed by clip leaf expansion. +/// GPU stage that runs the first pass of the clip-stack monoid scan: each workgroup reduces +/// its span of clip records to a single Bic (bicyclic semigroup) aggregate and records the +/// still-open BeginClip stack elements so clip-leaf can resolve pushes and pops across +/// workgroup boundaries. Wraps clip_reduce.wgsl. /// internal static unsafe class ClipReduceComputeShader { @@ -23,22 +26,32 @@ internal static unsafe class ClipReduceComputeShader /// /// Creates the bind-group layout required by the clip-reduce stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + // Bindings match clip_reduce.wgsl: + // 0 clip_inp (read-only ClipInp records emitted by draw_leaf) + // 1 path_bboxes (read-only per-path bounds) + // 2 reduced (read-write; one Bic aggregate written per workgroup) + // 3 clip_out (read-write; ClipEl written per open BeginClip) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[4]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.Storage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, - Entries = entries + entryCount = 4, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs index 7ea5bdf38..05cb74272 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/CoarseComputeShader.cs @@ -2,12 +2,15 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that emits per-tile PTCL from bin-compacted draw-object work, matching Vello's coarse stage shape. +/// GPU stage that runs coarse rasterization: one workgroup per 16x16-tile bin merges the bin +/// element lists from binning back into draw order and serializes each tile's per-tile command +/// list (PTCL) for the fine stage, matching Vello's coarse stage shape. Wraps +/// coarse.wgsl. /// internal static unsafe class CoarseComputeShader { @@ -23,13 +26,20 @@ internal static unsafe class CoarseComputeShader /// /// Gets the X workgroup count required to cover the bin grid width. + /// One workgroup covers one 16x16-tile bin (256 threads, one per tile), so the X count is + /// the bin grid width itself. /// + /// The bin grid width in 16x16-tile bins. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint widthInBins) => widthInBins; /// /// Gets the Y workgroup count required to cover the bin grid height. + /// One workgroup covers one 16x16-tile bin, so the Y count is the bin grid height itself. /// + /// The bin grid height in 16x16-tile bins. + /// The Y dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchY(uint heightInBins) => heightInBins; @@ -43,25 +53,35 @@ internal static unsafe class CoarseComputeShader /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; + // Bindings match coarse.wgsl: + // 0 config uniform + // 1 scene (read-only drawtags and drawdata) + // 2 draw_monoids (read-only) + // 3 info_bin_data (read-only draw info, bin headers, and bin element lists) + // 4 paths (read-only sparse tile-grid lookup) + // 5 rows (read-only sparse PathRow records) + // 6 tiles (read-write; segment_count_or_ix rewritten to the allocated segment index) + // 7 bump allocators (read-write; ptcl, segments and blend_spill counters, failure mask) + // 8 ptcl (read-write; per-tile command lists written) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[9]; entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage, 0); - entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); - entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); - entries[4] = CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage, 0); - entries[5] = CreateStorageEntry(5, BufferBindingType.ReadOnlyStorage, 0); - entries[6] = CreateStorageEntry(6, BufferBindingType.Storage, 0); - entries[7] = CreateStorageEntry(7, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[8] = CreateStorageEntry(8, BufferBindingType.Storage, 0); + entries[1] = CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[2] = CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[5] = CreateStorageEntry(5, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[6] = CreateStorageEntry(6, WGPUBufferBindingType.Storage, 0); + entries[7] = CreateStorageEntry(7, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[8] = CreateStorageEntry(8, WGPUBufferBindingType.Storage, 0); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 9, - Entries = entries + entryCount = 9, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); @@ -75,31 +95,44 @@ public static bool TryCreateBindGroupLayout( return true; } + /// + /// Creates one compute-stage storage-buffer binding entry. + /// + /// The WGSL binding index. + /// The storage-buffer access mode. + /// The minimum buffer binding size in bytes, or 0 to skip validation. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + private static WGPUBindGroupLayoutEntry CreateStorageEntry(uint binding, WGPUBufferBindingType type, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = type, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = type, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; + /// + /// Creates one compute-stage uniform-buffer binding entry. + /// + /// The WGSL binding index. + /// The minimum buffer binding size in bytes. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + private static WGPUBindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = WGPUBufferBindingType.Uniform, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs index 5f0a2efb7..cdd502524 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawLeafComputeShader.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that expands reduced draw metadata into concrete draw and clip inputs. +/// GPU stage that finishes the draw-tag prefix sum started by draw-reduce, producing an +/// exclusive DrawMonoid per draw object, then decodes each draw object into the per-draw info +/// stream and emits ClipInp records for the clip stack stages. Wraps draw_leaf.wgsl. /// internal static unsafe class DrawLeafComputeShader { @@ -23,25 +25,38 @@ internal static unsafe class DrawLeafComputeShader /// /// Creates the bind-group layout required by the draw-leaf stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + // Bindings match draw_leaf.wgsl: + // 0 config uniform + // 1 scene (read-only drawtag and drawdata stream) + // 2 reduced (read-only per-workgroup DrawMonoid aggregates from draw_reduce) + // 3 path_bbox (read-only per-path bounds and draw flags) + // 4 draw_monoid (read-write; exclusive prefix written per draw object) + // 5 info (read-write; per-draw brush info words for coarse and fine) + // 6 clip_inp (read-write; ClipInp records for clip_reduce and clip_leaf) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); - entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); - - BindGroupLayoutDescriptor descriptor = new() + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.Storage); + + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, - Entries = entries + entryCount = 7, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs index ec37862e9..0048a06b9 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/DrawReduceComputeShader.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that scans draw tags into draw-object offsets and counts. +/// GPU stage that runs the first pass of the two-pass prefix sum over the draw-tag stream: +/// each workgroup reduces its assigned draw tags to a single DrawMonoid aggregate that +/// draw-leaf later combines into a full exclusive prefix sum. Wraps draw_reduce.wgsl. /// internal static unsafe class DrawReduceComputeShader { @@ -23,21 +25,30 @@ internal static unsafe class DrawReduceComputeShader /// /// Creates the bind-group layout required by the draw-reduce stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + // Bindings match draw_reduce.wgsl: + // 0 config uniform + // 1 scene (read-only; draw tags read at config.drawtag_base) + // 2 reduced (read-write; one DrawMonoid aggregate written per workgroup) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[3]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 3, - Entries = entries + entryCount = 3, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAliasedThresholdComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAliasedThresholdComputeShader.cs deleted file mode 100644 index 51da7f91a..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAliasedThresholdComputeShader.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Text; -using Silk.NET.WebGPU; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Final staged-scene fine pass for thresholded aliased rasterization. -/// The WGSL stays shared with the analytic fine pass, but coverage is quantized -/// immediately after path evaluation so brush evaluation and composition still -/// reuse the same scene encoding and blend logic. -/// -internal static class FineAliasedThresholdComputeShader -{ - private const string OutputBindingMarker = "var output: texture_storage_2d;"; - private const string OutputStoreMarker = "textureStore(output, vec2(coords), rgba_sep);"; - private const string PremulAlphaMarker = "fn premul_alpha(rgba: vec4) -> vec4 {"; - private const string WorkgroupSizeMarker = "// The X size should be 16 / PIXELS_PER_THREAD"; - private const string AnalyticFillCallMarker = " fill_path(fill, local_xy, &area);"; - private const string ThresholdHelper = - """ - fn apply_aliased_threshold(result: ptr>) { - let threshold = config.fine_coverage_threshold; - for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - (*result)[i] = select(0.0, 1.0, (*result)[i] >= threshold); - } - } - - """; - - private static readonly object CacheSync = new(); - private static readonly Dictionary ShaderCache = []; - - /// - /// Gets the compute entry point exported by the aliased fine shader. - /// - public static ReadOnlySpan EntryPoint => "main\0"u8; - - /// - /// Gets the texture-format-specialized WGSL for the aliased threshold fine pass. - /// - public static byte[] GetCode(TextureFormat textureFormat) - { - ShaderTraits traits = GetTraits(textureFormat); - - lock (CacheSync) - { - if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) - { - return cachedCode; - } - - string source = GeneratedWgslShaderSources.FineText; - source = source.Replace(OutputBindingMarker, $"var output: texture_storage_2d<{traits.OutputFormat}, write>;", StringComparison.Ordinal); - source = source.Replace(OutputStoreMarker, traits.StoreOutputStatement, StringComparison.Ordinal); - source = source.Replace(PremulAlphaMarker, $"{traits.EncodeOutputFunction}\n\n{PremulAlphaMarker}", StringComparison.Ordinal); - source = source.Replace(WorkgroupSizeMarker, $"{ThresholdHelper}{WorkgroupSizeMarker}", StringComparison.Ordinal); - source = source.Replace(AnalyticFillCallMarker, $"{AnalyticFillCallMarker}\n apply_aliased_threshold(&area);", StringComparison.Ordinal); - - int byteCount = Encoding.UTF8.GetByteCount(source); - byte[] code = new byte[byteCount + 1]; - _ = Encoding.UTF8.GetBytes(source, code); - code[^1] = 0; - ShaderCache[textureFormat] = code; - return code; - } - } - - /// - /// Creates the bind-group layout consumed by the aliased threshold fine pass. - /// - public static unsafe bool TryCreateBindGroupLayout( - WebGPU api, - Device* device, - TextureFormat outputTextureFormat, - out BindGroupLayout* layout, - out string? error) - => FineAreaComputeShader.TryCreateBindGroupLayout(api, device, outputTextureFormat, out layout, out error); - - private static ShaderTraits GetTraits(TextureFormat textureFormat) - { - WebGPUDrawingBackend.CompositeTextureShaderTraits compositeTraits = WebGPUDrawingBackend.GetCompositeTextureShaderTraits(textureFormat); - -#pragma warning disable CS8524 - return compositeTraits.EncodingKind switch - { - WebGPUDrawingBackend.CompositeTextureEncodingKind.Float => CreateFloatTraits(compositeTraits.OutputFormat), - WebGPUDrawingBackend.CompositeTextureEncodingKind.Snorm => CreateSnormTraits(compositeTraits.OutputFormat) - }; -#pragma warning restore CS8524 - } - - private static ShaderTraits CreateFloatTraits(string outputFormat) - { - const string encodeOutput = - """ - fn encode_output(color: vec4) -> vec4 { - return color; - } - """; - - return new ShaderTraits( - outputFormat, - encodeOutput, - "textureStore(output, vec2(coords), encode_output(rgba_sep));"); - } - - private static ShaderTraits CreateSnormTraits(string outputFormat) - { - const string encodeOutput = - """ - fn encode_output(color: vec4) -> vec4 { - let clamped = clamp(color, vec4(0.0), vec4(1.0)); - return (clamped * 2.0) - vec4(1.0); - } - """; - - return new ShaderTraits( - outputFormat, - encodeOutput, - "textureStore(output, vec2(coords), encode_output(rgba_sep));"); - } - - private readonly struct ShaderTraits( - string outputFormat, - string encodeOutputFunction, - string storeOutputStatement) - { - public string OutputFormat { get; } = outputFormat; - - public string EncodeOutputFunction { get; } = encodeOutputFunction; - - public string StoreOutputStatement { get; } = storeOutputStatement; - } -} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs index 3035490a9..cc35ff0ff 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FineAreaComputeShader.cs @@ -2,21 +2,38 @@ // Licensed under the Six Labors Split License. using System.Text; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Final staged-scene fine pass driven from the source-of-truth WGSL stage. -/// Only the output storage texture encoding is specialized per target format. +/// GPU stage that runs the fine rasterizer, the final pass of the pipeline: each workgroup +/// shades one 16x16 tile by interpreting the tile's command list (PTCL) written by coarse, +/// computing analytic coverage and evaluating brushes. Wraps fine.wgsl; only the output +/// storage-texture encoding is specialized per target format and alpha representation. /// internal static class FineAreaComputeShader { + /// + /// The output texture declaration in fine.wgsl, replaced with the format-specific declaration. + /// private const string OutputBindingMarker = "var output: texture_storage_2d;"; - private const string OutputStoreMarker = "textureStore(output, vec2(coords), rgba_sep);"; + + /// + /// The output store statement in fine.wgsl, replaced with the target-specific encode-and-store statement. + /// + private const string OutputStoreMarker = "textureStore(output, vec2(coords), rgba[i]);"; + + /// + /// An anchor in fine.wgsl before which the target conversion functions are inserted. + /// private const string PremulAlphaMarker = "fn premul_alpha(rgba: vec4) -> vec4 {"; - private static readonly Dictionary ShaderCache = []; + /// + /// Specialized shader bytes per target texture format and alpha representation, guarded by its own monitor. + /// + private static readonly Dictionary<(WGPUTextureFormat TextureFormat, PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding), byte[]> ShaderCache = []; /// /// Gets the WGSL entry point used by this shader. @@ -24,15 +41,24 @@ internal static class FineAreaComputeShader public static ReadOnlySpan EntryPoint => "main\0"u8; /// - /// Gets or generates the fine-pass shader specialized for the requested output texture format. + /// Gets or generates the fine-pass shader specialized for the requested target. /// - public static byte[] GetCode(TextureFormat textureFormat) + /// The output texture format to specialize the shader for. + /// The alpha representation stored by the target. + /// The target's mapping between native channel values and ImageSharp unit values. + /// The null-terminated UTF-8 WGSL source bytes for the specialized shader. + public static byte[] GetCode( + WGPUTextureFormat textureFormat, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) { - ShaderTraits traits = GetTraits(textureFormat); + (WGPUTextureFormat TextureFormat, PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding) cacheKey = + (textureFormat, alphaRepresentation, numericEncoding); + ShaderTraits traits = GetTraits(textureFormat, alphaRepresentation, numericEncoding); lock (ShaderCache) { - if (ShaderCache.TryGetValue(textureFormat, out byte[]? cachedCode)) + if (ShaderCache.TryGetValue(cacheKey, out byte[]? cachedCode)) { return cachedCode; } @@ -40,13 +66,13 @@ public static byte[] GetCode(TextureFormat textureFormat) string source = GeneratedWgslShaderSources.FineText; source = source.Replace(OutputBindingMarker, $"var output: texture_storage_2d<{traits.OutputFormat}, write>;", StringComparison.Ordinal); source = source.Replace(OutputStoreMarker, traits.StoreOutputStatement, StringComparison.Ordinal); - source = source.Replace(PremulAlphaMarker, $"{traits.EncodeOutputFunction}\n\n{PremulAlphaMarker}", StringComparison.Ordinal); + source = source.Replace(PremulAlphaMarker, $"{traits.TargetConversionFunctions}\n\n{PremulAlphaMarker}", StringComparison.Ordinal); int byteCount = Encoding.UTF8.GetByteCount(source); byte[] code = new byte[byteCount + 1]; _ = Encoding.UTF8.GetBytes(source, code); code[^1] = 0; - ShaderCache[textureFormat] = code; + ShaderCache[cacheKey] = code; return code; } } @@ -54,28 +80,44 @@ public static byte[] GetCode(TextureFormat textureFormat) /// /// Creates the bind-group layout required by the fine area shader. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// The storage-texture format of the output binding. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static unsafe bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - TextureFormat outputTextureFormat, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + WGPUTextureFormat outputTextureFormat, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[9]; + // Bindings match fine.wgsl: + // 0 config uniform + // 1 segments (read-only tile-relative segments from path_tiling) + // 2 ptcl (read-only per-tile command lists from coarse) + // 3 info (read-only per-draw brush info from draw_leaf) + // 4 blend_spill (read-write scratch for clip stacks deeper than the in-shader split) + // 5 output (write-only storage texture in the caller-specified format) + // 6 gradients (sampled gradient ramp texture) + // 7 image_atlas (sampled image atlas texture) + // 8 backdrop_texture (sampled existing target contents) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[9]; entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage, 0); - entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); - entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); - entries[4] = CreateStorageEntry(4, BufferBindingType.Storage, 0); + entries[1] = CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[2] = CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, WGPUBufferBindingType.Storage, 0); entries[5] = CreateOutputTextureEntry(5, outputTextureFormat); entries[6] = CreateSampledTextureEntry(6); entries[7] = CreateSampledTextureEntry(7); entries[8] = CreateSampledTextureEntry(8); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 9, - Entries = entries + entryCount = 9, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); @@ -89,111 +131,203 @@ public static unsafe bool TryCreateBindGroupLayout( return true; } - private static ShaderTraits GetTraits(TextureFormat textureFormat) + /// + /// Resolves the WGSL specialization traits for the requested target. + /// + /// The output texture format. + /// The alpha representation stored by the target. + /// The target's mapping between native channel values and ImageSharp unit values. + /// The traits describing the output declaration, encode function and store statement. + private static ShaderTraits GetTraits( + WGPUTextureFormat textureFormat, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) { WebGPUDrawingBackend.CompositeTextureShaderTraits compositeTraits = WebGPUDrawingBackend.GetCompositeTextureShaderTraits(textureFormat); -#pragma warning disable CS8524 - return compositeTraits.EncodingKind switch +#pragma warning disable CS8509, CS8524 + (string Decode, string Encode) numericBodies = numericEncoding switch { - WebGPUDrawingBackend.CompositeTextureEncodingKind.Float => CreateFloatTraits(compositeTraits.OutputFormat), - WebGPUDrawingBackend.CompositeTextureEncodingKind.Snorm => CreateSnormTraits(compositeTraits.OutputFormat) + WebGPUTargetNumericEncoding.Unit => ("return color;", "return color;"), + WebGPUTargetNumericEncoding.SignedUnit => + ("return (color + vec4(1.0)) * 0.5;", "return (color * 2.0) - vec4(1.0);") }; -#pragma warning restore CS8524 - } - private static ShaderTraits CreateFloatTraits(string outputFormat) - { - const string encodeOutput = - """ - fn encode_output(color: vec4) -> vec4 { + // The CPU RecolorBrush observes a TPixel already written by earlier draws. A staged GPU + // scene keeps those draws in f32 registers, so Recolor alone must reproduce the target's + // physical storage conversion before comparing without reducing normal composition precision. + string targetFormatRoundTripBody = textureFormat switch + { + WGPUTextureFormat.RGBA8Unorm or WGPUTextureFormat.BGRA8Unorm => "return unpack4x8unorm(pack4x8unorm(color));", + WGPUTextureFormat.RGBA8Snorm => "return unpack4x8snorm(pack4x8snorm(color));", + WGPUTextureFormat.RGBA16Float => "return vec4(unpack2x16float(pack2x16float(color.rg)), unpack2x16float(pack2x16float(color.ba)));" + }; + + // Fine shading uses associated colors internally. Recolor explicitly crosses the target + // TPixel storage boundary, including associated-alpha rescaling when stored alpha quantizes. + (string Decode, string Encode, string RecolorNativeToInternal, string RecolorStoreTarget) alphaBodies = alphaRepresentation switch + { + PixelAlphaRepresentation.Associated => + ( + "return decode_numeric(color);", + "return encode_numeric(color);", + "return color;", + "if color.a <= 0.0 {\n return vec4(0.0);\n }\n\n let alpha_sample = decode_numeric(round_trip_target_format(encode_numeric(vec4(0.0, 0.0, 0.0, color.a))));\n let stored_alpha = alpha_sample.a;\n let rgb = clamp(color.rgb * (stored_alpha / color.a), vec3(0.0), vec3(stored_alpha));\n return decode_numeric(round_trip_target_format(encode_numeric(vec4(rgb, stored_alpha))));"), + PixelAlphaRepresentation.Unassociated => + ( + "return premul_alpha(decode_numeric(color));", + "let a_inv = select(0.0, 1.0 / color.a, color.a > 0.0);\n return encode_numeric(vec4(color.rgb * a_inv, color.a));", + "return premul_alpha(color);", + "let a_inv = select(0.0, 1.0 / color.a, color.a > 0.0);\n let native = vec4(color.rgb * a_inv, color.a);\n return decode_numeric(round_trip_target_format(encode_numeric(native)));") + }; +#pragma warning restore CS8509, CS8524 + + string targetConversionFunctions = + $$""" + fn decode_numeric(color: vec4) -> vec4 { + {{numericBodies.Decode}} + } + + fn encode_numeric(color: vec4) -> vec4 { + {{numericBodies.Encode}} + } + + fn round_trip_target_format(color: vec4) -> vec4 { + {{targetFormatRoundTripBody}} + } + + fn decode_target(color: vec4) -> vec4 { + {{alphaBodies.Decode}} + } + + fn encode_target(color: vec4) -> vec4 { + {{alphaBodies.Encode}} + } + + fn recolor_native_to_internal(color: vec4) -> vec4 { + {{alphaBodies.RecolorNativeToInternal}} + } + + fn recolor_store_target(color: vec4) -> vec4 { + {{alphaBodies.RecolorStoreTarget}} + } + + fn decode_paint_color(color: vec4) -> vec4 { return color; } - """; - return new ShaderTraits( - outputFormat, - encodeOutput, - "textureStore(output, vec2(coords), encode_output(rgba_sep));"); - } + fn pack_clip_color(color: vec4) -> vec2 { + return vec2(pack2x16float(color.rg), pack2x16float(color.ba)); + } - private static ShaderTraits CreateSnormTraits(string outputFormat) - { - const string encodeOutput = - """ - fn encode_output(color: vec4) -> vec4 { - let clamped = clamp(color, vec4(0.0), vec4(1.0)); - return (clamped * 2.0) - vec4(1.0); + fn unpack_clip_color(color: vec2) -> vec4 { + return vec4(unpack2x16float(color.x), unpack2x16float(color.y)); } """; - return new ShaderTraits( - outputFormat, - encodeOutput, - "textureStore(output, vec2(coords), encode_output(rgba_sep));"); + return new ShaderTraits(compositeTraits.OutputFormat, targetConversionFunctions, "textureStore(output, vec2(coords), encode_target(rgba[i]));"); } - private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + /// + /// Creates one compute-stage storage-buffer binding entry. + /// + /// The WGSL binding index. + /// The storage-buffer access mode. + /// The minimum buffer binding size in bytes, or 0 to skip validation. + /// The populated binding entry. + private static WGPUBindGroupLayoutEntry CreateStorageEntry(uint binding, WGPUBufferBindingType type, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = type, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = type, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; - private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + /// + /// Creates one compute-stage uniform-buffer binding entry. + /// + /// The WGSL binding index. + /// The minimum buffer binding size in bytes. + /// The populated binding entry. + private static WGPUBindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = WGPUBufferBindingType.Uniform, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; - private static BindGroupLayoutEntry CreateOutputTextureEntry(uint binding, TextureFormat outputTextureFormat) + /// + /// Creates the write-only storage-texture binding entry for the shaded output. + /// + /// The WGSL binding index. + /// The storage-texture format of the output binding. + /// The populated binding entry. + private static WGPUBindGroupLayoutEntry CreateOutputTextureEntry(uint binding, WGPUTextureFormat outputTextureFormat) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - StorageTexture = new StorageTextureBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + storageTexture = new WGPUStorageTextureBindingLayout { - Access = StorageTextureAccess.WriteOnly, - Format = outputTextureFormat, - ViewDimension = TextureViewDimension.Dimension2D + access = WGPUStorageTextureAccess.WriteOnly, + format = outputTextureFormat, + viewDimension = WGPUTextureViewDimension._2D } }; - private static BindGroupLayoutEntry CreateSampledTextureEntry(uint binding) + /// + /// Creates a 2D float sampled-texture binding entry (gradients, image atlas, backdrop). + /// + /// The WGSL binding index. + /// The populated binding entry. + private static WGPUBindGroupLayoutEntry CreateSampledTextureEntry(uint binding) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Texture = new TextureBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + texture = new WGPUTextureBindingLayout { - SampleType = TextureSampleType.Float, - ViewDimension = TextureViewDimension.Dimension2D, - Multisampled = false + sampleType = WGPUTextureSampleType.Float, + viewDimension = WGPUTextureViewDimension._2D, + multisampled = 0U, } }; + /// + /// The WGSL text fragments substituted into fine.wgsl to specialize the output encoding. + /// + /// The WGSL storage-texture format name for the output binding. + /// The WGSL target conversion functions inserted into the shader. + /// The statement that encodes and stores the shaded color. private readonly struct ShaderTraits( string outputFormat, - string encodeOutputFunction, + string targetConversionFunctions, string storeOutputStatement) { + /// + /// Gets the WGSL storage-texture format name for the output binding. + /// public string OutputFormat { get; } = outputFormat; - public string EncodeOutputFunction { get; } = encodeOutputFunction; + /// + /// Gets the WGSL target conversion functions inserted into the shader. + /// + public string TargetConversionFunctions { get; } = targetConversionFunctions; + /// + /// Gets the statement that encodes and stores the shaded color. + /// public string StoreOutputStatement { get; } = storeOutputStatement; } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs index 6cce7b878..b59b192ae 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/FlattenComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that flattens encoded paths into line records and coarse path bounds. +/// GPU stage that flattens encoded path segments into a device-space line soup, expands +/// strokes via the CPU PolygonStroker port, and accumulates per-path bounding boxes for the +/// downstream stages. Wraps flatten.wgsl. /// internal static unsafe class FlattenComputeShader { @@ -23,7 +25,11 @@ internal static unsafe class FlattenComputeShader /// /// Gets the X workgroup count required to cover the packed path-tag stream. + /// The shader runs one invocation per path-tag byte at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of path-tag bytes in the scene stream. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathTagCount) => (pathTagCount + 255U) / 256U; @@ -31,24 +37,36 @@ public static uint GetDispatchX(uint pathTagCount) /// /// Creates the bind-group layout required by the flatten stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + // Bindings match flatten.wgsl: + // 0 config uniform + // 1 scene (read-only path tags, points, styles and transforms) + // 2 tag_monoids (read-only pathtag prefix sums from the scan stages) + // 3 path_bboxes (read-write; extents merged with atomic min/max) + // 4 bump allocators (read-write; lines counter) + // 5 lines (read-write; LineSoup records appended) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[6]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 6, - Entries = entries + entryCount = 6, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs index 4f8f79937..7ec01b250 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that counts traversed tile slices from scene lines and writes segment-count records. +/// GPU stage that counts, per tile, how many flattened line segments cross it: one thread per +/// line walks the line's tile crossings, bumps tile segment counts and backdrops, and emits one +/// SegmentCount record per crossing for path-tiling. Wraps path_count.wgsl. /// internal static unsafe class PathCountComputeShader { @@ -23,7 +25,12 @@ internal static unsafe class PathCountComputeShader /// /// Gets the X workgroup count required to process every emitted line. + /// The shader runs one thread per line at a workgroup size of 256, so this is + /// ceil( / 256). At runtime the stage is normally driven by + /// the indirect count written by path-count-setup using the same divisor. /// + /// The number of flattened lines. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint lineCount) => (lineCount + 255U) / 256U; @@ -38,23 +45,31 @@ public static uint GetDispatchX(uint lineCount) /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; + // Bindings match path_count.wgsl: + // 0 config uniform + // 1 bump allocators (read-write; lines read, seg_counts bump-allocated) + // 2 lines (read-only LineSoup from flatten) + // 3 paths (read-only Path records from path_row_alloc) + // 4 rows (read-only PathRow spans finalized by tile_alloc) + // 5 tile (read-write; backdrop and segment counts updated atomically) + // 6 seg_counts (read-write; one SegmentCount record written per crossing) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; entries[0] = CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[2] = CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage, 0); - entries[3] = CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage, 0); - entries[4] = CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage, 0); - entries[5] = CreateStorageEntry(5, BufferBindingType.Storage, 0); - entries[6] = CreateStorageEntry(6, BufferBindingType.Storage, 0); + entries[1] = CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[3] = CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[4] = CreateStorageEntry(4, WGPUBufferBindingType.ReadOnlyStorage, 0); + entries[5] = CreateStorageEntry(5, WGPUBufferBindingType.Storage, 0); + entries[6] = CreateStorageEntry(6, WGPUBufferBindingType.Storage, 0); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, - Entries = entries + entryCount = 7, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); @@ -68,31 +83,44 @@ public static bool TryCreateBindGroupLayout( return true; } + /// + /// Creates one compute-stage storage-buffer binding entry. + /// + /// The WGSL binding index. + /// The storage-buffer access mode. + /// The minimum buffer binding size in bytes, or 0 to skip validation. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BindGroupLayoutEntry CreateStorageEntry(uint binding, BufferBindingType type, nuint minBindingSize) + private static WGPUBindGroupLayoutEntry CreateStorageEntry(uint binding, WGPUBufferBindingType type, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = type, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = type, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; + /// + /// Creates one compute-stage uniform-buffer binding entry. + /// + /// The WGSL binding index. + /// The minimum buffer binding size in bytes. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + private static WGPUBindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = WGPUBufferBindingType.Uniform, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs index 30b9bc2fc..ec96b3f08 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathCountSetupComputeShader.cs @@ -2,12 +2,15 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that initializes exact per-tile edge counts before ordered work counting. +/// GPU stage that computes the indirect dispatch size for the per-line stages +/// (path-row-span and path-count) by dividing the flattened line count by the workgroup +/// size, or zero workgroups after an upstream allocation failure. Wraps +/// path_count_setup.wgsl. /// internal static unsafe class PathCountSetupComputeShader { @@ -23,27 +26,37 @@ internal static unsafe class PathCountSetupComputeShader /// /// Gets the fixed X workgroup count required by the path-count-setup stage. + /// The stage runs as a single workgroup of one thread, so the count is always 1. /// + /// The X dispatch dimension in workgroups; always 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX() => 1; /// /// Creates the bind-group layout required by the path-count-setup stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); + // Bindings match path_count_setup.wgsl: + // 0 bump allocators (read-write because the buffer is atomic; lines counter and failure mask read) + // 1 indirect (read-write; workgroup counts written for path_row_span and path_count) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[2]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 2, - Entries = entries + entryCount = 2, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs index e08f25210..aba603e9e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowAllocComputeShader.cs @@ -2,12 +2,15 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that allocates sparse per-path row metadata before line-driven span discovery. +/// GPU stage that bump-allocates sparse per-path tile-row metadata: one thread per draw object +/// converts the draw bounds to a chunk-clamped tile bbox, writes the Path record, and resets +/// each covered row to an empty span before line-driven span discovery. Wraps +/// path_row_alloc.wgsl. /// internal static unsafe class PathRowAllocComputeShader { @@ -23,7 +26,11 @@ internal static unsafe class PathRowAllocComputeShader /// /// Gets the X workgroup count required to process every path. + /// The shader runs one thread per draw object at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of paths (draw objects) in the scene. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathCount) => (pathCount + 255U) / 256U; @@ -38,22 +45,29 @@ public static uint GetDispatchX(uint pathCount) /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[6]; + // Bindings match path_row_alloc.wgsl: + // 0 config uniform + // 1 scene (read-only draw tags) + // 2 draw_bboxes (read-only draw bounds from draw_leaf) + // 3 bump allocators (read-write; path_rows bump-allocated, failure bit set on overflow) + // 4 paths (read-write; Path record written per draw object) + // 5 rows (read-write; PathRow records initialized to empty spans) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[6]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 6, - Entries = entries + entryCount = 6, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs index c9be8fd22..40766e26e 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathRowSpanComputeShader.cs @@ -1,12 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that derives sparse per-row x spans from the flattened line stream. +/// GPU stage that derives sparse per-row tile column spans from the flattened line stream: +/// one thread per line replays the tile-crossing traversal, atomically growing each touched +/// row's span and accumulating per-row winding backdrops for tile-alloc. Wraps +/// path_row_span.wgsl. /// internal static unsafe class PathRowSpanComputeShader { @@ -30,21 +33,27 @@ internal static unsafe class PathRowSpanComputeShader /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + // Bindings match path_row_span.wgsl: + // 0 config uniform + // 1 bump allocators (read-write because the buffer is atomic; this stage only reads the lines counter) + // 2 lines (read-only LineSoup from flatten) + // 3 paths (read-only Path records from path_row_alloc) + // 4 rows (read-write; spans, backdrops and flags accumulated atomically) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[5]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, - Entries = entries + entryCount = 5, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs index 9e9e0521d..feb3683f3 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingComputeShader.cs @@ -1,13 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that writes final tile-relative segments from counted line slices. +/// GPU stage that writes the final per-tile path segments: one thread per SegmentCount record +/// from path-count replays the line's tile-crossing traversal, clips the line to its tile, and +/// stores the tile-relative segment in the slot range reserved by coarse. Wraps +/// path_tiling.wgsl. /// internal static unsafe class PathTilingComputeShader { @@ -31,23 +33,31 @@ internal static unsafe class PathTilingComputeShader /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[7]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.ReadOnlyStorage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.ReadOnlyStorage); - entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, BufferBindingType.ReadOnlyStorage); - entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, BufferBindingType.Storage); - - BindGroupLayoutDescriptor descriptor = new() + // Bindings match path_tiling.wgsl: + // 0 bump allocators (read-write because the buffer is atomic; this stage only reads the seg_counts total) + // 1 seg_counts (read-only SegmentCount records from path_count) + // 2 lines (read-only LineSoup from flatten) + // 3 paths (read-only Path records) + // 4 rows (read-only sparse PathRow records) + // 5 tiles (read-only; segment_count_or_ix holds the inverted segment base index from coarse) + // 6 segments (read-write; tile-relative Segment records written for fine) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[7]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.ReadOnlyStorage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.ReadOnlyStorage); + entries[5] = SceneShaderBindingLayoutHelper.CreateStorageEntry(5, WGPUBufferBindingType.ReadOnlyStorage); + entries[6] = SceneShaderBindingLayoutHelper.CreateStorageEntry(6, WGPUBufferBindingType.Storage); + + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 7, - Entries = entries + entryCount = 7, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs index b29676623..9a4e4612a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathTilingSetupComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that prepares the indirect dispatch size for the staged path-tiling phase. +/// GPU stage that computes the indirect dispatch size for path-tiling from the number of +/// SegmentCount records produced by path-count, performing the late overflow check for that +/// counter and zeroing the dispatch after a failure. Wraps path_tiling_setup.wgsl. /// internal static unsafe class PathTilingSetupComputeShader { @@ -23,29 +25,41 @@ internal static unsafe class PathTilingSetupComputeShader /// /// Gets the fixed X workgroup count required by the path-tiling-setup stage. + /// The stage runs as a single workgroup of one thread, so the count is always 1. /// + /// The X dispatch dimension in workgroups; always 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX() => 1; /// /// Creates the bind-group layout required by the path-tiling-setup stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + // Bindings match path_tiling_setup.wgsl: + // 0 config uniform + // 1 bump allocators (read-write; seg_counts read, overflow flagged in failure mask) + // 2 indirect (read-write; workgroup counts written for path_tiling) + // 3 ptcl (read-write; slot 0 set to the abort marker on failure) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[4]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneIndirectCount)); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, - Entries = entries + entryCount = 4, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs index 7b5c9afbf..e3e83134c 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduce2ComputeShader.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that performs the second reduction step for the large pathtag scan path. +/// GPU stage that performs the second-level reduction for the large pathtag scan variant, +/// reducing the first-level partials from pathtag-reduce by another factor of the workgroup +/// size for consumption by pathtag-scan1. Wraps pathtag_reduce2.wgsl. /// internal static unsafe class PathtagReduce2ComputeShader { @@ -23,20 +25,28 @@ internal static unsafe class PathtagReduce2ComputeShader /// /// Creates the bind-group layout required by the pathtag-reduce2 stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage); - - BindGroupLayoutDescriptor descriptor = new() + // Bindings match pathtag_reduce2.wgsl: + // 0 reduced_in (read-only first-level partials from pathtag_reduce) + // 1 reduced (read-write; one second-level TagMonoid aggregate written per workgroup) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[2]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage); + + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 2, - Entries = entries + entryCount = 2, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs index 05cd6ede0..42d3507a5 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagReduceComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that reduces packed path tags into one monoid per workgroup. +/// GPU stage that runs the first level of the multi-level prefix scan over the packed path-tag +/// stream, reducing each workgroup's slice of tag words to a single TagMonoid that the scan +/// stages later turn into exclusive prefixes. Wraps pathtag_reduce.wgsl. /// internal static unsafe class PathtagReduceComputeShader { @@ -23,7 +25,11 @@ internal static unsafe class PathtagReduceComputeShader /// /// Gets the X workgroup count required to cover the packed path-tag words. + /// The shader reduces one 4-byte tag word per thread at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of packed 4-byte path-tag words. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathTagWords) => (pathTagWords + 255U) / 256U; @@ -31,21 +37,30 @@ public static uint GetDispatchX(uint pathTagWords) /// /// Creates the bind-group layout required by the pathtag-reduce stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; + // Bindings match pathtag_reduce.wgsl: + // 0 config uniform + // 1 scene (read-only; tag words read at config.pathtag_base) + // 2 reduced (read-write; one TagMonoid aggregate written per workgroup) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[3]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 3, - Entries = entries + entryCount = 3, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs index f8a6b5a1b..4a0c4fe16 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScan1ComputeShader.cs @@ -1,12 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that prepares the prefix buffer used by the large pathtag scan variant. +/// GPU stage that runs the middle level of the large pathtag scan: it combines the two levels +/// of reduction partials into an exclusive prefix per first-level partial, which the large +/// pathtag-scan variant consumes as its per-workgroup carry-in. Wraps pathtag_scan1.wgsl. /// internal static unsafe class PathtagScan1ComputeShader { @@ -23,21 +25,30 @@ internal static unsafe class PathtagScan1ComputeShader /// /// Creates the bind-group layout required by the pathtag-scan1 stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[3]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.ReadOnlyStorage); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.Storage); + // Bindings match pathtag_scan1.wgsl: + // 0 reduced (read-only first-level partials from pathtag_reduce) + // 1 reduced2 (read-only second-level partials from pathtag_reduce2) + // 2 tag_monoids (read-write; one exclusive prefix written per first-level partial) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[3]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.ReadOnlyStorage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 3, - Entries = entries + entryCount = 3, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs index 797f6f08a..b019a9b50 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PathtagScanComputeShader.cs @@ -1,22 +1,28 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that prefix-scans packed path tags, with both normal and small generated variants. +/// GPU stage that finishes the pathtag scan, writing an exclusive prefix TagMonoid per 4-byte +/// tag word for flatten to locate path data in the scene stream. Two generated variants share +/// this wrapper: the small variant scans the pathtag-reduce partials directly, while the large +/// variant reads per-workgroup prefixes precomputed by pathtag-scan1. Wraps +/// pathtag_scan.wgsl. /// internal static unsafe class PathtagScanComputeShader { /// - /// Gets the generated WGSL source bytes for the normal pathtag-scan variant. + /// Gets the generated WGSL source bytes for the large pathtag-scan variant, which reads + /// per-workgroup exclusive prefixes precomputed by pathtag-scan1. /// public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PathtagScanCode; /// - /// Gets the generated WGSL source bytes for the small pathtag-scan variant. + /// Gets the generated WGSL source bytes for the small pathtag-scan variant, which scans the + /// per-workgroup totals from pathtag-reduce in shared memory. /// public static ReadOnlySpan SmallShaderCode => GeneratedWgslShaderSources.PathtagScanSmallCode; @@ -28,22 +34,32 @@ internal static unsafe class PathtagScanComputeShader /// /// Creates the bind-group layout required by both pathtag-scan variants. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[4]; + // Bindings match pathtag_scan.wgsl (both variants): + // 0 config uniform + // 1 scene (read-only tag words) + // 2 reduced (read-only; per-workgroup totals for the small variant, exclusive prefixes for the large one) + // 3 tag_monoids (read-write; exclusive prefix written per tag word) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[4]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.ReadOnlyStorage); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.ReadOnlyStorage); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 4, - Entries = entries + entryCount = 4, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PrepareComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PrepareComputeShader.cs index 0ea11223c..874f405e7 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/PrepareComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PrepareComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that resets bump counters and can cancel later stages when the prior run already proved the current scratch sizes are too small. +/// GPU stage that starts a scheduling run by zeroing every bump allocator counter and the +/// failure mask on the GPU, avoiding a CPU buffer write. It never cancels later stages; all +/// stages run so the allocators report true demand in a single pass. Wraps prepare.wgsl. /// internal static unsafe class PrepareComputeShader { @@ -23,27 +25,35 @@ internal static unsafe class PrepareComputeShader /// /// Gets the fixed X workgroup count required by the prepare stage. + /// The stage runs as a single workgroup of one thread, so the count is always 1. /// + /// The X dispatch dimension in workgroups; always 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX() => 1; /// /// Creates the bind-group layout required by the prepare stage. /// + /// The WebGPU API facade. + /// The device that owns the staged-scene pipelines. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[2]; - entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, BufferBindingType.Storage, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + // Bindings match prepare.wgsl: + // 0 bump allocators (read-write; all counters and the failure mask zeroed) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[1]; + entries[0] = SceneShaderBindingLayoutHelper.CreateStorageEntry(0, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 2, - Entries = entries + entryCount = 1, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/PresentationShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/PresentationShader.cs new file mode 100644 index 000000000..34a027d4e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/PresentationShader.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU stage that transfers an ImageSharp-owned frame texture into a presentable surface attachment. +/// Wraps present.wgsl. +/// +internal static unsafe class PresentationShader +{ + /// + /// Gets the generated WGSL source bytes for the presentation stage. + /// + public static ReadOnlySpan ShaderCode => GeneratedWgslShaderSources.PresentCode; + + /// + /// Creates the bind-group layout required by the presentation stage. + /// + /// The WebGPU API facade. + /// The device that owns the presentation pipeline. + /// Receives the created bind-group layout on success. + /// Receives the creation failure reason when layout creation fails. + /// when the bind-group layout was created successfully; otherwise, . + public static bool TryCreateBindGroupLayout(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutImpl* layout, out string? error) + { + WGPUBindGroupLayoutEntry entry = new() + { + binding = 0, + visibility = (ulong)ShaderStage.Fragment, + texture = new WGPUTextureBindingLayout + { + sampleType = WGPUTextureSampleType.Float, + viewDimension = WGPUTextureViewDimension._2D, + multisampled = 0U + } + }; + + WGPUBindGroupLayoutDescriptor descriptor = new() + { + entryCount = 1, + entries = &entry + }; + + layout = api.DeviceCreateBindGroupLayout(device, in descriptor); + if (layout is null) + { + error = "Failed to create the WebGPU presentation bind-group layout."; + return false; + } + + error = null; + return true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs index 5797fbd4c..0d5047d05 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/SceneShaderBindingLayoutHelper.cs @@ -2,46 +2,57 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; +/// +/// Shared factory helpers for the buffer binding entries used by the staged-scene compute +/// shader bind-group layouts. +/// internal static class SceneShaderBindingLayoutHelper { /// /// Creates one compute-stage storage-buffer binding entry. /// + /// The WGSL binding index. + /// The storage-buffer access mode. + /// The minimum buffer binding size in bytes, or 0 to skip validation. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BindGroupLayoutEntry CreateStorageEntry( + public static WGPUBindGroupLayoutEntry CreateStorageEntry( uint binding, - BufferBindingType type, + WGPUBufferBindingType type, nuint minBindingSize = 0) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = type, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = type, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; /// /// Creates one compute-stage uniform-buffer binding entry. /// + /// The WGSL binding index. + /// The minimum buffer binding size in bytes. + /// The populated binding entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static BindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) + public static WGPUBindGroupLayoutEntry CreateUniformEntry(uint binding, nuint minBindingSize) => new() { - Binding = binding, - Visibility = ShaderStage.Compute, - Buffer = new BufferBindingLayout + binding = binding, + visibility = (ulong)ShaderStage.Compute, + buffer = new WGPUBufferBindingLayout { - Type = BufferBindingType.Uniform, - HasDynamicOffset = false, - MinBindingSize = minBindingSize + type = WGPUBufferBindingType.Uniform, + hasDynamicOffset = 0U, + minBindingSize = minBindingSize } }; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs b/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs index f0c5ff29c..93a12e316 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/TileAllocComputeShader.cs @@ -2,12 +2,14 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU stage that allocates and zeroes sparse per-path tile ranges after the row spans are known. +/// GPU stage that finalizes the horizontal extent of every sparse path row (widening for +/// backdrop seeds and right-boundary touches), bump-allocates the backing tile storage, and +/// zeroes the allocated tiles. Wraps tile_alloc.wgsl. /// internal static unsafe class TileAllocComputeShader { @@ -23,7 +25,11 @@ internal static unsafe class TileAllocComputeShader /// /// Gets the X workgroup count required to process every path. + /// The shader runs one thread per path at a workgroup size of 256, so this is + /// ceil( / 256). /// + /// The number of paths (draw objects) in the scene. + /// The X dispatch dimension in workgroups. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint GetDispatchX(uint pathCount) => (pathCount + 255U) / 256U; @@ -38,21 +44,27 @@ public static uint GetDispatchX(uint pathCount) /// when the bind-group layout was created successfully; otherwise, . public static bool TryCreateBindGroupLayout( WebGPU api, - Device* device, - out BindGroupLayout* layout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* layout, out string? error) { - BindGroupLayoutEntry* entries = stackalloc BindGroupLayoutEntry[5]; + // Bindings match tile_alloc.wgsl: + // 0 config uniform + // 1 bump allocators (read-write; tile counter bump-allocated, failure bit set on overflow) + // 2 paths (read-only Path records from path_row_alloc) + // 3 rows (read-write; final x0/x1 and base tile index written) + // 4 tiles (read-write; allocated tiles zeroed) + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[5]; entries[0] = SceneShaderBindingLayoutHelper.CreateUniformEntry(0, (nuint)sizeof(GpuSceneConfig)); - entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, BufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); - entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, BufferBindingType.ReadOnlyStorage); - entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, BufferBindingType.Storage); - entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, BufferBindingType.Storage); + entries[1] = SceneShaderBindingLayoutHelper.CreateStorageEntry(1, WGPUBufferBindingType.Storage, (nuint)sizeof(GpuSceneBumpAllocators)); + entries[2] = SceneShaderBindingLayoutHelper.CreateStorageEntry(2, WGPUBufferBindingType.ReadOnlyStorage); + entries[3] = SceneShaderBindingLayoutHelper.CreateStorageEntry(3, WGPUBufferBindingType.Storage); + entries[4] = SceneShaderBindingLayoutHelper.CreateStorageEntry(4, WGPUBufferBindingType.Storage); - BindGroupLayoutDescriptor descriptor = new() + WGPUBindGroupLayoutDescriptor descriptor = new() { - EntryCount = 5, - Entries = entries + entryCount = 5, + entries = entries }; layout = api.DeviceCreateBindGroupLayout(device, in descriptor); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl index 389268577..4859d45eb 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bbox.wgsl @@ -1,23 +1,38 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// The annotated bounding box for a path. It has been transformed, -// but contains a link to the active transform, mostly for gradients. +// Per-path device-space bounding box record. +// +// Cleared by bbox_clear.wgsl, atomically widened by flatten.wgsl (via its +// field-compatible AtomicPathBbox view of the same buffer), then consumed by +// binning.wgsl, clip_reduce.wgsl, clip_leaf.wgsl, and draw_leaf.wgsl. +// Ported from Vello's shader/shared/bbox.wgsl (linebender/vello) with +// ImageSharp additions (coverage_threshold, interest). + +// The annotated bounding box for a transformed path. // Coordinates are integer pixels (for the convenience of atomic update) // but will probably become fixed-point fractions for rectangles. // -// TODO: This also carries a `draw_flags` field that contains information that gets propagated to -// the draw info stream. This is currently only used for the fill rule. If the other bits remain -// unused we could possibly pack this into some other field, such as the MSB of `trans_ix`. +// `draw_flags` is propagated to the draw info stream and carries fill-rule, aliasing, and blend state. +// Field layout must mirror AtomicPathBbox in flatten.wgsl. struct PathBbox { - x0: i32, + x0: i32, // min bounds; i32 max while empty (see bbox_clear) y0: i32, - x1: i32, + x1: i32, // max bounds; x0 > x1 denotes an empty box y1: i32, draw_flags: u32, - trans_ix: u32, + trans_ix: u32, // index of the path's transform in the scene stream + // Per-fill coverage parameter, propagated from the style record to the fine pass. + // For aliased fills this is the quantization cutoff in [0,1]; for antialiased fills it + // carries the perceptual coverage boost for text (zero when disabled). The draw-flags + // aliased bit selects the interpretation; the two uses are mutually exclusive. + coverage_threshold: f32, + _padding: u32, + interest: vec4, // raster interest rect (x0, y0, x1, y1) in pixels } +// Intersection of two (x0, y0, x1, y1) boxes; may produce an empty or +// inverted box, which callers must treat as zero area. fn bbox_intersect(a: vec4, b: vec4) -> vec4 { return vec4(max(a.xy, b.xy), min(a.zw, b.zw)); } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl index d59a51e93..fc94cf028 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/blend.wgsl @@ -1,7 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Color mixing modes +// Color mixing and compositing math for layer blending. +// +// Imported by fine.wgsl, which calls blend_mix_compose when popping a clip or +// layer (CMD_END_CLIP) to combine the layer contents with its backdrop. The +// math mirrors the CPU drawing backend's blending so both backends produce +// the same output for a given GraphicsOptions. +// +// Ported from Vello's shader/shared/blend.wgsl (linebender/vello). + +// Color mixing modes. +// +// These are the high byte of the packed blend-mode word ((mix << 8) | compose) +// produced by WebGPUSceneEncoder.PackBlendMode, so every value documents the +// wire format shared with the C# encoder even where no WGSL code names it. +// The values mirror the CSS mix-blend-mode list from Vello; the C# encoder +// currently emits only NORMAL, MULTIPLY, SCREEN, OVERLAY, DARKEN, LIGHTEN, +// HARD_LIGHT, ADD, and SUBTRACT. const MIX_NORMAL = 0u; const MIX_MULTIPLY = 1u; @@ -21,12 +37,20 @@ const MIX_HUE = 12u; const MIX_SATURATION = 13u; const MIX_COLOR = 14u; const MIX_LUMINOSITY = 15u; +// Wire value written by the C# encoder (WebGPUSceneEncoder.ClipBlendMode) to +// mark a pure clip layer. As the mix half of the packed word it occupies bit +// 15, which blend_mix_compose masks off so clip layers take the plain +// src-over fast path. No WGSL code names this constant; it documents the +// wire format shared with the encoder. const MIX_CLIP = 128u; +// Screen blend: inverted multiply of the inverted channels. fn screen(cb: vec3, cs: vec3) -> vec3 { return cb + cs - (cb * cs); } +// Per-channel color-dodge blend. The zero and one cases are handled +// explicitly to avoid the division blowing up. fn color_dodge(cb: f32, cs: f32) -> f32 { if cb == 0.0 { return 0.0; @@ -37,6 +61,7 @@ fn color_dodge(cb: f32, cs: f32) -> f32 { } } +// Per-channel color-burn blend, the dual of color_dodge. fn color_burn(cb: f32, cs: f32) -> f32 { if cb == 1.0 { return 1.0; @@ -47,6 +72,8 @@ fn color_burn(cb: f32, cs: f32) -> f32 { } } +// Hard-light blend: multiply for dark source channels, screen for light ones. +// Overlay is implemented as hard_light with the operands swapped. fn hard_light(cb: vec3, cs: vec3) -> vec3 { return select( screen(cb, 2.0 * cs - 1.0), @@ -55,6 +82,8 @@ fn hard_light(cb: vec3, cs: vec3) -> vec3 { ); } +// Soft-light blend per the W3C compositing spec, using the piecewise +// polynomial approximation of the darkening curve for cb <= 0.25. fn soft_light(cb: vec3, cs: vec3) -> vec3 { let d = select( sqrt(cb), @@ -68,20 +97,27 @@ fn soft_light(cb: vec3, cs: vec3) -> vec3 { ); } +// Saturation of a color: the spread between its largest and smallest channel. fn sat(c: vec3) -> f32 { return max(c.x, max(c.y, c.z)) - min(c.x, min(c.y, c.z)); } +// Luminosity using the NTSC weights specified by the W3C compositing spec +// for the non-separable blend modes. fn lum(c: vec3) -> f32 { let f = vec3(0.3, 0.59, 0.11); return dot(c, f); } +// Luminance using the SVG/Rec. 709 coefficients. Used by fine.wgsl to build +// luminance masks for hard-mask clips rather than for blending. fn svg_lum(c: vec3) -> f32 { let f = vec3(0.2125, 0.7154, 0.0721); return dot(c, f); } +// Clamps an out-of-gamut color back into [0, 1] by scaling its channels +// toward the luminosity, preserving perceived lightness (spec ClipColor). fn clip_color(c_in: vec3) -> vec3 { var c = c_in; let l = lum(c); @@ -96,10 +132,13 @@ fn clip_color(c_in: vec3) -> vec3 { return c; } +// Shifts a color to the target luminosity, then re-clips into gamut. fn set_lum(c: vec3, l: f32) -> vec3 { return clip_color(c + (l - lum(c))); } +// Rescales three channel values, already ordered min/mid/max, so that the +// spread becomes s while the mid channel keeps its relative position. fn set_sat_inner( cmin: ptr, cmid: ptr, @@ -116,6 +155,8 @@ fn set_sat_inner( *cmin = 0.0; } +// Sets the saturation of a color to s (spec SetSat). The branch ladder +// orders the channels so set_sat_inner receives them as min/mid/max. fn set_sat(c: vec3, s: f32) -> vec3 { var r = c.r; var g = c.g; @@ -144,8 +185,9 @@ fn set_sat(c: vec3, s: f32) -> vec3 { return vec3(r, g, b); } -// Blends two RGB colors together. The colors are assumed to be in sRGB -// color space, and this function does not take alpha into account. +// Blends two RGB colors together using the given MIX_* mode. The colors are +// assumed to be in sRGB color space, and this function does not take alpha +// into account; unknown modes (including MIX_NORMAL) return the source. fn blend_mix(cb: vec3, cs: vec3, mode: u32) -> vec3 { var b = vec3(0.0); switch mode { @@ -207,7 +249,12 @@ fn blend_mix(cb: vec3, cs: vec3, mode: u32) -> vec3 { return b; } -// Composition modes +// Composition modes. +// +// Porter-Duff style alpha composition operators. These are the low byte of +// the packed blend-mode word and match the values produced by the C# encoder +// (WebGPUSceneEncoder.MapAlphaCompositionMode), so unused-looking entries +// still document the shared wire format. const COMPOSE_CLEAR = 0u; const COMPOSE_COPY = 1u; @@ -226,6 +273,8 @@ const COMPOSE_PLUS_LIGHTER = 13u; // Apply general compositing operation. // Inputs are separated colors and alpha, output is premultiplied. +// fa/fb are the Porter-Duff source and backdrop fractions for the mode; +// COMPOSE_CLEAR falls through the default with fa = fb = 0. fn blend_compose( cb: vec3, cs: vec3, @@ -296,6 +345,7 @@ fn blend_compose( return vec4(co, min(as_fa + ab_fb, 1.0)); } +// Converts a premultiplied color to separated RGB by dividing out alpha. fn unpremultiply(color: vec4) -> vec3 { let EPSILON = 1e-15; // Max with a small epsilon to avoid NaNs. @@ -304,14 +354,16 @@ fn unpremultiply(color: vec4) -> vec3 { } // Apply color mixing and composition. Both input and output colors are -// premultiplied RGB. +// premultiplied RGB. `mode` is the packed word (mix << 8) | compose written +// by the C# encoder's PackBlendMode. fn blend_mix_compose(backdrop: vec4, src: vec4, mode: u32) -> vec4 { let BLEND_DEFAULT = ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER); + // The 0x7fff mask strips bit 15, the MIX_CLIP marker, so both the plain + // normal+src_over blend and the pure clip case take this fast path. if (mode & 0x7fffu) == BLEND_DEFAULT { - // Both normal+src_over blend and clip case return backdrop * (1.0 - src.a) + src; } - // Un-premultiply colors for blending. + // Un-premultiply colors for blending. var cs = unpremultiply(src); let cb = unpremultiply(backdrop); let mix_mode = mode >> 8u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl index 747b70256..9d40f8b73 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/bump.wgsl @@ -1,6 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// The shared bump allocator block for dynamically sized intermediate buffers. +// +// Stages reserve space with atomicAdd on the counter for their buffer and +// compare the result against the capacity in Config; on overflow they set +// their STAGE_* bit in `failed` and the C# side reads the block back, grows +// the buffers, and retries the render. Imported by most compute stages +// (binning, flatten, tile/row allocation, counting, coarse, chunk_reset, +// prepare, and the indirect-dispatch setup shaders). +// +// Ported from Vello's shader/shared/bump.wgsl (linebender/vello), where the +// struct was synced with the encoding crate's config.rs. + // Bitflags for each stage that can fail allocation. const STAGE_BINNING: u32 = 0x1u; const STAGE_TILE_ALLOC: u32 = 0x2u; @@ -8,20 +20,25 @@ const STAGE_FLATTEN: u32 = 0x4u; const STAGE_PATH_COUNT: u32 = 0x8u; const STAGE_COARSE: u32 = 0x10u; -// This must be kept in sync with the struct in config.rs in the encoding crate. +// Bump counters, one per dynamically sized buffer. Field order must be kept +// in sync with GpuSceneBumpAllocators in WebGPUSceneResources.cs, which the +// C# side reads back to detect overflow. struct BumpAllocators { // Bitmask of stages that have failed allocation. failed: atomic, - binning: atomic, - ptcl: atomic, - path_rows: atomic, - tile: atomic, - seg_counts: atomic, - segments: atomic, - blend_spill: atomic, - lines: atomic, + binning: atomic, // bin data words (info/bin-data buffer) + ptcl: atomic, // per-tile command list words + path_rows: atomic, // sparse PathRow records + tile: atomic, // Tile records + seg_counts: atomic, // SegmentCount records + segments: atomic, // Segment records + blend_spill: atomic, // blend stack slots spilled past BLEND_STACK_SPLIT + lines: atomic, // LineSoup records from flattening } +// Workgroup counts for dispatchWorkgroupsIndirect, written by the setup +// shaders (path_count_setup, path_tiling_setup) from the bump counters. +// Mirrors GpuSceneIndirectCount in WebGPUSceneResources.cs. struct IndirectCount { count_x: u32, count_y: u32, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/clip.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/clip.wgsl index 37f3be37c..3b59f98df 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/clip.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/clip.wgsl @@ -1,16 +1,29 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Types for the clip stack monoid used to resolve nested clips. +// +// Imported by clip_reduce.wgsl and clip_leaf.wgsl, which match BeginClip and +// EndClip records via a stack-monoid scan, and by draw_leaf.wgsl, which emits +// the ClipInp stream. Ported from Vello's shader/shared/clip.wgsl +// (linebender/vello) with the ImageSharp clip-operation extension. + +// Stack-monoid element (the "bicyclic semigroup"): a is the number of +// unmatched EndClips (pops) and b the number of unmatched BeginClips +// (pushes) in a range of the clip stream. struct Bic { a: u32, b: u32, } +// The monoid combine operator: pops from y cancel pushes from x. fn bic_combine(x: Bic, y: Bic) -> Bic { let m = min(x.b, y.a); return Bic(x.a + y.a - m, x.b + y.b - m); } +// One clip stream record, written per BeginClip/EndClip draw object by +// draw_leaf (mirrored by GpuClipInp in WebGPUSceneResources.cs). struct ClipInp { // Index of the draw object. ix: u32, @@ -18,9 +31,22 @@ struct ClipInp { // this entry is a BeginClip and contains the associated path index. If negative, // it is an EndClip and contains the bitwise-not of the EndClip draw object index. path_ix: i32, + // ImageSharp extension over Vello's clip record. Vello's Clip has only ix/path_ix; + // we carry the render clip operation so Difference clips can remain stack records + // instead of being rewritten as inverse geometry. + operation: u32, } +// ClipInp.operation values, matching ImageSharp's ClipOperation enum as +// encoded by the C# encoder. clip_leaf treats a Difference clip's bbox as +// infinite so it never narrows descendant bounds (removing area from a clip +// cannot be bounded by the removed shape). +const CLIP_OPERATION_INTERSECTION = 0u; +const CLIP_OPERATION_DIFFERENCE = 1u; + +// Per-BeginClip stack element written by clip_reduce and consumed by +// clip_leaf when resolving each clip's ancestor chain. struct ClipEl { - parent_ix: u32, - bbox: vec4, + parent_ix: u32, // index of the enclosing clip stream element + bbox: vec4, // clip path bounds used to narrow descendant draw bboxes } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl index 0f2695b35..2510c8004 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/config.wgsl @@ -1,7 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// This must be kept in sync with `ConfigUniform` in `vello_encoding/src/config.rs` +// The per-scene GPU configuration uniform plus tile-geometry constants. +// +// Imported by every compute stage; bound at group slot zero. Derived from +// Vello's shader/shared/config.wgsl (linebender/vello), where the struct was +// synced with ConfigUniform in vello_encoding/src/config.rs. +// +// Written by the C# side each render attempt. Field order and sizes must be +// kept in sync with GpuSceneConfig in WebGPUSceneResources.cs (values are +// computed by WebGPUSceneConfig.cs). struct Config { width_in_tiles: u32, height_in_tiles: u32, @@ -9,6 +17,8 @@ struct Config { target_width: u32, target_height: u32, + // Chunked rendering window: the first global tile row rendered by this + // attempt and the number of real tile rows it covers. chunk_tile_y_start: u32, chunk_tile_height: u32, @@ -16,15 +26,15 @@ struct Config { // The format is packed RGBA8 in MSB order. base_color: u32, - n_drawobj: u32, - n_path: u32, - n_clip: u32, + n_drawobj: u32, // number of draw objects in the scene + n_path: u32, // number of paths in the scene + n_clip: u32, // number of clip begin/end records in the scene - // To reduce the number of bindings, info, path-gradient data, and bin data are combined + // To reduce the number of bindings, info, auxiliary brush data, and bin data are combined // into one buffer. - bin_data_start: u32, - path_gradient_data_base: u32, - ptcl_dyn_start: u32, + bin_data_start: u32, // start of bump-allocated bin data within that buffer + brush_data_base: u32, // start of auxiliary brush data within that buffer + ptcl_dyn_start: u32, // start of the bump-allocated region of the ptcl buffer // offsets within scene buffer (in u32 units) pathtag_base: u32, @@ -45,6 +55,8 @@ struct Config { segments_size: u32, blend_size: u32, ptcl_size: u32, + // Scene-wide coverage cutoff consumed by the aliased fine pass; per-fill + // thresholds (CmdFill.coverage_threshold) override it where set. fine_coverage_threshold: f32, } @@ -57,12 +69,14 @@ const N_TILE_X = 16u; const N_TILE_Y = 16u; const N_TILE = N_TILE_X * N_TILE_Y; -// Not currently supporting non-square tiles +// Reciprocal of TILE_WIDTH/TILE_HEIGHT, used to map pixel coordinates to +// tile coordinates. Not currently supporting non-square tiles. const TILE_SCALE = 0.0625; // The "split" point between using local memory in fine for the blend stack and spilling to the blend_spill buffer. // A higher value will increase vgpr ("register") pressure in fine, but decrease required dynamic memory allocation. -// If changing, also change in vello_shaders/src/cpu/coarse.rs. +// Used by coarse.wgsl to reserve blend_spill scratch for tiles whose clip depth +// exceeds the split, and by fine.wgsl to pick the stack slot at run time. const BLEND_STACK_SPLIT = 4u; // The following are computed in draw_leaf from the generic gradient parameters @@ -77,3 +91,9 @@ const RAD_GRAD_KIND_CONE = 4u; // Radial gradient flags const RAD_GRAD_SWAPPED = 1u; + +// Elliptic gradient kinds. Degenerate kinds preserve the CPU brush's IEEE-754 +// division behavior without placing infinities or NaNs in the transform matrix. +const ELLIPTIC_GRAD_KIND_NORMAL = 0u; +const ELLIPTIC_GRAD_KIND_POINT = 1u; +const ELLIPTIC_GRAD_KIND_LINE = 2u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/cubic.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/cubic.wgsl deleted file mode 100644 index 5071cac3e..000000000 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/cubic.wgsl +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -struct Cubic { - p0: vec2, - p1: vec2, - p2: vec2, - p3: vec2, - stroke: vec2, - path_ix: u32, - flags: u32, -} - -const CUBIC_IS_STROKE = 1u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl index 83eb1398b..f6e1d3a0d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/drawtag.wgsl @@ -1,6 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Draw tag stream decoding: the draw monoid, tag values, and draw-flag bits. +// +// Imported by binning.wgsl, clip_leaf.wgsl, coarse.wgsl, draw_leaf.wgsl, +// draw_reduce.wgsl, fine.wgsl, flatten.wgsl, and path_row_alloc.wgsl. +// Ported from Vello's shader/shared/drawtag.wgsl (linebender/vello) with +// ImageSharp additions (recolor, elliptic and path gradient tags, the +// aliased-coverage bit, and the blend mode/alpha draw-flag fields). +// Tag words are produced by the C# encoder (WebGPUSceneEncoder.cs), so the +// constants below document the shared wire format even where WGSL does not +// reference them all. + // The DrawMonoid is computed as a prefix sum to aid in decoding // the variable-length encoding of draw objects. struct DrawMonoid { @@ -15,32 +26,56 @@ struct DrawMonoid { } // Each draw object has a 32-bit draw tag, which is a bit-packed -// version of the draw monoid. +// version of the draw monoid: bit 0 = clip count, bits 2..4 = scene words, +// bits 6..9 = info words (see map_draw_tag). +// Visible-fill draw tags carry five extra info words: coverage threshold plus raster interest. const DRAWTAG_NOP = 0u; -const DRAWTAG_FILL_COLOR = 0x44u; -const DRAWTAG_FILL_RECOLOR = 0x4cu; -const DRAWTAG_FILL_LIN_GRADIENT = 0x114u; -const DRAWTAG_FILL_RAD_GRADIENT = 0x29cu; -const DRAWTAG_FILL_ELLIPTIC_GRADIENT = 0x1dcu; -const DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u; -const DRAWTAG_FILL_PATH_GRADIENT = 0x50u; -const DRAWTAG_FILL_IMAGE = 0x294u; +const DRAWTAG_FILL_COLOR = 0x188u; +const DRAWTAG_FILL_RECOLOR = 0x184u; +const DRAWTAG_FILL_LIN_GRADIENT = 0x254u; +const DRAWTAG_FILL_RAD_GRADIENT = 0x3dcu; +const DRAWTAG_FILL_ELLIPTIC_GRADIENT = 0x35cu; +const DRAWTAG_FILL_SWEEP_GRADIENT = 0x394u; +const DRAWTAG_FILL_PATH_GRADIENT = 0x190u; +const DRAWTAG_FILL_IMAGE = 0x3d4u; const DRAWTAG_BEGIN_CLIP = 0x49u; const DRAWTAG_END_CLIP = 0x21u; -/// The first word of each draw info stream entry contains the flags. This is not a part of the -/// draw object stream but get used after the draw objects have been reduced on the GPU. -/// 0 represents a non-zero fill. 1 represents an even-odd fill. +// The first word of each draw info stream entry contains the flags. This is not part of the +// draw object stream but is used after the draw objects have been reduced on the GPU. +// 0 represents a non-zero fill. 1 represents an even-odd fill. const DRAW_INFO_FLAGS_FILL_RULE_BIT = 1u; +// Per-fill coverage rule. When set, the fill is rasterized aliased (coverage quantized against +// config.fine_coverage_threshold) instead of using analytic area coverage. Carried in a free +// high bit of the draw-flags word alongside the fill-rule bit. +const DRAW_INFO_FLAGS_ALIASED_BIT = 0x40000000u; +// Blend state packed into the draw-flags word by the C# encoder +// (WebGPUSceneEncoder.PackStyleDrawFlags): bits 1..13 hold the (mix << 8) | compose +// blend word and bits 14..29 hold the blend percentage quantized to 16 bits. const DRAW_FLAGS_BLEND_MODE_SHIFT = 1u; const DRAW_FLAGS_BLEND_MODE_MASK = 0x3ffeu; const DRAW_FLAGS_BLEND_ALPHA_SHIFT = 14u; const DRAW_FLAGS_BLEND_ALPHA_MASK = 0x3fffc000u; +// Flag bits carried in the high bits of the clip blend word, set by the C# encoder +// (WebGPUSceneEncoder.AppendClipBeginData). DIFFERENCE marks an ImageSharp Difference +// clip so fine inverts the mask; HARD marks a hard-edge (aliased) clip mask. Declared +// here because every consumer (draw_leaf, coarse, fine) imports this module. +const CLIP_DIFFERENCE_MASK_BIT = 0x80000000u; +const CLIP_HARD_MASK_BIT = 0x40000000u; +// Marks an ISOLATED group (a layer). Isolated groups seed with transparent content and +// composite back with blend_mix_compose, matching the CPU backend's clean layer targets. +// Non-isolated groups (canvas clips) seed with a copy of the current tile content and pop +// with a coverage lerp, matching the CPU backend's per-draw clip masking so composition +// modes such as Src behave identically whether or not a tile goes through the clip group. +const CLIP_ISOLATED_MASK_BIT = 0x20000000u; + +// The scan identity: all counters zero. fn draw_monoid_identity() -> DrawMonoid { return DrawMonoid(); } +// The monoid combine operator: plain component-wise addition. fn combine_draw_monoid(a: DrawMonoid, b: DrawMonoid) -> DrawMonoid { var c: DrawMonoid; c.path_ix = a.path_ix + b.path_ix; @@ -50,6 +85,8 @@ fn combine_draw_monoid(a: DrawMonoid, b: DrawMonoid) -> DrawMonoid { return c; } +// Maps a draw tag word to its DrawMonoid contribution by unpacking the +// per-object counts from the tag's bit fields. fn map_draw_tag(tag_word: u32) -> DrawMonoid { var c: DrawMonoid; c.path_ix = u32(tag_word != DRAWTAG_NOP); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl index a795913f3..89f5a448d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/pathtag.wgsl @@ -1,15 +1,32 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Path tag stream decoding: the tag monoid and its scan operators. +// +// The scene encodes each path as a byte stream of tags (one per segment, +// plus transform/style/path markers). The pathtag_reduce/scan stages compute +// a prefix sum of TagMonoid over that stream so flatten.wgsl can locate each +// segment's point data, transform, and style in O(1). Imported by +// flatten.wgsl, pathtag_reduce.wgsl, pathtag_reduce2.wgsl, pathtag_scan.wgsl, +// and pathtag_scan1.wgsl. +// +// Ported from Vello's shader/shared/pathtag.wgsl (linebender/vello). Tag +// bytes are produced by the C# encoder (WebGPUSceneEncoder.cs), so the +// constants below document the shared wire format even where WGSL does not +// reference them all. + +// Running counts accumulated over the tag stream. Each field is the number +// of stream elements of that kind preceding the current position. struct TagMonoid { - trans_ix: u32, - // TODO: I don't think pathseg_ix is used. - pathseg_ix: u32, - pathseg_offset: u32, - style_ix: u32, - path_ix: u32, + trans_ix: u32, // transform marker count (index into the transform stream) + pathseg_offset: u32, // offset into the path data stream, in u32 words + style_ix: u32, // style marker count premultiplied by STYLE_SIZE_IN_WORDS + path_ix: u32, // path marker count (index of the current path) } +// Path tag byte layout. The low two bits select the segment type; bit 2 +// marks the last segment of a subpath, bit 3 selects f32 (vs i16) point +// encoding, and the high bits mark path/transform/style stream advances. const PATH_TAG_SEG_TYPE = 3u; const PATH_TAG_LINETO = 1u; const PATH_TAG_QUADTO = 2u; @@ -21,8 +38,12 @@ const PATH_TAG_STYLE = 0x40u; const PATH_TAG_SUBPATH_END = 4u; // Size of the `Style` data structure in words -const STYLE_SIZE_IN_WORDS: u32 = 5u; +const STYLE_SIZE_IN_WORDS: u32 = 10u; +// Flag bits of the first word of a Style record, packed by the C# encoder. +// STYLE marks a stroke (vs fill) and FILL marks the even-odd fill rule +// (clear = non-zero); the cap and join fields drive the GPU stroker in +// flatten.wgsl. const STYLE_FLAGS_STYLE: u32 = 0x80000000u; const STYLE_FLAGS_FILL: u32 = 0x40000000u; @@ -42,25 +63,33 @@ const STYLE_FLAGS_JOIN_MITER_ROUND: u32 = 0x00800000u; // TODO: Declare the remaining STYLE flags here. +// The scan identity: all counters zero. fn tag_monoid_identity() -> TagMonoid { return TagMonoid(); } +// The monoid combine operator: plain component-wise addition. fn combine_tag_monoid(a: TagMonoid, b: TagMonoid) -> TagMonoid { var c: TagMonoid; c.trans_ix = a.trans_ix + b.trans_ix; - c.pathseg_ix = a.pathseg_ix + b.pathseg_ix; c.pathseg_offset = a.pathseg_offset + b.pathseg_offset; c.style_ix = a.style_ix + b.style_ix; c.path_ix = a.path_ix + b.path_ix; return c; } +// Maps four packed tag bytes to their combined TagMonoid contribution. +// Works on all four bytes at once using SWAR arithmetic: each 0x01010101 +// multiply broadcasts a mask to every byte lane. fn reduce_tag(tag_word: u32) -> TagMonoid { var c: TagMonoid; let point_count = tag_word & 0x3030303u; - c.pathseg_ix = countOneBits((point_count * 7u) & 0x4040404u); c.trans_ix = countOneBits(tag_word & (PATH_TAG_TRANSFORM * 0x1010101u)); + // Points consumed per byte: the 2-bit segment type (1..3), plus one when + // the subpath-end bit is set since that segment's last point is not + // shared with a successor. The f32 flag (bit 3) doubles the word count + // (2 words per point instead of 1 for packed i16). The trailing shifts + // horizontally add the four byte lanes into a single total. let n_points = point_count + ((tag_word >> 2u) & 0x1010101u); var a = n_points + (n_points & (((tag_word >> 3u) & 0x1010101u) * 15u)); a += a >> 8u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl index e5c74236b..56d9942d3 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/ptcl.wgsl @@ -1,18 +1,30 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Layout of per-tile command list -// Initial allocation, in u32's. +// Layout of the per-tile command list (PTCL). +// +// The PTCL is the intermediate representation between the coarse and fine +// stages: coarse.wgsl appends a command stream per 16x16 tile into the ptcl +// buffer, and fine.wgsl walks that stream to shade each pixel. Imported by +// coarse.wgsl (writer) and fine.wgsl (reader). +// +// Ported from Vello's shader/shared/ptcl.wgsl (linebender/vello) with +// ImageSharp additions (recolor, elliptic and path gradients, per-fill +// aliased coverage, clip mask bits). + +// PTCL allocation sizes, in u32 units. Each tile starts with a fixed +// PTCL_INITIAL_ALLOC slice; when a stream outgrows its slice, coarse bump +// allocates another PTCL_INCREMENT and links it with a CMD_JUMP. const PTCL_INITIAL_ALLOC = 64u; const PTCL_INCREMENT = 256u; -// Amount of space taken by jump +// Amount of space reserved at the end of each slice for the CMD_JUMP +// (tag word plus destination index). const PTCL_HEADROOM = 2u; -// Tags for PTCL commands +// Tags for PTCL commands. Slots 2u and 4u are retired Vello opcodes and stay unassigned. const CMD_END = 0u; const CMD_FILL = 1u; -const CMD_STROKE = 2u; const CMD_SOLID = 3u; const CMD_COLOR = 5u; const CMD_RECOLOR = 14u; @@ -29,59 +41,75 @@ const CMD_PATH_GRAD = 16u; // 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. struct CmdFill { - size_and_rule: u32, - seg_data: u32, - backdrop: i32, -} - -struct CmdStroke { - tile: u32, - half_width: f32, + 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 + backdrop: i32, // winding number at the tile's left edge + // Per-fill coverage parameter, mode-dependent because the two uses are mutually + // exclusive: the quantization threshold when the aliased bit in size_and_rule is set, + // otherwise the perceptual coverage boost for antialiased text (zero when disabled). + coverage_threshold: f32, + interest: vec4, // pixel rect (x0, y0, x1, y1); coverage outside it is zeroed } +// Continue reading the command stream at new_ix; links bump-allocated +// PTCL slices together. struct CmdJump { new_ix: u32, } +// Solid color paint. struct CmdColor { - rgba_color: u32, - draw_flags: u32, + color_rg: u32, // associated RG packed as binary16 + color_ba: u32, // associated BA packed as binary16 + draw_flags: u32, // blend mode and alpha, see DRAW_FLAGS_* in drawtag.wgsl } +// Recolor paint. The target-specialized source, target and threshold live in +// the shared auxiliary brush-data region so their f32 precision survives PTCL lowering. struct CmdRecolor { - source_color: u32, - target_color: u32, - threshold: f32, + data_offset: u32, draw_flags: u32, } +// Linear gradient paint. line_x/line_y/line_c define the implicit line +// equation whose signed value is the gradient parameter t. struct CmdLinGrad { - index: u32, - extend_mode: u32, + index: u32, // ramp row in the gradients texture + extend_mode: u32, // pad, repeat, or reflect line_x: f32, line_y: f32, line_c: f32, } +// Radial gradient paint using the two-circle formulation computed in +// draw_leaf (see RAD_GRAD_KIND_* in config.wgsl). struct CmdRadGrad { index: u32, extend_mode: u32, - matrx: vec4, - xlat: vec2, + matrx: vec4, // inverse transform, row-pair packed like Transform.matrx + xlat: vec2, // inverse transform translation focal_x: f32, radius: f32, - kind: u32, - flags: u32, + kind: u32, // RAD_GRAD_KIND_* selector + flags: u32, // RAD_GRAD_SWAPPED when the circle order was flipped } +// Elliptic gradient paint. Normal ellipses map pixels into normalized unit +// space; point and line kinds retain finite local coordinates so fine can +// reproduce the CPU brush's zero-radius division semantics. struct CmdEllipticGrad { index: u32, extend_mode: u32, matrx: vec4, xlat: vec2, + kind: u32, // ELLIPTIC_GRAD_KIND_* selector } +// Sweep (conic) gradient paint sweeping angles t0..t1 about the origin of +// the inverse-transformed space. struct CmdSweepGrad { index: u32, extend_mode: u32, @@ -91,26 +119,35 @@ struct CmdSweepGrad { t1: f32, } +// Path gradient paint: t is derived from edge distances of the source path, +// 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. data_offset: u32, edge_count: u32, flags: u32, draw_flags: u32, } +// Image paint sampled from the atlas texture. struct CmdImage { - matrx: vec4, + matrx: vec4, // inverse transform mapping device to image space xlat: vec2, - atlas_offset: vec2, - extents: vec2, + atlas_offset: vec2, // top-left of the image within the atlas + extents: vec2, // image size in pixels format: u32, x_extend_mode: u32, y_extend_mode: u32, alpha: f32, alpha_type: u32, + signed_unit: u32, } +// Pops a clip/layer: blends the layer against its backdrop and, for clips, +// applies the mask coverage. struct CmdEndClip { - blend: u32, - alpha: f32, + blend: u32, // packed blend word plus CLIP_*_MASK_BIT flags + alpha: f32, // layer opacity } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl index 70a0d7f84..7c9abbd8d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/segment.wgsl @@ -1,11 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Segments laid out for contiguous storage +// Line segment records flowing between flattening and fine rasterization. +// +// Imported by fine.wgsl, flatten.wgsl, path_count.wgsl, path_row_span.wgsl, +// and path_tiling.wgsl. Ported from Vello's shader/shared/segment.wgsl +// (linebender/vello). + +// Segments laid out for contiguous storage: path_tiling clips each LineSoup +// line against its tiles and writes one Segment per (tile, line) crossing +// for fine to integrate. struct Segment { // Points are relative to tile origin point0: vec2, point1: vec2, + // Tile-relative y at which the segment meets the tile's left edge, or + // 1e9 if it does not; fine accumulates the implied vertical edge there + // to keep winding consistent after clipping. y_edge: f32, } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl index 7861ab682..ec256f0cc 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/tile.wgsl @@ -2,7 +2,16 @@ // Licensed under the Six Labors Split License. // Common datatypes for path and tile intermediate info. +// +// Imported by backdrop_dyn.wgsl, coarse.wgsl, path_count.wgsl, +// path_row_alloc.wgsl, path_row_span.wgsl, path_tiling.wgsl, and +// tile_alloc.wgsl. Derived from Vello's shader/shared/tile.wgsl +// (linebender/vello); the sparse PathRow row records are an ImageSharp +// addition that lets tile storage be allocated per touched row span rather +// than for the whole path bounding box. +// Per-path tiling record: written by path_row_alloc, consumed by the +// stages that walk a path's tiles. struct Path { // bounding box in tiles bbox: vec4, @@ -10,13 +19,18 @@ struct Path { rows: u32, } +// One tile row of a path's bounding box. path_row_span narrows x0/x1 to the +// columns actually touched, then tile_alloc allocates that span and rewrites +// `tiles` from flags to the base tile index. struct PathRow { - x0: u32, - x1: u32, - backdrop: i32, - tiles: u32, + x0: u32, // leftmost touched tile column (min-reduced from 0xffffffff) + x1: u32, // exclusive right bound of touched tile columns (max-reduced from 0) + backdrop: i32, // accumulated winding delta carried into the row's left edge + tiles: u32, // PATH_ROW_FLAG_* bits before tile_alloc, base tile index afterwards } +// Atomic view of PathRow for the stages that reduce into it concurrently. +// Must mirror PathRow field-for-field: both views alias the same buffer. struct AtomicPathRow { x0: atomic, x1: atomic, @@ -24,10 +38,14 @@ struct AtomicPathRow { tiles: atomic, } +// Set on PathRow.tiles while it still holds flags: the row has geometry +// crossing its right edge, so the span must extend to the bbox edge for +// backdrop propagation. const PATH_ROW_FLAG_TOUCHES_RIGHT = 0x1u; +// Per-tile accumulator filled in between tile_alloc and coarse. struct Tile { - backdrop: i32, + backdrop: i32, // winding number carried into the tile's left edge // This is used for the count of the number of segments in the // tile up to coarse rasterization, and the index afterwards. // In the latter variant, the bits are inverted so that tiling diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/transform.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/transform.wgsl index 190f4eb57..7fab534e1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/transform.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/transform.wgsl @@ -3,6 +3,11 @@ // Helpers for working with projective 2D transforms. // +// Imported by draw_leaf.wgsl, which reads transforms from the scene stream to +// compute gradient and image inverse matrices. Replaces Vello's affine-only +// shader/shared/transform.wgsl with a projective form so ImageSharp's +// Matrix4x4-based transforms (including perspective) round-trip exactly. +// // Each transform stores the 9 elements of a 3x3 projective matrix extracted // from a Matrix4x4 with z=0: // X = x*M11 + y*M21 + M41 @@ -20,6 +25,7 @@ struct Transform { // Matches TransformUtilities.ProjectiveTransform2D from ImageSharp: // Vector4.Transform(new Vector4(x, y, 0, 1), matrix) then divide by W. +// The divisor is clamped away from zero to avoid infinities at the horizon. fn transform_apply(transform: Transform, p: vec2) -> vec2 { let x = fma(transform.matrx.x, p.x, fma(transform.matrx.z, p.y, transform.translate.x)); let y = fma(transform.matrx.y, p.x, fma(transform.matrx.w, p.y, transform.translate.y)); @@ -27,6 +33,7 @@ fn transform_apply(transform: Transform, p: vec2) -> vec2 { return vec2(x, y) / max(w, 0.0000001); } +// The identity transform. fn transform_identity() -> Transform { return Transform(vec4(1.0, 0.0, 0.0, 1.0), vec2(0.0), vec3(0.0, 0.0, 1.0)); } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl index 5cd99879e..014b44dc2 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/Shared/util.wgsl @@ -1,9 +1,13 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// This file defines utility functions that interact with host-shareable buffer objects. It should -// be imported once following the resource binding declarations in the shader module that access -// them. +// 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). // 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 diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/backdrop_dyn.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/backdrop_dyn.wgsl index e7676d9d0..d27b339bd 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/backdrop_dyn.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/backdrop_dyn.wgsl @@ -1,7 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Prefix sum for dynamically allocated backdrops +// The backdrop propagation stage. +// +// Converts the per-tile backdrop deltas accumulated by earlier stages into +// absolute winding numbers. For each sparse path row, a running prefix sum +// walks the row's tiles left to right, seeded with the row's own backdrop, +// so that each tile's backdrop field ends up holding the winding number that +// applies at that tile (its own delta included). +// +// Inputs: paths (tile-space bbox and row base per draw object), rows (sparse +// row extents, seed backdrop, and base tile index), tiles (per-tile deltas). +// Outputs: tiles (backdrop rewritten in place as absolute winding). +// +// Derived from Vello's backdrop_dyn.wgsl. Local divergences: rows come from +// the sparse row records rather than a dense per-path tile grid, each row +// carries a backdrop seed for winding that enters from the left of its span, +// and rows are walked serially per draw object instead of being distributed +// across the workgroup. #import bump #import config @@ -22,6 +38,9 @@ var rows: array; @group(0) @binding(4) var tiles: array; +// One thread per draw object. Exits the whole dispatch when any prior stage +// recorded an allocation failure, then prefix-sums the backdrop deltas of +// each of the object's sparse rows. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -45,6 +64,8 @@ fn main( let width = path_row.x1 - path_row.x0; var tile_ix = path_row.tiles; + // The row seed is the winding contributed by geometry left of the + // row's first allocated tile; each tile then adds its own delta. var sum = path_row.backdrop; for (var x = 0u; x < width; x += 1u) { sum += tiles[tile_ix].backdrop; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl index 7d8935385..5dc3e92bf 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/bbox_clear.wgsl @@ -1,6 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Initializes the per-path bounding boxes to an empty (inverted) box so +// that later stages (flatten) can accumulate extents with atomic min/max. +// +// Inputs: config uniform (n_path). +// Outputs: path_bboxes, one PathBbox per path with min fields set to the +// i32 maximum and max fields set to the i32 minimum. +// +// Ported from Vello's bbox_clear.wgsl. + #import config #import bbox @@ -10,6 +19,8 @@ var config: Config; @group(0) @binding(1) var path_bboxes: array; +// Resets one path bbox per thread; any box where x0 > x1 after +// accumulation is treated as empty downstream. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl index 872bad873..0f2a285e1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/binning.wgsl @@ -1,7 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// The binning stage +// The binning stage. +// +// Assigns each draw object to every 16x16-tile bin that its clipped bounding +// box touches. Each workgroup covers one 256-element draw partition crossed +// with one 256-bin chunk of the global bin grid; per bin it counts covering +// draw objects, bump-allocates a slice of the element list, and writes a bin +// header (element count + chunk offset) followed by the draw-object indices, +// which coarse later merges back into draw order. +// +// Inputs: draw_monoids, path_bbox_buf (path bboxes plus interest rects), +// clip_bbox_buf (clip-stack bboxes from clip_leaf). +// Outputs: intersected_bbox (clip- and interest-intersected bbox per draw +// object), info_bin_data (bin headers and element lists), bump.binning. +// +// Derived from Vello's binning.wgsl. Local divergences: a second dispatch +// axis chunks the bin grid so it may exceed 256 bins, and per-path interest +// rectangles participate in the bbox intersection. #import config #import drawtag @@ -38,7 +54,7 @@ const N_SLICE = WG_SIZE / 32u; const N_SUBSLICE = 4u; // sh_bitmaps holds one bit per (element, bin-in-chunk) pair, so its inner -// dimension is fixed at N_TILE (= 256) — the number of bins processed by a +// dimension is fixed at N_TILE (= 256), the number of bins processed by a // single workgroup. The dispatch tiles bin space in Y so the full bin grid // can exceed 256 without overflowing this shared array. var sh_bitmaps: array, N_TILE>, N_SLICE>; @@ -56,10 +72,18 @@ fn bin_header_stride(width_in_bins: u32, height_in_bins: u32) -> u32 { return (n_bins + N_TILE - 1u) / N_TILE * N_TILE; } +// Returns the u32 offset in info_bin_data of the 2-word bin header (element +// count, chunk offset) for the given bin within the given draw partition. +// Headers live after the element-list region, which starts at bin_data_start +// and spans binning_size words. fn bin_header_ix(partition_ix: u32, bin_ix: u32, stride: u32) -> u32 { return config.bin_data_start + config.binning_size + (partition_ix * stride + bin_ix) * 2u; } +// One thread per draw object within this workgroup's partition. Computes the +// object's clipped bin coverage, rasterizes it into shared bitmaps for the +// bins in this chunk, then cooperatively writes bin headers and element +// lists. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -116,7 +140,7 @@ fn main( let path_bbox = path_bbox_buf[draw_monoid.path_ix]; let pb = vec4(vec4(path_bbox.x0, path_bbox.y0, path_bbox.x1, path_bbox.y1)); - let bbox = bbox_intersect(clip_bbox, pb); + let bbox = bbox_intersect(clip_bbox, bbox_intersect(pb, path_bbox.interest)); // Only the first bin-chunk workgroup writes the intersected bbox, since // the value is identical across chunks and later stages read it once. @@ -124,9 +148,9 @@ fn main( intersected_bbox[element_ix] = bbox; } - // `bbox_intersect` can result in a zero or negative area intersection if the path bbox lies - // outside the clip bbox. If that is the case, Don't round up the bottom-right corner of the - // and leave the coordinates at 0. This way the path will get clipped out and won't get + // `bbox_intersect` can result in a zero or negative area intersection if the path bbox + // lies outside the clip bbox. If that is the case, skip the conversion to bin + // coordinates and leave them at 0. This way the path gets clipped out and is never // assigned to a bin. if bbox.x < bbox.z && bbox.y < bbox.w { x0 = i32(floor(bbox.x * SX)); @@ -141,6 +165,7 @@ fn main( y0 = clamp(y0, 0, height_in_bins_i); x1 = clamp(x1, 0, width_in_bins_i); y1 = clamp(y1, 0, height_in_bins_i); + // Zero-width coverage touches no bins; collapse the loop bounds. if x0 == x1 { y1 = y0; } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl index 38f9c1bc0..829132a9b 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/chunk_reset.wgsl @@ -1,11 +1,28 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Resets the chunk-local bump allocator counters between chunk dispatches +// while preserving the full-scene state produced by the shared scheduling +// stages. The scene is rendered in vertical tile-row chunks; the shared +// stages (flatten, binning, ...) run once, but the chunk-local stages +// (path_row_alloc through path_tiling) rerun per chunk and must start +// from zeroed counters. +// +// Inputs/outputs: bump. Retained across chunks: the binning and lines +// counters plus the STAGE_BINNING and STAGE_FLATTEN failure bits, since +// those belong to the shared stages that are not rerun. Everything else +// (ptcl, path_rows, tile, seg_counts, segments, blend_spill and the +// chunk-local failure bits) is cleared. +// +// Local addition; no Vello shader of this name exists. + #import bump @group(0) @binding(0) var bump: BumpAllocators; +// Single-thread stage: reads the retained values first, then rewrites the +// whole struct so chunk-local counters start at zero. @compute @workgroup_size(1) fn main() { let retained_failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_FLATTEN); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_leaf.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_leaf.wgsl index 00d36fda0..a9aeba0eb 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_leaf.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_leaf.wgsl @@ -1,6 +1,24 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Clip-stack scan stage. Second pass of the stack-monoid scan: resolves the +// BeginClip/EndClip stack across the whole scene. Every clip record gets a +// conservative clip bbox (the intersection of its ancestor clip bboxes), +// and each EndClip's draw monoid is rewritten to reference the matching +// BeginClip's path, draw data, and info so the fine stage can composite +// real antialiased path coverage when the clip is popped. +// +// Inputs: config uniform; clip_inp (ClipInp records from draw_leaf); +// path_bboxes (per-path bounds); reduced (per-workgroup Bic aggregates from +// clip_reduce); clip_els (open-clip stack elements from clip_reduce). +// Outputs: draw_monoids (EndClip entries rewritten in place); clip_bboxes +// (conservative bbox per clip record, consumed by binning). +// +// Ported from Vello's clip_leaf.wgsl (linebender/vello, +// vello_shaders/shader). Local divergence: ClipInp carries a clip +// operation, and Difference clips contribute an infinite bbox because they +// retain everything outside their path. + #import config #import bbox #import clip @@ -34,6 +52,14 @@ var sh_stack_bbox: array, WG_SIZE>; var sh_bbox: array, WG_SIZE>; var sh_link: array; +// Searches the binary Bic tree in sh_bic (leaves at 0..WG_SIZE, internal +// levels packed above) for the nearest preceding element whose push is +// still open at the element at local index ix_in. Walks up the tree +// combining aggregates into *bic until an unmatched push (b > 0) is found, +// then walks back down to the exact leaf. Returns the predecessor's local +// index, or, when the match lies outside this workgroup, i32(~0u - a) +// where a is the count of still-unmatched pops; that negative value indexes +// the cross-workgroup prefix stack as sh_stack[WG_SIZE + link]. fn search_link(bic: ptr, ix_in: u32) -> i32 { var ix = ix_in; var j = 0u; @@ -67,22 +93,47 @@ fn search_link(bic: ptr, ix_in: u32) -> i32 { } } +// Loads the sign-tagged path index of clip record ix (non-negative for +// BeginClip, negative for EndClip). Lanes past config.n_clip get i32 min, +// a negative sentinel so padding lanes are never treated as pushes. fn load_clip_path(ix: u32) -> i32 { if ix < config.n_clip { return clip_inp[ix].path_ix; } else { + // 0x80000000 does not fit in an i32 literal, hence the decimal form. return -2147483648; - // literal too large? - // return 0x80000000; } } +// Loads the clip operation (intersection or difference) of clip record ix. +// Lanes past config.n_clip report intersection; the operation only affects +// push records, so the value is inert for padding. +fn load_clip_operation(ix: u32) -> u32 { + if ix < config.n_clip { + return clip_inp[ix].operation; + } + + return CLIP_OPERATION_INTERSECTION; +} + +// Resolves the clip stack for one workgroup's span of clip records. +// Phase 1: scan the Bic aggregates of preceding workgroups to size the +// cross-workgroup prefix stack, binary-search clip_els to load its top +// WG_SIZE elements into sh_stack, and prefix-intersect their bboxes. +// Phase 2: build a binary Bic tree over this span, find each record's +// predecessor (the enclosing open push) with search_link, and intersect +// bboxes along the resulting parent links. +// Phase 3: for EndClips, rewrite the draw monoid to the matching +// BeginClip's path/scene/info and use the grandparent bbox (the clip state +// after the pop); emit the conservative clip bbox for every record. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @builtin(local_invocation_id) local_id: vec3, @builtin(workgroup_id) wg_id: vec3, ) { + // Scan the aggregates of workgroups preceding this one; sh_bic[0] then + // holds their combined Bic, whose b is the prefix stack depth. var bic: Bic; if local_id.x < wg_id.x { bic = reduced[local_id.x]; @@ -101,7 +152,9 @@ fn main( let stack_size = sh_bic[0].b; // TODO: if stack depth > WG_SIZE desired, scan here - // binary search in stack + // Binary search in the prefix stack: lane local_id.x loads the element + // sp levels below the stack top; ix locates the workgroup whose + // clip_els partition still holds that element. let sp = WG_SIZE - 1u - local_id.x; var ix = 0u; for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { @@ -129,13 +182,23 @@ fn main( sh_stack_bbox[local_id.x] = bbox; // Read input and compute Bic binary tree + let is_active = global_id.x < config.n_clip; let inp = load_clip_path(global_id.x); - let is_push = inp >= 0; - bic = Bic(1u - u32(is_push), u32(is_push)); + let operation = load_clip_operation(global_id.x); + let is_push = is_active && inp >= 0; + bic = Bic(); + if is_active { + bic = Bic(1u - u32(is_push), u32(is_push)); + } + sh_bic[local_id.x] = bic; if is_push { let path_bbox = path_bboxes[inp]; - bbox = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + let path_box = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + // Difference clips are an ImageSharp extension over Vello's ix/path_ix clip + // record. They keep everything outside the path, so their conservative stack + // bbox stays unchanged even though fine still needs the path coverage. + bbox = select(path_box, vec4(-1e9, -1e9, 1e9, 1e9), operation == CLIP_OPERATION_DIFFERENCE); } else { bbox = vec4(-1e9, -1e9, 1e9, 1e9); } @@ -152,10 +215,21 @@ fn main( workgroupBarrier(); // search for predecessor node bic = Bic(); - var link = search_link(&bic, local_id.x); + var link = -1; + if global_id.x < config.n_clip { + link = search_link(&bic, local_id.x); + } + sh_link[local_id.x] = link; workgroupBarrier(); - let grandparent = select(link - 1, sh_link[link], link >= 0); + + // Keep the indexed branch explicit. Some GPU backends can still materialize + // inactive select operands, and sh_link[-1] is an invalid workgroup read. + var grandparent = link - 1; + if link >= 0 { + grandparent = sh_link[link]; + } + var parent: i32; if link >= 0 { parent = i32(wg_id.x * WG_SIZE) + link; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_reduce.wgsl index 55136abce..f55cb689a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_reduce.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/clip_reduce.wgsl @@ -1,6 +1,23 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Clip-stack reduction stage. First pass of the stack-monoid scan over the +// scene's clip records: each workgroup reduces its span of 256 ClipInp +// entries to a single Bic (bicyclic semigroup) aggregate and records the +// stack elements (BeginClips left open at the end of the span) so clip_leaf +// can resolve pushes and pops across workgroup boundaries. +// +// Inputs: clip_inp (ClipInp records emitted by draw_leaf); path_bboxes +// (per-path bounds from the path bbox stages). +// Outputs: reduced[wg] = Bic aggregate of workgroup wg's span; clip_out = +// ClipEl (parent draw object index plus conservative bbox) for each open +// BeginClip, stored in stack order at the start of the workgroup's range. +// +// Ported from Vello's clip_reduce.wgsl (linebender/vello, +// vello_shaders/shader). Local divergence: each ClipInp carries a clip +// operation, and Difference clips publish an infinite bbox because they +// retain everything outside their path. + #import bbox #import clip @@ -20,17 +37,27 @@ const WG_SIZE = 256u; var sh_bic: array; var sh_parent: array; var sh_path_ix: array; +var sh_operation: array; +// Reduces one workgroup's span of clip records. A reverse inclusive scan +// computes the Bic aggregate (written to reduced[wg_id.x]); the suffix +// values from the same scan identify which BeginClips remain open at the +// end of the span, and those are written as ClipEl stack elements into the +// first sh_bic[0].b slots of this workgroup's clip_out range. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @builtin(local_invocation_id) local_id: vec3, @builtin(workgroup_id) wg_id: vec3, ) { - let inp = clip_inp[global_id.x].path_ix; + let clip_input = clip_inp[global_id.x]; + let inp = clip_input.path_ix; + let operation = clip_input.operation; let is_push = inp >= 0; var bic = Bic(1u - u32(is_push), u32(is_push)); - // reverse scan of bicyclic semigroup + + // Reverse inclusive scan of the bicyclic semigroup: after the loop, + // sh_bic[i] combines elements i..WG_SIZE-1, so sh_bic[0] is the span total. sh_bic[local_id.x] = bic; for (var i = 0u; i < firstTrailingBit(WG_SIZE); i += 1u) { workgroupBarrier(); @@ -50,10 +77,14 @@ fn main( if local_id.x + 1u < WG_SIZE { bic = sh_bic[local_id.x + 1u]; } + // A push survives the span when its suffix contains no unmatched pop + // (bic.a == 0). Its slot orders the surviving pushes bottom-up: bic.b + // counts the surviving pushes that follow it. if is_push && bic.a == 0u { let local_ix = size - bic.b - 1u; sh_parent[local_ix] = local_id.x; sh_path_ix[local_ix] = u32(inp); + sh_operation[local_ix] = operation; } workgroupBarrier(); // TODO: possibly do forward scan here if depth can exceed wg size @@ -61,7 +92,11 @@ fn main( let path_ix = sh_path_ix[local_id.x]; let path_bbox = path_bboxes[path_ix]; let parent_ix = sh_parent[local_id.x] + wg_id.x * WG_SIZE; - let bbox = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + // Difference is ImageSharp's extension over Vello's clip record. The clip + // still has a path for fine-stage coverage, but its retained area is outside + // that path, so the path box cannot narrow descendant conservative bounds. + let path_box = vec4(f32(path_bbox.x0), f32(path_bbox.y0), f32(path_bbox.x1), f32(path_bbox.y1)); + let bbox = select(path_box, vec4(-1e9, -1e9, 1e9, 1e9), sh_operation[local_id.x] == CLIP_OPERATION_DIFFERENCE); clip_out[global_id.x] = ClipEl(parent_ix, bbox); } } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl index 236850e6a..94ed25b6d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/coarse.wgsl @@ -2,6 +2,26 @@ // Licensed under the Six Labors Split License. // The coarse rasterization stage. +// +// One workgroup per 16x16-tile bin (wg x = bin column, wg y = bin row within +// the current chunk); each of the 256 threads owns one tile. The stage +// streams the per-partition bin element lists written by binning, merges +// them back into draw order, marks which draw objects touch which tiles via +// shared bitmaps, and then each thread serializes its own tile's per-tile +// command list (PTCL) for the fine stage: fill/solid coverage, paint, +// and clip/blend commands. +// +// Inputs: scene (drawtags and drawdata), draw_monoids, info_bin_data (draw +// info, bin headers, and bin element lists), paths and rows (sparse +// tile-grid lookup), tiles (per-tile segment counts and backdrops). +// Outputs: ptcl (command lists), tiles (segment_count_or_ix rewritten to the +// allocated segment index), bump (ptcl, segments, blend_spill, failed). +// +// Derived from Vello's coarse.wgsl. Local divergences: sparse row-based tile +// lookup instead of dense per-path tile grids, per-draw coverage thresholds +// and interest rectangles, aliased-coverage fills, difference/hard clip +// bits, extra draw tags (recolor, elliptic/sweep/path gradients), and +// chunked rendering via config.chunk_tile_y_start. #import config #import bump @@ -36,11 +56,12 @@ var bump: BumpAllocators; @group(0) @binding(8) var ptcl: array; - - // Much of this code assumes WG_SIZE == N_TILE. If these diverge, then // a fair amount of fixup is needed. const WG_SIZE = 256u; +// Packed blend word (mix 128 = clip, compose 3 = src-over) identifying a +// plain clip layer with no custom blending. +const BLEND_CLIP = (128u << 8u) | 3u; const N_SLICE = WG_SIZE / 32u; var sh_bitmaps: array, N_TILE>, N_SLICE>; @@ -49,6 +70,9 @@ var sh_part_offsets: array; var sh_drawobj_ix: array; var sh_tile_count: array; +// Result of decode_bin_tile: a tile located by its bin-relative coordinates +// (x, y) plus its global storage index. valid is 0 when the sequential index +// fell outside the path's sparse rows. struct SparseBinTileRef { valid: u32, x: u32, @@ -56,11 +80,14 @@ struct SparseBinTileRef { tile_ix: u32, } +// Result of lookup_tile: valid is 0 when the queried tile coordinates lie +// outside the path's sparse coverage. struct SparseTileRef { valid: u32, tile_ix: u32, } +// Per-partition, per-bin header written by the binning stage. struct BinHeader { element_count: u32, chunk_offset: u32, @@ -71,7 +98,10 @@ struct BinHeader { var cmd_offset: u32; var cmd_limit: u32; -// Make sure there is space for a command of given size, plus a jump if needed +// 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 +// failure flag is raised and writes are redirected to the buffer start. fn alloc_cmd(size: u32) { if cmd_offset + size >= cmd_limit { var new_cmd = atomicAdd(&bump.ptcl, PTCL_INCREMENT); @@ -90,7 +120,36 @@ fn alloc_cmd(size: u32) { } } -fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) { +// Determines whether a tile with no crossing segments produces any visible +// coverage. The backdrop winding is resolved through the fill rule (even-odd +// folds the winding, non-zero saturates it) and, for aliased fills, +// quantized against the coverage threshold. Returns true when the resulting +// coverage is non-zero. +fn solid_tile_has_coverage(draw_flags: u32, backdrop: i32, coverage_threshold: f32) -> bool { + let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; + let aliased = (draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u; + var coverage = f32(backdrop); + + if even_odd { + coverage = abs(coverage - 2.0 * round(0.5 * coverage)); + } else { + coverage = min(abs(coverage), 1.0); + } + + if aliased { + coverage = select(0.0, 1.0, coverage >= coverage_threshold); + } + + return coverage != 0.0; +} + +// Writes the coverage command for a tile: CMD_FILL when line segments cross +// it (also reserving the tile's segment allocation and storing the inverted +// index back into the tile so path_tiling can find it), otherwise CMD_SOLID. +// 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, draw_flags: u32, coverage_threshold: f32, interest: vec4, 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). @@ -98,40 +157,61 @@ fn write_path(tile: Tile, tile_ix: u32, draw_flags: u32) { if n_segs != 0u { var seg_ix = atomicAdd(&bump.segments, n_segs); tiles[tile_ix].segment_count_or_ix = ~seg_ix; - alloc_cmd(4u); + alloc_cmd(9u); ptcl[cmd_offset] = CMD_FILL; let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; - let size_and_rule = (n_segs << 1u) | u32(even_odd); - let fill = CmdFill(size_and_rule, seg_ix, tile.backdrop); + let aliased = (draw_flags & DRAW_INFO_FLAGS_ALIASED_BIT) != 0u; + // 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, coverage_threshold, interest); ptcl[cmd_offset + 1u] = fill.size_and_rule; ptcl[cmd_offset + 2u] = fill.seg_data; ptcl[cmd_offset + 3u] = u32(fill.backdrop); - cmd_offset += 4u; + ptcl[cmd_offset + 4u] = bitcast(fill.coverage_threshold); + 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); + cmd_offset += 9u; + return true; } else { - alloc_cmd(1u); + if !emit_empty_solid && !solid_tile_has_coverage(draw_flags, tile.backdrop, coverage_threshold) { + return false; + } + + alloc_cmd(5u); ptcl[cmd_offset] = CMD_SOLID; - cmd_offset += 1u; + 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; + return true; } } +// Emits a CMD_COLOR paint command (binary16 RGBA color plus draw flags). fn write_color(color: CmdColor) { - alloc_cmd(3u); + alloc_cmd(4u); ptcl[cmd_offset] = CMD_COLOR; - ptcl[cmd_offset + 1u] = color.rgba_color; - ptcl[cmd_offset + 2u] = color.draw_flags; - cmd_offset += 3u; + ptcl[cmd_offset + 1u] = color.color_rg; + ptcl[cmd_offset + 2u] = color.color_ba; + ptcl[cmd_offset + 3u] = color.draw_flags; + cmd_offset += 4u; } -fn write_recolor(source_color: u32, target_color: u32, threshold: u32, draw_flags: u32) { - alloc_cmd(5u); +// Emits a CMD_RECOLOR command referencing one target-specialized auxiliary record. +fn write_recolor(data_offset: u32, draw_flags: u32) { + alloc_cmd(3u); ptcl[cmd_offset] = CMD_RECOLOR; - ptcl[cmd_offset + 1u] = source_color; - ptcl[cmd_offset + 2u] = target_color; - ptcl[cmd_offset + 3u] = threshold; - ptcl[cmd_offset + 4u] = draw_flags; - cmd_offset += 5u; + ptcl[cmd_offset + 1u] = data_offset; + ptcl[cmd_offset + 2u] = draw_flags; + cmd_offset += 3u; } +// Emits a gradient paint command. ty selects the CMD_*_GRAD opcode, index is +// the packed gradient index word from the scene, and info_offset points at +// the gradient parameters computed by draw_leaf. fn write_grad(ty: u32, index: u32, info_offset: u32) { alloc_cmd(3u); ptcl[cmd_offset] = ty; @@ -140,6 +220,8 @@ fn write_grad(ty: u32, index: u32, info_offset: u32) { cmd_offset += 3u; } +// Emits a CMD_PATH_GRAD paint command. data_offset points at the packed edge +// data in the combined info/bin-data buffer; edge_count edges follow. fn write_path_grad(data_offset: u32, edge_count: u32, flags: u32, draw_flags: u32) { alloc_cmd(5u); ptcl[cmd_offset] = CMD_PATH_GRAD; @@ -150,6 +232,8 @@ fn write_path_grad(data_offset: u32, edge_count: u32, flags: u32, draw_flags: u3 cmd_offset += 5u; } +// Emits a CMD_IMAGE paint command; info_offset points at the image draw info +// (transform, extents, and atlas placement) written by draw_leaf. fn write_image(info_offset: u32) { alloc_cmd(2u); ptcl[cmd_offset] = CMD_IMAGE; @@ -157,12 +241,18 @@ fn write_image(info_offset: u32) { cmd_offset += 2u; } -fn write_begin_clip() { - alloc_cmd(1u); +// Emits CMD_BEGIN_CLIP, which pushes a new group in the fine stage. The payload word +// carries bit 0 = isolated: 1 seeds the group transparent (layer semantics), 0 seeds it +// with a copy of the current tile content (clip mask semantics). +fn write_begin_clip(isolated: u32) { + alloc_cmd(2u); ptcl[cmd_offset] = CMD_BEGIN_CLIP; - cmd_offset += 1u; + ptcl[cmd_offset + 1u] = isolated; + cmd_offset += 2u; } +// Emits CMD_END_CLIP with the blend word and alpha used to composite the +// layer back onto its parent. fn write_end_clip(end_clip: CmdEndClip) { alloc_cmd(3u); ptcl[cmd_offset] = CMD_END_CLIP; @@ -171,6 +261,9 @@ fn write_end_clip(end_clip: CmdEndClip) { cmd_offset += 3u; } +// Counts how many of the path's sparse tiles fall inside the bin whose +// top-left tile is (bin_tile_x, bin_tile_y), by clipping each row span to +// the bin's horizontal range. fn get_bin_tile_count(path: Path, bin_tile_x: u32, bin_tile_y: u32) -> u32 { let y0 = max(path.bbox.y, bin_tile_y); let y1 = min(path.bbox.w, bin_tile_y + N_TILE_Y); @@ -187,6 +280,10 @@ fn get_bin_tile_count(path: Path, bin_tile_x: u32, bin_tile_y: u32) -> u32 { return count; } +// Maps a sequential tile index within this bin (seq_ix, in the same +// row-major order counted by get_bin_tile_count) back to a concrete tile. +// Returns bin-relative coordinates plus the global tile index, or valid = 0 +// when seq_ix falls outside the path's coverage of the bin. fn decode_bin_tile(path: Path, bin_tile_x: u32, bin_tile_y: u32, seq_ix: u32) -> SparseBinTileRef { let y0 = max(path.bbox.y, bin_tile_y); let y1 = min(path.bbox.w, bin_tile_y + N_TILE_Y); @@ -210,6 +307,9 @@ fn decode_bin_tile(path: Path, bin_tile_x: u32, bin_tile_y: u32, seq_ix: u32) -> return SparseBinTileRef(0u, 0u, 0u, 0u); } +// Resolves the tile storage index for global tile coordinates through the +// path's sparse rows. Returns valid = 0 when the coordinates fall outside +// the path's row coverage. fn lookup_tile(path: Path, global_x: u32, global_y: u32) -> SparseTileRef { if global_y < path.bbox.y || global_y >= path.bbox.w { return SparseTileRef(0u, 0u); @@ -223,19 +323,31 @@ fn lookup_tile(path: Path, global_x: u32, global_y: u32) -> SparseTileRef { return SparseTileRef(1u, row.tiles + global_x - row.x0); } +// Reads the (element count, chunk offset) bin header written by the binning +// stage. bin_ix is the flat header slot index, already including the +// per-partition stride; headers live after the binning_size element-list +// region of info_bin_data. fn load_bin_header(bin_ix: u32) -> BinHeader { let base = config.bin_data_start + config.binning_size + (bin_ix * 2u); return BinHeader(info_bin_data[base], info_bin_data[base + 1u]); } +// One workgroup per bin; each thread owns one tile. The outer loop batches +// up to N_TILE draw objects at a time: bin element lists from successive +// partitions are prefix-summed and binary-searched into a contiguous, +// draw-ordered window (sh_drawobj_ix), each object's bin tiles are scattered +// into per-tile bitmaps, and finally every thread walks its own tile's +// bitmap in draw order to emit PTCL commands, tracking clip and blend depth +// as it goes. @compute @workgroup_size(256) fn main( @builtin(local_invocation_id) local_id: vec3, @builtin(workgroup_id) wg_id: vec3, ) { // Exit early if prior stages failed, as we can't run this stage. - // We need to check only prior stages, as if this stage has failed in another workgroup, - // we still want to know this workgroup's memory requirement. + // We need to check only prior stages, as if this stage has failed in + // another workgroup, we still want to know this workgroup's memory + // requirement. if local_id.x == 0u { var failed = atomicLoad(&bump.failed) & (STAGE_BINNING | STAGE_TILE_ALLOC | STAGE_FLATTEN); if atomicLoad(&bump.seg_counts) > config.seg_counts_size { @@ -286,6 +398,8 @@ fn main( var render_blend_depth = 0u; var max_blend_depth = 0u; + // The first word of each tile's PTCL is reserved for the blend-spill + // offset and patched in after the command list is complete. let blend_offset = cmd_offset; cmd_offset += 1u; @@ -389,14 +503,29 @@ fn main( let y = tile_ref.y; let tile_ix = tile_ref.tile_ix; let tile = tiles[tile_ix]; + // Bit 0 of the draw tag marks clip operations (begin and end). let is_clip = (tag & 1u) != 0u; var is_blend = false; + var is_difference_clip = false; if is_clip { - let BLEND_CLIP = (128u << 8u) | 3u; let scene_offset = draw_monoids[drawobj_ix].scene_offset; let dd = config.drawdata_base + scene_offset; - let blend = scene[dd]; - is_blend = blend != BLEND_CLIP; + // Difference clips carry their operation in the high bit of the blend word. + // Coarse only needs to know whether this is a plain clip marker or a true + // blend layer, so mask the operation bit before comparing with BLEND_CLIP. + let raw_blend = scene[dd]; + is_difference_clip = (raw_blend & CLIP_DIFFERENCE_MASK_BIT) != 0u; + let is_hard_clip = (raw_blend & CLIP_HARD_MASK_BIT) != 0u; + var blend = raw_blend & ~(CLIP_DIFFERENCE_MASK_BIT | CLIP_ISOLATED_MASK_BIT); + if is_hard_clip { + blend &= ~CLIP_HARD_MASK_BIT; + } + + // Isolated groups (layers) must never take the solid-tile skip below: their + // contents composite against a transparent seed, so skipping the group would + // change results for any non-src-over content inside. Treat them like blend + // layers so every covered tile opens the group. + is_blend = blend != BLEND_CLIP || (raw_blend & CLIP_ISOLATED_MASK_BIT) != 0u; } let di = draw_monoids[drawobj_ix].info_offset; @@ -405,10 +534,11 @@ fn main( let n_segs = tile.segment_count_or_ix; // If this draw object represents an even-odd fill and we know that no line segment - // crosses this tile and then this draw object should not contribute to the tile if its + // crosses this tile, then this draw object should not contribute to the tile if its // backdrop (i.e. the winding number of its top-left corner) is even. let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; - let include_tile = n_segs != 0u || (backdrop_clear == is_clip) || is_blend; + let include_clip_tile = select(backdrop_clear, !backdrop_clear, is_difference_clip); + let include_tile = n_segs != 0u || (include_clip_tile == is_clip) || is_blend; if include_tile { let el_slice = el_ix / 32u; let el_mask = 1u << (el_ix & 31u); @@ -444,6 +574,21 @@ fn main( let dd = config.drawdata_base + dm.scene_offset; let di = dm.info_offset; let draw_flags = info_bin_data[di]; + var coverage_threshold = -1.0; + var interest = vec4(0.0, 0.0, f32(config.target_width), f32(config.target_height)); + // Draw tags whose info block spans at least five words append a + // coverage threshold 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_threshold = 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])); + } + if clip_zero_depth == 0u { let path = paths[dm.path_ix]; let tile_ref = lookup_tile(path, bin_tile_x + tile_x, bin_tile_y + tile_y); @@ -455,62 +600,74 @@ fn main( let tile = tiles[tile_ix]; switch drawtag { case DRAWTAG_FILL_COLOR: { - write_path(tile, tile_ix, draw_flags); - let rgba_color = scene[dd]; - write_color(CmdColor(rgba_color, draw_flags)); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + write_color(CmdColor(scene[dd], scene[dd + 1u], draw_flags)); + } } case DRAWTAG_FILL_RECOLOR: { - write_path(tile, tile_ix, draw_flags); - write_recolor(scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + write_recolor(config.brush_data_base + scene[dd], draw_flags); + } } case DRAWTAG_FILL_LIN_GRADIENT: { - write_path(tile, tile_ix, draw_flags); - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_LIN_GRAD, index, info_offset); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_LIN_GRAD, index, info_offset); + } } case DRAWTAG_FILL_RAD_GRADIENT: { - write_path(tile, tile_ix, draw_flags); - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_RAD_GRAD, index, info_offset); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_RAD_GRAD, index, info_offset); + } } case DRAWTAG_FILL_ELLIPTIC_GRADIENT: { - write_path(tile, tile_ix, draw_flags); - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_ELLIPTIC_GRAD, index, info_offset); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_ELLIPTIC_GRAD, index, info_offset); + } } case DRAWTAG_FILL_SWEEP_GRADIENT: { - write_path(tile, tile_ix, draw_flags); - let index = scene[dd]; - let info_offset = di + 1u; - write_grad(CMD_SWEEP_GRAD, index, info_offset); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + let index = scene[dd]; + let info_offset = di + 1u; + write_grad(CMD_SWEEP_GRAD, index, info_offset); + } } case DRAWTAG_FILL_PATH_GRADIENT: { - write_path(tile, tile_ix, draw_flags); - write_path_grad(config.path_gradient_data_base + scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + write_path_grad(config.brush_data_base + scene[dd], scene[dd + 1u], scene[dd + 2u], draw_flags); + } } case DRAWTAG_FILL_IMAGE: { - write_path(tile, tile_ix, draw_flags); - write_image(di + 1u); + if write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, false) { + write_image(di + 1u); + } } case DRAWTAG_BEGIN_CLIP: { let even_odd = (draw_flags & DRAW_INFO_FLAGS_FILL_RULE_BIT) != 0u; let backdrop_clear = select(tile.backdrop, abs(tile.backdrop) & 1, even_odd) == 0; - if tile.segment_count_or_ix == 0u && backdrop_clear { + let raw_blend = scene[dd]; + let is_difference_clip = (raw_blend & CLIP_DIFFERENCE_MASK_BIT) != 0u; + let retained_area_clear = select(backdrop_clear, !backdrop_clear, is_difference_clip); + if tile.segment_count_or_ix == 0u && retained_area_clear { clip_zero_depth = clip_depth + 1u; } else { - write_begin_clip(); + let isolated = u32((raw_blend & CLIP_ISOLATED_MASK_BIT) != 0u); + write_begin_clip(isolated); render_blend_depth += 1u; max_blend_depth = max(max_blend_depth, render_blend_depth); } + clip_depth += 1u; } case DRAWTAG_END_CLIP: { clip_depth -= 1u; - write_path(tile, tile_ix, draw_flags); let blend = scene[dd]; + write_path(tile, tile_ix, draw_flags, coverage_threshold, interest, true); let alpha = bitcast(scene[dd + 1u]); write_end_clip(CmdEndClip(blend, alpha)); render_blend_depth -= 1u; @@ -540,6 +697,10 @@ fn main( } workgroupBarrier(); } + // Only tiles inside the real viewport finalize their command list: write + // CMD_END and, when the clip stack ran deeper than the in-register blend + // stack, reserve blend-spill scratch and patch its offset into the + // reserved first word. if bin_tile_x + tile_x < config.width_in_tiles && bin_tile_y + tile_y < config.height_in_tiles { ptcl[cmd_offset] = CMD_END; var blend_ix = 0u; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl index 9d5d9f44e..e5e148d86 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_leaf.wgsl @@ -1,7 +1,25 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Finish prefix sum of drawtags, decode draw objects. +// Draw-tag scan stage. Finishes the prefix sum of draw tags started by +// draw_reduce, producing an exclusive DrawMonoid for every draw object, then +// decodes each draw object: brush parameters are materialized into the +// per-draw info stream and BeginClip/EndClip records are emitted as ClipInp +// entries for the clip stack stages. +// +// Inputs: config uniform; scene stream; reduced (per-workgroup DrawMonoid +// aggregates from draw_reduce); path_bbox (per-path bounds, draw flags, +// coverage threshold and raster interest from the path bbox stages). +// Outputs: draw_monoid (exclusive prefix per draw object); info (per-draw +// brush info words consumed by coarse and fine); clip_inp (ClipInp records +// consumed by clip_reduce and clip_leaf). +// +// Ported from Vello's draw_leaf.wgsl (linebender/vello, +// vello_shaders/shader). Local divergences: extra draw tags (recolor, +// elliptic and path gradients), 9-word transforms carrying a perspective +// row, coverage-threshold plus raster-interest words appended to +// visible-fill info entries, and a clip operation (intersection or +// difference) carried in each ClipInp record. #import config #import clip @@ -32,6 +50,10 @@ var clip_inp: array; #import util +// Reads one transform from the scene stream. ImageSharp encodes 9 words per +// transform (2x2 matrix, translation, perspective row) where Vello encodes +// 6 affine words. transform_base is the u32 offset of the transform stream +// within the scene; ix is the transform index. fn read_transform(transform_base: u32, ix: u32) -> Transform { let base = transform_base + ix * 9u; let matrx = vec4( @@ -53,6 +75,13 @@ const WG_SIZE = 256u; var sh_scratch: array; +// Completes the draw-monoid prefix sum and decodes draw objects. +// First the aggregates of all preceding workgroups (reduced) are scanned to +// obtain this workgroup's starting prefix. Then, for each of this +// workgroup's blocks: an intra-workgroup scan yields the exclusive prefix m +// for each draw object, draw_monoid[ix] is written, the brush payload is +// decoded into info[m.info_offset..], and ClipInp records are emitted for +// BeginClip/EndClip objects. @compute @workgroup_size(256) fn main( @builtin(local_invocation_id) local_id: vec3, @@ -118,12 +147,6 @@ fn main( { let bbox = path_bbox[m.path_ix]; let draw_flags = bbox.draw_flags; - var transform = transform_identity(); - if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT || - tag_word == DRAWTAG_FILL_ELLIPTIC_GRADIENT || tag_word == DRAWTAG_FILL_SWEEP_GRADIENT - { - transform = read_transform(config.transform_base, bbox.trans_ix); - } switch tag_word { case DRAWTAG_FILL_COLOR: { info[di] = draw_flags; @@ -136,14 +159,22 @@ fn main( } case DRAWTAG_FILL_LIN_GRADIENT: { info[di] = draw_flags; - var p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); - var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); - p0 = transform_apply(transform, p0); - p1 = transform_apply(transform, p1); + let p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + let p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); + // Encode the gradient as a line equation so fine can + // evaluate the parameter as t = dot(p, line_xy) + line_c. let dxy = p1 - p0; - let scale = 1.0 / dot(dxy, dxy); - let line_xy = dxy * scale; - let line_c = -dot(p0, line_xy); + let axis_squared = dot(dxy, dxy); + var line_xy = vec2(0.0, 0.0); + var line_c = 1.0; + if axis_squared != 0.0 { + let scale = 1.0 / axis_squared; + line_xy = dxy * scale; + line_c = -dot(p0, line_xy); + } + + // The CPU brush defines a zero-length axis as the gradient end. The + // initialized equation therefore evaluates t=1 everywhere without NaN. info[di + 1u] = bitcast(line_xy.x); info[di + 2u] = bitcast(line_xy.y); info[di + 3u] = bitcast(line_c); @@ -158,7 +189,7 @@ fn main( var p1 = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); var r0 = bitcast(scene[dd + 5u]); var r1 = bitcast(scene[dd + 6u]); - let user_to_gradient = transform_inverse(transform); + let user_to_gradient = transform_identity(); var xform = transform_identity(); var focal_x = 0.0; var radius = 0.0; @@ -226,45 +257,71 @@ fn main( } case DRAWTAG_FILL_ELLIPTIC_GRADIENT: { info[di] = draw_flags; - var center = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); - var axis_end = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); - var second_end = bitcast>(vec2(scene[dd + 5u], scene[dd + 6u])); - center = transform_apply(transform, center); - axis_end = transform_apply(transform, axis_end); - second_end = transform_apply(transform, second_end); + let center = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); + let axis_end = bitcast>(vec2(scene[dd + 3u], scene[dd + 4u])); + let second_end = bitcast>(vec2(scene[dd + 5u], scene[dd + 6u])); let dxy = axis_end - center; let axis = length(dxy); - let inv_axis = 1.0 / axis; let second_axis_len = length(second_end - center); - let transformed_axis_ratio = select(1.0, second_axis_len / axis, axis > 0.0); - let inv_second_axis = inv_axis / transformed_axis_ratio; - let cos_theta = dxy.x * inv_axis; - let sin_theta = dxy.y * inv_axis; - // Map the ellipse to the unit circle so the fill parameter is length(local_xy). + var kind = ELLIPTIC_GRAD_KIND_NORMAL; + var inv_axis = 1.0; + var inv_second_axis = 1.0; + var cos_theta = 1.0; + var sin_theta = 0.0; + + if axis == 0.0 { + // MathF.Atan2(0, 0) gives the CPU brush an identity rotation. Keep the + // matrix finite; fine reproduces the two zero-radius divisions explicitly. + kind = ELLIPTIC_GRAD_KIND_POINT; + } else { + cos_theta = dxy.x / axis; + sin_theta = dxy.y / axis; + + if second_axis_len == 0.0 { + // The raw axis vector makes local y the perpendicular dot product. + // Its exact zero is the collapsed-axis test and avoids normalization + // residue changing an undefined CPU sample into an off-axis sample. + kind = ELLIPTIC_GRAD_KIND_LINE; + cos_theta = dxy.x; + sin_theta = dxy.y; + } else { + inv_axis = 1.0 / axis; + inv_second_axis = 1.0 / second_axis_len; + } + } + + // Normal ellipses map to the unit circle so length(local_xy) is the gradient + // parameter. Every kind uses the NEGATED axis angle, matching the CPU brush; + // degenerate kinds retain the unscaled rotated coordinates for zero tests. let m0 = cos_theta * inv_axis; - let m1 = sin_theta * inv_second_axis; - let m2 = -sin_theta * inv_axis; + let m1 = -sin_theta * inv_second_axis; + let m2 = sin_theta * inv_axis; let m3 = cos_theta * inv_second_axis; - let xlat_x = -(m0 * center.x + m2 * center.y); - let xlat_y = -(m1 * center.x + m3 * center.y); + var xlat_x = center.x; + var xlat_y = center.y; + + if kind == ELLIPTIC_GRAD_KIND_NORMAL { + xlat_x = -(m0 * center.x + m2 * center.y); + xlat_y = -(m1 * center.x + m3 * center.y); + } + info[di + 1u] = bitcast(m0); info[di + 2u] = bitcast(m1); info[di + 3u] = bitcast(m2); info[di + 4u] = bitcast(m3); info[di + 5u] = bitcast(xlat_x); info[di + 6u] = bitcast(xlat_y); + info[di + 7u] = kind; } case DRAWTAG_FILL_SWEEP_GRADIENT: { info[di] = draw_flags; let p0 = bitcast>(vec2(scene[dd + 1u], scene[dd + 2u])); - let xform = transform_mul(transform, Transform(vec4(1.0, 0.0, 0.0, 1.0), p0, vec3(0.0, 0.0, 1.0))); - let inv = transform_inverse(xform); - info[di + 1u] = bitcast(inv.matrx.x); - info[di + 2u] = bitcast(inv.matrx.y); - info[di + 3u] = bitcast(inv.matrx.z); - info[di + 4u] = bitcast(inv.matrx.w); - info[di + 5u] = bitcast(inv.translate.x); - info[di + 6u] = bitcast(inv.translate.y); + info[di + 1u] = bitcast(1.0); + info[di + 2u] = bitcast(0.0); + info[di + 3u] = bitcast(0.0); + info[di + 4u] = bitcast(1.0); + info[di + 5u] = bitcast(-p0.x); + info[di + 6u] = bitcast(-p0.y); info[di + 7u] = scene[dd + 3u]; info[di + 8u] = scene[dd + 4u]; } @@ -285,13 +342,38 @@ fn main( } default: {} } + + // Visible fills and begin clips carry raster interest in the info stream so coarse can + // read it without another storage-buffer binding. Bits 6..9 of the tag give the info + // word count (matching map_draw_tag); the threshold plus interest block occupies the + // final five words of the entry. + let tag_info_size = (tag_word >> 6u) & 0xfu; + if tag_info_size >= 5u { + let interest_offset = di + tag_info_size - 5u; + info[interest_offset] = bitcast(bbox.coverage_threshold); + info[interest_offset + 1u] = bitcast(bbox.interest.x); + info[interest_offset + 2u] = bitcast(bbox.interest.y); + info[interest_offset + 3u] = bitcast(bbox.interest.z); + info[interest_offset + 4u] = bitcast(bbox.interest.w); + } } if tag_word == DRAWTAG_BEGIN_CLIP || tag_word == DRAWTAG_END_CLIP { + // EndClip records carry ~ix, matching ClipInp.path_ix's sign-tagged + // encoding; clip_leaf recovers ix to rewrite the draw monoid. var path_ix = ~ix; + var operation = CLIP_OPERATION_INTERSECTION; if tag_word == DRAWTAG_BEGIN_CLIP { path_ix = m.path_ix; + // ImageSharp carries ClipOperation through the same begin/end records that + // Vello uses for ordinary clip stacks. The high bit is masked out again by + // coarse/fine when they need the underlying blend marker. + operation = select( + CLIP_OPERATION_INTERSECTION, + CLIP_OPERATION_DIFFERENCE, + (scene[dd] & CLIP_DIFFERENCE_MASK_BIT) != 0u); } - clip_inp[m.clip_ix] = ClipInp(ix, i32(path_ix)); + + clip_inp[m.clip_ix] = ClipInp(ix, i32(path_ix), operation); } block_start += WG_SIZE; // break here on end to save monoid aggregation? @@ -299,6 +381,8 @@ fn main( } } +// Builds the transform that maps p0 to (0, 0) and p1 to (1, 0). Used to +// canonicalize two-point conical gradients onto the unit line. fn two_point_to_unit_line(p0: vec2, p1: vec2) -> Transform { let tmp1 = from_poly2(p0, p1); let inv = transform_inverse(tmp1); @@ -306,6 +390,8 @@ fn two_point_to_unit_line(p0: vec2, p1: vec2) -> Transform { return transform_mul(tmp2, inv); } +// Builds the similarity transform that maps (0, 0) to p0 and (0, 1) to p1, +// with an identity perspective row (Skia-style two-point poly transform). fn from_poly2(p0: vec2, p1: vec2) -> Transform { return Transform( vec4(p1.y - p0.y, p0.x - p1.x, p1.x - p0.x, p1.y - p0.y), diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_reduce.wgsl index 648572cbf..49c670c53 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_reduce.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/draw_reduce.wgsl @@ -1,6 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Draw-tag reduction stage. First pass of the two-pass prefix sum over the +// scene's draw-tag stream: each workgroup maps its assigned draw tags to +// DrawMonoid values and reduces them to a single aggregate. draw_leaf later +// combines these per-workgroup aggregates into a full exclusive prefix sum. +// +// Inputs: config uniform; scene stream (draw tags read at +// config.drawtag_base via read_draw_tag_from_scene). +// Outputs: reduced[wg] holds the combined DrawMonoid of workgroup wg's blocks. +// +// Ported from Vello's draw_reduce.wgsl (linebender/vello, +// vello_shaders/shader). + #import config #import drawtag @@ -19,6 +31,11 @@ var sh_scratch: array; #import util +// Reduces this workgroup's blocks of draw tags to a single DrawMonoid. +// Each thread serially accumulates one lane across its strided block reads, +// then a workgroup tree reduction folds the lane values together; thread 0 +// writes the workgroup aggregate to reduced[wg_id.x]. Out-of-range reads +// yield DRAWTAG_NOP, which maps to the monoid identity. @compute @workgroup_size(256) fn main( @builtin(local_invocation_id) local_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl index 0a110e47e..d25027956 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/fine.wgsl @@ -1,72 +1,115 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Fine rasterizer. - -struct Tile { - backdrop: i32, - segments: u32, -} +// Fine rasterizer: the final stage of the pipeline. Each workgroup shades one +// 16x16 tile by interpreting the tile's command list (PTCL) written by +// coarse.wgsl. Coverage is computed analytically from the tile's line segments +// plus backdrop winding, then brushes (solid color, recolor, linear/radial/ +// elliptic/sweep/path gradients, images) are evaluated and composited, +// including clip mask begin/end handling. +// +// Inputs: config uniform, segment buffer, ptcl and info streams, gradient ramp +// texture, image atlas, and a backdrop texture holding the existing target +// contents. Output format, numeric encoding, and alpha representation are +// specialized by FineAreaComputeShader. blend_spill provides scratch for clip +// stacks deeper than BLEND_STACK_SPLIT. +// +// Ported from Vello's fine.wgsl (vello_shaders/shader/fine.wgsl). Local +// divergences from upstream: per-fill aliased coverage thresholds, extra PTCL +// commands (CMD_RECOLOR, CMD_ELLIPTIC_GRAD, CMD_PATH_GRAD), clip difference and +// hard mask bits, per-command raster interest rectangles, backdrop texture +// seeding, tile-row chunking via config.chunk_tile_y_start, and gradient extend +// behavior deliberately matched to the CPU brushes. #import segment #import config #import drawtag +// Scene configuration: target dimensions, tile counts, and the chunk window. @group(0) @binding(0) var config: Config; +// Tile-relative line segments referenced by CMD_FILL commands. @group(0) @binding(1) var segments: array; #import blend #import ptcl +// Width in texels of each gradient ramp row. const GRADIENT_WIDTH = 512; +// Sentinel end-clip blend value: the layer is applied to its backdrop as a +// luminance mask rather than composited with a regular blend mode. const LUMINANCE_MASK_LAYER = 0x10000u; +// Per-tile command lists written by coarse.wgsl. @group(0) @binding(2) var ptcl: array; +// Draw info stream: per-draw flags followed by brush payload words. @group(0) @binding(3) var info: array; +// Scratch for clip/blend stack entries deeper than BLEND_STACK_SPLIT. @group(0) @binding(4) -var blend_spill: array; +var blend_spill: array>; +// Final render target; its declaration and encoding are specialized by the host. @group(0) @binding(5) var output: texture_storage_2d; +// Gradient ramp texture: one GRADIENT_WIDTH-texel row per gradient. @group(0) @binding(6) var gradients: texture_2d; +// Atlas holding image brush sources. @group(0) @binding(7) var image_atlas: texture_2d; +// Existing target contents used to seed each pixel in the target representation. @group(0) @binding(8) var backdrop_texture: texture_2d; +// Decodes a CMD_FILL payload: packed segment-count/fill-rule word, segment +// base index, backdrop winding, per-fill aliased coverage threshold, and the +// raster interest rectangle. As for all read_* decoders, cmd_ix addresses the +// command tag and the payload words follow it. fn read_fill(cmd_ix: u32) -> CmdFill { let size_and_rule = ptcl[cmd_ix + 1u]; let seg_data = ptcl[cmd_ix + 2u]; let backdrop = i32(ptcl[cmd_ix + 3u]); - return CmdFill(size_and_rule, seg_data, backdrop); + let coverage_threshold = bitcast(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])); + return CmdFill(size_and_rule, seg_data, backdrop, coverage_threshold, interest); +} + +// 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. fn read_color(cmd_ix: u32) -> CmdColor { - let rgba_color = ptcl[cmd_ix + 1u]; - let draw_flags = ptcl[cmd_ix + 2u]; - return CmdColor(rgba_color, draw_flags); + 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); } +// Decodes a CMD_RECOLOR reference to its target-specialized auxiliary record. fn read_recolor(cmd_ix: u32) -> CmdRecolor { - let source_color = ptcl[cmd_ix + 1u]; - let target_color = ptcl[cmd_ix + 2u]; - let threshold = bitcast(ptcl[cmd_ix + 3u]); - let draw_flags = ptcl[cmd_ix + 4u]; - return CmdRecolor(source_color, target_color, threshold, draw_flags); + return CmdRecolor(ptcl[cmd_ix + 1u], ptcl[cmd_ix + 2u]); } +// Decodes a CMD_LIN_GRAD payload. The first word packs the gradient ramp index +// (high bits) with the extend mode (low two bits); the implicit line equation +// coefficients are read from the info stream. fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { let index_mode = ptcl[cmd_ix + 1u]; let index = index_mode >> 2u; @@ -78,6 +121,9 @@ fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { return CmdLinGrad(index, extend_mode, line_x, line_y, line_c); } +// Decodes a CMD_RAD_GRAD payload. The info stream supplies a 2x2 matrix plus +// translation mapping pixels into gradient space, the focal parameters, and a +// word packing the gradient kind (low three bits) with the flags above them. fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { let index_mode = ptcl[cmd_ix + 1u]; let index = index_mode >> 2u; @@ -97,6 +143,10 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { return CmdRadGrad(index, extend_mode, matrx, xlat, focal_x, radius, kind, flags); } +// Decodes a CMD_ELLIPTIC_GRAD payload: ramp index and extend mode, plus a 2x2 +// matrix and translation (from the info stream) mapping pixels into gradient +// space, plus the kind used to evaluate zero-width ellipses without undefined +// matrix arithmetic. fn read_elliptic_grad(cmd_ix: u32) -> CmdEllipticGrad { let index_mode = ptcl[cmd_ix + 1u]; let index = index_mode >> 2u; @@ -108,9 +158,13 @@ fn read_elliptic_grad(cmd_ix: u32) -> CmdEllipticGrad { let m3 = bitcast(info[info_offset + 3u]); let matrx = vec4(m0, m1, m2, m3); let xlat = vec2(bitcast(info[info_offset + 4u]), bitcast(info[info_offset + 5u])); - return CmdEllipticGrad(index, extend_mode, matrx, xlat); + let kind = info[info_offset + 6u]; + return CmdEllipticGrad(index, extend_mode, matrx, xlat, kind); } +// Decodes a CMD_SWEEP_GRAD payload: ramp index and extend mode, matrix and +// translation from the info stream, and the t0/t1 angular range expressed in +// normalized turns. fn read_sweep_grad(cmd_ix: u32) -> CmdSweepGrad { let index_mode = ptcl[cmd_ix + 1u]; let index = index_mode >> 2u; @@ -127,6 +181,8 @@ fn read_sweep_grad(cmd_ix: u32) -> CmdSweepGrad { return CmdSweepGrad(index, extend_mode, matrx, xlat, t0, t1); } +// Decodes a CMD_PATH_GRAD payload: info-stream offset of the gradient data, +// edge count, gradient flags, and draw flags. fn read_path_grad(cmd_ix: u32) -> CmdPathGrad { let data_offset = ptcl[cmd_ix + 1u]; let edge_count = ptcl[cmd_ix + 2u]; @@ -135,6 +191,9 @@ fn read_path_grad(cmd_ix: u32) -> CmdPathGrad { return CmdPathGrad(data_offset, edge_count, flags, draw_flags); } +// Decodes a CMD_IMAGE payload from the info stream: inverse transform (matrix +// plus translation), atlas placement and extents, and a packed word carrying +// alpha, pixel format, alpha type, and per-axis extend modes. fn read_image(cmd_ix: u32) -> CmdImage { let info_offset = ptcl[cmd_ix + 1u]; let m0 = bitcast(info[info_offset]); @@ -147,51 +206,145 @@ fn read_image(cmd_ix: u32) -> CmdImage { let width_height = info[info_offset + 7u]; let sample_alpha = info[info_offset + 8u]; let alpha = f32(sample_alpha & 0xFFu) / 255.0; - let format = sample_alpha >> 15u; + let format = (sample_alpha >> 15u) & 0x1u; let alpha_type = (sample_alpha >> 14u) & 0x1u; + let signed_unit = (sample_alpha >> 16u) & 0x1u; let x_extend = (sample_alpha >> 10u) & 0x3u; let y_extend = (sample_alpha >> 8u) & 0x3u; - // The following are not intended to be bitcasts + // xy and width_height each pack two u16 values; these are numeric + // conversions, not bitcasts. let x = f32(xy >> 16u); let y = f32(xy & 0xffffu); let width = f32(width_height >> 16u); let height = f32(width_height & 0xffffu); - return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), format, x_extend, y_extend, alpha, alpha_type); + return CmdImage(matrx, xlat, vec2(x, y), vec2(width, height), format, x_extend, y_extend, alpha, alpha_type, signed_unit); } +// Decodes a CMD_END_CLIP payload: the packed blend mode (which may carry the +// clip difference/hard mask bits) and the layer alpha, which doubles as the +// coverage threshold for hard mask clips. fn read_end_clip(cmd_ix: u32) -> CmdEndClip { let blend = ptcl[cmd_ix + 1u]; let alpha = bitcast(ptcl[cmd_ix + 2u]); return CmdEndClip(blend, alpha); } +// Extracts the packed blend mode ((mix << 8) | compose) from a draw-flags word. fn read_draw_blend_mode(draw_flags: u32) -> u32 { return (draw_flags & DRAW_FLAGS_BLEND_MODE_MASK) >> DRAW_FLAGS_BLEND_MODE_SHIFT; } +// Extracts the mix (blending) mode from a draw-flags word. fn read_draw_mix_mode(draw_flags: u32) -> u32 { return read_draw_blend_mode(draw_flags) >> 8u; } +// Extracts the Porter-Duff compose mode from a draw-flags word. fn read_draw_compose_mode(draw_flags: u32) -> u32 { return read_draw_blend_mode(draw_flags) & 0xffu; } +// Extracts the layer alpha from a draw-flags word, stored as a 16-bit +// normalized value. fn read_draw_blend_alpha(draw_flags: u32) -> f32 { let packed = (draw_flags & DRAW_FLAGS_BLEND_ALPHA_MASK) >> DRAW_FLAGS_BLEND_ALPHA_SHIFT; return f32(packed) / 65535.0; } +// True when the flags encode plain source-over at full layer alpha, which +// allows compose_draw to take the fast path. fn is_default_draw_blend(draw_flags: u32) -> bool { return read_draw_blend_mode(draw_flags) == ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER) && (draw_flags & DRAW_FLAGS_BLEND_ALPHA_MASK) == DRAW_FLAGS_BLEND_ALPHA_MASK; } +// Recolor's inner blend uses a distance-derived alpha rather than the layer alpha in +// draw_flags. Keep that distinct from compose_draw so ordinary draws retain their exact path. +fn compose_recolor_inner(backdrop: vec4, source: vec4, blend_alpha: f32, draw_flags: u32) -> vec4 { + let effective_alpha = source.a * blend_alpha; + + if read_draw_blend_mode(draw_flags) == ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER) { + return backdrop * (1.0 - effective_alpha) + (source * blend_alpha); + } + + let cb = unpremultiply(backdrop); + let cs = unpremultiply(source); + let ab = backdrop.a; + let as_ = effective_alpha; + let mix_mode = read_draw_mix_mode(draw_flags); + let compose_mode = read_draw_compose_mode(draw_flags); + let shared_alpha = as_ * ab; + + switch compose_mode { + case COMPOSE_CLEAR: { + return vec4(0.0); + } + case COMPOSE_COPY: { + return vec4(cs * as_, as_); + } + case COMPOSE_DEST: { + return backdrop; + } + case COMPOSE_SRC_OVER: { + let blend = blend_mix(cb, cs, mix_mode); + let dst_weight = ab - shared_alpha; + let src_weight = as_ - shared_alpha; + let alpha = dst_weight + as_; + let premul = (cb * dst_weight) + (cs * src_weight) + (blend * shared_alpha); + return vec4(premul, alpha); + } + case COMPOSE_DEST_OVER: { + let blend = blend_mix(cs, cb, mix_mode); + let dst_weight = as_ - shared_alpha; + let src_weight = ab - shared_alpha; + let alpha = dst_weight + ab; + let premul = (cs * dst_weight) + (cb * src_weight) + (blend * shared_alpha); + return vec4(premul, alpha); + } + case COMPOSE_SRC_IN: { + return vec4(cs * shared_alpha, shared_alpha); + } + case COMPOSE_DEST_IN: { + return vec4(cb * shared_alpha, shared_alpha); + } + case COMPOSE_SRC_OUT: { + let alpha = as_ * (1.0 - ab); + return vec4(cs * alpha, alpha); + } + case COMPOSE_DEST_OUT: { + let alpha = ab * (1.0 - as_); + return vec4(cb * alpha, alpha); + } + case COMPOSE_SRC_ATOP: { + let blend = blend_mix(cb, cs, mix_mode); + let dst_weight = ab - shared_alpha; + let premul = (cb * dst_weight) + (blend * shared_alpha); + return vec4(premul, ab); + } + case COMPOSE_DEST_ATOP: { + let blend = blend_mix(cs, cb, mix_mode); + let dst_weight = as_ - shared_alpha; + let premul = (cs * dst_weight) + (blend * shared_alpha); + return vec4(premul, as_); + } + case COMPOSE_XOR: { + let src_weight = as_ * (1.0 - ab); + let dst_weight = ab * (1.0 - as_); + return vec4((cs * src_weight) + (cb * dst_weight), src_weight + dst_weight); + } + default: { + return blend_mix_compose(backdrop, source * blend_alpha, read_draw_blend_mode(draw_flags)); + } + } +} + +// Composites a premultiplied source over a premultiplied backdrop using the +// mix/compose modes and layer alpha packed in draw_flags. Plain source-over at +// full alpha takes a fast path. The common Porter-Duff compose operators are +// expanded inline with the mix result weighted by the shared-alpha region; +// anything else falls back to blend_mix_compose. fn compose_draw(backdrop: vec4, source: vec4, draw_flags: u32) -> vec4 { let effective_alpha = source.a * read_draw_blend_alpha(draw_flags); - if effective_alpha <= (0.5 / 255.0) { - return backdrop; - } if is_default_draw_blend(draw_flags) { return backdrop * (1.0 - source.a) + source; @@ -268,9 +421,18 @@ fn compose_draw(backdrop: vec4, source: vec4, draw_flags: u32) -> vec4 } } +// Applies compose_draw, then lerps between the backdrop and the composed +// result by coverage so partial coverage attenuates the whole composite +// (including destructive compose modes), not just the source alpha. +fn compose_draw_with_coverage(backdrop: vec4, source: vec4, coverage: f32, draw_flags: u32) -> vec4 { + let composed = compose_draw(backdrop, source, draw_flags); + return backdrop + ((composed - backdrop) * coverage); +} + const PIXEL_FORMAT_RGBA: u32 = 0u; const PIXEL_FORMAT_BGRA: u32 = 1u; -// Normalises subpixel order loaded from an image, based on the image's format. +// Normalises the channel order of a pixel loaded from an image, based on the +// image's declared format. fn pixel_format(pixel: vec4f, format: u32) -> vec4f { switch format { case PIXEL_FORMAT_BGRA: { @@ -285,7 +447,15 @@ fn pixel_format(pixel: vec4f, format: u32) -> vec4f { const ALPHA: u32 = 0u; const PREMULTIPLIED_ALPHA: u32 = 1u; -// Premultiplies alpha if not already + +// CPU-backed atlases contain native TPixel values. NormalizedByte4 maps its signed +// native components into ImageSharp's unit color range. +fn decode_image_numeric(pixel: vec4f, signed_unit: u32) -> vec4f { + return select(pixel, (pixel + vec4f(1.0)) * 0.5, signed_unit != 0u); +} + +// Premultiplies the pixel's alpha unless the image already stores +// premultiplied values. fn maybe_premul_alpha(pixel: vec4f, alpha_type: u32) -> vec4f { switch alpha_type { case PREMULTIPLIED_ALPHA: { @@ -297,9 +467,14 @@ fn maybe_premul_alpha(pixel: vec4f, alpha_type: u32) -> vec4f { } } +// Extend (wrap) modes for gradient parameters and image sampling. const EXTEND_PAD: u32 = 0u; const EXTEND_REPEAT: u32 = 1u; const EXTEND_REFLECT: u32 = 2u; +const EXTEND_DECAL: u32 = 3u; +// Maps a gradient parameter t into [0, 1] according to the extend mode. +// Negative-t handling deliberately matches the CPU gradient brushes; see the +// per-case notes. fn extend_mode_normalized(t: f32, mode: u32) -> f32 { switch mode { case EXTEND_PAD: { @@ -310,29 +485,22 @@ fn extend_mode_normalized(t: f32, mode: u32) -> f32 { // They hold the first stop for t < 0 and only repeat for t >= 0. return select(fract(t), 0.0, t < 0.0); } - case EXTEND_REFLECT, default: { + case EXTEND_REFLECT: { // Likewise, CPU reflection clamps negative values to the first stop // and only reflects once the parameter moves forward beyond 0. let clamped = max(t, 0.0); return abs(clamped - 2.0 * round(0.5 * clamped)); } - } -} - -fn extend_mode(t: f32, mode: u32, max: f32) -> f32 { - switch mode { - case EXTEND_PAD: { - return clamp(t, 0.0, max); - } - case EXTEND_REPEAT: { - return extend_mode_normalized(t / max, mode) * max; - } - case EXTEND_REFLECT, default: { - return extend_mode_normalized(t / max, mode) * max; + case EXTEND_DECAL, default: { + // DontFill contributes no color outside the finite gradient interval. A negative + // sentinel lets every gradient evaluator skip its texture load without adding a + // second mode-dependent range test to the normal pad/repeat/reflect paths. + return select(t, -1.0, t < 0.0 || t > 1.0); } } } +// Wraps an integer sample coordinate into [0, max) for repeat tiling. fn image_repeat_mode_i32(t: f32, max: i32) -> i32 { let value = i32(t); let magnitude = select(value, -value, value < 0); @@ -343,12 +511,25 @@ fn image_repeat_mode_i32(t: f32, max: i32) -> i32 { return (signed_remainder + max) % max; } +// Reflects an integer sample coordinate into [0, max), mirroring every other +// tile. fn image_reflect_mode_i32(t: f32, max: i32) -> i32 { - let reflected = fract(0.5 * t) * 2.0; - let wrapped = (1.0 - abs(reflected - 1.0)) * f32(max); - return i32(clamp(wrapped, 0.0, f32(max - 1))); + // Reflect in pixel space with period 2*max, mirroring once per tile. + // Matches the CPU ImageBrush: wrap into [0, 2*max) then fold the back half. + let period = max * 2; + let value = i32(t); + let magnitude = select(value, -value, value < 0); + let remainder = magnitude % period; + let signed_remainder = select(remainder, -remainder, value < 0); + var m = (signed_remainder + period) % period; + if (m >= max) { + m = period - 1 - m; + } + return m; } +// Converts a continuous sample coordinate to a texel index under the given +// wrap mode. Decal returns -1 for samples outside the source region. fn image_extend_mode_i32(t: f32, mode: u32, max: i32) -> i32 { switch mode { case EXTEND_PAD: { @@ -357,42 +538,65 @@ fn image_extend_mode_i32(t: f32, mode: u32, max: i32) -> i32 { case EXTEND_REPEAT: { return image_repeat_mode_i32(t, max); } + case EXTEND_DECAL: { + // Outside the source region samples as transparent; signalled with a negative index. + if (t < 0.0 || t >= f32(max)) { + return -1; + } + return i32(t); + } case EXTEND_REFLECT, default: { return image_reflect_mode_i32(t, max); } } } +// Flag bit indicating the gradient carries an explicit center color, which +// disables the single-triangle barycentric shortcut. const PATH_GRAD_HAS_EXPLICIT_CENTER_COLOR = 1u; -const PATH_GRAD_HEADER_WORD_COUNT = 4u; -const PATH_GRAD_EDGE_WORD_COUNT = 6u; - +// Info-stream layout: a 7-word header (center point, max ray distance, and four +// center-color components) followed by 12-word edge records (start point, end +// point, and four components for each endpoint color). +const PATH_GRAD_HEADER_WORD_COUNT = 7u; +const PATH_GRAD_EDGE_WORD_COUNT = 12u; + +// Returns the info-stream offset of edge record edge_ix, past the gradient's +// 7-word header. fn path_grad_edge_offset(path_grad: CmdPathGrad, edge_ix: u32) -> u32 { return path_grad.data_offset + PATH_GRAD_HEADER_WORD_COUNT + edge_ix * PATH_GRAD_EDGE_WORD_COUNT; } +// Loads a point stored as two f32 words in the info stream. fn path_grad_load_point(offset: u32) -> vec2 { return vec2(bitcast(info[offset]), bitcast(info[offset + 1u])); } +// Loads an associated color stored as four f32 words in the info stream. fn path_grad_load_color(offset: u32) -> vec4 { - return unpack4x8unorm(info[offset]); + return vec4( + bitcast(info[offset]), + bitcast(info[offset + 1u]), + bitcast(info[offset + 2u]), + bitcast(info[offset + 3u])); } +// 2D cross product (the z component of the 3D cross product). fn cross2(a: vec2, b: vec2) -> f32 { return a.x * b.y - a.y * b.x; } +// Intersects the segment p + t*r with the segment q + u*s using the same +// epsilon-expanded parameter bounds as PolygonUtilities on the CPU. fn path_grad_line_intersection(p: vec2, r: vec2, q: vec2, s: vec2) -> vec4 { let denominator = cross2(r, s); - if abs(denominator) <= 1.0e-6 { + if denominator > -1.0e-3 && denominator < 1.0e-3 { return vec4(0.0); } let qp = q - p; let t = cross2(qp, s) / denominator; let u = cross2(qp, r) / denominator; - if t < 0.0 || t > 1.0 || u < 0.0 || u > 1.0 { + if t <= -1.0e-3 || t >= 1.001 || u <= -1.0e-3 || u >= 1.001 { return vec4(0.0); } @@ -400,6 +604,8 @@ fn path_grad_line_intersection(p: vec2, r: vec2, q: vec2, s: vec2 return vec4(1.0, point.x, point.y, u); } +// Point-in-triangle test with barycentric output. The sign-product test is a +// direct port of the CPU brush, including its treatment of a zero cross product. fn path_grad_point_on_triangle(v1: vec2, v2: vec2, v3: vec2, point: vec2) -> vec3 { let e1 = v2 - v1; let e2 = v3 - v2; @@ -410,9 +616,8 @@ fn path_grad_point_on_triangle(v1: vec2, v2: vec2, v3: vec2, poin let d1 = cross2(e1, pv1); let d2 = cross2(e2, pv2); let d3 = cross2(e3, pv3); - let has_negative = d1 < 0.0 || d2 < 0.0 || d3 < 0.0; - let has_positive = d1 > 0.0 || d2 > 0.0 || d3 > 0.0; - if has_negative && has_positive { + let d12_sign = sign(d1 * d2); + if d12_sign * sign(d1 * d3) == -1.0 || d12_sign * sign(d2 * d3) == -1.0 { return vec3(0.0); } @@ -422,21 +627,26 @@ fn path_grad_point_on_triangle(v1: vec2, v2: vec2, v3: vec2, poin let d20 = dot(pv1, e1); let d21 = -dot(pv1, e3); let denominator = (d00 * d11) - (d01 * d01); - if abs(denominator) <= 1.0e-6 { - return vec3(0.0); - } - let u = ((d11 * d20) - (d01 * d21)) / denominator; let v = ((d00 * d21) - (d01 * d20)) / denominator; return vec3(1.0, u, v); } +// Evaluates the path gradient brush at a point, returning an associated color. +// Payload colors are associated before interpolation as required by CSS Color 4: +// https://www.w3.org/TR/css-color-4/#interpolation-alpha +// A three-edge gradient without an explicit center color interpolates +// its vertex colors barycentrically. Otherwise a ray is cast from the point +// away from the gradient center to find the nearest edge intersection; the +// interpolated edge color is then mixed toward the center color by the ratio +// of the point's distance from the edge to the center's distance from the +// edge. Returns transparent when the point is not covered by any edge. fn evaluate_path_gradient(path_grad: CmdPathGrad, point: vec2) -> vec4 { let center = path_grad_load_point(path_grad.data_offset); let center_color = path_grad_load_color(path_grad.data_offset + 3u); if all(point == center) { - return premul_alpha(center_color); + return center_color; } if path_grad.edge_count == 3u && (path_grad.flags & PATH_GRAD_HAS_EXPLICIT_CENTER_COLOR) == 0u { @@ -452,15 +662,15 @@ fn evaluate_path_gradient(path_grad: CmdPathGrad, point: vec2) -> vec4 } let c0 = path_grad_load_color(edge0 + 4u); - let c1 = path_grad_load_color(edge0 + 5u); + let c1 = path_grad_load_color(edge0 + 8u); let c2 = path_grad_load_color(edge2 + 4u); - return premul_alpha(((1.0 - triangle.y - triangle.z) * c0) + (triangle.y * c1) + (triangle.z * c2)); + return ((1.0 - triangle.y - triangle.z) * c0) + (triangle.y * c1) + (triangle.z * c2); } let delta = point - center; let delta_length_squared = dot(delta, delta); if delta_length_squared == 0.0 { - return premul_alpha(center_color); + return center_color; } let max_distance = bitcast(info[path_grad.data_offset + 2u]); @@ -483,7 +693,9 @@ fn evaluate_path_gradient(path_grad: CmdPathGrad, point: vec2) -> vec4 if distance_squared < closest_distance { closest_distance = distance_squared; closest_point = intersection_point; - closest_color = mix(path_grad_load_color(edge + 4u), path_grad_load_color(edge + 5u), intersection.w); + // PolygonUtilities accepts a small negative edge parameter, while the CPU + // edge-color ratio is distance based and therefore uses its magnitude. + closest_color = mix(path_grad_load_color(edge + 4u), path_grad_load_color(edge + 8u), abs(intersection.w)); found = true; } } @@ -495,17 +707,27 @@ fn evaluate_path_gradient(path_grad: CmdPathGrad, point: vec2) -> vec4 let center_distance = distance(closest_point, center); let ratio = select(0.0, distance(closest_point, point) / center_distance, center_distance > 0.0); - return premul_alpha(mix(closest_color, center_color, ratio)); + return mix(closest_color, center_color, ratio); } +// Number of horizontally adjacent pixels shaded by each thread. const PIXELS_PER_THREAD = 4u; -// Analytic area anti-aliasing. +// Computes per-pixel coverage for a CMD_FILL using analytic area +// anti-aliasing. Signed winding is accumulated from the backdrop and every +// segment in the tile, the fill rule is applied, aliased fills are quantized +// against their per-fill threshold, and coverage outside the fill's raster +// interest rectangle is zeroed. xy is the thread's first pixel in tile-local +// space (segments are tile-relative); global_xy is the same pixel in +// full-target space. result receives coverage for PIXELS_PER_THREAD adjacent +// pixels. // // FIXME: This should return an array when https://github.com/gfx-rs/naga/issues/1930 is fixed. -fn fill_path(fill: CmdFill, xy: vec2, result: ptr>) { - let n_segs = fill.size_and_rule >> 1u; +fn fill_path(fill: CmdFill, xy: vec2, global_xy: vec2, result: ptr>) { + // size_and_rule: bit 0 = even-odd, bit 1 = aliased coverage, bits 2.. = segment count. + let n_segs = fill.size_and_rule >> 2u; let even_odd = (fill.size_and_rule & 1u) != 0u; + let aliased = (fill.size_and_rule & 2u) != 0u; var area: array; let backdrop_f = f32(fill.backdrop); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { @@ -519,6 +741,8 @@ fn fill_path(fill: CmdFill, xy: vec2, result: ptr, result: ptr, result: ptr= threshold); + } + } else if fill.coverage_threshold > 0.0 { + // For antialiased fills the coverage_threshold word carries the perceptual coverage + // boost instead (the two uses are mutually exclusive). An S-curve darkens coverage + // above one half and lightens it below, so text stems solidify while nearly-open + // counters stay bright; at full strength the remap is exactly smoothstep. Matches + // the CPU rasterizer's AreaToCoverage boost. + let boost = fill.coverage_threshold; + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let a = area[i]; + area[i] = a + boost * a * (1.0 - a) * (2.0 * a - 1.0); + } + } + // Discard coverage outside the fill's raster interest rectangle + // (expressed in full-target coordinates). + for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { + let pixel = global_xy + vec2(f32(i), 0.0); + if pixel.x < fill.interest.x || pixel.y < fill.interest.y || pixel.x >= fill.interest.z || pixel.y >= fill.interest.w { + area[i] = 0.0; + } + } + *result = area; } -// The X size should be 16 / PIXELS_PER_THREAD +// Entry point: one workgroup per tile, each thread shading PIXELS_PER_THREAD +// horizontally adjacent pixels. Seeds pixel state from the backdrop texture, +// interprets the tile's PTCL commands until CMD_END, and writes the result in +// the target's configured alpha representation. +// +// The X workgroup size should be TILE_WIDTH / PIXELS_PER_THREAD. @compute @workgroup_size(4, 16) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -572,19 +833,26 @@ fn main( return; } let tile_ix = wg_id.y * config.width_in_tiles + wg_id.x; + // Full-target pixel position; y is offset by the chunk's starting tile row + // so oversized scenes render correctly in tile-row windows. let xy = vec2(f32(global_id.x * PIXELS_PER_THREAD), f32(config.chunk_tile_y_start * TILE_HEIGHT + global_id.y)); let xy_uint = vec2(xy); let local_xy = vec2(f32(local_id.x * PIXELS_PER_THREAD), f32(local_id.y)); var rgba: array, PIXELS_PER_THREAD>; + // Seed each pixel from the existing target contents. The generated target + // decoder normalizes numeric encoding and returns the associated form used internally. for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let coords = vec2(xy_uint + vec2(i, 0u)); let backdrop_raw = textureLoad(backdrop_texture, coords, 0); - rgba[i] = vec4(backdrop_raw.rgb * backdrop_raw.a, backdrop_raw.a); + rgba[i] = decode_target(backdrop_raw); } - var blend_stack: array, BLEND_STACK_SPLIT>; + // Clip saves remain in the fine shader's associated working representation. Two + // binary16 pairs avoid the severe RGBA8 loss that a nested clip previously introduced. + var blend_stack: array, PIXELS_PER_THREAD>, BLEND_STACK_SPLIT>; var clip_depth = 0u; var area: array; 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]; cmd_ix += 1u; // main interpretation loop @@ -596,70 +864,97 @@ fn main( switch tag { case CMD_FILL: { let fill = read_fill(cmd_ix); - fill_path(fill, local_xy, &area); - cmd_ix += 4u; + fill_path(fill, local_xy, xy, &area); + cmd_ix += 9u; } 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])); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - area[i] = 1.0; + 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 += 1u; + cmd_ix += 5u; } case CMD_COLOR: { let color = read_color(cmd_ix); - let fg = unpack4x8unorm(color.rgba_color); + 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 { - let fg_i = fg * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, color.draw_flags); + rgba[i] = compose_draw_with_coverage(rgba[i], fg, area[i], color.draw_flags); } } - cmd_ix += 3u; + cmd_ix += 4u; } case CMD_RECOLOR: { let recolor = read_recolor(cmd_ix); - let source = unpack4x8unorm(recolor.source_color); - let target_color = unpack4x8unorm(recolor.target_color); + let source_words = vec4(info[recolor.data_offset], info[recolor.data_offset + 1u], info[recolor.data_offset + 2u], info[recolor.data_offset + 3u]); + let target_words = vec4(info[recolor.data_offset + 4u], info[recolor.data_offset + 5u], info[recolor.data_offset + 6u], info[recolor.data_offset + 7u]); + let source = bitcast>(source_words); + let target_native = bitcast>(target_words); + let target_internal = recolor_native_to_internal(target_native); + let threshold = bitcast(info[recolor.data_offset + 8u]); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { if area[i] != 0.0 { - let bg = rgba[i]; - let bg_sep = vec4(bg.rgb / max(bg.a, 1e-6), bg.a); - let delta = bg_sep - source; + // CPU Recolor reads a stored TPixel, uses that same value for matching and + // both blend backdrops, then writes a TPixel after each blend boundary. + let bg_native = recolor_store_target(rgba[i]); + let bg_internal = recolor_native_to_internal(bg_native); + let delta = bg_native - source; let distance = dot(delta, delta); - if distance <= recolor.threshold { - let t = (recolor.threshold - distance) / recolor.threshold; - let target_premul = premul_alpha(target_color); - let recolored = target_premul * t + bg * (1.0 - target_color.a * t); - let fg = (recolored - bg) * area[i] + bg * area[i]; - rgba[i] = compose_draw(bg, fg, recolor.draw_flags); + var overlay_internal = bg_internal; + if distance <= threshold { + // Blend strength ramps from 1 at an exact match to + // 0 at the threshold boundary. + let t = (threshold - distance) / threshold; + let inner = compose_recolor_inner(bg_internal, target_internal, t, recolor.draw_flags); + overlay_internal = recolor_native_to_internal(recolor_store_target(inner)); } + + let outer = compose_draw_with_coverage(bg_internal, overlay_internal, area[i], recolor.draw_flags); + rgba[i] = recolor_native_to_internal(recolor_store_target(outer)); } } - cmd_ix += 5u; + cmd_ix += 3u; } case CMD_BEGIN_CLIP: { + // Save the current tile content, then seed the new group. Isolated groups + // (layers) start transparent so their contents composite as a unit; clip + // groups keep the current content so composition modes inside the clip see + // the same destination the CPU backend's per-draw masking sees. Shallow + // stack entries live in registers; deeper ones spill to the blend buffer. + let isolated = ptcl[cmd_ix + 1u] != 0u; if clip_depth < BLEND_STACK_SPLIT { for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - blend_stack[clip_depth][i] = pack4x8unorm(rgba[i]); - rgba[i] = vec4(0.0); + blend_stack[clip_depth][i] = pack_clip_color(rgba[i]); + if isolated { + rgba[i] = vec4(0.0); + } } } else { let blend_in_scratch = clip_depth - BLEND_STACK_SPLIT; let local_tile_ix = local_id.x * PIXELS_PER_THREAD + local_id.y * TILE_WIDTH; let local_blend_start = blend_offset + blend_in_scratch * TILE_WIDTH * TILE_HEIGHT + local_tile_ix; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - blend_spill[local_blend_start + i] = pack4x8unorm(rgba[i]); - rgba[i] = vec4(0.0); + blend_spill[local_blend_start + i] = pack_clip_color(rgba[i]); + if isolated { + rgba[i] = vec4(0.0); + } } } clip_depth += 1u; - cmd_ix += 1u; + cmd_ix += 2u; } case CMD_END_CLIP: { let end_clip = read_end_clip(cmd_ix); clip_depth -= 1u; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - var bg_rgba: u32; + var bg_rgba: vec2; if clip_depth < BLEND_STACK_SPLIT { bg_rgba = blend_stack[clip_depth][i]; } else { @@ -668,39 +963,84 @@ fn main( let local_blend_start = blend_offset + blend_in_scratch * TILE_WIDTH * TILE_HEIGHT + local_tile_ix; bg_rgba = blend_spill[local_blend_start + i]; } - let bg = unpack4x8unorm(bg_rgba); - let fg = rgba[i] * area[i] * end_clip.alpha; - if end_clip.blend == LUMINANCE_MASK_LAYER { + // Difference clips reuse the same clip path but invert the mask at the + // point where the saved backdrop is restored. This keeps clip operation + // semantics in the clip stack instead of fabricating inverse path geometry. + // Hard mask clips binarize coverage against the stored alpha + // threshold instead of scaling the layer by it. + let is_hard_clip = (end_clip.blend & CLIP_HARD_MASK_BIT) != 0u; + var source_clip_area = area[i]; + if is_hard_clip { + source_clip_area = select(0.0, 1.0, source_clip_area > end_clip.alpha); + } + + // Strip the local mask bits so only the packed blend mode remains. + let clip_area = select(source_clip_area, 1.0 - source_clip_area, (end_clip.blend & CLIP_DIFFERENCE_MASK_BIT) != 0u); + let isolated = (end_clip.blend & CLIP_ISOLATED_MASK_BIT) != 0u; + var clip_blend = end_clip.blend & ~(CLIP_DIFFERENCE_MASK_BIT | CLIP_ISOLATED_MASK_BIT); + if is_hard_clip { + clip_blend &= ~CLIP_HARD_MASK_BIT; + } + + let bg = unpack_clip_color(bg_rgba); + + // Non-isolated groups (clip masks) already contain the saved content, so + // the pop is a pure coverage lerp between the saved backdrop and the group. + // This matches the CPU backend, which applies clip coverage per draw against + // the real target, and keeps solid-tile skipped clips and grouped clips + // producing identical results for every composition mode. + if !isolated { + rgba[i] = bg + ((rgba[i] - bg) * clip_area); + continue; + } + + let clip_alpha = select(end_clip.alpha, 1.0, is_hard_clip); + let fg = rgba[i] * clip_alpha; + + if clip_blend == LUMINANCE_MASK_LAYER { // TODO: Does this case apply more generally? // See https://github.com/linebender/vello/issues/1061 // TODO: How do we handle anti-aliased edges here? // This is really an imaging model question - if area[i] == 0f { + if clip_area == 0.0 { rgba[i] = bg; continue; } + let luminance = clamp(svg_lum(unpremultiply(fg)) * fg.a, 0.0, 1.0); - rgba[i] = bg * luminance; + let composed = bg * luminance; + rgba[i] = bg + ((composed - bg) * clip_area); } else { - rgba[i] = blend_mix_compose(bg, fg, end_clip.blend); + let composed = blend_mix_compose(bg, fg, clip_blend); + rgba[i] = bg + ((composed - bg) * clip_area); } } cmd_ix += 3u; } case CMD_JUMP: { + // Continue interpretation in the tile's next PTCL block. cmd_ix = ptcl[cmd_ix + 1u]; } case CMD_LIN_GRAD: { let lin = read_lin_grad(cmd_ix); + // The draw-flags word sits immediately before the brush data + // in the info stream (same layout for all gradient commands). 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) { if area[i] != 0.0 { let my_d = d + lin.line_x * f32(i); - let x = i32(round(extend_mode_normalized(my_d, lin.extend_mode) * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + let t = extend_mode_normalized(my_d, lin.extend_mode); + var fg_rgba = vec4(0.0); + if t >= 0.0 { + let x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0); + } + + // 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); } } cmd_ix += 3u; @@ -708,6 +1048,9 @@ fn main( case CMD_RAD_GRAD: { let rad = read_rad_grad(cmd_ix); let draw_flags = info[ptcl[cmd_ix + 2u] - 1u]; + // Two-point conical gradient. The kind, focal parameters, and + // flags are precomputed by draw_leaf; the parameter t is + // evaluated per kind below. let focal_x = rad.focal_x; let radius = rad.radius; let is_strip = rad.kind == RAD_GRAD_KIND_STRIP; @@ -740,14 +1083,23 @@ fn main( t = less_scale * sqrt(a) - x * r1_recip; is_valid = a >= 0.0 && t >= 0.0; } + var fg_rgba = vec4(0.0); if is_valid { - t = extend_mode_normalized(focal_x + t_sign * t, rad.extend_mode); + t = focal_x + t_sign * t; + + // The conical solver swaps the circles to obtain a stable canonical form. + // Restore the brush parameter before applying its repetition semantics. t = select(t, 1.0 - t, is_swapped); - let x = i32(round(t * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + t = extend_mode_normalized(t, rad.extend_mode); + if t >= 0.0 { + let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(rad.index)), 0); + } } + + // 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); } cmd_ix += 3u; } @@ -757,14 +1109,42 @@ fn main( for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { if area[i] != 0.0 { let my_xy = vec2(xy.x + f32(i) + 0.5, xy.y + 0.5); - let local_xy = elliptic.matrx.xy * my_xy.x + elliptic.matrx.zw * my_xy.y + elliptic.xlat; - let radius = length(local_xy); - if radius == radius { - let t = extend_mode_normalized(radius, elliptic.extend_mode); - let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(elliptic.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + if elliptic.kind == ELLIPTIC_GRAD_KIND_NORMAL { + let local_xy = elliptic.matrx.xy * my_xy.x + elliptic.matrx.zw * my_xy.y + elliptic.xlat; + let radius = length(local_xy); + var fg_rgba = vec4(0.0); + + // A NaN brush transform and DontFill both return a transparent CPU + // overlay. Skip only the ramp lookup, not coverage or composition. + if radius == radius { + let t = extend_mode_normalized(radius, elliptic.extend_mode); + if t >= 0.0 { + let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(elliptic.index)), 0); + } + } + + rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], draw_flags); + } else { + // Keep the CPU order of operations for a collapsed ellipse: subtract + // the center first, then rotate. An affine translation would introduce + // cancellation error and turn exact on-axis zeroes into tiny values. + let center_relative = my_xy - elliptic.xlat; + let local_xy = elliptic.matrx.xy * center_relative.x + elliptic.matrx.zw * center_relative.y; + let is_undefined = select( + local_xy.y == 0.0, + local_xy.x == 0.0 || local_xy.y == 0.0, + elliptic.kind == ELLIPTIC_GRAD_KIND_POINT); + var fg_rgba = vec4(0.0); + + // CPU division gives NaN on a collapsed axis and +Inf everywhere else. + // Its sole NaN check occurs before repetition, so +Inf ultimately selects + // the last stop for pad, repeat, and reflect; DontFill remains transparent. + if !is_undefined && elliptic.extend_mode != EXTEND_DECAL { + 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); } } } @@ -780,6 +1160,7 @@ fn main( let local_xy = sweep.matrx.xy * my_xy.x + sweep.matrx.zw * my_xy.y + sweep.xlat; let x = local_xy.x; let y = local_xy.y; + let is_center = x == 0.0 && y == 0.0; // xy_to_unit_angle from Skia: // See let xabs = abs(x); @@ -798,11 +1179,20 @@ fn main( phi = select(phi, 0.0, phi != phi); // check for NaN phi = fract(1.0 - phi); phi = (phi - sweep.t0) * scale; + + // The center has no direction. Match the CPU brush's stable definition + // of final gradient parameter zero, independent of the start angle. + phi = select(phi, 0.0, is_center); let t = extend_mode_normalized(phi, sweep.extend_mode); - let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(sweep.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + var fg_rgba = vec4(0.0); + if t >= 0.0 { + let ramp_x = i32(round(t * f32(GRADIENT_WIDTH - 1))); + fg_rgba = textureLoad(gradients, vec2(ramp_x, i32(sweep.index)), 0); + } + + // 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); } } cmd_ix += 3u; @@ -813,8 +1203,7 @@ fn main( 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); - let fg_i = fg_rgba * area[i]; - rgba[i] = compose_draw(rgba[i], fg_i, path_grad.draw_flags); + rgba[i] = compose_draw_with_coverage(rgba[i], fg_rgba, area[i], path_grad.draw_flags); } } cmd_ix += 5u; @@ -829,10 +1218,18 @@ fn main( var atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat; let atlas_ix = image_extend_mode_i32(atlas_uv.x, image.x_extend_mode, i32(image.extents.x)); let atlas_iy = image_extend_mode_i32(atlas_uv.y, image.y_extend_mode, i32(image.extents.y)); - let atlas_uv_clamped = vec2(i32(image.atlas_offset.x) + atlas_ix, i32(image.atlas_offset.y) + atlas_iy); - let fg_rgba = maybe_premul_alpha(textureLoad(image_atlas, atlas_uv_clamped, 0), image.alpha_type); - let fg_i = pixel_format(fg_rgba * area[i] * image.alpha, image.format); - rgba[i] = compose_draw(rgba[i], fg_i, draw_flags); + // A negative index means the decal (None) wrap mode fell outside the source + // region; that pixel contributes nothing. + if (atlas_ix >= 0 && atlas_iy >= 0) { + let atlas_uv_clamped = vec2(i32(image.atlas_offset.x) + atlas_ix, i32(image.atlas_offset.y) + atlas_iy); + // Atlas texels use the target TPixel's physical numeric encoding. Decode + // SNORM storage before interpreting alpha so associated and unassociated + // 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); + } } } cmd_ix += 2u; @@ -843,16 +1240,12 @@ fn main( for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let coords = xy_uint + vec2(i, 0u); if coords.x < config.target_width && coords.y < config.target_height { - let fg = rgba[i]; - // let fg = base_color * (1.0 - foreground.a) + foreground; - // Max with a small epsilon to avoid NaNs - let a_inv = 1.0 / max(fg.a, 1e-6); - let rgba_sep = vec4(fg.rgb * a_inv, fg.a); - textureStore(output, vec2(coords), rgba_sep); + textureStore(output, vec2(coords), rgba[i]); } } } +// Converts a straight-alpha color to premultiplied form. fn premul_alpha(rgba: vec4) -> vec4 { return vec4(rgba.rgb * rgba.a, rgba.a); } diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl index ba7f8ddf4..849c36651 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/flatten.wgsl @@ -1,13 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Flatten curves to lines +// Flatten stage: converts encoded path segments into a device-space "line soup" and accumulates +// per-path bounding boxes for the downstream draw_leaf/binning/coarse/fine stages. Inputs are the +// scene stream (path tags, points, styles, transforms) at binding 1, the config uniform at +// binding 0, and the pathtag prefix sums at binding 2; outputs are the atomic path bboxes +// (binding 3), the bump allocators (binding 4), and the LineSoup buffer (binding 5). Ported from +// Vello's flatten.wgsl. Fills of every segment type route through flatten_euler exactly as in +// upstream Vello: read_path_segment lowers lines to degenerate cubics and the Euler-spiral math +// resolves them through its low-curvature path. The C# encoder currently pre-flattens fill curves +// on the CPU, so in practice only line segments arrive for fills. Stroke expansion diverges from +// upstream: it is a port of the CPU PolygonStroker (chained joins, miters, arcs, and caps) with +// quadto tags reused as stroke tangent markers rather than curves. #import config #import drawtag #import pathtag #import segment -#import cubic #import bump @group(0) @binding(0) @@ -19,6 +28,8 @@ var scene: array; @group(0) @binding(2) var tag_monoids: array; +// Per-path device-space bounding box plus draw metadata copied from the style stream. The extents +// are atomic so the many invocations flattening segments of the same path can merge their results. struct AtomicPathBbox { x0: atomic, y0: atomic, @@ -26,6 +37,10 @@ struct AtomicPathBbox { y1: atomic, draw_flags: u32, trans_ix: u32, + // Must mirror PathBbox in bbox.wgsl: both views alias the same buffer, so the strides must match. + coverage_threshold: f32, + _padding: u32, + interest: vec4, } @group(0) @binding(3) @@ -37,24 +52,10 @@ var bump: BumpAllocators; @group(0) @binding(5) var lines: array; -struct SubdivResult { - val: f32, - a0: f32, - a2: f32, -} - -const D = 0.67; -fn approx_parabola_integral(x: f32) -> f32 { - return x * inverseSqrt(sqrt(1.0 - D + (D * D * D * D + 0.25 * x * x))); -} - -const B = 0.39; -fn approx_parabola_inv_integral(x: f32) -> f32 { - return x * sqrt(1.0 - B + (B * B + 0.5 * x * x)); -} - // Functions for Euler spirals +// Geometric Hermite parameters of a curve segment: tangent angles at each endpoint measured +// relative to the chord, the chord length, and an estimate of the Euler-spiral fit error. struct CubicParams { th0: f32, th1: f32, @@ -62,6 +63,8 @@ struct CubicParams { err: f32, } +// Normalized Euler spiral parameters: th0 is the start tangent angle relative to the chord, +// k0 and k1 the curvature and its derivative, ch the chord length of the unit-arclength spiral. struct EulerParams { th0: f32, // th1 need not be explicitly stored, as it can be derived from k0 - th0 @@ -70,6 +73,7 @@ struct EulerParams { ch: f32, } +// An Euler spiral segment between two endpoints with its normalized spiral parameters. struct EulerSeg { p0: vec2f, p1: vec2f, @@ -81,7 +85,7 @@ const DERIV_THRESH: f32 = 1e-6; const DERIV_THRESH_SQUARED: f32 = DERIV_THRESH * DERIV_THRESH; // Amount to nudge t when derivative is near-zero. const DERIV_EPS: f32 = 1e-6; -// Limit for subdivision of cubic Béziers. +// Limit for subdivision of cubic Beziers. const SUBDIV_LIMIT: f32 = 1.0 / 65536.0; // Robust ESPC computation: below this value, treat curve as circular arc const K1_THRESH: f32 = 1e-3; @@ -90,7 +94,9 @@ const DIST_THRESH: f32 = 1e-3; // Threshold for tangents to be considered near zero length const TANGENT_THRESH: f32 = 1e-6; -/// Compute cubic parameters from endpoints and derivatives. +// Computes geometric Hermite parameters for a curve piece from its endpoints (p0, p1) and +// endpoint derivatives (q0, q1) over parameter width dt, including an error estimate for +// approximating the piece with a single Euler spiral segment. fn cubic_from_points_derivs(p0: vec2f, p1: vec2f, q0: vec2f, q1: vec2f, dt: f32) -> CubicParams { let chord = p1 - p0; let chord_squared = dot(chord, chord); @@ -132,6 +138,8 @@ fn cubic_from_points_derivs(p0: vec2f, p1: vec2f, q0: vec2f, q1: vec2f, dt: f32) return CubicParams(th0, th1, chord_len, err); } +// Solves for the Euler spiral whose endpoint tangent angles (relative to the chord) are th0 and +// th1, using polynomial approximations for the curvature terms and the normalized chord length. fn es_params_from_angles(th0: f32, th1: f32) -> EulerParams { let k0 = th0 + th1; let dth = th1 - th0; @@ -157,11 +165,13 @@ fn es_params_from_angles(th0: f32, th1: f32) -> EulerParams { return EulerParams(th0, k0, k1, ch); } +// Evaluates the tangent angle of the Euler spiral at parameter t, relative to the chord. fn es_params_eval_th(params: EulerParams, t: f32) -> f32 { return (params.k0 + 0.5 * params.k1 * (t - 1.0)) * t - params.th0; } -// Integrate Euler spiral. +// Integrates the Euler spiral with curvature k0 and curvature derivative k1 over unit arclength, +// using a degree-10 polynomial expansion. Returns the endpoint displacement as (u, v). fn integ_euler_10(k0: f32, k1: f32) -> vec2f { let t1_1 = k0; let t1_2 = 0.5 * k1; @@ -194,6 +204,8 @@ fn integ_euler_10(k0: f32, k1: f32) -> vec2f { return vec2(u, v); } +// Evaluates the Euler spiral point at parameter t in chord-relative coordinates, where the +// segment endpoints map to (0, 0) at t = 0 and (1, 0) at t = 1. fn es_params_eval(params: EulerParams, t: f32) -> vec2f { let thm = es_params_eval_th(params, t * 0.5); let k0 = params.k0; @@ -207,27 +219,33 @@ fn es_params_eval(params: EulerParams, t: f32) -> vec2f { return vec2(x, y); } +// Evaluates the parallel curve of the Euler spiral at parameter t, displaced along the spiral +// normal by `offset` (in chord-relative units). fn es_params_eval_with_offset(params: EulerParams, t: f32, offset: f32) -> vec2f { let th = es_params_eval_th(params, t); let v = offset * vec2f(sin(th), cos(th)); return es_params_eval(params, t) + v; } +// Constructs an Euler spiral segment from its endpoints and normalized spiral parameters. fn es_seg_from_params(p0: vec2f, p1: vec2f, params: EulerParams) -> EulerSeg { return EulerSeg(p0, p1, params); } -// Note: offset provided is scaled so that 1 = chord length +// Evaluates the parallel curve of an Euler spiral segment at parameter t, mapped back into the +// coordinate space of the segment endpoints. Note: the offset is normalized so 1 = chord length. fn es_seg_eval_with_offset(es: EulerSeg, t: f32, normalized_offset: f32) -> vec2f { let chord = es.p1 - es.p0; let xy = es_params_eval_with_offset(es.params, t, normalized_offset); return es.p0 + vec2f(chord.x * xy.x - chord.y * xy.y, chord.x * xy.y + chord.y * xy.x); } +// Computes sign(x) * |x|^1.5, used by the ESPC subdivision integrals. fn pow_1_5_signed(x: f32) -> f32 { return x * sqrt(abs(x)); } +// Breakpoints and fitted coefficients for the piecewise ESPC integral approximation below. const BREAK1: f32 = 0.8; const BREAK2: f32 = 1.25; const BREAK3: f32 = 2.1; @@ -247,6 +265,8 @@ const QUAD_U2: f32 = QUAD_W2 * QUAD_W2 - QUAD_C2 / QUAD_A2; const FRAC_PI_4: f32 = 0.7853981633974483; const CBRT_9_8: f32 = 1.040041911525952; +// Piecewise approximation of the ESPC (Euler spiral parallel curve) subdivision density +// integral. Odd in x: the sign of the input is preserved. fn espc_int_approx(x: f32) -> f32 { let y = abs(x); var a: f32; @@ -261,6 +281,8 @@ fn espc_int_approx(x: f32) -> f32 { return a * sign(x); } +// Piecewise approximation of the inverse of espc_int_approx, used to map evenly spaced +// subdivision fractions back to spiral parameters. Odd in x. fn espc_int_inv_approx(x: f32) -> f32 { let y = abs(x); var a: f32; @@ -277,11 +299,14 @@ fn espc_int_inv_approx(x: f32) -> f32 { return a * sign(x); } +// A point on a curve together with its first derivative at the same parameter. struct PointDeriv { point: vec2f, deriv: vec2f, } +// Evaluates a cubic Bezier and its first derivative at parameter t. The returned derivative is +// scaled by 1/3 relative to the true derivative. fn eval_cubic_and_deriv(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f, t: f32) -> PointDeriv { let m = 1.0 - t; let mm = m * m; @@ -292,6 +317,8 @@ fn eval_cubic_and_deriv(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f, t: f32) -> P return PointDeriv(p, q); } +// Returns the start tangent of a cubic Bezier, falling back to later control points when the +// leading ones are coincident so degenerate segments still yield a usable direction. fn cubic_start_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { let EPS = 1e-12; let d01 = p1 - p0; @@ -300,31 +327,19 @@ fn cubic_start_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { return select(select(d03, d02, dot(d02, d02) > EPS), d01, dot(d01, d01) > EPS); } -fn cubic_end_tangent(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { - let EPS = 1e-12; - let d23 = p3 - p2; - let d13 = p3 - p1; - let d03 = p3 - p0; - return select(select(d03, d13, dot(d13, d13) > EPS), d23, dot(d23, d23) > EPS); -} - -fn cubic_start_normal(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { - let tangent = normalize(cubic_start_tangent(p0, p1, p2, p3)); - return vec2(-tangent.y, tangent.x); -} - -fn cubic_end_normal(p0: vec2f, p1: vec2f, p2: vec2f, p3: vec2f) -> vec2f { - let tangent = normalize(cubic_end_tangent(p0, p1, p2, p3)); - return vec2(-tangent.y, tangent.x); -} - +// Robustness regimes for the ESPC subdivision integral in flatten_euler: near-zero curvature +// derivative and near-zero offset each get a numerically stable specialization. const ESPC_ROBUST_NORMAL = 0; const ESPC_ROBUST_LOW_K1 = 1; const ESPC_ROBUST_LOW_DIST = 2; -// This function flattens a cubic Bézier by first converting it into Euler spiral -// segments, and then computes a near-optimal flattening of the parallel curves of -// the Euler spiral segments. +// Flattens a cubic Bezier by first converting it into Euler spiral segments, then computing a +// near-optimal flattening of the parallel curves of the Euler spiral segments. With offset == 0 +// the curve itself is flattened in device space; a non-zero offset flattens the parallel curve in +// local space and transforms each emitted line. `start_p`/`end_p` pin the exact endpoints so +// adjacent segments stay watertight. Retained from Vello: the C# encoder currently pre-flattens +// fill curves to lines (degree-raised to cubics), so this path sees no true curves today, but the +// capability is kept intact. fn flatten_euler( cubic: CubicPoints, path_ix: u32, @@ -469,274 +484,332 @@ fn flatten_euler( last_p = this_pq1.point; last_q = this_pq1.deriv; last_t = t1; + // Dyadic bookkeeping: advance to the next interval, then merge fully consumed + // sibling intervals back into a coarser dt so the walk stays O(log) deep. t0_u += 1u; let shift = countTrailingZeros(t0_u); t0_u >>= shift; dt *= f32(1u << shift); } else { + // Fit error too large for one Euler segment: halve the interval and retry. t0_u = t0_u * 2u; dt *= 0.5; } } } -// Flattens the circular arc that subtends the angle begin-center-end. It is assumed that -// ||begin - center|| == ||end - center||. `begin`, `end`, and `center` are defined in the path's -// local coordinate space. +// ---- CPU stroker port ---- // -// The direction of the arc is always a counter-clockwise (Y-down) rotation starting from `begin`, -// towards `end`, centered at `center`, and will be subtended by `angle` (which is assumed to be -// positive). A line segment will always be drawn from the arc's terminus to `end`, regardless of -// `angle`. -// -// `begin`, `end`, center`, and `angle` should be chosen carefully to ensure a smooth arc with the -// correct winding. -fn flatten_arc( - path_ix: u32, begin: vec2f, end: vec2f, center: vec2f, - angle: f32, arc_detail_scale: f32, transform: Transform -) { - var p0 = transform_apply(transform, begin); - var r = begin - center; - - let MIN_THETA = 0.0001; - let radius = max(0.25, length(p0 - transform_apply(transform, center))); - // Matches PolygonStroker.CalcArc's default `da = 2 * acos(r / (r + 0.125/scale))`. - let safe_scale = max(arc_detail_scale, 0.01); - let ratio = clamp(radius / (radius + (0.125 / safe_scale)), -1., 1.); - let theta = max(MIN_THETA, 2. * acos(ratio)); - - // Always output at least one line so that we always draw the chord. - let n_lines = max(1u, u32(ceil(angle / theta))); +// The stroke geometry below is a direct port of the CPU backend's stroker +// (DefaultRasterizer.StrokeLinearizer, itself a port of PolygonStroker) so the GPU emits the +// same outline vertices: identical joins, miters, arcs, and caps. Emission is chained: each +// appended point writes one line from the previously appended point, so the per-vertex join +// connectors splice exactly into the per-segment offset edges. Overshoots along a shared offset +// line cancel in the signed winding sum, reproducing the CPU's trimmed outline coverage. + +// Appends `p` to the outline chain, writing a line from the previous chain point (`last`) and +// advancing it. Coincident points are skipped so the chain never emits zero-length lines. +fn stroke_chain_point(path_ix: u32, last: ptr, p: vec2f, transform: Transform) { + if all(*last == p) { + return; + } + output_line_with_transform(path_ix, *last, p, transform); + *last = p; +} - let c = cos(theta); - let s = sin(theta); - let rot = mat2x2(c, -s, s, c); +// Port of the CPU stroker's NormalizePositiveAngle: wraps into [0, 2*PI). +fn stroke_normalize_positive_angle(angle: f32) -> f32 { + let full_turn = 6.283185307179586; + var a = angle - (full_turn * floor(angle / full_turn)); + if a < 0.0 { + a += full_turn; + } + if a >= full_turn { + a -= full_turn; + } + return a; +} - let line_ix = atomicAdd(&bump.lines, n_lines); - for (var i = 0u; i < n_lines - 1u; i += 1u) { - r = rot * r; - let p1 = transform_apply(transform, center + r); - write_line(line_ix + i, path_ix, p0, p1); - p0 = p1; +// Port of the CPU stroker's GetArcSubdivisionCount (round cap tessellation): returns the number +// of interior vertices needed to keep the arc's chordal error within the arc detail scale. +fn stroke_arc_subdivision_count(radius: f32, sweep: f32, arc_detail_scale: f32) -> u32 { + let safe_radius = max(radius, TANGENT_THRESH); + let safe_scale = max(arc_detail_scale, 0.01); + let ratio = clamp(safe_radius / (safe_radius + (0.125 / safe_scale)), -1.0, 1.0); + let theta = acos(ratio) * 2.0; + if theta <= 0.0 { + return 0u; } - let p1 = transform_apply(transform, end); - write_line(line_ix + n_lines - 1u, path_ix, p0, p1); + // Bounded for GPU safety; far above any real tessellation demand. + return min(u32(max(sweep / theta, 0.0)), 1024u); } -fn draw_cap( - path_ix: u32, cap_style: u32, arc_detail_scale: f32, point: vec2f, - cap0: vec2f, cap1: vec2f, offset_tangent: vec2f, - transform: Transform, +// Port of the CPU stroker's AppendDirectedArcContour (round caps): steps through the positive +// angular sweep from center + from_offset to center + to_offset. The chain is assumed to already +// sit at center + from_offset; only interior and end points are appended. +fn stroke_chain_arc( + path_ix: u32, last: ptr, + center: vec2f, from_offset: vec2f, to_offset: vec2f, + arc_detail_scale: f32, transform: Transform, ) { - if cap_style == STYLE_FLAGS_CAP_ROUND { - flatten_arc(path_ix, cap0, cap1, point, 3.1415927, arc_detail_scale, transform); + let radius = length(from_offset); + if radius <= TANGENT_THRESH { + stroke_chain_point(path_ix, last, center + to_offset, transform); return; } - - var start = cap0; - var end = cap1; - let is_square = (cap_style == STYLE_FLAGS_CAP_SQUARE); - let line_ix = atomicAdd(&bump.lines, select(1u, 3u, is_square)); - if is_square { - let v = offset_tangent; - let p0 = start + v; - let p1 = end + v; - write_line_with_transform(line_ix + 1u, path_ix, start, p0, transform); - write_line_with_transform(line_ix + 2u, path_ix, p1, end, transform); - start = p0; - end = p1; + // WGSL atan2 is implementation-defined when x is zero, and axis-aligned caps produce + // exactly those inputs. The sweep must come from atan2(cross, dot) and interior points + // must rotate the start offset; absolute angles of the offsets are not computable here. + let cross_ft = (from_offset.x * to_offset.y) - (from_offset.y * to_offset.x); + let sweep = stroke_normalize_positive_angle(atan2(cross_ft, dot(from_offset, to_offset))); + let n = stroke_arc_subdivision_count(radius, sweep, arc_detail_scale); + let step = sweep / f32(n + 1u); + let rot_c = cos(step); + let rot_s = sin(step); + var offset = from_offset; + for (var i = 1u; i <= n; i += 1u) { + offset = vec2((offset.x * rot_c) - (offset.y * rot_s), (offset.x * rot_s) + (offset.y * rot_c)); + stroke_chain_point(path_ix, last, center + offset, transform); } - write_line_with_transform(line_ix, path_ix, start, end, transform); + stroke_chain_point(path_ix, last, center + to_offset, transform); } -fn output_inner_edge_with_transform( - path_ix: u32, p0: vec2f, - tan_prev: vec2f, tan_next: vec2f, - len_prev: f32, len_next: f32, half_width: f32, - inner_prev: vec2f, inner_next: vec2f, - inner_before: vec2f, inner_after: vec2f, - cr: f32, - transform: Transform, +// Port of PolygonStroker.CalcArc (round joins): sweeps from offset o1 to offset o2 around corner +// v1 with a fixed angular step derived from the arc detail scale. Both endpoints are appended, +// so the caller's chain need not already sit on the arc. +fn stroke_calc_arc( + path_ix: u32, last: ptr, + v1: vec2f, o1: vec2f, o2: vec2f, + half_width: f32, arc_detail_scale: f32, transform: Transform, ) { - var miter_limit = min(len_prev, len_next) / half_width; - if miter_limit < 1.01 { - miter_limit = 1.01; + // WGSL atan2 is implementation-defined when x is zero, and axis-aligned joins produce + // exactly those inputs. The sweep must come from atan2(cross, dot) and interior points + // must rotate o1; absolute angles of the offsets are not computable here. + let cross_oo = (o1.x * o2.y) - (o1.y * o2.x); + let sweep = stroke_normalize_positive_angle(atan2(cross_oo, dot(o1, o2))); + let da = acos(half_width / (half_width + (0.125 / arc_detail_scale))) * 2.0; + stroke_chain_point(path_ix, last, v1 + o1, transform); + // Bounded for GPU safety; matches the CPU count for all real detail scales. + let n = clamp(i32(sweep / da), 0, 1024); + let step = sweep / f32(n + 1); + let rot_c = cos(step); + let rot_s = sin(step); + var offset = o1; + for (var i = 0; i < n; i += 1) { + offset = vec2((offset.x * rot_c) - (offset.y * rot_s), (offset.x * rot_s) + (offset.y * rot_c)); + stroke_chain_point(path_ix, last, v1 + offset, transform); } - let limit = half_width * miter_limit; + stroke_chain_point(path_ix, last, v1 + o2, transform); +} + +// Port of PolygonStroker.CrossProduct: signed area of triangle (a, b, p), used as a +// side-of-line test for p against the line through a and b. +fn stroke_cross_product(a: vec2f, b: vec2f, p: vec2f) -> f32 { + return ((p.x - b.x) * (b.y - a.y)) - ((p.y - b.y) * (b.x - a.x)); +} + +// Port of PolygonStroker.TryCalcIntersection: infinite line intersection of (a, b) and (c, d). +// Returns false for near-parallel lines; `hit` is written only on success. +fn stroke_try_intersect(a: vec2f, b: vec2f, c: vec2f, d: vec2f, hit: ptr) -> bool { + let ab = b - a; + let cd = d - c; + let denominator = (ab.x * cd.y) - (ab.y * cd.x); + if abs(denominator) < 1e-7 { + return false; + } + let ca = c - a; + let t = ((ca.x * cd.y) - (ca.y * cd.x)) / denominator; + *hit = a + (ab * t); + return true; +} - if abs(cr) > TANGENT_THRESH * TANGENT_THRESH { - let v = inner_after - inner_before; - let h = (tan_prev.x * v.y - tan_prev.y * v.x) / cr; - let miter_pt = inner_after - tan_next * h; - if distance(p0, miter_pt) <= limit { - let line_ix = atomicAdd(&bump.lines, 2u); - write_line_with_transform(line_ix, path_ix, inner_prev, miter_pt, transform); - write_line_with_transform(line_ix + 1u, path_ix, miter_pt, inner_next, transform); - return; +// Port of PolygonStroker.CalcMiter: appends the miter apex at corner v1 between segments +// (v0 -> v1) and (v1 -> v2) with offset vectors o1/o2, or the configured fallback when the apex +// exceeds half_width * miter_limit. `overflow_mode`: 0 = clip, 1 = revert, 2 = round. +// `bevel_distance` is the distance from v1 to the bevel midpoint, used to interpolate the +// clipped miter. +fn stroke_calc_miter( + path_ix: u32, last: ptr, + v0: vec2f, v1: vec2f, v2: vec2f, + o1: vec2f, o2: vec2f, + overflow_mode: u32, miter_limit: f32, bevel_distance: f32, + half_width: f32, arc_detail_scale: f32, transform: Transform, +) { + var xi = v1; + var intersection_distance = 1.0; + let limit = half_width * miter_limit; + var miter_limit_exceeded = true; + var intersection_failed = true; + var hit = vec2(0.0); + + if stroke_try_intersect(v0 + o1, v1 + o1, v1 + o2, v2 + o2, &hit) { + xi = hit; + intersection_distance = distance(v1, hit); + if intersection_distance <= limit { + stroke_chain_point(path_ix, last, hit, transform); + miter_limit_exceeded = false; } + intersection_failed = false; } else { - let probe = inner_before; - let probe_offset = probe - p0; - let prev_side = ((probe_offset.x * tan_prev.y) - (probe_offset.y * tan_prev.x)) < 0.; - let next_side = ((probe_offset.x * tan_next.y) - (probe_offset.y * tan_next.x)) < 0.; - if prev_side == next_side { - let line_ix = atomicAdd(&bump.lines, 2u); - write_line_with_transform(line_ix, path_ix, inner_prev, probe, transform); - write_line_with_transform(line_ix + 1u, path_ix, probe, inner_next, transform); - return; + // Parallel/near-parallel fallback: probe a candidate offset point. + let probe = v1 + o1; + if (stroke_cross_product(v0, v1, probe) < 0.0) == (stroke_cross_product(v1, v2, probe) < 0.0) { + stroke_chain_point(path_ix, last, probe, transform); + miter_limit_exceeded = false; } } - output_line_with_transform(path_ix, inner_prev, inner_next, transform); + if !miter_limit_exceeded { + return; + } + + switch overflow_mode { + case 1u: { + stroke_chain_point(path_ix, last, v1 + o1, transform); + stroke_chain_point(path_ix, last, v1 + o2, transform); + } + case 2u: { + stroke_calc_arc(path_ix, last, v1, o1, o2, half_width, arc_detail_scale, transform); + } + default: { + if intersection_failed { + // No reliable apex: project a clipped bevel from local tangent/perpendicular vectors. + stroke_chain_point(path_ix, last, v1 + o1 + (miter_limit * vec2(-o1.y, o1.x)), transform); + stroke_chain_point(path_ix, last, v1 + o2 - (miter_limit * vec2(-o2.y, o2.x)), transform); + } else { + let q1 = v1 + o1; + let q2 = v1 + o2; + let ratio = (limit - bevel_distance) / (intersection_distance - bevel_distance); + stroke_chain_point(path_ix, last, q1 + ((xi - q1) * ratio), transform); + stroke_chain_point(path_ix, last, q2 + ((xi - q2) * ratio), transform); + } + } + } } -fn draw_join( - path_ix: u32, style_flags: u32, miter_limit: f32, arc_detail_scale: f32, - p0: vec2f, - tan_prev: vec2f, tan_next: vec2f, - len_prev: f32, len_next: f32, half_width: f32, - n_prev: vec2f, n_next: vec2f, +// Port of PolygonStroker.CalcJoin (the CPU stroker's AppendSideJoinContour): emits one side's +// join point sequence at corner v1 between segments (v0 -> v1, len1) and (v1 -> v2, len2). +fn stroke_side_join( + path_ix: u32, last: ptr, + style_flags: u32, miter_limit: f32, arc_detail_scale: f32, half_width: f32, + v0: vec2f, v1: vec2f, v2: vec2f, + len1: f32, len2: f32, transform: Transform, ) { - var front0 = p0 + n_prev; - let front1 = p0 + n_next; - var back0 = p0 - n_next; - let back1 = p0 - n_prev; - - let cr = tan_prev.x * tan_next.y - tan_prev.y * tan_next.x; - let d = dot(tan_prev, tan_next); - let is_backside = cr > 0.; - - // Contour-flow endpoints. `*_prev` connects to the incoming offset curve at this vertex - // and `*_next` starts the outgoing offset curve, so emitting the join straight from - // `*_prev` to `*_next` preserves the signed-winding direction of the stroke outline. - let outer_prev = select(front0, back1, is_backside); - let outer_next = select(front1, back0, is_backside); - let inner_prev = select(back0, front0, is_backside); - let inner_next = select(back1, front1, is_backside); - let inner_before = select(back1, front0, is_backside); - let inner_after = select(back0, front1, is_backside); - - // The outer-side endpoints emitted in the contour's natural flow direction: front0 -> - // front1 on forward turns and back0 -> back1 on backside turns. - let outer_start = select(front0, back0, is_backside); - let outer_end = select(front1, back1, is_backside); + let eps = TANGENT_THRESH; + if len1 < eps || len2 < eps { + // Degenerate neighborhood: fall back to best available segment direction. + let l1 = select(len2, len1, len1 >= eps); + let l2 = select(len1, len2, len2 >= eps); + let seg1 = v1 - v0; + let seg2 = v2 - v1; + let d1 = vec2(seg1.y, -seg1.x) * (half_width / l1); + let d2 = vec2(seg2.y, -seg2.x) * (half_width / l2); + stroke_chain_point(path_ix, last, v1 + d1, transform); + stroke_chain_point(path_ix, last, v1 + d2, transform); + return; + } - let join_style = style_flags & STYLE_FLAGS_JOIN_MASK; - let bevel_distance = length(((outer_prev - p0) + (outer_next - p0)) * 0.5); - var outer_done = false; + let seg_forward = v1 - v0; + let seg_next = v2 - v1; + let o1 = vec2(seg_forward.y, -seg_forward.x) * (half_width / len1); + let o2 = vec2(seg_next.y, -seg_next.x) * (half_width / len2); + let cp = (seg_next.x * seg_forward.y) - (seg_next.y * seg_forward.x); + + if cp > 0.0 { + // Inner corner: miter to the offset-line intersection, reverting past the local limit. + var limit = min(len1, len2) / half_width; + if limit < 1.01 { + limit = 1.01; + } + stroke_calc_miter(path_ix, last, v0, v1, v2, o1, o2, 1u, limit, 0.0, half_width, arc_detail_scale, transform); + return; + } - if ((join_style == STYLE_FLAGS_JOIN_BEVEL || join_style == STYLE_FLAGS_JOIN_ROUND) - && (arc_detail_scale * (half_width - bevel_distance) < half_width / 1024.)) + // Outer corner. + let bevel_distance = length((o1 + o2) * 0.5); + let join_style = style_flags & STYLE_FLAGS_JOIN_MASK; + if (join_style == STYLE_FLAGS_JOIN_ROUND || join_style == STYLE_FLAGS_JOIN_BEVEL) + && ((arc_detail_scale * (half_width - bevel_distance)) < (half_width / 1024.0)) { - if abs(cr) > TANGENT_THRESH * TANGENT_THRESH { - let v = outer_next - outer_prev; - let h = (tan_prev.x * v.y - tan_prev.y * v.x) / cr; - let miter_pt = outer_next - tan_next * h; - let line_ix = atomicAdd(&bump.lines, 2u); - write_line_with_transform(line_ix, path_ix, outer_start, miter_pt, transform); - write_line_with_transform(line_ix + 1u, path_ix, miter_pt, outer_end, transform); + var hit = vec2(0.0); + if stroke_try_intersect(v0 + o1, v1 + o1, v1 + o2, v2 + o2, &hit) { + stroke_chain_point(path_ix, last, hit, transform); } else { - output_line_with_transform(path_ix, outer_start, outer_end, transform); + stroke_chain_point(path_ix, last, v1 + o1, transform); } - outer_done = true; + return; } - if !outer_done { - switch join_style { - case STYLE_FLAGS_JOIN_BEVEL: { - output_line_with_transform(path_ix, outer_start, outer_end, transform); + switch join_style { + case STYLE_FLAGS_JOIN_MITER: { + var overflow_mode = 0u; + if (style_flags & STYLE_FLAGS_JOIN_MITER_REVERT) != 0u { + overflow_mode = 1u; + } else if (style_flags & STYLE_FLAGS_JOIN_MITER_ROUND) != 0u { + overflow_mode = 2u; } - case STYLE_FLAGS_JOIN_MITER: { - let hypot = length(vec2f(cr, d)); - - var line_ix: u32; - // Given the two tangents `tan_prev` and `tan_next` arranged tail-to-tail, the - // miter length ratio is `1 / |cos(theta/2)|`, where `theta` is the angle - // between the tangents. - // - // `hypot` is `|tan_prev| * |tan_next|` (since cr^2 + d^2 = |a|^2 |b|^2) and - // `hypot + d` is `2 * |tan_prev| * |tan_next| * cos^2(theta/2)`. After - // rearranging, the following tests whether `1/|cos(theta/2)| < miter_limit`. - // - // Also avoid the miter computation when `cr` is very small; the intersection - // math divides by `cr` and becomes numerically unstable for near-collinear - // tangents. - if 2. * hypot < (hypot + d) * miter_limit * miter_limit - && abs(cr) > TANGENT_THRESH * TANGENT_THRESH - { - let p = select(front0, back0, is_backside); - - let v = outer_next - outer_prev; - let h = (tan_prev.x * v.y - tan_prev.y * v.x) / cr; - let miter_pt = outer_next - tan_next * h; - - line_ix = atomicAdd(&bump.lines, 2u); - write_line_with_transform(line_ix, path_ix, p, miter_pt, transform); - - // Preserve the contour winding on backside turns by stitching the outer - // edge as back0 -> miter -> back1 rather than reversing the join wedge. - if is_backside { - back0 = miter_pt; - write_line_with_transform(line_ix + 1u, path_ix, back0, back1, transform); - } else { - front0 = miter_pt; - write_line_with_transform(line_ix + 1u, path_ix, front0, front1, transform); - } - } else { - let use_round_overflow = (style_flags & STYLE_FLAGS_JOIN_MITER_ROUND) != 0u; - let use_revert_overflow = (style_flags & STYLE_FLAGS_JOIN_MITER_REVERT) != 0u; - if use_round_overflow { - let arc_start = select(outer_prev, outer_next, is_backside); - let arc_end = select(outer_next, outer_prev, is_backside); - flatten_arc(path_ix, arc_start, arc_end, p0, abs(atan2(cr, d)), arc_detail_scale, transform); - } else if use_revert_overflow || abs(cr) <= TANGENT_THRESH * TANGENT_THRESH { - output_line_with_transform(path_ix, outer_start, outer_end, transform); - } else { - let v = outer_next - outer_prev; - let h = (tan_prev.x * v.y - tan_prev.y * v.x) / cr; - let miter_pt = outer_next - tan_next * h; - let limit = half_width * miter_limit; - let intersection_distance = distance(p0, miter_pt); - if intersection_distance <= bevel_distance + TANGENT_THRESH { - output_line_with_transform(path_ix, outer_start, outer_end, transform); - } else { - let ratio = (limit - bevel_distance) / (intersection_distance - bevel_distance); - let clipped_prev = outer_prev + ((miter_pt - outer_prev) * ratio); - let clipped_next = outer_next + ((miter_pt - outer_next) * ratio); - let clip_outer_start = select(outer_prev, outer_next, is_backside); - let clipped_start = select(clipped_prev, clipped_next, is_backside); - let clipped_end = select(clipped_next, clipped_prev, is_backside); - let clip_outer_end = select(outer_next, outer_prev, is_backside); - line_ix = atomicAdd(&bump.lines, 3u); - write_line_with_transform(line_ix, path_ix, clip_outer_start, clipped_start, transform); - write_line_with_transform(line_ix + 1u, path_ix, clipped_start, clipped_end, transform); - write_line_with_transform(line_ix + 2u, path_ix, clipped_end, clip_outer_end, transform); - } - } - } - } - case STYLE_FLAGS_JOIN_ROUND: { - let arc_start = select(outer_prev, outer_next, is_backside); - let arc_end = select(outer_next, outer_prev, is_backside); - flatten_arc(path_ix, arc_start, arc_end, p0, abs(atan2(cr, d)), arc_detail_scale, transform); - } - default: {} + stroke_calc_miter(path_ix, last, v0, v1, v2, o1, o2, overflow_mode, miter_limit, bevel_distance, half_width, arc_detail_scale, transform); + } + case STYLE_FLAGS_JOIN_ROUND: { + stroke_calc_arc(path_ix, last, v1, o1, o2, half_width, arc_detail_scale, transform); + } + default: { + stroke_chain_point(path_ix, last, v1 + o1, transform); + stroke_chain_point(path_ix, last, v1 + o2, transform); } } +} - output_inner_edge_with_transform( - path_ix, p0, tan_prev, tan_next, len_prev, len_next, half_width, - inner_prev, inner_next, inner_before, inner_after, - cr, transform); +// Port of the CPU stroker's cap emission: butt connects the offset ends, square extends them by +// the tangent before connecting, round steps the CPU cap arc. The chain runs cap0 -> cap1 so +// caps splice into the offset edges with consistent winding. +fn draw_cap( + path_ix: u32, cap_style: u32, arc_detail_scale: f32, point: vec2f, + cap0: vec2f, cap1: vec2f, offset_tangent: vec2f, + transform: Transform, +) { + var last = cap0; + if cap_style == STYLE_FLAGS_CAP_ROUND { + stroke_chain_arc(path_ix, &last, point, cap0 - point, cap1 - point, arc_detail_scale, transform); + return; + } + + if cap_style == STYLE_FLAGS_CAP_SQUARE { + stroke_chain_point(path_ix, &last, cap0 + offset_tangent, transform); + stroke_chain_point(path_ix, &last, cap1 + offset_tangent, transform); + } + stroke_chain_point(path_ix, &last, cap1, transform); +} + +// Emits both join connectors at corner v1, splicing the per-segment offset edges into the CPU +// stroker's outline: the forward side walks (v0, v1, v2) and the reverse side walks (v2, v1, v0), +// exactly like the CPU stroker's two contour passes over the same corner. +fn draw_join( + path_ix: u32, style_flags: u32, miter_limit: f32, arc_detail_scale: f32, half_width: f32, + v0: vec2f, v1: vec2f, v2: vec2f, + len1: f32, len2: f32, + n_prev: vec2f, n_next: vec2f, + transform: Transform, +) { + var last = v1 + n_prev; + stroke_side_join(path_ix, &last, style_flags, miter_limit, arc_detail_scale, half_width, v0, v1, v2, len1, len2, transform); + stroke_chain_point(path_ix, &last, v1 + n_next, transform); + + last = v1 - n_next; + stroke_side_join(path_ix, &last, style_flags, miter_limit, arc_detail_scale, half_width, v2, v1, v0, len2, len1, transform); + stroke_chain_point(path_ix, &last, v1 - n_prev, transform); } +// Reads a point stored as two f32 words at offset `ix` in the path data stream. fn read_f32_point(ix: u32) -> vec2f { let x = bitcast(scene[pathdata_base + ix]); let y = bitcast(scene[pathdata_base + ix + 1u]); return vec2(x, y); } +// Reads a point packed as two signed 16-bit values in a single word at offset `ix` in the path +// data stream (x in the low half, y in the high half). fn read_i16_point(ix: u32) -> vec2f { let raw = scene[pathdata_base + ix]; let x = f32(i32(raw << 16u) >> 16u); @@ -744,16 +817,22 @@ fn read_i16_point(ix: u32) -> vec2f { return vec2(x, y); } +// A 2D transform: 2x2 matrix (columns (mat.x, mat.y) and (mat.z, mat.w)) plus translation, +// extended with a perspective row to support projective transforms. Local divergence from Vello, +// whose transforms are affine only. struct Transform { mat: vec4f, translate: vec2f, perspective: vec3f, } +// Returns the identity transform: unit matrix, zero translation, trivial perspective row. fn transform_identity() -> Transform { return Transform(vec4(1., 0., 0., 1.), vec2(0.), vec3(0., 0., 1.)); } +// Reads transform `ix` from the scene stream: 9 words covering the 2x2 matrix, translation, and +// perspective row. Local divergence: Vello stores 6-word affine transforms. fn read_transform(transform_base: u32, ix: u32) -> Transform { let base = transform_base + ix * 9u; let mat = vec4( @@ -771,6 +850,8 @@ fn read_transform(transform_base: u32, ix: u32) -> Transform { return Transform(mat, translate, perspective); } +// Applies the projective transform to point p. The homogeneous divisor is clamped away from +// zero so points at or behind the horizon cannot produce infinities or NaNs. fn transform_apply(transform: Transform, p: vec2f) -> vec2f { let px = fma(transform.mat.x, p.x, fma(transform.mat.z, p.y, transform.translate.x)); let py = fma(transform.mat.y, p.x, fma(transform.mat.w, p.y, transform.translate.y)); @@ -778,19 +859,25 @@ fn transform_apply(transform: Transform, p: vec2f) -> vec2f { return vec2(px, py) / max(w, 0.0000001); } +// Floors to i32; used for the conservative lower bounds of the path bbox. fn round_down(x: f32) -> i32 { return i32(floor(x)); } +// Ceils to i32; used for the conservative upper bounds of the path bbox. fn round_up(x: f32) -> i32 { return i32(ceil(x)); } +// A path tag byte paired with its exclusive-prefix tag monoid (running stream offsets). struct PathTagData { tag_byte: u32, monoid: TagMonoid, } +// Computes the exclusive-prefix tag monoid for path tag index `ix` by combining the workgroup +// prefix from tag_monoids (produced by pathtag_reduce/scan) with a local reduction of the +// preceding bytes in the same tag word. fn compute_tag_monoid(ix: u32) -> PathTagData { let tag_word = scene[config.pathtag_base + (ix >> 2u)]; let shift = (ix & 3u) * 8u; @@ -805,6 +892,7 @@ fn compute_tag_monoid(ix: u32) -> PathTagData { return PathTagData(tag_byte, tm); } +// Control points of a cubic Bezier; lines and quads are degree-raised to this common form. struct CubicPoints { p0: vec2f, p1: vec2f, @@ -812,6 +900,9 @@ struct CubicPoints { p3: vec2f, } +// Decodes the control points of the segment described by `tag`, reading f32 or packed i16 points +// as indicated by the tag, translating stroke cap markers (quadto-encoded tangents) into lines, +// and degree-raising lines and quads so downstream code handles a single cubic form. fn read_path_segment(tag: PathTagData, is_stroke: bool) -> CubicPoints { var p0: vec2f; var p1: vec2f; @@ -870,30 +961,49 @@ fn read_path_segment(tag: PathTagData, is_stroke: bool) -> CubicPoints { return CubicPoints(p0, p1, p2, p3); } -// Writes a line into a the `lines` buffer at a pre-allocated location designated by `line_ix`. +// Half-width of the band around integer device coordinates that is snapped onto the grid by +// snap_to_pixel_grid. Sub-milli-pixel, so the coverage change is imperceptible, while safely +// exceeding the rounding spread of the flattening and stroking arithmetic. +const PIXEL_GRID_SNAP_EPSILON: f32 = 1e-3; + +// Snaps coordinates lying within PIXEL_GRID_SNAP_EPSILON of an integer device coordinate onto +// that integer. The counting stages resolve grid ties with exact comparisons (the boundary +// horizontal cull and the start-on-boundary backdrop bump in path_count pair up, as do the +// left-edge y_edge assignments in path_tiling); those pairings only hold when every endpoint +// meeting a boundary agrees on whether it lies exactly on it. Flattening and stroking round +// independently per point, so a contour can otherwise reach the boundary with mixed exactness +// and flip the winding of a whole tile row. +fn snap_to_pixel_grid(p: vec2f) -> vec2f { + let r = round(p); + return select(p, r, abs(p - r) < vec2(PIXEL_GRID_SNAP_EPSILON)); +} + +// Writes a line into the `lines` buffer at the pre-allocated slot `line_ix` and grows this +// invocation's running bbox. Out-of-range writes are dropped (the bump counter may legitimately +// exceed the buffer size). fn write_line(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f) { - bbox = vec4(min(bbox.xy, min(p0, p1)), max(bbox.zw, max(p0, p1))); + let s0 = snap_to_pixel_grid(p0); + let s1 = snap_to_pixel_grid(p1); + bbox = vec4(min(bbox.xy, min(s0, s1)), max(bbox.zw, max(s0, s1))); if line_ix < config.lines_size { - lines[line_ix] = LineSoup(path_ix, p0, p1); + lines[line_ix] = LineSoup(path_ix, s0, s1); } } +// Transforms both endpoints into device space and writes the line at slot `line_ix`. fn write_line_with_transform(line_ix: u32, path_ix: u32, p0: vec2f, p1: vec2f, t: Transform) { let tp0 = transform_apply(t, p0); let tp1 = transform_apply(t, p1); write_line(line_ix, path_ix, tp0, tp1); } -fn output_line(path_ix: u32, p0: vec2f, p1: vec2f) { - let line_ix = atomicAdd(&bump.lines, 1u); - write_line(line_ix, path_ix, p0, p1); -} - +// Bump-allocates one slot in the lines buffer and writes the transformed line into it. fn output_line_with_transform(path_ix: u32, p0: vec2f, p1: vec2f, transform: Transform) { let line_ix = atomicAdd(&bump.lines, 1u); write_line_with_transform(line_ix, path_ix, p0, p1, transform); } +// Bump-allocates two consecutive slots in the lines buffer and writes both transformed lines. fn output_two_lines_with_transform( path_ix: u32, p00: vec2f, p01: vec2f, @@ -905,6 +1015,9 @@ fn output_two_lines_with_transform( write_line_with_transform(line_ix + 1u, path_ix, p10, p11, transform); } +// Join information for the segment following the current one: whether to draw a join (versus an +// end cap), its chord length, and its start tangent. The `length` field is a local addition for +// the CPU stroker port, which needs neighbor segment lengths for the inner-corner miter limit. struct NeighboringSegment { do_join: bool, @@ -912,6 +1025,9 @@ struct NeighboringSegment { tangent: vec2f, } +// Reads the segment at tag index `ix` (the segment following the current one) and derives its +// join information. A cap marker encoding a closed subpath still joins back to the start; only +// open subpath cap markers suppress the join in favor of an end cap. fn read_neighboring_segment(ix: u32) -> NeighboringSegment { let tag = compute_tag_monoid(ix); let pts = read_path_segment(tag, true); @@ -933,6 +1049,10 @@ var pathdata_base: u32; // during LineSoup generation. var bbox: vec4f; +// Entry point: one invocation per path tag. Decodes the tag's style, transform, and control +// points; records per-path draw metadata on PATH tags; emits fill geometry via flatten_euler or +// stroke outline geometry via the CPU stroker port; then merges this invocation's device-space +// extents into the path's atomic bbox. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -948,13 +1068,25 @@ fn main( let trans_ix = tag.monoid.trans_ix; let out = &path_bboxes[path_ix]; + // Style stream layout (words): 0 flags, 1 line width, 2 draw flags, 3 miter limit, + // 4 arc detail scale, 5 coverage parameter (aliased threshold or antialiased text + // coverage boost; the uses are mutually exclusive), 6..9 interest rect. let style_flags = scene[config.style_base + style_ix]; let style_draw_flags = scene[config.style_base + style_ix + 2u]; + let coverage_threshold = bitcast(scene[config.style_base + style_ix + 5u]); + let style_interest = vec4( + bitcast(scene[config.style_base + style_ix + 6u]), + bitcast(scene[config.style_base + style_ix + 7u]), + bitcast(scene[config.style_base + style_ix + 8u]), + bitcast(scene[config.style_base + style_ix + 9u])); + // The fill bit is always zero for strokes, which selects the non-zero fill rule. let fill_rule = select(DRAW_INFO_FLAGS_FILL_RULE_BIT, 0u, (style_flags & STYLE_FLAGS_FILL) == 0u); let draw_flags = style_draw_flags | fill_rule; if (tag.tag_byte & PATH_TAG_PATH) != 0u { (*out).draw_flags = draw_flags; (*out).trans_ix = trans_ix; + (*out).coverage_threshold = coverage_threshold; + (*out).interest = style_interest; } // Decode path data let seg_type = tag.tag_byte & PATH_TAG_SEG_TYPE; @@ -965,7 +1097,7 @@ fn main( if is_stroke { let linewidth = bitcast(scene[config.style_base + style_ix + 1u]); - let offset = 0.5 * linewidth; + let half_width = 0.5 * linewidth; let miter_limit = bitcast(scene[config.style_base + style_ix + 3u]); let arc_detail_scale = bitcast(scene[config.style_base + style_ix + 4u]); @@ -973,48 +1105,52 @@ fn main( let is_stroke_cap_marker = (tag.tag_byte & PATH_TAG_SUBPATH_END) != 0u; if is_stroke_cap_marker { if is_open { - // Draw start cap - let tangent = pts.p3 - pts.p0; - let offset_tangent = offset * normalize(tangent); - let n = offset_tangent.yx * vec2f(-1., 1.); + // Start cap. The marker carries the first segment's tangent; the CPU stroker's + // caps chain from cap0 to cap1 so they splice into the offset edges. + var tangent = pts.p3 - pts.p0; + if dot(tangent, tangent) < TANGENT_THRESH * TANGENT_THRESH { + tangent = vec2(TANGENT_THRESH, 0.); + } + let offset_tangent = half_width * normalize(tangent); + let n = vec2(offset_tangent.y, -offset_tangent.x); draw_cap(path_ix, (style_flags & STYLE_FLAGS_START_CAP_MASK) >> 2u, arc_detail_scale, pts.p0, pts.p0 - n, pts.p0 + n, -offset_tangent, transform); } else { // Don't draw anything if the path is closed. } } else { - // Read the neighboring segment. - let neighbor = read_neighboring_segment(ix + 1u); - var tan_start = cubic_start_tangent(pts.p0, pts.p1, pts.p2, pts.p3); - if dot(tan_start, tan_start) < TANGENT_THRESH * TANGENT_THRESH { - tan_start = vec2(TANGENT_THRESH, 0.); - } - var tan_prev = cubic_end_tangent(pts.p0, pts.p1, pts.p2, pts.p3); - if dot(tan_prev, tan_prev) < TANGENT_THRESH * TANGENT_THRESH { - tan_prev = vec2(TANGENT_THRESH, 0.); - } - var tan_next = neighbor.tangent; - if dot(tan_next, tan_next) < TANGENT_THRESH * TANGENT_THRESH { - tan_next = vec2(TANGENT_THRESH, 0.); - } - let len_prev = length(pts.p3 - pts.p0); - let len_next = neighbor.length; - let n_start = offset * normalize(vec2(-tan_start.y, tan_start.x)); - let offset_tangent = offset * normalize(tan_prev); - let n_prev = offset_tangent.yx * vec2f(-1., 1.); - let n_next = offset * normalize(tan_next).yx * vec2f(-1., 1.); - - // Render offset curves - flatten_euler(pts, path_ix, transform, offset, pts.p0 + n_start, pts.p3 + n_prev); - flatten_euler(pts, path_ix, transform, -offset, pts.p0 - n_start, pts.p3 - n_prev); - - if neighbor.do_join { - draw_join(path_ix, style_flags, miter_limit, arc_detail_scale, - pts.p3, tan_prev, tan_next, len_prev, len_next, offset, n_prev, n_next, transform); - } else { - // Draw end cap. - draw_cap(path_ix, (style_flags & STYLE_FLAGS_END_CAP_MASK), arc_detail_scale, - pts.p3, pts.p3 + n_prev, pts.p3 - n_prev, offset_tangent, transform); + // CPU stroker port: stroke centerline segments are always straight lines, so the + // offset edges are straight offset lines; joins and caps carry all the curvature. + // The encoder collapses micro-segments (1/64 px) exactly like the CPU stroker. + let p0 = pts.p0; + let p1 = pts.p3; + let seg = p1 - p0; + let len1 = length(seg); + if len1 >= TANGENT_THRESH { + let tangent = seg / len1; + let n_prev = half_width * vec2(tangent.y, -tangent.x); + + // Body offset edges; the reverse-side edge is emitted backwards so the outline + // keeps a consistent ring winding. + output_two_lines_with_transform(path_ix, p0 + n_prev, p1 + n_prev, p1 - n_prev, p0 - n_prev, transform); + + let neighbor = read_neighboring_segment(ix + 1u); + if neighbor.do_join { + var tan_next = neighbor.tangent; + if dot(tan_next, tan_next) < TANGENT_THRESH * TANGENT_THRESH { + tan_next = vec2(TANGENT_THRESH, 0.); + } + let next_dir = normalize(tan_next); + let len2 = neighbor.length; + let v2 = p1 + (next_dir * max(len2, TANGENT_THRESH)); + let n_next = half_width * vec2(next_dir.y, -next_dir.x); + draw_join(path_ix, style_flags, miter_limit, arc_detail_scale, half_width, + p0, p1, v2, len1, len2, n_prev, n_next, transform); + } else { + // End cap. + draw_cap(path_ix, (style_flags & STYLE_FLAGS_END_CAP_MASK), arc_detail_scale, + p1, p1 + n_prev, p1 - n_prev, tangent * half_width, transform); + } } } } else { diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl index 1e3d0caba..c7ef13caf 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count.wgsl @@ -1,7 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Stage to compute counts of number of segments in each tile +// Counts, per tile, how many line segments from the flattened line soup +// cross it. One thread per line: walks the line's tile boundary crossings, +// atomically increments the crossed tile's segment count, accumulates +// top-edge winding deltas into tile backdrops, and emits one SegmentCount +// record per crossing so path_tiling can later write the clipped segments. +// +// Inputs: config uniform, bump (lines counter, seg_counts allocator), +// lines (LineSoup from flatten), paths (Path records from path_row_alloc), +// rows (PathRow spans finalized by tile_alloc). +// Outputs: tile (backdrop and segment_count_or_ix, atomically updated), +// seg_counts (one record per line/tile crossing), bump.seg_counts. +// +// Ported from Vello's path_count.wgsl, modified for the local sparse +// tile-row model: tiles are addressed through per-row PathRow records +// (row.tiles + x - row.x0) instead of a dense per-path bbox grid, and +// crossings outside a row's allocated column span are skipped. #import bump #import config @@ -34,13 +49,27 @@ var tile: array; @group(0) @binding(6) var seg_counts: array; +// Returns the number of tile grid cells spanned by the interval [a, b] +// (in tile units), with a minimum of 1. fn span(a: f32, b: f32) -> u32 { return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); } +// Largest f32 strictly less than 1.0; clamping b below 1 keeps the first +// floor(a * i + b) evaluation from overshooting a boundary. const ONE_MINUS_ULP: f32 = 0.99999994; +// Slope nudge applied when accumulated rounding in floor(a * i + b) +// disagrees with the exact crossing count for the full line. const ROBUST_EPSILON: f32 = 2e-7; +// Walks one line across the tile grid and records its crossings. +// +// The line is first normalized to point downward (is_down), then +// parameterized by crossing index i in [imin, imax): each step crosses +// exactly one tile boundary, and z = floor(a * i + b) is the number of +// x-boundary crossings after i steps, giving the tile coordinate +// (x0 + x_sign * z, y0 + i - z). imin/imax clip the traversal to the +// path's tile bbox; delta is the winding direction (+1 up, -1 down). @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -63,9 +92,13 @@ fn main( let dx = abs(s1.x - s0.x); let dy = s1.y - s0.y; + // Zero-length segment, drop it. This could be culled in the flattening + // stage, but letting one slip through here would be much worse. if dx + dy == 0.0 { return; } + // A horizontal line lying exactly on a tile boundary crosses no + // horizontal scanline, so it contributes no winding; drop it. if dy == 0.0 && floor(s0.y) == s0.y { return; } @@ -155,13 +188,13 @@ fn main( let top_edge = select(last_z == z, y0 == s0.y, i == 0u); // Top-edge backdrop propagation must fire even when the crossing - // column itself lies outside the sparse row's allocated column span — - // for example, a line entering from the left of bbox (x < bbox.x). + // column itself lies outside the sparse row's allocated column span, + // for example a line entering from the left of bbox (x < bbox.x). // path_row_span already clamped x_bump to bbox.x and expanded row.x0 // accordingly, so clamp the bump target to row.x0 here to route the // winding into the row's leftmost allocated tile. Skipping this (as // the old in-range-only path did) left fills whose left edges crossed - // the bbox boundary with zero-backdrop interior tiles — rendering + // the bbox boundary with zero-backdrop interior tiles, rendering // horizontal row-aligned gaps. if top_edge && row.x0 < row.x1 && x + 1 < i32(row.x1) { let x_bump = max(x + 1, i32(row.x0)); @@ -169,6 +202,8 @@ fn main( atomicAdd(&tile[bump_ix].backdrop, delta); } + // No tile is allocated for columns outside the row's sparse span; + // the crossing still advanced last_z for top-edge detection above. if u32(x) < row.x0 || u32(x) >= row.x1 { last_z = z; continue; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl index 3e9a8aebc..0a51863f5 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_count_setup.wgsl @@ -1,7 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Set up dispatch size for path count stage. +// Computes the indirect dispatch size for the per-line stages. Divides the +// flattened line count (bump.lines) by the workgroup size, or dispatches +// zero workgroups when an earlier stage recorded an allocation failure. +// +// Inputs: bump (lines counter, failed mask). +// Outputs: indirect (workgroup counts). The same indirect buffer drives +// both the path_row_span and path_count dispatches, which each run one +// thread per line with the same WG_SIZE. +// +// Ported from Vello's path_count_setup.wgsl; locally the result is reused +// for the sparse row span stage. #import bump @@ -14,6 +24,9 @@ var indirect: IndirectCount; // Partition size for path count stage const WG_SIZE = 256u; +// Single-thread stage: writes ceil(lines / WG_SIZE) workgroups on x, or 0 +// to cancel the per-line stages after an upstream failure; y and z are +// always 1. @compute @workgroup_size(1) fn main() { if atomicLoad(&bump.failed) != 0u { diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl index 4e9147839..6a3b8266a 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_alloc.wgsl @@ -1,7 +1,21 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Allocate sparse per-path row metadata after draw reduction has produced final path bounds. +// Allocates sparse per-path tile-row metadata after draw reduction has +// produced final draw object bounds. One thread per draw object: converts +// the pixel-space draw bbox to a tile-space bbox clamped to the current +// chunk's tile-row window, bump-allocates one PathRow record per covered +// row, writes the Path record (tile bbox plus row base offset) and resets +// each row to an empty span (x0 = u32 max, x1 = 0, no backdrop, no flags). +// +// Inputs: config uniform (chunk window, buffer limits), scene (draw tags), +// draw_bboxes (from draw_leaf). +// Outputs: paths (Path records), rows (initialized AtomicPathRow records), +// bump.path_rows; sets the STAGE_TILE_ALLOC failure bit on overflow. +// +// Local addition for the sparse tile-row model; no Vello counterpart +// (it takes over the per-path setup half of Vello's tile_alloc, which +// allocates a dense tile grid instead). #import config #import bump @@ -26,6 +40,9 @@ var paths: array; @group(0) @binding(5) var rows: array; +// Allocates and initializes the row records for one draw object. NOP and +// end-clip objects, and objects with an empty bbox, keep an all-zero tile +// bbox and therefore allocate no rows. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -63,6 +80,9 @@ fn main( let row_base = atomicAdd(&bump.path_rows, row_count); let row_limit_exceeded = row_base + row_count > config.path_rows_size; + // On overflow still write a Path record (with row base 0) so the + // buffer holds no uninitialized data; the failure bit makes the later + // setup stages dispatch zero work, and the CPU resizes and retries. if row_limit_exceeded { atomicOr(&bump.failed, STAGE_TILE_ALLOC); paths[drawobj_ix] = Path(bbox, 0u); @@ -71,6 +91,9 @@ fn main( paths[drawobj_ix] = Path(bbox, row_base); + // Empty-span sentinel: x0 at u32 max and x1 at 0 so the atomicMin/Max + // updates in path_row_span establish the true span; a row with + // x0 >= x1 after that stage covers no tiles. for (var i = 0u; i < row_count; i += 1u) { let row_ix = row_base + i; atomicStore(&rows[row_ix].x0, 0xffffffffu); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl index 1ff77b4fd..9e427f8e9 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_row_span.wgsl @@ -1,7 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Derive sparse per-row tile spans from the flattened line stream. +// Derives sparse per-row tile spans from the flattened line stream. One +// thread per line: replays the same tile-crossing traversal as path_count +// and, for every tile row the line touches, atomically grows that row's +// column span (rows[].x0/x1), accumulates a per-row winding backdrop for +// crossings left of the path bbox, and sets PATH_ROW_FLAG_TOUCHES_RIGHT on +// rows where activity extends past the right bbox edge so tile_alloc can +// widen the span to cover the fill interior. +// +// Inputs: config uniform, bump (lines counter), lines (LineSoup from +// flatten), paths (Path records from path_row_alloc). +// Outputs: rows (AtomicPathRow spans, backdrops and flags), consumed by +// tile_alloc. +// +// Local addition for the sparse tile-row model; no Vello counterpart +// (Vello allocates the full dense bbox grid in tile_alloc instead). The +// traversal math is shared with the Vello-derived path_count.wgsl. #import bump #import config @@ -23,13 +38,28 @@ var paths: array; @group(0) @binding(4) var rows: array; +// Returns the number of tile grid cells spanned by the interval [a, b] +// (in tile units), with a minimum of 1. fn span(a: f32, b: f32) -> u32 { return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); } +// Largest f32 strictly less than 1.0; clamping b below 1 keeps the first +// floor(a * i + b) evaluation from overshooting a boundary. const ONE_MINUS_ULP: f32 = 0.99999994; +// Slope nudge applied when accumulated rounding in floor(a * i + b) +// disagrees with the exact crossing count for the full line. const ROBUST_EPSILON: f32 = 2e-7; +// Expands row spans, row backdrops and right-touch flags for one line. +// +// Uses the same downward-normalized crossing parameterization as +// path_count: crossing index i maps to tile (x0 + x_sign * z, y0 + i - z) +// with z = floor(a * i + b). Crossings clipped away on the left of the +// bbox are folded into rows[].backdrop (per-row winding carried into the +// leftmost tile); crossings clipped on the right only set the +// TOUCHES_RIGHT flag, since winding to the right of the span never +// affects covered tiles. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -50,9 +80,12 @@ fn main( let dx = abs(s1.x - s0.x); let dy = s1.y - s0.y; + // Zero-length segment, drop it (same culling rule as path_count). if dx + dy == 0.0 { return; } + // A horizontal line lying exactly on a tile boundary crosses no + // horizontal scanline, so it contributes no winding; drop it. if dy == 0.0 && floor(s0.y) == s0.y { return; } @@ -79,6 +112,9 @@ fn main( return; } + // The whole line lies right of the bbox: it cannot affect any covered + // tile's winding, but the rows it crosses must still be flagged so + // tile_alloc extends their spans over the fill interior. if xmin > f32(bbox.z) { let ymin_right = max(i32(ceil(s0.y)), bbox.y); let ymax_right = min(i32(ceil(s1.y)), bbox.w); @@ -109,8 +145,13 @@ fn main( } let delta = select(1, -1, is_down); + // [ymin, ymax) collects the rows whose crossings were clipped away on + // the left of the bbox; their winding is folded into row backdrops + // after this block. var ymin = 0; var ymax = 0; + // The whole line lies left of the bbox: every row it crosses receives + // its winding as backdrop and no tile crossings remain. if max(s0.x, s1.x) <= f32(bbox.x) { ymin = i32(ceil(s0.y)); ymax = i32(ceil(s1.y)); @@ -147,7 +188,7 @@ fn main( // will expand row.x1 to cover the sparse fill interior. Without // this, a row whose only line activity is off the right side of // the bbox leaves row.x1 at its initial (0) or too-narrow value, - // and backdrop propagation stops short — producing a horizontal + // and backdrop propagation stops short, producing a horizontal // gap in the fill at exactly that tile row. This mirrors the // row.backdrop accumulation performed in the xmin < bbox.x // branch above. @@ -198,6 +239,9 @@ fn main( atomicMin(&rows[row_ix].x0, min_x); atomicMax(&rows[row_ix].x1, min_x + 1u); + // A top-edge crossing makes path_count bump the backdrop of the + // tile at x + 1 (clamped to row.x0), so that tile must be inside + // the allocated span even if no segment ever lands in it. let top_edge = select(last_z == z, y0 == s0.y, i == 0u); if top_edge && x + 1 < bbox.z { let x_bump = u32(max(x + 1, bbox.x)); diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl index 3c0ec5f4e..65d3ba785 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling.wgsl @@ -1,7 +1,21 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Write path segments +// Writes the final per-tile path segments. One thread per SegmentCount +// record from path_count: replays the line's tile-crossing traversal to +// find the record's tile, clips the line to that tile, applies numerical +// robustness nudges, and stores the tile-relative Segment in the slot +// range reserved by the coarse stage. +// +// Inputs: bump (seg_counts total), seg_counts (from path_count), lines +// (LineSoup), paths and rows (sparse row records), tiles (coarse output; +// segment_count_or_ix holds the inverted segment base index). +// Outputs: segments (tile-relative endpoints plus y_edge, consumed by +// fine). +// +// Ported from Vello's path_tiling.wgsl, modified for the local sparse +// tile-row model: the tile index is resolved through the path's PathRow +// record (row.tiles + x - row.x0) instead of a dense bbox grid. #import bump #import config @@ -29,13 +43,24 @@ var tiles: array; @group(0) @binding(6) var segments: array; +// Returns the number of tile grid cells spanned by the interval [a, b] +// (in tile units), with a minimum of 1. fn span(a: f32, b: f32) -> u32 { return u32(max(ceil(max(a, b)) - floor(min(a, b)), 1.0)); } +// Largest f32 strictly less than 1.0; clamping b below 1 keeps the first +// floor(a * i + b) evaluation from overshooting a boundary. const ONE_MINUS_ULP: f32 = 0.99999994; +// Slope nudge applied when accumulated rounding in floor(a * i + b) +// disagrees with the exact crossing count for the full line. Must match +// path_count exactly so both stages assign a crossing to the same tile. const ROBUST_EPSILON: f32 = 2e-7; +// Writes one clipped segment. seg_within_line selects which of the line's +// tile crossings this record represents (recomputed with the same +// parameterization as path_count); seg_within_slice is the segment's slot +// within the tile's reserved range. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, @@ -82,6 +107,9 @@ fn main( let row = rows[path.rows + u32(y) - path.bbox.y]; let tile_ix = row.tiles + u32(x) - row.x0; let tile = tiles[tile_ix]; + // Coarse stores the inverted segment base index; a non-inverted value + // (negative after ~) means the tile was never allocated, so the + // segment has nowhere to go. let seg_start = ~tile.segment_count_or_ix; if i32(seg_start) < 0 { return; @@ -90,26 +118,34 @@ fn main( let tile_xy = vec2(f32(x) * f32(TILE_WIDTH), f32(y) * f32(TILE_HEIGHT)); let tile_xy1 = tile_xy + vec2(f32(TILE_WIDTH), f32(TILE_HEIGHT)); + // Clip the segment's start to the tile edge it entered through, unless + // this is the line's first crossing (the true endpoint is inside). if seg_within_line > 0u { let z_prev = floor(a * (f32(seg_within_line) - 1.0) + b); if z == z_prev { + // Top edge is clipped. var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy.y - xy0.y) / (xy1.y - xy0.y); xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); xy0 = vec2(xt, tile_xy.y); } else { + // If is_positive_slope, the left edge is clipped, else the right. let x_clip = select(tile_xy1.x, tile_xy.x, is_positive_slope); var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); xy0 = vec2(x_clip, yt); } } + // Likewise clip the segment's end to the tile edge it exits through, + // unless this is the line's last crossing. if seg_within_line < count - 1u { let z_next = floor(a * (f32(seg_within_line) + 1.0) + b); if z == z_next { + // Bottom edge is clipped. var xt = xy0.x + (xy1.x - xy0.x) * (tile_xy1.y - xy0.y) / (xy1.y - xy0.y); xt = clamp(xt, tile_xy.x + 1e-3, tile_xy1.x); xy1 = vec2(xt, tile_xy1.y); } else { + // If is_positive_slope, the right edge is clipped, else the left. let x_clip = select(tile_xy.x, tile_xy1.x, is_positive_slope); var yt = xy0.y + (xy1.y - xy0.y) * (x_clip - xy0.x) / (xy1.x - xy0.x); yt = clamp(yt, tile_xy.y + 1e-3, tile_xy1.y); @@ -117,6 +153,10 @@ fn main( } } + // Numerical robustness for fine: y_edge records where the segment + // meets the tile's left edge (1e9 means it does not), and endpoints + // exactly on that edge are nudged inward by EPSILON so fine's winding + // computation never sees an ambiguous x == 0 coordinate. var y_edge = 1e9; var p0 = xy0 - tile_xy; var p1 = xy1 - tile_xy; @@ -125,9 +165,11 @@ fn main( if p1.x == 0.0 { p0.x = EPSILON; if p0.y == 0.0 { + // Vertical line covering the entire left edge of the tile. p1.x = EPSILON; p1.y = f32(TILE_HEIGHT); } else { + // Make the segment disappear (zero vertical extent). p1.x = 2.0 * EPSILON; p1.y = p0.y; } @@ -143,12 +185,17 @@ fn main( y_edge = p1.y; } } + // Make sure there are no vertical lines aligned to the pixel grid in + // the tile interior; doing this here is cheaper than handling the + // degenerate case in fine. if p0.x == floor(p0.x) && p0.x != 0.0 { p0.x -= EPSILON; } if p1.x == floor(p1.x) && p1.x != 0.0 { p1.x -= EPSILON; } + // The traversal used downward-normalized endpoints; swap back so the + // stored segment keeps the line's original winding direction. if !is_down { let tmp = p0; p0 = p1; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling_setup.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling_setup.wgsl index b88dd8e33..9f6a3b46d 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling_setup.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/path_tiling_setup.wgsl @@ -1,7 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Set up dispatch size for path tiling stage. +// Computes the indirect dispatch size for the path tiling stage from the +// number of SegmentCount records produced by path_count, and performs the +// late overflow check for that counter. +// +// Inputs: config uniform (segments_size), bump (seg_counts, failed mask). +// Outputs: indirect (workgroup counts for path_tiling). On a prior stage +// failure or seg_counts overflow the dispatch is zeroed, STAGE_COARSE is +// flagged for the overflow case, and ptcl[0] is set to ~0u as an abort +// marker for the fine stage. +// +// Ported from Vello's path_tiling_setup.wgsl. #import config #import bump @@ -21,15 +31,23 @@ var ptcl: array; // Partition size for path tiling stage const WG_SIZE = 256u; +// Single-thread stage: writes ceil(seg_counts / WG_SIZE) workgroups on x, +// or 0 plus the ptcl abort marker on failure; y and z are always 1. @compute @workgroup_size(1) fn main() { let segments = atomicLoad(&bump.seg_counts); + // Compared against segments_size (not seg_counts_size) deliberately: every counted + // crossing becomes one Segment written by the path_tiling stage this dispatch launches, + // so it is the segments buffer that must hold the full count. path_count clamps its own + // seg_counts writes against seg_counts_size at write time. let overflowed = segments > config.segments_size; if atomicLoad(&bump.failed) != 0u || overflowed { if overflowed { atomicOr(&bump.failed, STAGE_COARSE); } indirect.count_x = 0u; + // Signal the fine rasterizer that a failure happened; fine does + // not bind the bump buffer, so it cannot read the failed mask. ptcl[0] = ~0u; } else { indirect.count_x = (segments + (WG_SIZE - 1u)) / WG_SIZE; diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce.wgsl index 946f25333..095721f84 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce.wgsl @@ -1,6 +1,18 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// First stage of the multi-level parallel prefix scan over the packed path +// tag stream. Each workgroup reduces WG_SIZE consecutive tag words from the +// scene buffer into a single TagMonoid (running counts of transforms, path +// segments, segment data words, styles and paths) and writes one element +// per workgroup to the reduced buffer. The scan stages (pathtag_scan / +// pathtag_scan1) later turn these partials into exclusive prefixes. +// +// Inputs: config uniform, scene buffer (tag words at config.pathtag_base). +// Outputs: reduced (one TagMonoid per workgroup). +// +// Ported from Vello's pathtag_reduce.wgsl. + #import config #import pathtag @@ -18,6 +30,11 @@ const WG_SIZE = 256u; var sh_scratch: array; +// Reduces one WG_SIZE slice of the tag stream to a single TagMonoid. +// Each thread reduces its own tag word, then a log2(WG_SIZE) shared-memory +// tree folds in partials from threads to the right, so after the loop +// thread 0 holds the reduction of the whole slice and writes it to +// reduced[workgroup index]. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce2.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce2.wgsl index ccab3e6ec..ed2229fa1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce2.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_reduce2.wgsl @@ -1,8 +1,16 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// This shader is the second stage of reduction for the pathtag -// monoid scan, needed when the number of tags is large. +// Second-level reduction for the path tag monoid scan, dispatched only +// when the tag stream is too large for a single level of workgroup +// partials (the "large" scan variant). Reduces the first-level partials +// produced by pathtag_reduce by another factor of WG_SIZE. +// +// Inputs: reduced_in (first-level partials from pathtag_reduce). +// Outputs: reduced (one TagMonoid per WG_SIZE input partials), consumed +// by pathtag_scan1. +// +// Ported from Vello's pathtag_reduce2.wgsl. #import config #import pathtag @@ -18,6 +26,9 @@ const WG_SIZE = 256u; var sh_scratch: array; +// Reduces WG_SIZE first-level partials to a single TagMonoid using the +// same rightward shared-memory tree as pathtag_reduce; thread 0 writes +// the workgroup total to reduced[workgroup index]. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl index 321b63b27..f71780bd2 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan.wgsl @@ -1,6 +1,22 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +// Final stage of the path tag monoid scan: writes, for every 4-byte tag +// word, the exclusive prefix TagMonoid (counts of transforms, segments, +// segment data words, styles and paths preceding that word). Flatten uses +// these prefixes to locate each path element's data in the scene buffer. +// +// Two compile-time variants exist, selected by the "small" define: +// - small: reduced holds per-workgroup totals from pathtag_reduce; this +// shader scans them in shared memory to get the carry-in per workgroup. +// - large: reduced holds per-workgroup exclusive prefixes precomputed by +// pathtag_scan1, read directly. +// +// Inputs: config uniform, scene tag words, reduced (see variants above). +// Outputs: tag_monoids (exclusive prefix per tag word). +// +// Ported from Vello's pathtag_scan.wgsl. + #import config #import pathtag @@ -22,9 +38,16 @@ const WG_SIZE = 256u; #ifdef small var sh_parent: array; #endif -// These could be combined? +// Note: sh_parent and sh_monoid could potentially share storage. var sh_monoid: array; +// Computes the exclusive prefix monoid for each tag word in this +// workgroup's slice. In the small variant, first suffix-reduces the +// per-workgroup totals of all preceding workgroups into sh_parent[0] to +// form the carry-in; the large variant reads the carry-in directly from +// reduced[wg_id.x]. Then performs an inclusive Hillis-Steele scan of this +// slice's tag monoids in shared memory and combines carry-in with the +// preceding thread's inclusive value to produce the exclusive prefix. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan1.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan1.wgsl index 9ff06604e..aed2c0579 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan1.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/pathtag_scan1.wgsl @@ -1,8 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// This shader computes the scan of reduced tag monoids given -// two levels of reduction. +// Middle stage of the large path tag scan: computes the exclusive prefix +// of the first-level partials given two levels of reduction. Only +// dispatched when the tag stream exceeds the single-level scan capacity. +// +// Inputs: reduced (first-level partials from pathtag_reduce), reduced2 +// (second-level partials from pathtag_reduce2). +// Outputs: tag_monoids, here holding one exclusive prefix per first-level +// partial (granularity of 4 tag bytes * workgroup size); consumed as the +// per-workgroup carry-in by the large variant of pathtag_scan. +// +// Ported from Vello's pathtag_scan1.wgsl. #import config #import pathtag @@ -20,9 +29,14 @@ const LG_WG_SIZE = 8u; const WG_SIZE = 256u; var sh_parent: array; -// These could be combined? +// Note: sh_parent and sh_monoid could potentially share storage. var sh_monoid: array; +// Computes the exclusive prefix for each first-level partial. First +// suffix-reduces the second-level totals of all preceding workgroups into +// sh_parent[0] to form the carry-in, then performs an inclusive +// Hillis-Steele scan of this workgroup's first-level partials and combines +// carry-in with the preceding thread's inclusive value. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/prepare.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/prepare.wgsl index 769818ea9..931248172 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/prepare.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/prepare.wgsl @@ -1,15 +1,21 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -#import config +// First stage of a scheduling run: clears every bump allocator counter and +// the failure mask on the GPU so the pipeline starts from a clean slate +// without a CPU buffer write. +// +// Inputs/outputs: bump (all counters and the failed mask zeroed). +// +// Local addition; no Vello shader of this name exists (Vello clears the +// bump buffer with a recorded buffer clear instead). + #import bump @group(0) @binding(0) -var config: Config; - -@group(0) @binding(1) var bump: BumpAllocators; +// Single-thread stage that zeroes all bump state. @compute @workgroup_size(1) fn main() { // Never cancel. Let all stages run so the bump allocators report the true diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/present.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/present.wgsl new file mode 100644 index 000000000..b12465fa9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/present.wgsl @@ -0,0 +1,18 @@ +@group(0) @binding(0) var source_texture: texture_2d; + +@vertex +fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4 { + // One oversized triangle covers the viewport without a vertex buffer or a shared-edge seam. + let positions = array, 3>( + vec2(-1.0, -1.0), + vec2(3.0, -1.0), + vec2(-1.0, 3.0)); + + return vec4(positions[vertex_index], 0.0, 1.0); +} + +@fragment +fn fs_main(@builtin(position) position: vec4) -> @location(0) vec4 { + // Fragment positions address the same top-left-origin texels in the equally sized source. + return textureLoad(source_texture, vec2(position.xy), 0); +} diff --git a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/tile_alloc.wgsl b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/tile_alloc.wgsl index dbab50eba..ff8dc2fa1 100644 --- a/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/tile_alloc.wgsl +++ b/src/ImageSharp.Drawing.WebGPU/Shaders/WgslSource/tile_alloc.wgsl @@ -1,7 +1,24 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -// Tile allocation (and zeroing of sparse per-row tiles) +// The tile allocation stage. +// +// Finalizes the horizontal extent of every sparse path row and bump-allocates +// the backing Tile storage. A row is widened to the path bbox left edge when +// it carries a nonzero backdrop seed (the fill continues left of the leftmost +// recorded segment) and to the bbox right edge when a segment touched the +// right boundary (PATH_ROW_FLAG_TOUCHES_RIGHT). Each row's tiles field is +// then rewritten from its flag value to the base index of the row's +// contiguous tile run, and all allocated tiles are zeroed. +// +// Inputs: paths (tile-space bbox and row base, written by path_row_alloc), +// rows (extents, backdrop seeds, and flags accumulated by path_row_span). +// Outputs: rows (final x0/x1 and base tile index), tiles (zeroed), +// bump.tile. +// +// Derived from Vello's tile_alloc.wgsl. Local divergence: upstream allocates +// a dense width-by-height tile grid per path from the draw-object bboxes; +// this variant allocates only the tiles inside each sparse row span. #import config #import bump @@ -22,10 +39,15 @@ var rows: array; @group(0) @binding(4) var tiles: array; +// One thread per draw object. Widens each of the object's rows, reserves the +// path's total tile count from bump.tile in a single allocation, assigns +// per-row base indices, and clears the newly allocated tiles. @compute @workgroup_size(256) fn main( @builtin(global_invocation_id) global_id: vec3, ) { + // Row allocation overflowed; path_row_alloc already raised + // STAGE_TILE_ALLOC, so bail without touching the (invalid) row records. if atomicLoad(&bump.path_rows) > config.path_rows_size { return; } @@ -46,6 +68,8 @@ fn main( var x0 = stored_x0; var x1 = stored_x1; let backdrop = atomicLoad(&rows[row_ix].backdrop); + // The tiles field is overloaded: it holds row flags until the second + // loop below replaces it with the row's base tile index. let row_flags = atomicLoad(&rows[row_ix].tiles); if backdrop != 0 { x0 = min(x0, path.bbox.x); diff --git a/src/ImageSharp.Drawing.WebGPU/SilkNativeSurfaceAdapter.cs b/src/ImageSharp.Drawing.WebGPU/SilkNativeSurfaceAdapter.cs deleted file mode 100644 index a28a8a059..000000000 --- a/src/ImageSharp.Drawing.WebGPU/SilkNativeSurfaceAdapter.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using Silk.NET.Core.Contexts; - -namespace SixLabors.ImageSharp.Drawing.Processing.Backends; - -/// -/// Converts a descriptor into the native surface source used by the WebGPU surface factory. -/// -internal sealed class SilkNativeSurfaceAdapter : INativeWindowSource, INativeWindow -{ - private readonly WebGPUSurfaceHost host; - - public SilkNativeSurfaceAdapter(WebGPUSurfaceHost host) => this.host = host; - - public INativeWindow? Native => this; - - public NativeWindowFlags Kind => this.host.Kind switch - { - WebGPUSurfaceHostKind.Glfw => NativeWindowFlags.Glfw, - WebGPUSurfaceHostKind.Sdl => NativeWindowFlags.Sdl, - WebGPUSurfaceHostKind.Win32 => NativeWindowFlags.Win32, - WebGPUSurfaceHostKind.X11 => NativeWindowFlags.X11, - WebGPUSurfaceHostKind.Cocoa => NativeWindowFlags.Cocoa, - WebGPUSurfaceHostKind.UIKit => NativeWindowFlags.UIKit, - WebGPUSurfaceHostKind.Wayland => NativeWindowFlags.Wayland, - WebGPUSurfaceHostKind.WinRT => NativeWindowFlags.WinRT, - WebGPUSurfaceHostKind.Android => NativeWindowFlags.Android, - WebGPUSurfaceHostKind.Vivante => NativeWindowFlags.Vivante, - WebGPUSurfaceHostKind.EGL => NativeWindowFlags.EGL, - _ => 0, - }; - - public nint? Glfw - => this.host.Kind == WebGPUSurfaceHostKind.Glfw ? this.host.Handle0 : null; - - public nint? Sdl - => this.host.Kind == WebGPUSurfaceHostKind.Sdl ? this.host.Handle0 : null; - - public (nint Hwnd, nint HDC, nint HInstance)? Win32 - => this.host.Kind == WebGPUSurfaceHostKind.Win32 - ? (this.host.Handle0, this.host.Handle1, this.host.Handle2) - : null; - - public (nint Display, nuint Window)? X11 - => this.host.Kind == WebGPUSurfaceHostKind.X11 ? (this.host.Handle0, this.host.Number0) : null; - - public nint? Cocoa - => this.host.Kind == WebGPUSurfaceHostKind.Cocoa ? this.host.Handle0 : null; - - public (nint Window, uint Framebuffer, uint Colorbuffer, uint ResolveFramebuffer)? UIKit - => this.host.Kind == WebGPUSurfaceHostKind.UIKit - ? (this.host.Handle0, this.host.Number1, this.host.Number2, this.host.Number3) - : null; - - public (nint Display, nint Surface)? Wayland - => this.host.Kind == WebGPUSurfaceHostKind.Wayland ? (this.host.Handle0, this.host.Handle1) : null; - - public nint? WinRT - => this.host.Kind == WebGPUSurfaceHostKind.WinRT ? this.host.Handle0 : null; - - public (nint Window, nint Surface)? Android - => this.host.Kind == WebGPUSurfaceHostKind.Android ? (this.host.Handle0, this.host.Handle1) : null; - - public (nint Display, nint Window)? Vivante - => this.host.Kind == WebGPUSurfaceHostKind.Vivante ? (this.host.Handle0, this.host.Handle1) : null; - - public (nint? Display, nint? Surface)? EGL - => this.host.Kind == WebGPUSurfaceHostKind.EGL - ? (this.host.Handle0 == 0 ? null : this.host.Handle0, this.host.Handle1 == 0 ? null : this.host.Handle1) - : null; - - public nint? DXHandle - => this.host.Kind == WebGPUSurfaceHostKind.Win32 ? this.host.Handle0 : null; -} diff --git a/src/ImageSharp.Drawing.WebGPU/TextureUsage.cs b/src/ImageSharp.Drawing.WebGPU/TextureUsage.cs new file mode 100644 index 000000000..8e0574246 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/TextureUsage.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes how a WebGPU texture may be used. +/// +[Flags] +internal enum TextureUsage : ulong +{ + /// + /// The texture has no permitted usage. + /// + None = 0, + + /// + /// The texture can be the source of a copy operation. + /// + CopySrc = 1, + + /// + /// The texture can be the destination of a copy operation. + /// + CopyDst = 2, + + /// + /// The texture can be sampled by a shader. + /// + TextureBinding = 4, + + /// + /// The texture can be bound for shader storage access. + /// + StorageBinding = 8, + + /// + /// The texture can be used as a render-pass attachment. + /// + RenderAttachment = 16, + + /// + /// The texture can be used as a transient attachment. + /// + TransientAttachment = 32 +} diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND.md index bfc53f759..4260b5113 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND.md @@ -18,13 +18,15 @@ This document explains the backend as a newcomer would need to understand it: The public WebGPU surface area around this backend is small and target-first. -**Public types** — the entry points applications should use: +**Public types**, the entry points applications should use: - `WebGPUEnvironment` exposes explicit support probes for the library-managed WebGPU environment - `WebGPUWindow` owns a native window and either runs a render loop or returns `WebGPUSurfaceFrame` instances through `TryAcquireFrame(...)` - `WebGPUExternalSurface` attaches to a caller-owned native host via `WebGPUSurfaceHost`; the host application owns the UI object and tells the external surface when the drawable framebuffer resizes - `WebGPURenderTarget` owns an offscreen native target for GPU rendering and readback +`WebGPURenderTarget.CreateRenderTarget(...)` and `WebGPUSurfaceFrame.CreateRenderTarget(...)` create empty offscreen render targets with the same texture format as the source target or frame. They do not copy pixels from the source. + `WebGPUDeviceContext` is internal infrastructure used by targets and surfaces. It is not part of the public WebGPU entry-point model. `WebGPUEnvironment.Options` configures the library-managed WebGPU environment before first use. Set it before @@ -35,7 +37,7 @@ Those types all exist to get a `DrawingCanvas` over a native WebGPU target. Once The support probes also live outside the backend: -- `WebGPUEnvironment.ProbeAvailability()` checks whether the library-managed WebGPU device and queue can be acquired +- `WebGPUEnvironment.ProbeAvailability()` checks whether the required WGPU extension is available and the library-managed WebGPU device and queue can be acquired; it reports `WgpuExtensionUnavailable` separately from core API initialization failure - `WebGPUEnvironment.ProbeComputePipelineSupport()` runs the crash-isolated trivial compute-pipeline probe That split keeps support probing separate from flush execution. `WebGPUDrawingBackend` is the flush executor, not the public support API. The WebGPU constructors create their objects directly; callers use `WebGPUEnvironment` when they want explicit preflight checks. @@ -73,7 +75,7 @@ That means `WebGPUDrawingBackend` is responsible for entry-point orchestration, - per-flush diagnostic state - staged-scene creation attempts during render -- lowering explicit layer boundaries into the staged scene path +- keeping explicit layer boundaries in the shared command stream until scene encoding It is the policy boundary of the GPU path. @@ -133,11 +135,13 @@ The staged scene pipeline itself is described in [`WEBGPU_RASTERIZER.md`](d:/Git Its responsibilities are: - clear per-render diagnostics -- encode retained WebGPU scenes from prepared command batches +- encode retained WebGPU scenes from prepared command batches, choosing between the parallel encoder and the ordered encoder - create render-scoped staged scene resources -- run the staged path +- run the staged path inside a bounded scratch-growth retry loop +- execute the transactional ordered plan (render ranges, Apply groups, and explicit layer transitions) when the scene retains one - keep explicit layer boundaries in the shared flush model until the staged scene encoder lowers them -- retain the last successful scratch capacities and reuse backend-local GPU arenas across renders when possible +- retain the last successful scratch capacities, chunk-height hints, and backend-local GPU arenas across renders when possible +- serve strictly typed GPU readback through `ReadRegion(...)` The expensive staged work is delegated: @@ -145,6 +149,7 @@ The expensive staged work is delegated: - `WebGPUSceneConfig` owns planning data - `WebGPUSceneResources` owns flush-scoped GPU resources - `WebGPUSceneDispatch` owns the staged compute pipeline +- `GpuSceneDrawTag` and related GPU scene structs describe packed scene words shared with WGSL The public object graph around those responsibilities is also separate: @@ -159,16 +164,16 @@ The public object graph around those responsibilities is also separate: `CreateScene(...)`: - receives target bounds and a prepared command batch -- encodes the retained `WebGPUEncodedScene` +- encodes the retained `WebGPUEncodedScene` through `WebGPUSceneEncoder.TryEncode(...)`, or through `WebGPUSceneEncoder.TryEncodeOrdered(...)` when the batch contains Apply - stores retained scratch-size state and reusable arena slots with the scene - does not need `TPixel`, the target frame, or a `WebGPUFlushContext` `RenderScene(...)`: -- validates the retained scene type and target bounds +- validates the retained scene type, the target texture format, and target bounds - resolves `TPixel` to the WebGPU texture format and required feature - creates the render-scoped `WebGPUFlushContext` -- creates staged resources and dispatches the GPU pipeline +- creates staged resources and dispatches the GPU pipeline, retrying with grown scratch capacities when the GPU reports overflow Keeping those lifetimes separate avoids duplicate target setup when the same retained scene is rendered repeatedly. @@ -182,6 +187,25 @@ That step answers the first important question: This is distinct from later GPU planning checks. Scene encoding uses target bounds and the allocator, but not the typed target frame. +Encoding is parallel by default. `TryEncode(...)` splits large batches into contiguous command-range partitions and encodes them concurrently: + +- clip scopes do not serialize the scene; a cheap sequential prescan records the clip scopes open at each partition boundary, and each partition replays those seed clips before its range and closes its own, so the concatenated partitions form one balanced clip stream +- layer scopes cannot be replayed that way because a layer composites its contents as one group when it closes, so partition boundaries snap forward to the next command index where no layer scope is open (layer depth zero) +- the partitions are concatenated in timeline order into one packed scene buffer + +Batches containing Apply take `TryEncodeOrdered(...)` instead: Apply reads pixels back mid-scene, so operations before and after it must stay in submission order and encoding stays sequential. + +## Ordered Scenes: Apply And Scoped Layers + +Most scenes encode into one packed draw stream that renders as a single staged dispatch. Scenes containing Apply, and layers that contain Apply, encode instead into a flat ordered `WebGPUSceneOperation` plan retained with the scene. There are four operation kinds: + +- `RenderRange` renders one encoded draw range into the current target +- `Apply` retains the processor and draw range; maximal consecutive Applies whose source reads cannot observe an earlier Apply write share one pixel-readback barrier +- `BeginLayer` enters a stable transient layer target cleared to transparent black +- `EndLayer` returns to the parent target and composites the layer through the retained range's external texture binding + +`RenderScene(...)` executes that plan in order inside one flush context. Range output stays in per-target ping-pong textures until allocator status has been validated at an Apply or final barrier. A barrier maps one flush-local status/pixel buffer once; allocator overflow grows all scratch capacities together and replays only retained GPU ranges from durable target checkpoints. Apply callbacks are never replayed. Plain scenes never allocate the operation plan and take the single staged-dispatch path. + ## Flush Context Creation When the retained scene is rendered, the backend creates a `WebGPUFlushContext`. @@ -199,7 +223,7 @@ The flush context is also the ownership boundary for flush-scoped GPU resources. Explicit layers are not a separate compose pass in this backend. -They stay in the shared composition command stream as `BeginLayer` and `EndLayer`, then the staged scene encoder lowers those boundaries into `BeginClip` and `EndClip` records inside the encoded scene. The fine shader interprets those records directly: it pushes the current tile color to its clip stack at `BeginClip`, renders the isolated layer contents, and then blends that isolated result back into the saved backdrop at `EndClip`. +They stay in the shared composition command stream as `BeginLayer` and `EndLayer`, then the staged scene encoder lowers those boundaries into `BeginClip` and `EndClip` records inside the encoded scene (the layer bounds become a rectangular clip path). The fine shader interprets those records directly: it pushes the current tile color to its clip stack at `BeginClip`, renders the isolated layer contents, and then blends that isolated result back into the saved backdrop at `EndClip` with the layer's stored blend mode and alpha. ```mermaid flowchart TD @@ -210,6 +234,23 @@ flowchart TD That means layer semantics are part of the main staged scene pipeline rather than a second GPU composition subsystem. +The one exception is a layer containing Apply. The ordered encoder retains explicit `BeginLayer` and `EndLayer` operations around the layer's child ranges. The transactional executor can then replay work across root and nested-layer targets without recursively rebuilding operation topology. + +## Scratch Growth And Retry + +The staged pipeline's scratch buffers (lines, binning, path rows, path tiles, segment counts, segments, blend spill, PTCL) are sized by GPU bump allocators, not by exact CPU precomputation. The backend therefore runs each staged render inside a bounded retry loop: + +- the first GPU stage zeroes the bump counters on the GPU and never cancels the pipeline, so every stage reports its true scratch demand in a single pass +- after submission the backend reads the counters back; if any counter exceeded its capacity the attempt's output is discarded and the render retries with grown sizes +- earlier overflows can hide later-stage demand, so the retry budget allows one failed pass per tracked allocator plus a small margin (`MaxDynamicGrowthAttempts`) +- the largest observed sizes are retained both on the scene and on the backend instance, so later flushes start at proven capacities + +## Typed Readback + +`ReadRegion(...)` copies a region of a native WebGPU target back into an ImageSharp pixel buffer. Readback is strictly typed to the surface format by design: the requested pixel type must match the target's native texture format exactly, otherwise the call throws. No pixel conversion happens on this path; callers that need a different layout read the native format and convert on the CPU. + +The canvas factory applies the same typing rule at creation time: a canvas over an `Rgba8Unorm` surface is typed `Rgba32`, and a canvas over a `Bgra8Unorm` surface is typed `Bgra32`. + ## Runtime And Caching `WebGPURuntime` and `WebGPURuntime.DeviceSharedState` hold the device-scoped resources that outlive a single flush. @@ -221,12 +262,13 @@ They cache things such as: - composite pipelines - a small amount of reusable device-scoped support state -`WebGPURuntime` also backs the explicit support probes surfaced by `WebGPUEnvironment`. The probe and runtime layer is where the library-managed device/queue availability and crash-isolated compute-pipeline test are cached. +`WebGPURuntime` also backs the explicit support probes surfaced by `WebGPUEnvironment`. The probe and runtime layer is where required WGPU-extension availability, library-managed device/queue availability, and the crash-isolated compute-pipeline test are cached. `WebGPUDrawingBackend` adds one more cache layer above that device-scoped runtime state. It retains: - the last successful staged-scene scratch capacities - reusable scheduling and resource arenas whose buffers can be leased by later flushes on the same backend instance +- an advisory chunk-height hint for repeated oversized scenes, so the chunked path starts near a proven window size (every hinted chunk is still validated before dispatch) The arena contents are still per-flush; only the underlying allocations are reused. @@ -243,8 +285,9 @@ The staged scene pipeline itself is described in [`WEBGPU_RASTERIZER.md`](d:/Git - resource creation - scheduling passes - fine rasterization +- the scratch overflow protocol - chunked oversized-scene execution -- copy and submission +- readback, copy, and submission ## Reading Guide @@ -252,11 +295,13 @@ If you want to understand the backend first, read the code in this order: 1. `WebGPUEnvironment.cs` 2. `WebGPUWindow.cs`, `WebGPUSurfaceFrame.cs`, `WebGPUExternalSurface.cs`, `WebGPUSurfaceHost.cs`, `WebGPURenderTarget.cs`, and `WebGPUDeviceContext.cs` -3. `WebGPUDrawingBackend.cs` -4. `WebGPUFlushContext.cs` -5. `WebGPURuntime.cs` -6. `WebGPURuntime.DeviceSharedState.cs` -7. `WEBGPU_RASTERIZER.md` +3. `WebGPUDrawingBackend.cs` and its partials (`WebGPUDrawingBackend.Readback.cs`, `WebGPUDrawingBackend.CopyPixels.cs`, `WebGPUDrawingBackend.CompositePixels.cs`) +4. `WebGPUSceneOperations.cs` and `WebGPUDrawingBackend.Ordered.cs` for the ordered Apply/layer plan and transactional executor +5. `WebGPUFlushContext.cs` +6. `WebGPURuntime.cs` +7. `WebGPURuntime.DeviceSharedState.cs` +8. `WEBGPU_RASTERIZER.md` +9. `GpuSceneDrawTag.cs`, `GpuSceneDrawMonoid.cs`, and the packed GPU record structs in `WebGPUSceneResources.cs` when following the shader contract That order mirrors the newcomer view of the system: diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md index bb3bb97a6..f6918d339 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND_PROCESS.md @@ -3,10 +3,10 @@ The WebGPU documentation is split into two newcomer-first documents: - [`WEBGPU_BACKEND.md`](d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WEBGPU_BACKEND.md) - Explains how `WebGPUEnvironment`, the public target types, and `WebGPUDrawingBackend` fit together, how retained scene creation reaches the GPU path, where explicit support probing fits, how explicit layers are lowered into the staged scene, and how runtime/device-scoped state relates to flush-scoped work. + Explains how `WebGPUEnvironment`, the public target types, and `WebGPUDrawingBackend` fit together, how retained scene creation reaches the GPU path (including the parallel and ordered encoders), where explicit support probing fits, how explicit layers and ordered Apply/scoped-layer scenes execute, how the scratch-growth retry loop and typed readback work, and how runtime/device-scoped state relates to flush-scoped work. - [`WEBGPU_RASTERIZER.md`](d:/GitHub/SixLabors/ImageSharp.Drawing/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md) - Explains the staged scene pipeline itself: scene encoding, planning, resource creation, scheduling passes, fine rasterization, chunked oversized-scene execution, and submission. + Explains the staged scene pipeline itself: scene encoding (including parallel partitioned encoding), packed scene format types, planning, resource creation, scheduling passes, GPU stroking in the flatten stage, fine rasterization, the scratch overflow protocol, chunked oversized-scene execution, and submission. If you are new to the GPU path, read them in this order: diff --git a/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md b/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md index b949d40dc..c30d3c55c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md +++ b/src/ImageSharp.Drawing.WebGPU/WEBGPU_RASTERIZER.md @@ -8,6 +8,7 @@ In this codebase, the WebGPU rasterizer is not a single type with one scan-conve - `WebGPUSceneConfig` - `WebGPUSceneResources` - `WebGPUSceneDispatch` +- `GpuSceneDrawTag`, `GpuSceneDrawMonoid`, and the packed GPU scene structs - the WGSL shader set under `Shaders/WgslSource` Together, these types turn one retained encoded scene into staged GPU work, schedule that scene into tile-relative work, run the fine raster pass, and write final pixels. @@ -50,6 +51,7 @@ That split explains the major responsibilities: - `WebGPUSceneConfig` owns planning - `WebGPUSceneResources` owns flush-scoped buffers and textures - `WebGPUSceneDispatch` owns pass ordering and submission +- the `GpuScene*` format types own the CPU-side constants and structs that mirror WGSL scene layout ## The Most Important Terms @@ -76,10 +78,14 @@ It tells the rasterizer: This includes: - the packed scene buffer -- the config buffer -- the scheduling scratch buffers +- the config (header) buffer +- the scene-derived intermediate buffers (path monoids, path/draw/clip bboxes, draw monoids, the combined info/bin-data buffer, paths, lines) - the gradient texture -- the image atlas texture +- the image atlas texture, optionally sampling an external texture view (used by scoped-layer compositing) + +The bump-allocated scheduling scratch (bin headers, path rows, path tiles, segment counts, segments, blend spill, PTCL, the bump buffer, and the status readback buffer) lives in a separate scheduling arena owned by the dispatch layer. + +It does not own the draw-tag contract itself. `GpuSceneDrawTag` and `GpuSceneDrawMonoid` live in files named for those types. The remaining shader-visible record structs are still grouped in `WebGPUSceneResources.cs` near the resource set they back. ### Scheduling Passes @@ -138,13 +144,35 @@ The encoder first builds several logical streams such as: - transforms - styles - gradient ramp pixels +- path-gradient edge data - deferred image atlas descriptors Those streams are then packed into the final scene word buffer plus separate gradient and image payloads. +The draw-tag words and draw-info flag bits are defined by `GpuSceneDrawTag` and must match `Shaders/WgslSource/Shared/drawtag.wgsl`. The encoder chooses which tag or flag to write; the format type owns the numeric shader contract. The clip mask bits (`CLIP_DIFFERENCE_MASK_BIT`, `CLIP_HARD_MASK_BIT`) are declared once in `drawtag.wgsl` and set by the encoder in the high bits of the clip blend word. + Explicit layers are part of this encoding step too. `BeginLayer` and `EndLayer` stay in the prepared command stream until `WebGPUSceneEncoder` lowers them into `BeginClip` and `EndClip` draw records inside the encoded scene. -That split matters because the shaders consume offsets into one shared packed scene layout. The encoder therefore separates "append logical scene data" from "pack the final GPU-facing layout". +The stream split matters because the shaders consume offsets into one shared packed scene layout. The encoder therefore separates "append logical scene data" from "pack the final GPU-facing layout". + +Three geometry rules the encoder applies matter downstream: + +- fill geometry is pre-flattened on the CPU, so only line segments reach the flatten stage for fills; curve handling on the GPU exists but is not exercised by fills in practice +- stroke geometry is not expanded on the CPU. The encoder emits the stroke's segment list after collapsing micro-segments shorter than 1/64 px (mirroring the CPU stroker's preprocessing), and reuses quad-to path tags as stroke tangent markers rather than curves; the GPU stroker in `flatten.wgsl` does the expansion +- clip paths carry a full-target raster interest rectangle; a clipped interest would make binning intersect the clip coverage away + +Per-draw rasterization state is also encoded here: each visible fill carries its own coverage threshold and raster interest rectangle, and an aliased fill sets `DRAW_INFO_FLAGS_ALIASED_BIT` in its draw flags. Antialiased and aliased fills therefore coexist in one scene. + +### Parallel Encoding + +Large batches are planned and encoded in parallel over contiguous command-range partitions: + +- a single sequential prescan (`CreatePartitionCommandRanges`) records, for each partition boundary, the stack of clip scopes opened by earlier partitions +- each partition replays those seed clips before its range and closes every open clip after it, so the concatenated partitions form one balanced clip stream and clipped scenes encode fully parallel +- layer scopes composite their contents as one group when they close, so a boundary may not cut through an open layer; boundaries snap forward to the next command index where the layer depth is zero, which can leave empty trailing partitions that simply encode nothing +- the partitions are concatenated by partition index, preserving timeline order + +Batches containing Apply are encoded by `TryEncodeOrdered(...)` instead: Apply reads pixels back mid-scene, so those scenes encode sequentially into an ordered operation list (render ranges, Apply items, scoped layers) that the backend walks at render time. The backend document describes that model. ## Stage 2: Planning @@ -166,10 +194,7 @@ This check answers: "can the planned staged scene be bound legally on this device" -If not, the dispatch layer decides whether the scene: - -- must fail back out to the backend -- or can be routed into the chunked oversized-scene path +If not, the failure is classified by buffer (`BindingLimitBuffer`): overflows of the tile-dependent buffers (path rows, path tiles, segment counts, segments, blend spill, PTCL) route the scene into the chunked oversized-scene path, while any other failure fails the flush. This validation happens before the expensive dispatch work begins. @@ -180,8 +205,8 @@ This validation happens before the expensive dispatch work begins. That includes: - the packed scene buffer -- the scene config buffer -- the scheduling scratch buffers +- the scene config (header) buffer +- the scene-derived intermediate buffers - the gradient texture - the image atlas texture @@ -189,7 +214,7 @@ That includes: flowchart TD A[Encoded scene and config] --> B[Create scene buffer] A --> C[Create config buffer] - A --> D[Create scheduling scratch buffers] + A --> D[Create scene-derived intermediate buffers] A --> E[Upload gradient texture] A --> F[Create image atlas texture] B --> G[WebGPUSceneResourceSet] @@ -199,7 +224,9 @@ flowchart TD F --> G ``` -The resource contents are flush-scoped, but the underlying allocations can be leased from backend-cached arenas and returned there after submission. That reuse keeps later flushes from recreating the same large GPU buffers when the current backend instance can keep reusing them safely. +The bump-allocated scheduling scratch buffers are created separately by the dispatch layer's scheduling arena when the staged scene renders. + +The resource contents are flush-scoped, but the underlying buffer allocations can be leased from backend-cached arenas and returned there after submission; the textures are scene-dependent and not pooled. That reuse keeps later flushes from recreating the same large GPU buffers when the current backend instance can keep reusing them safely. ## Stage 5: Scheduling Passes @@ -207,7 +234,9 @@ The scheduling passes transform the packed scene into tile-relative raster work. Their purpose is structural. They: +- reset the GPU bump allocators (prepare) - scan the packed path and draw streams +- flatten path segments into a device-space line soup, expanding strokes on the GPU - build path and clip metadata - bin work into tiles - allocate sparse per-path row metadata from clipped draw bounds @@ -220,15 +249,16 @@ The result is not final pixels. It is the scene structure needed by the final ra ```mermaid flowchart TD - A[PathtagReduce] --> B[PathtagReduce2 if needed] + Z[Prepare] --> A[PathtagReduce] + A --> B[PathtagReduce2 if needed] B --> C[PathtagScan1 if needed] C --> D[PathtagScan] D --> E[BboxClear] E --> F[Flatten] F --> G[DrawReduce] G --> H[DrawLeaf] - H --> I[ClipReduce] - I --> J[ClipLeaf] + H --> I[ClipReduce if needed] + I --> J[ClipLeaf if clips exist] J --> K[Binning] K --> L[PathRowAlloc] L --> M[PathCountSetup] @@ -241,36 +271,86 @@ flowchart TD S --> T[PathTiling] ``` -## Stage 6: Fine Raster Pass +A few stage details worth knowing: -The fine pass is where the scheduled scene becomes final pixel writes. +- `prepare.wgsl` binds only the bump buffer. It zeroes every bump-allocator counter and the failure mask on the GPU, and the pipeline never cancels mid-flush: all stages run so the counters report the true demand for every scratch buffer in a single pass +- the pathtag scan computes a prefix sum of a four-word `TagMonoid` (`trans_ix`, `pathseg_offset`, `style_ix`, `path_ix`) so flatten can locate each segment's points, transform, and style in O(1) +- `bbox_clear` is dispatched over the scene's path count and resets each atomic path bbox to an inverted empty state before flatten accumulates into it +- `clip_reduce`/`clip_leaf` are skipped entirely when the scene has no clip records, and `clip_reduce` is skipped when the clip stream fits one 256-element partition + +### Flatten: Fills And GPU Stroking + +`flatten.wgsl` converts encoded path segments into the device-space line soup consumed by the later stages, and it is where strokes become geometry. + +For fills, every segment type routes through `flatten_euler` exactly as in upstream Vello: `read_path_segment` lowers lines to degenerate cubics and the Euler-spiral math resolves them through its low-curvature path. Because the C# encoder pre-flattens fill curves on the CPU, only line segments arrive for fills in practice. -Two fine shaders exist: +Stroke expansion diverges from upstream Vello. It is a direct port of the CPU `PolygonStroker`: `stroke_chain_point` walks the offset chain, `stroke_side_join` dispatches per-join handling, and `stroke_calc_miter` and `stroke_calc_arc` produce miters and round joins/caps. Caps, joins, and arcs are all generated on the GPU. The encoder supports this with two conventions: micro-segments shorter than 1/64 px are collapsed CPU-side before encoding (matching the CPU stroker's preprocessing), and quad-to path tags in a stroke are tangent markers for the stroker, not curve segments. Stroking never falls back to the CPU. -- `FineAreaComputeShader` -- `FineAliasedThresholdComputeShader` +## Stage 6: Fine Raster Pass + +The fine pass is where the scheduled scene becomes final pixel writes. -Only one is selected for a flush. +A single fine shader (`FineAreaComputeShader`) handles every flush; its pipeline is cached per +output texture format. It computes analytic area coverage and, per fill, quantizes that +coverage against the fill's own threshold when the fill carries the aliased bit in +`CmdFill.size_and_rule`. The per-fill threshold travels in the `CmdFill` command itself and +overrides the scene-wide `config.fine_coverage_threshold` fallback. Antialiased and aliased +fills therefore coexist within one flush, matching the CPU rasterizer's per-fill +`RasterizationMode`. Each fill also carries a raster interest rectangle; coverage outside it +is zeroed. + +For antialiased fills the `CmdFill.coverage_threshold` word is reused to carry the perceptual +coverage boost for text (the two uses are mutually exclusive, so no extra command word is +needed). When non-zero, fine remaps partial coverage with the S-curve +`f(a) = a + boost * a * (1 - a) * (2a - 1)`, equivalently a blend +`(1 - boost) * a + boost * smoothstep(a)`: coverage above one half darkens and coverage below +it lightens, so stems solidify while counters stay bright. The remap is monotone and range +preserving, no pixel moves by more than `0.0962 * boost` coverage, and faint fringes keep at +least `(1 - boost)` of their value, which bounds erosion of sub-half-pixel features. Only text +fills carry a non-zero boost (`DrawingOptions.TextContrast`); plain vector fills, strokes, and +clips always encode zero. This matches the CPU rasterizer's `AreaToCoverage` boost; the full +derivation and the rationale versus Skia's mask-gamma remap live in +`ImageSharp.Drawing/Processing/DRAWING_CANVAS.md` under "The TextContrast curve". The fine pass consumes data such as: - the segment buffer - the PTCL buffer -- info and bin data +- the info stream (shared with bin data) - blend-spill storage -- gradient and image textures -- the target backdrop input +- gradient and image atlas textures +- the backdrop texture holding the existing target contents -and writes the result into the output texture. +and writes the result into the output texture with straight (unpremultiplied) alpha. -That is also where explicit layers are composited. The fine shader handles `BeginClip` and `EndClip` records inline by saving the current tile color, rendering the isolated layer contents, and then blending that isolated result back into the saved backdrop with the layer's stored blend mode and alpha. +The PTCL command set extends Vello's. Alongside the fill, color, gradient, image, clip, and +jump commands, the fine pass interprets `CMD_RECOLOR` (the `RecolorBrush`), `CMD_ELLIPTIC_GRAD`, +and `CMD_PATH_GRAD`. Brush evaluation is deliberately matched to the CPU brushes, including +gradient extend behavior; the recolor threshold is pre-transformed by the encoder +(`Threshold * 4`) into the shader's squared-color-distance domain so the shader compares +distances directly. -## Stage 7: Copy And Submit +That is also where explicit layers are composited. The fine shader handles `BeginClip` and `EndClip` records inline by saving the current tile color, rendering the isolated layer contents, and then blending that isolated result back into the saved backdrop with the layer's stored blend mode and alpha. The clip blend word's high bits select clip variants: `CLIP_DIFFERENCE_MASK_BIT` inverts the mask coverage for Difference clips and `CLIP_HARD_MASK_BIT` marks a hard-edge (aliased) clip mask. -After the fine pass completes, the rasterizer copies the output texture to the target texture and submits the command buffer. +## Stage 7: Readback, Copy, And Submit + +Scheduling, fine, and the bump-allocator status readback are recorded into one command encoder and submitted together; mapping the readback buffer blocks until the GPU finishes. + +The CPU then inspects the bump counters. If any allocator exceeded its capacity the fine output is discarded and the attempt reports the grown sizes back to the backend for a retry (see the next section). Otherwise the rasterizer copies the output texture to the target texture, submits the final copy, and reports the actual GPU usage so the caller can cache known-good sizes for later renders. At that point the staged scene has completed and the per-flush resource contents can be discarded while any reusable arena allocations are returned to the backend cache. +## Scratch Overflow And Retry + +The scratch buffers written by the scheduling passes (lines, binning, path rows, path tiles, segment counts, segments, blend spill, PTCL) are allocated by GPU bump allocators sized from cached estimates. The overflow protocol is: + +- `prepare.wgsl` zeroes every bump counter and the failure mask at the start of each attempt and the pipeline never cancels: every stage runs and reports its true demand even after an overflow, so one readback can expose as much growth as possible +- an overflowing stage stops writing past its capacity but keeps counting, and sets the failure mask so the CPU knows the attempt's output is invalid +- the CPU readback detects the overflow and the backend retries the whole attempt with grown sizes +- earlier overflows can still hide later-stage demand, so the backend bounds the loop at roughly one failed pass per tracked allocator plus a safety margin + +There is no CPU-side pre-validation of scratch capacities; the GPU counters are the single source of truth. + ## Chunked Oversized-Scene Execution Some scenes exceed the device's single-binding limits even though they are otherwise valid staged scenes. Common examples are `segments`, `path rows`, or `path tiles` growing beyond the device's `MaxStorageBufferBindingSize`. @@ -279,7 +359,14 @@ The chunked path exists for that case. The important design point is that chunking does not re-clip or re-encode the scene on the CPU. The encoded scene remains whole. What changes is the GPU consumption window. -The dispatch layer executes the staged pipeline in chunk-local tile-row windows so each chunk stays within device limits while still using the same encoded scene. +The dispatch layer executes the staged pipeline in chunk-local tile-row windows so each chunk stays within device limits while still using the same encoded scene. The mechanics are: + +- the output texture is seeded with the current target contents first, so pixels no chunk writes keep their original values when the full rectangle is copied back +- the chunk-invariant stages (prepare through binning) run exactly once per flush; each chunk then replays only the chunk-local stages, with `chunk_reset` clearing the chunk-local bump counters while preserving the shared flatten/binning state +- each chunk window is validated against the binding limits before dispatch; if a chunk's buffers still exceed the limit the window shrinks and validation repeats +- a chunk attempt that fails after passing binding validation fails the flush, because shrinking cannot cure it and retrying would re-record the identical chunk forever +- every chunk copies its bump-allocator status into a distinct offset of one readback buffer; all chunks are checked in a single batch readback at the end, and any overflow feeds the same grow-and-retry loop as the monolithic path +- the backend caches the largest successful chunk height per binding category and target size as an advisory first guess for later flushes That keeps the normal fast path unchanged and reserves chunking for the oversized path only. @@ -313,14 +400,15 @@ That separation is why it helps to document them separately. If you want to understand the staged rasterizer itself, read the code in this order: 1. `WebGPUSceneEncoder.cs` -2. `WebGPUSceneConfig.cs` -3. `WebGPUSceneResources.cs` -4. `WebGPUSceneDispatch.cs` -5. `Shaders` +2. `GpuSceneDrawTag.cs` and `GpuSceneDrawMonoid.cs` +3. `WebGPUSceneConfig.cs` +4. `WebGPUSceneResources.cs` for resource creation and the remaining packed GPU record structs +5. `WebGPUSceneDispatch.cs` +6. `Shaders` That order mirrors the data lifecycle: -encoded scene -> planning -> resources -> staged execution -> shader contract +encoded scene -> draw-tag format -> planning -> resources and record layout -> staged execution -> WGSL ## The Mental Model To Keep @@ -331,6 +419,7 @@ it is a staged scene pipeline. It stages one retained encoded scene, plans the w If that model is clear, the major types fall into place: - `WebGPUSceneEncoder` encodes +- `GpuScene*` types define the packed CPU/WGSL scene contract - `WebGPUSceneConfig` plans - `WebGPUSceneResources` creates flush-scoped resources - `WebGPUSceneDispatch` records and submits the staged pipeline diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPU.cs b/src/ImageSharp.Drawing.WebGPU/WebGPU.cs new file mode 100644 index 000000000..d978a3027 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPU.cs @@ -0,0 +1,959 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +// The instance-shaped facade is intentional: resource owners retain the exact API dependency used +// to create their handles, keeping native lifetime calls on the same boundary as construction. +#pragma warning disable CA1822 + +/// +/// Provides the WebGPU operations used by the drawing backend. +/// +internal sealed unsafe class WebGPU +{ + private static readonly WebGPU Shared = new(); + + private WebGPU() + { + } + + /// + /// Gets the process-wide WebGPU API facade. + /// + /// The shared API facade. + public static WebGPU GetApi() => Shared; + + /// + /// Creates a WebGPU instance. + /// + /// The instance configuration, or for defaults. + /// The created instance, or on failure. + public WGPUInstanceImpl* CreateInstance(WGPUInstanceDescriptor* descriptor) + => WebGPUNative.wgpuCreateInstance(descriptor); + + /// + /// Reports whether an adapter supports a feature. + /// + /// The adapter to query. + /// The feature to query. + /// when the feature is supported. + public bool AdapterHasFeature(WGPUAdapterImpl* adapter, WGPUFeatureName feature) + => WebGPUNative.wgpuAdapterHasFeature(adapter, feature) != 0; + + /// + /// Requests a device from an adapter. + /// + /// The adapter that creates the device. + /// The requested device configuration. + /// The rooted completion callback. + /// The context pointer passed to the callback. + public void AdapterRequestDevice( + WGPUAdapterImpl* adapter, + in WGPUDeviceDescriptor descriptor, + WebGPURequestDeviceCallback callback, + void* userData) + { + WGPURequestDeviceCallbackInfo callbackInfo = new() + { + mode = WGPUCallbackMode.AllowSpontaneous, + callback = callback.Pointer, + userdata1 = userData + }; + + // Register before entering native code because AllowSpontaneous permits the callback to + // run before the request function returns. If P/Invoke cannot enter native code, cancel + // that registration so the managed thunk does not retain a root for an invocation that + // native WebGPU never accepted. + callback.RegisterInvocation(); + + fixed (WGPUDeviceDescriptor* descriptorPtr = &descriptor) + { + try + { + _ = WebGPUNative.wgpuAdapterRequestDevice(adapter, descriptorPtr, callbackInfo); + } + catch + { + callback.CancelInvocation(); + throw; + } + } + } + + /// + /// Reads the adapter limits. + /// + /// The adapter to query. + /// Receives the adapter limits. + /// The query status. + public WGPUStatus AdapterGetLimits(WGPUAdapterImpl* adapter, WGPULimits* limits) + => WebGPUNative.wgpuAdapterGetLimits(adapter, limits); + + /// + /// Queries an adapter's identity and classification. + /// + /// The adapter to query. + /// Receives the adapter info; release with . + /// The query status. + public WGPUStatus AdapterGetInfo(WGPUAdapterImpl* adapter, WGPUAdapterInfo* info) + => WebGPUNative.wgpuAdapterGetInfo(adapter, info); + + /// + /// Releases the allocated string members of an adapter info value. + /// + /// The adapter info whose members are released. + public void AdapterInfoFreeMembers(WGPUAdapterInfo info) + => WebGPUNative.wgpuAdapterInfoFreeMembers(info); + + /// + /// Releases an adapter reference. + /// + /// The adapter to release. + public void AdapterRelease(WGPUAdapterImpl* adapter) + => WebGPUNative.wgpuAdapterRelease(adapter); + + /// + /// Releases an instance reference. + /// + /// The instance to release. + public void InstanceRelease(WGPUInstanceImpl* instance) + => WebGPUNative.wgpuInstanceRelease(instance); + + /// + /// Requests an adapter from an instance. + /// + /// The instance that discovers the adapter. + /// The adapter selection criteria. + /// The rooted completion callback. + /// The context pointer passed to the callback. + public void InstanceRequestAdapter( + WGPUInstanceImpl* instance, + in WGPURequestAdapterOptions options, + WebGPURequestAdapterCallback callback, + void* userData) + { + WGPURequestAdapterCallbackInfo callbackInfo = new() + { + mode = WGPUCallbackMode.AllowSpontaneous, + callback = callback.Pointer, + userdata1 = userData + }; + + // The callback may run from inside this request, so establish its managed lifetime before + // native code sees the function pointer. A failed P/Invoke leaves no native invocation to + // retire and must undo the registration immediately. + callback.RegisterInvocation(); + + fixed (WGPURequestAdapterOptions* optionsPtr = &options) + { + try + { + _ = WebGPUNative.wgpuInstanceRequestAdapter(instance, optionsPtr, callbackInfo); + } + catch + { + callback.CancelInvocation(); + throw; + } + } + } + + /// + /// Creates a presentation surface for a native platform source. + /// + /// The instance that owns the surface. + /// The platform surface descriptor. + /// The created surface, or on failure. + public WGPUSurfaceImpl* InstanceCreateSurface(WGPUInstanceImpl* instance, in WGPUSurfaceDescriptor descriptor) + { + fixed (WGPUSurfaceDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuInstanceCreateSurface(instance, descriptorPtr); + } + } + + /// + /// Creates a bind group. + /// + /// The device that owns the bind group. + /// The bind-group configuration. + /// The created bind group, or on failure. + public WGPUBindGroupImpl* DeviceCreateBindGroup(WGPUDeviceImpl* device, in WGPUBindGroupDescriptor descriptor) + { + fixed (WGPUBindGroupDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateBindGroup(device, descriptorPtr); + } + } + + /// + /// Creates a bind-group layout. + /// + /// The device that owns the layout. + /// The layout configuration. + /// The created layout, or on failure. + public WGPUBindGroupLayoutImpl* DeviceCreateBindGroupLayout(WGPUDeviceImpl* device, in WGPUBindGroupLayoutDescriptor descriptor) + { + fixed (WGPUBindGroupLayoutDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateBindGroupLayout(device, descriptorPtr); + } + } + + /// + /// Creates a buffer. + /// + /// The device that owns the buffer. + /// The buffer configuration. + /// The created buffer, or on failure. + public WGPUBufferImpl* DeviceCreateBuffer(WGPUDeviceImpl* device, in WGPUBufferDescriptor descriptor) + { + fixed (WGPUBufferDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateBuffer(device, descriptorPtr); + } + } + + /// + /// Creates a command encoder. + /// + /// The device that owns the encoder. + /// The encoder configuration. + /// The created encoder, or on failure. + public WGPUCommandEncoderImpl* DeviceCreateCommandEncoder(WGPUDeviceImpl* device, in WGPUCommandEncoderDescriptor descriptor) + { + fixed (WGPUCommandEncoderDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateCommandEncoder(device, descriptorPtr); + } + } + + /// + /// Creates a compute pipeline. + /// + /// The device that owns the pipeline. + /// The compute-pipeline configuration. + /// The created pipeline, or on failure. + public WGPUComputePipelineImpl* DeviceCreateComputePipeline(WGPUDeviceImpl* device, in WGPUComputePipelineDescriptor descriptor) + { + fixed (WGPUComputePipelineDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateComputePipeline(device, descriptorPtr); + } + } + + /// + /// Creates a pipeline layout. + /// + /// The device that owns the layout. + /// The pipeline-layout configuration. + /// The created layout, or on failure. + public WGPUPipelineLayoutImpl* DeviceCreatePipelineLayout(WGPUDeviceImpl* device, in WGPUPipelineLayoutDescriptor descriptor) + { + fixed (WGPUPipelineLayoutDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreatePipelineLayout(device, descriptorPtr); + } + } + + /// + /// Creates a render pipeline. + /// + /// The device that owns the pipeline. + /// The render-pipeline configuration. + /// The created pipeline, or on failure. + public WGPURenderPipelineImpl* DeviceCreateRenderPipeline(WGPUDeviceImpl* device, in WGPURenderPipelineDescriptor descriptor) + { + fixed (WGPURenderPipelineDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateRenderPipeline(device, descriptorPtr); + } + } + + /// + /// Creates a shader module. + /// + /// The device that owns the module. + /// The shader-module configuration. + /// The created module, or on failure. + public WGPUShaderModuleImpl* DeviceCreateShaderModule(WGPUDeviceImpl* device, in WGPUShaderModuleDescriptor descriptor) + { + fixed (WGPUShaderModuleDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateShaderModule(device, descriptorPtr); + } + } + + /// + /// Creates a texture sampler. + /// + /// The device that owns the sampler. + /// The sampler configuration. + /// The created sampler, or on failure. + public WGPUSamplerImpl* DeviceCreateSampler(WGPUDeviceImpl* device, in WGPUSamplerDescriptor descriptor) + { + fixed (WGPUSamplerDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateSampler(device, descriptorPtr); + } + } + + /// + /// Begins an error scope for subsequent device operations. + /// + /// The device that owns the scope. + /// The class of error captured by the scope. + public void DevicePushErrorScope(WGPUDeviceImpl* device, WGPUErrorFilter filter) + => WebGPUNative.wgpuDevicePushErrorScope(device, filter); + + /// + /// Ends the current error scope and reports its captured error. + /// + /// The device that owns the scope. + /// The rooted callback that receives the scope result. + /// The context pointer passed to the callback. + public void DevicePopErrorScope(WGPUDeviceImpl* device, WebGPUPopErrorScopeCallback callback, void* userData) + { + WGPUPopErrorScopeCallbackInfo callbackInfo = new() + { + mode = WGPUCallbackMode.AllowSpontaneous, + callback = callback.Pointer, + userdata1 = userData + }; + + callback.RegisterInvocation(); + + try + { + _ = WebGPUNative.wgpuDevicePopErrorScope(device, callbackInfo); + } + catch + { + callback.CancelInvocation(); + throw; + } + } + + /// + /// Creates a texture. + /// + /// The device that owns the texture. + /// The texture configuration. + /// The created texture, or on failure. + public WGPUTextureImpl* DeviceCreateTexture(WGPUDeviceImpl* device, in WGPUTextureDescriptor descriptor) + { + fixed (WGPUTextureDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuDeviceCreateTexture(device, descriptorPtr); + } + } + + /// + /// Reads the features supported by a device. + /// + /// The device to query. + /// Receives the native feature array. + public void DeviceGetFeatures(WGPUDeviceImpl* device, WGPUSupportedFeatures* features) + => WebGPUNative.wgpuDeviceGetFeatures(device, features); + + /// + /// Reads the device limits. + /// + /// The device to query. + /// Receives the device limits. + /// The query status. + public WGPUStatus DeviceGetLimits(WGPUDeviceImpl* device, WGPULimits* limits) + => WebGPUNative.wgpuDeviceGetLimits(device, limits); + + /// + /// Frees members allocated by a supported-features query. + /// + /// The supported-features result to release. + public void SupportedFeaturesFreeMembers(WGPUSupportedFeatures features) + => WebGPUNative.wgpuSupportedFeaturesFreeMembers(features); + + /// + /// Gets the device's default queue. + /// + /// The device that owns the queue. + /// The default queue. + public WGPUQueueImpl* DeviceGetQueue(WGPUDeviceImpl* device) + => WebGPUNative.wgpuDeviceGetQueue(device); + + /// + /// Polls a device for asynchronous progress. + /// + /// The device to poll. + /// Whether to wait for the requested work to complete. + /// The exact submission to wait for, or for all submitted work. + /// when the queue is empty after polling. + public bool DevicePoll(WGPUDeviceImpl* device, bool wait, ulong* submissionIndex) + => WebGPUNative.wgpuDevicePoll(device, wait ? 1U : 0U, submissionIndex) != 0; + + /// + /// Releases a device reference. + /// + /// The device to release. + public void DeviceRelease(WGPUDeviceImpl* device) + => WebGPUNative.wgpuDeviceRelease(device); + + /// + /// Releases a queue reference. + /// + /// The queue to release. + public void QueueRelease(WGPUQueueImpl* queue) + => WebGPUNative.wgpuQueueRelease(queue); + + /// + /// Releases a sampler reference. + /// + /// The sampler to release. + public void SamplerRelease(WGPUSamplerImpl* sampler) + => WebGPUNative.wgpuSamplerRelease(sampler); + + /// + /// Registers a callback for completion of work submitted before this call. + /// + /// The queue whose submitted work is observed. + /// The rooted completion callback. + /// The context pointer passed to the callback. + public void QueueOnSubmittedWorkDone( + WGPUQueueImpl* queue, + WebGPUQueueWorkDoneCallback callback, + void* userData) + { + WGPUQueueWorkDoneCallbackInfo callbackInfo = new() + { + mode = WGPUCallbackMode.AllowSpontaneous, + callback = callback.Pointer, + userdata1 = userData + }; + + callback.RegisterInvocation(); + + try + { + _ = WebGPUNative.wgpuQueueOnSubmittedWorkDone(queue, callbackInfo); + } + catch + { + callback.CancelInvocation(); + throw; + } + } + + /// + /// Writes bytes into a buffer. + /// + /// The queue performing the write. + /// The destination buffer. + /// The destination byte offset. + /// The source bytes. + /// The number of source bytes. + public void QueueWriteBuffer(WGPUQueueImpl* queue, WGPUBufferImpl* buffer, ulong offset, void* data, nuint size) + => WebGPUNative.wgpuQueueWriteBuffer(queue, buffer, offset, data, size); + + /// + /// Writes texels into a texture. + /// + /// The queue performing the write. + /// The destination texture region. + /// The source bytes. + /// The number of source bytes. + /// The source layout. + /// The destination extent. + public void QueueWriteTexture( + WGPUQueueImpl* queue, + in WGPUTexelCopyTextureInfo destination, + void* data, + nuint dataSize, + in WGPUTexelCopyBufferLayout dataLayout, + in WGPUExtent3D writeSize) + { + fixed (WGPUTexelCopyTextureInfo* destinationPtr = &destination) + { + fixed (WGPUTexelCopyBufferLayout* dataLayoutPtr = &dataLayout) + { + fixed (WGPUExtent3D* writeSizePtr = &writeSize) + { + WebGPUNative.wgpuQueueWriteTexture(queue, destinationPtr, data, dataSize, dataLayoutPtr, writeSizePtr); + } + } + } + } + + /// + /// Submits command buffers to a queue. + /// + /// The destination queue. + /// The number of command-buffer pointers. + /// The first command-buffer pointer. + public void QueueSubmit(WGPUQueueImpl* queue, nuint commandCount, ref WGPUCommandBufferImpl* commands) + { + fixed (WGPUCommandBufferImpl** commandsPtr = &commands) + { + WebGPUNative.wgpuQueueSubmit(queue, commandCount, commandsPtr); + } + } + + /// + /// Submits command buffers and returns the native submission index. + /// + /// The destination queue. + /// The number of command-buffer pointers. + /// The first command-buffer pointer. + /// The submission index assigned by the queue. + public ulong QueueSubmitForIndex(WGPUQueueImpl* queue, nuint commandCount, ref WGPUCommandBufferImpl* commands) + { + fixed (WGPUCommandBufferImpl** commandsPtr = &commands) + { + return WebGPUNative.wgpuQueueSubmitForIndex(queue, commandCount, commandsPtr); + } + } + + /// + /// Begins a compute pass. + /// + /// The command encoder. + /// The compute-pass configuration. + /// The compute-pass encoder. + public WGPUComputePassEncoderImpl* CommandEncoderBeginComputePass(WGPUCommandEncoderImpl* encoder, in WGPUComputePassDescriptor descriptor) + { + fixed (WGPUComputePassDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuCommandEncoderBeginComputePass(encoder, descriptorPtr); + } + } + + /// + /// Begins a render pass. + /// + /// The command encoder. + /// The render-pass configuration. + /// The render-pass encoder. + public WGPURenderPassEncoderImpl* CommandEncoderBeginRenderPass(WGPUCommandEncoderImpl* encoder, in WGPURenderPassDescriptor descriptor) + { + fixed (WGPURenderPassDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuCommandEncoderBeginRenderPass(encoder, descriptorPtr); + } + } + + /// + /// Copies bytes between buffers. + /// + /// The command encoder. + /// The source buffer. + /// The source byte offset. + /// The destination buffer. + /// The destination byte offset. + /// The number of bytes to copy. + public void CommandEncoderCopyBufferToBuffer( + WGPUCommandEncoderImpl* encoder, + WGPUBufferImpl* source, + ulong sourceOffset, + WGPUBufferImpl* destination, + ulong destinationOffset, + ulong size) + => WebGPUNative.wgpuCommandEncoderCopyBufferToBuffer( + encoder, + source, + sourceOffset, + destination, + destinationOffset, + size); + + /// + /// Copies texture data into a buffer. + /// + /// The command encoder. + /// The source texture region. + /// The destination buffer layout. + /// The copied extent. + public void CommandEncoderCopyTextureToBuffer( + WGPUCommandEncoderImpl* encoder, + in WGPUTexelCopyTextureInfo source, + in WGPUTexelCopyBufferInfo destination, + in WGPUExtent3D copySize) + { + fixed (WGPUTexelCopyTextureInfo* sourcePtr = &source) + { + fixed (WGPUTexelCopyBufferInfo* destinationPtr = &destination) + { + fixed (WGPUExtent3D* copySizePtr = ©Size) + { + WebGPUNative.wgpuCommandEncoderCopyTextureToBuffer(encoder, sourcePtr, destinationPtr, copySizePtr); + } + } + } + } + + /// + /// Copies data between textures. + /// + /// The command encoder. + /// The source texture region. + /// The destination texture region. + /// The copied extent. + public void CommandEncoderCopyTextureToTexture( + WGPUCommandEncoderImpl* encoder, + in WGPUTexelCopyTextureInfo source, + in WGPUTexelCopyTextureInfo destination, + in WGPUExtent3D copySize) + { + fixed (WGPUTexelCopyTextureInfo* sourcePtr = &source) + { + fixed (WGPUTexelCopyTextureInfo* destinationPtr = &destination) + { + fixed (WGPUExtent3D* copySizePtr = ©Size) + { + WebGPUNative.wgpuCommandEncoderCopyTextureToTexture(encoder, sourcePtr, destinationPtr, copySizePtr); + } + } + } + } + + /// + /// Finishes command recording. + /// + /// The command encoder to finish. + /// The command-buffer configuration. + /// The recorded command buffer. + public WGPUCommandBufferImpl* CommandEncoderFinish(WGPUCommandEncoderImpl* encoder, in WGPUCommandBufferDescriptor descriptor) + { + fixed (WGPUCommandBufferDescriptor* descriptorPtr = &descriptor) + { + return WebGPUNative.wgpuCommandEncoderFinish(encoder, descriptorPtr); + } + } + + /// + /// Sets the active compute pipeline. + /// + /// The compute-pass encoder. + /// The compute pipeline. + public void ComputePassEncoderSetPipeline(WGPUComputePassEncoderImpl* encoder, WGPUComputePipelineImpl* pipeline) + => WebGPUNative.wgpuComputePassEncoderSetPipeline(encoder, pipeline); + + /// + /// Sets one compute bind group. + /// + /// The compute-pass encoder. + /// The bind-group slot. + /// The bind group. + /// The number of dynamic offsets. + /// The dynamic offsets, or when the count is zero. + public void ComputePassEncoderSetBindGroup( + WGPUComputePassEncoderImpl* encoder, + uint groupIndex, + WGPUBindGroupImpl* group, + nuint dynamicOffsetCount, + uint* dynamicOffsets) + => WebGPUNative.wgpuComputePassEncoderSetBindGroup( + encoder, + groupIndex, + group, + dynamicOffsetCount, + dynamicOffsets); + + /// + /// Dispatches compute workgroups. + /// + /// The compute-pass encoder. + /// The workgroup count in X. + /// The workgroup count in Y. + /// The workgroup count in Z. + public void ComputePassEncoderDispatchWorkgroups(WGPUComputePassEncoderImpl* encoder, uint x, uint y, uint z) + => WebGPUNative.wgpuComputePassEncoderDispatchWorkgroups(encoder, x, y, z); + + /// + /// Dispatches compute workgroups using indirect counts. + /// + /// The compute-pass encoder. + /// The buffer containing the dispatch counts. + /// The byte offset of the dispatch counts. + public void ComputePassEncoderDispatchWorkgroupsIndirect( + WGPUComputePassEncoderImpl* encoder, + WGPUBufferImpl* indirectBuffer, + ulong indirectOffset) + => WebGPUNative.wgpuComputePassEncoderDispatchWorkgroupsIndirect(encoder, indirectBuffer, indirectOffset); + + /// + /// Ends a compute pass. + /// + /// The compute-pass encoder. + public void ComputePassEncoderEnd(WGPUComputePassEncoderImpl* encoder) + => WebGPUNative.wgpuComputePassEncoderEnd(encoder); + + /// + /// Ends a render pass. + /// + /// The render-pass encoder. + public void RenderPassEncoderEnd(WGPURenderPassEncoderImpl* encoder) + => WebGPUNative.wgpuRenderPassEncoderEnd(encoder); + + /// + /// Binds a resource group to a render pass. + /// + /// The render-pass encoder. + /// The pipeline bind-group index. + /// The bind group to bind. + public void RenderPassEncoderSetBindGroup(WGPURenderPassEncoderImpl* encoder, uint groupIndex, WGPUBindGroupImpl* bindGroup) + => WebGPUNative.wgpuRenderPassEncoderSetBindGroup(encoder, groupIndex, bindGroup, 0, null); + + /// + /// Binds a graphics pipeline to a render pass. + /// + /// The render-pass encoder. + /// The graphics pipeline to bind. + public void RenderPassEncoderSetPipeline(WGPURenderPassEncoderImpl* encoder, WGPURenderPipelineImpl* pipeline) + => WebGPUNative.wgpuRenderPassEncoderSetPipeline(encoder, pipeline); + + /// + /// Sets the rectangular viewport used by subsequent render-pass draws. + /// + /// The render pass to update. + /// The viewport's left coordinate in attachment pixels. + /// The viewport's top coordinate in attachment pixels. + /// The viewport width in attachment pixels. + /// The viewport height in attachment pixels. + /// The minimum viewport depth. + /// The maximum viewport depth. + public void RenderPassEncoderSetViewport(WGPURenderPassEncoderImpl* encoder, float x, float y, float width, float height, float minDepth, float maxDepth) + => WebGPUNative.wgpuRenderPassEncoderSetViewport(encoder, x, y, width, height, minDepth, maxDepth); + + /// + /// Restricts subsequent render-pass writes to an integer rectangle. + /// + /// The render pass to update. + /// The scissor rectangle's left coordinate in attachment pixels. + /// The scissor rectangle's top coordinate in attachment pixels. + /// The scissor rectangle width in attachment pixels. + /// The scissor rectangle height in attachment pixels. + public void RenderPassEncoderSetScissorRect(WGPURenderPassEncoderImpl* encoder, uint x, uint y, uint width, uint height) + => WebGPUNative.wgpuRenderPassEncoderSetScissorRect(encoder, x, y, width, height); + + /// + /// Draws non-indexed geometry in a render pass. + /// + /// The render-pass encoder. + /// The number of vertices per instance. + /// The number of instances. + /// The first vertex index. + /// The first instance index. + public void RenderPassEncoderDraw(WGPURenderPassEncoderImpl* encoder, uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance) + => WebGPUNative.wgpuRenderPassEncoderDraw(encoder, vertexCount, instanceCount, firstVertex, firstInstance); + + /// + /// Configures a presentation surface. + /// + /// The surface to configure. + /// The surface configuration. + public void SurfaceConfigure(WGPUSurfaceImpl* surface, in WGPUSurfaceConfiguration configuration) + { + fixed (WGPUSurfaceConfiguration* configurationPtr = &configuration) + { + WebGPUNative.wgpuSurfaceConfigure(surface, configurationPtr); + } + } + + /// + /// Reads the capabilities supported by a surface and adapter pair. + /// + /// The surface to query. + /// The adapter used to present to the surface. + /// Receives the supported surface capabilities. + /// The query status. + public WGPUStatus SurfaceGetCapabilities(WGPUSurfaceImpl* surface, WGPUAdapterImpl* adapter, WGPUSurfaceCapabilities* capabilities) + => WebGPUNative.wgpuSurfaceGetCapabilities(surface, adapter, capabilities); + + /// + /// Frees members allocated by a surface-capabilities query. + /// + /// The surface-capabilities result to release. + public void SurfaceCapabilitiesFreeMembers(WGPUSurfaceCapabilities capabilities) + => WebGPUNative.wgpuSurfaceCapabilitiesFreeMembers(capabilities); + + /// + /// Acquires the surface's current texture. + /// + /// The surface to acquire from. + /// Receives the acquired texture and status. + public void SurfaceGetCurrentTexture(WGPUSurfaceImpl* surface, WGPUSurfaceTexture* surfaceTexture) + => WebGPUNative.wgpuSurfaceGetCurrentTexture(surface, surfaceTexture); + + /// + /// Presents the surface's current texture. + /// + /// The surface to present. + /// The presentation status. + public WGPUStatus SurfacePresent(WGPUSurfaceImpl* surface) + => WebGPUNative.wgpuSurfacePresent(surface); + + /// + /// Creates a texture view. + /// + /// The source texture. + /// The view configuration, or for defaults. + /// The created view. + public WGPUTextureViewImpl* TextureCreateView(WGPUTextureImpl* texture, WGPUTextureViewDescriptor* descriptor) + => WebGPUNative.wgpuTextureCreateView(texture, descriptor); + + /// + /// Releases a bind group. + /// + /// The bind group to release. + public void BindGroupRelease(WGPUBindGroupImpl* bindGroup) => WebGPUNative.wgpuBindGroupRelease(bindGroup); + + /// + /// Releases a bind-group layout. + /// + /// The layout to release. + public void BindGroupLayoutRelease(WGPUBindGroupLayoutImpl* layout) => WebGPUNative.wgpuBindGroupLayoutRelease(layout); + + /// + /// Releases a buffer. + /// + /// The buffer to release. + public void BufferRelease(WGPUBufferImpl* buffer) => WebGPUNative.wgpuBufferRelease(buffer); + + /// + /// Returns a read-only mapped buffer range. + /// + /// The mapped buffer. + /// The byte offset. + /// The byte length. + /// The first mapped byte. + public void* BufferGetConstMappedRange(WGPUBufferImpl* buffer, nuint offset, nuint size) + => WebGPUNative.wgpuBufferGetConstMappedRange(buffer, offset, size); + + /// + /// Maps a buffer range asynchronously. + /// + /// The buffer to map. + /// The requested CPU access. + /// The first mapped byte. + /// The mapped byte length. + /// The rooted completion callback. + /// The context pointer passed to the callback. + public void BufferMapAsync( + WGPUBufferImpl* buffer, + MapMode mode, + nuint offset, + nuint size, + WebGPUBufferMapCallback callback, + void* userData) + { + WGPUBufferMapCallbackInfo callbackInfo = new() + { + mode = WGPUCallbackMode.AllowSpontaneous, + callback = callback.Pointer, + userdata1 = userData + }; + + callback.RegisterInvocation(); + + try + { + _ = WebGPUNative.wgpuBufferMapAsync(buffer, (ulong)mode, offset, size, callbackInfo); + } + catch + { + callback.CancelInvocation(); + throw; + } + } + + /// + /// Unmaps a buffer. + /// + /// The buffer to unmap. + public void BufferUnmap(WGPUBufferImpl* buffer) => WebGPUNative.wgpuBufferUnmap(buffer); + + /// + /// Releases a command buffer. + /// + /// The command buffer to release. + public void CommandBufferRelease(WGPUCommandBufferImpl* commandBuffer) => WebGPUNative.wgpuCommandBufferRelease(commandBuffer); + + /// + /// Releases a command encoder. + /// + /// The command encoder to release. + public void CommandEncoderRelease(WGPUCommandEncoderImpl* encoder) => WebGPUNative.wgpuCommandEncoderRelease(encoder); + + /// + /// Releases a compute-pass encoder. + /// + /// The encoder to release. + public void ComputePassEncoderRelease(WGPUComputePassEncoderImpl* encoder) => WebGPUNative.wgpuComputePassEncoderRelease(encoder); + + /// + /// Releases a compute pipeline. + /// + /// The pipeline to release. + public void ComputePipelineRelease(WGPUComputePipelineImpl* pipeline) => WebGPUNative.wgpuComputePipelineRelease(pipeline); + + /// + /// Releases a pipeline layout. + /// + /// The layout to release. + public void PipelineLayoutRelease(WGPUPipelineLayoutImpl* layout) => WebGPUNative.wgpuPipelineLayoutRelease(layout); + + /// + /// Releases a render-pass encoder. + /// + /// The encoder to release. + public void RenderPassEncoderRelease(WGPURenderPassEncoderImpl* encoder) => WebGPUNative.wgpuRenderPassEncoderRelease(encoder); + + /// + /// Releases a render pipeline. + /// + /// The pipeline to release. + public void RenderPipelineRelease(WGPURenderPipelineImpl* pipeline) => WebGPUNative.wgpuRenderPipelineRelease(pipeline); + + /// + /// Releases a shader module. + /// + /// The module to release. + public void ShaderModuleRelease(WGPUShaderModuleImpl* module) => WebGPUNative.wgpuShaderModuleRelease(module); + + /// + /// Releases a surface. + /// + /// The surface to release. + public void SurfaceRelease(WGPUSurfaceImpl* surface) => WebGPUNative.wgpuSurfaceRelease(surface); + + /// + /// Releases a texture. + /// + /// The texture to release. + public void TextureRelease(WGPUTextureImpl* texture) => WebGPUNative.wgpuTextureRelease(texture); + + /// + /// Releases a texture view. + /// + /// The view to release. + public void TextureViewRelease(WGPUTextureViewImpl* view) => WebGPUNative.wgpuTextureViewRelease(view); + + /// + /// Registers the process-wide wgpu-native logging callback. + /// + /// The unmanaged logging callback. + /// The context pointer passed to the callback. + public void SetLogCallback( + delegate* unmanaged[Cdecl] callback, + void* userData) + => WebGPUNative.wgpuSetLogCallback(callback, userData); + + /// + /// Sets the process-wide wgpu-native log level. + /// + /// The minimum emitted log level. + public void SetLogLevel(WGPULogLevel level) => WebGPUNative.wgpuSetLogLevel(level); +} + +#pragma warning restore CA1822 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUAdapterHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUAdapterHandle.cs index 95138edc0..2b5f37770 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUAdapterHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUAdapterHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -24,7 +24,7 @@ internal sealed unsafe class WebGPUAdapterHandle : WebGPUHandle /// when this wrapper owns the adapter and must release it; /// when the caller retains ownership. /// - internal WebGPUAdapterHandle(WebGPU? api, nint adapterHandle, bool ownsHandle) + public WebGPUAdapterHandle(WebGPU? api, nint adapterHandle, bool ownsHandle) : base(adapterHandle, ownsHandle) => this.api = api; @@ -33,7 +33,7 @@ protected override bool ReleaseHandle() { try { - this.api?.AdapterRelease((Adapter*)this.handle); + this.api?.AdapterRelease((WGPUAdapterImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropAcrylicLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropAcrylicLayerEffect.cs new file mode 100644 index 000000000..ebaf47eb7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropAcrylicLayerEffect.cs @@ -0,0 +1,81 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU blurs and tints the backdrop with shader +/// passes; a CPU backend applies the equivalent . +/// +public sealed class WebGPUBackdropAcrylicLayerEffect : WebGPUBackdropShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels. + /// The tint blended over the blurred backdrop. + public WebGPUBackdropAcrylicLayerEffect(float sigma, Color tint) + : base( + new BackdropAcrylicLayerEffect(sigma, tint), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Sigma = sigma; + this.Tint = tint; + this.AddPasses(sigma, tint); + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the tint blended over the blurred backdrop. + /// + public Color Tint { get; } + + /// + /// Adds the blur and tint passes which implement the acrylic effect. + /// + /// The Gaussian blur sigma, in pixels. + /// The acrylic tint. + private void AddPasses(float sigma, Color color) + { + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + // Source RGB is retained by (1 - tint alpha), while the constant row contributes the + // premultiplied tint. Alpha is preserved so the backdrop keeps its existing coverage. + float keep = 1F - vector.W; + ColorMatrix tint = default; + tint.M11 = keep; + tint.M22 = keep; + tint.M33 = keep; + tint.M44 = 1F; + tint.M51 = vector.X * vector.W; + tint.M52 = vector.Y * vector.W; + tint.M53 = vector.Z * vector.W; + + WebGPUColorMatrixLayerEffect tintEffect = new(tint); + ReadOnlySpan tintPasses = tintEffect.GetShaderPasses(); + + if (sigma == 0) + { + this.AddShaderPasses(tintPasses); + return; + } + + WebGPUGaussianBlurLayerEffect blurEffect = new(sigma); + ReadOnlySpan blurPasses = blurEffect.GetShaderPasses(); + + // Acrylic first blurs the captured backdrop, then blends the tint over that result. + this.AddShaderPasses(blurPasses); + this.AddShaderPasses(tintPasses); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropColorMatrixLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropColorMatrixLayerEffect.cs new file mode 100644 index 000000000..a8f20e2b9 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropColorMatrixLayerEffect.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU transforms the backdrop with a shader; +/// a CPU backend applies the equivalent . +/// +public sealed class WebGPUBackdropColorMatrixLayerEffect : WebGPUBackdropShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The colour matrix applied to the backdrop. + public WebGPUBackdropColorMatrixLayerEffect(ColorMatrix matrix) + : base( + new BackdropColorMatrixLayerEffect(matrix), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Matrix = matrix; + + // Backdrop capture changes where the source comes from, not how a color matrix transforms + // it. Copy the content adapter's pass into this effect's base-owned sequence. + WebGPUColorMatrixLayerEffect shaderEffect = new(matrix); + this.AddShaderPasses(shaderEffect.GetShaderPasses()); + } + + /// + /// Gets the colour matrix applied to the backdrop. + /// + public ColorMatrix Matrix { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropDropShadowLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropDropShadowLayerEffect.cs new file mode 100644 index 000000000..6be976732 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropDropShadowLayerEffect.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU creates the backdrop shadow with shader +/// passes; a CPU backend applies the equivalent . +/// +public sealed class WebGPUBackdropDropShadowLayerEffect : WebGPUBackdropShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The shadow offset, in pixels. + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// The shadow colour. + public WebGPUBackdropDropShadowLayerEffect(Point offset, float sigma, Color color) + : base( + new BackdropDropShadowLayerEffect(offset, sigma, color), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + this.AddPasses(sigma, color); + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + /// Adds the tint and blur passes which form the backdrop shadow image. + /// + /// The Gaussian blur sigma, in pixels. + /// The shadow colour. + private void AddPasses(float sigma, Color color) + { + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + // Zero RGB rows replace the backdrop colour with the constant shadow colour. Scaling alpha + // by the colour alpha preserves the backdrop silhouette at the requested shadow opacity. + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + WebGPUColorMatrixLayerEffect tintEffect = new(tint); + ReadOnlySpan tintPasses = tintEffect.GetShaderPasses(); + + if (sigma == 0) + { + this.AddShaderPasses(tintPasses); + return; + } + + WebGPUGaussianBlurLayerEffect blurEffect = new(sigma); + ReadOnlySpan blurPasses = blurEffect.GetShaderPasses(); + + // Tinting before blur spreads only the requested constant shadow colour. The inherited + // write-back metadata applies Offset after these passes, so it is not a shader uniform. + this.AddShaderPasses(tintPasses); + this.AddShaderPasses(blurPasses); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropGaussianBlurLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropGaussianBlurLayerEffect.cs new file mode 100644 index 000000000..cde85cdd7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropGaussianBlurLayerEffect.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU blurs the backdrop with separable +/// shader passes; a CPU backend applies the equivalent . +/// +public sealed class WebGPUBackdropGaussianBlurLayerEffect : WebGPUBackdropShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels; zero leaves the backdrop unchanged. + public WebGPUBackdropGaussianBlurLayerEffect(float sigma) + : base( + new BackdropBlurLayerEffect(sigma), + WebGPUGaussianBlurLayerEffect.ShaderSource, + WebGPUGaussianBlurLayerEffect.UniformLayout) + { + this.Sigma = sigma; + + // Backdrop capture changes the source snapshot, but Gaussian sampling is otherwise + // identical. Copy the content adapter's passes into this effect's base-owned sequence. + WebGPUGaussianBlurLayerEffect shaderEffect = new(sigma); + this.AddShaderPasses(shaderEffect.GetShaderPasses()); + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropShaderLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropShaderLayerEffect.cs new file mode 100644 index 000000000..48aa4781e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUBackdropShaderLayerEffect.cs @@ -0,0 +1,143 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Defines a backdrop effect with complete WGSL shader passes and an equivalent ImageSharp CPU fallback. +/// +/// +/// Instances work with both drawing backends. WebGPU executes the configured WGSL passes, while CPU rendering +/// executes the required fallback effect. Source follows the same complete-shader contract as +/// . +/// +public abstract class WebGPUBackdropShaderLayerEffect : BackdropLayerEffect, IWebGPUShaderEffect, IWebGPUShaderEffectSource +{ + private readonly string shaderSource; + private readonly WebGPUShaderUniformLayout uniformLayout; + private WebGPUShaderProgram? program; + private WebGPUShaderPass[] shaderPasses = []; + private int shaderPassCount; + + /// + /// Initializes a new instance of the class. + /// + /// The complete WGSL source for each pass added by this effect. + /// The named uniform values available to the shader. + /// The equivalent operation used by CPU rendering. + protected WebGPUBackdropShaderLayerEffect( + string shaderSource, + WebGPUShaderUniformLayout uniformLayout, + Action fallback) + : base(fallback) + { + this.shaderSource = shaderSource; + this.uniformLayout = uniformLayout; + } + + /// + /// Initializes a new instance of the class. + /// + /// The equivalent backdrop effect used by CPU rendering. + /// The complete WGSL source for each pass added by this effect. + /// The named uniform values available to the shader. + protected WebGPUBackdropShaderLayerEffect( + BackdropLayerEffect fallbackEffect, + string shaderSource, + WebGPUShaderUniformLayout uniformLayout) + : base(fallbackEffect) + { + this.shaderSource = shaderSource; + this.uniformLayout = uniformLayout; + } + + /// + /// Gets the shared primary program when the first pass is configured. + /// + private WebGPUShaderProgram Program + => this.program ??= WebGPUShaderProgram.GetOrCreate(this.shaderSource, this.uniformLayout); + + /// + /// Configures and adds an invocation of the complete shader source to this effect's ordered pass sequence. + /// + /// The action that assigns this pass's named uniform values. + protected void AddShaderPass(Action configureUniforms) + { + Guard.NotNull(configureUniforms, nameof(configureUniforms)); + + // The base type owns layout, program, and immutable snapshot plumbing. Derived effects only + // assign the values their WGSL declares; the action executes synchronously and is not retained. + WebGPUShaderUniformBuilder uniforms = this.uniformLayout.CreateUniforms(); + configureUniforms(uniforms); + this.AddShaderPass(new WebGPUShaderPass(this.Program, uniforms.Build())); + } + + /// + /// Configures and adds an invocation whose filtered samples use the supplied border modes. + /// + /// The wrapping mode applied beyond the horizontal input borders. + /// The wrapping mode applied beyond the vertical input borders. + /// The action that assigns this pass's named uniform values. + protected void AddShaderPass( + BorderWrappingMode xBorderMode, + BorderWrappingMode yBorderMode, + Action configureUniforms) + { + Guard.NotNull(configureUniforms, nameof(configureUniforms)); + + // Border behavior belongs to an individual pass because one effect may combine shaders with + // different sampling contracts. Module specialization removes the selection from the pixel loop. + WebGPUShaderUniformBuilder uniforms = this.uniformLayout.CreateUniforms(); + configureUniforms(uniforms); + this.AddShaderPass(new WebGPUShaderPass(this.Program, uniforms.Build(), xBorderMode, yBorderMode)); + } + + /// + /// Adds internal passes owned by another built-in effect. + /// + private protected void AddShaderPasses(ReadOnlySpan shaderPasses) + { + this.EnsureShaderPassCapacity(shaderPasses.Length); + shaderPasses.CopyTo(this.shaderPasses.AsSpan(this.shaderPassCount)); + this.shaderPassCount += shaderPasses.Length; + } + + /// + /// Gets the ordered internal pass sequence. + /// + internal ReadOnlySpan GetShaderPasses() + => this.shaderPasses.AsSpan(0, this.shaderPassCount); + + /// + ReadOnlySpan IWebGPUShaderEffectSource.GetShaderPasses() + => this.GetShaderPasses(); + + /// + /// Adds one internal pass to the sequence. + /// + private void AddShaderPass(WebGPUShaderPass shaderPass) + { + this.EnsureShaderPassCapacity(1); + this.shaderPasses[this.shaderPassCount++] = shaderPass; + } + + /// + /// Ensures that the pass storage can append the requested number of entries. + /// + private void EnsureShaderPassCapacity(int additionalCount) + { + int requiredCapacity = checked(this.shaderPassCount + additionalCount); + if (requiredCapacity <= this.shaderPasses.Length) + { + return; + } + + // Four entries cover every built-in sequence in one allocation. Larger internal + // sequences double geometrically so repeated additions remain amortized O(1). + int grownCapacity = this.shaderPasses.Length == 0 ? 4 : checked(this.shaderPasses.Length * 2); + Array.Resize(ref this.shaderPasses, Math.Max(requiredCapacity, grownCapacity)); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCanvasFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCanvasFactory.cs index e38a2cbea..f585fd0f4 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUCanvasFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCanvasFactory.cs @@ -6,57 +6,194 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Creates typed canvas objects for the fixed set of WebGPU texture formats. +/// Creates typed canvas objects for the supported WebGPU target descriptors. /// +/// +/// The texture format selects the channel layout and component encoding. The alpha representation +/// selects the associated or unassociated CLR pixel type with that physical layout. +/// internal static class WebGPUCanvasFactory { /// /// Creates a typed canvas over a WebGPU native surface. /// - internal static DrawingCanvas CreateCanvas( + /// The active processing configuration. + /// The drawing options for the canvas. + /// The drawing backend the canvas renders through. + /// The canvas bounds within the surface. + /// The WebGPU native surface backing the canvas. + /// The surface texture format and alpha representation that select the canvas pixel type. + /// The typed drawing canvas. + public static DrawingCanvas CreateCanvas( Configuration configuration, DrawingOptions options, IDrawingBackend backend, Rectangle bounds, NativeSurface surface, - WebGPUTextureFormat format) -#pragma warning disable CS8524 - => format switch + WebGPUTargetDescriptor targetDescriptor) + + // CS8524 (unnamed enum values) is suppressed rather than adding a discard arm: + // WebGPUTextureFormat is a closed set and every named value is matched, so an + // out-of-range value can only come from invalid casting elsewhere. +#pragma warning disable CS8509, CS8524 + => (targetDescriptor.Format, targetDescriptor.AlphaRepresentation) switch { - WebGPUTextureFormat.Rgba8Unorm => CreateCanvas( + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated) => CreateCanvas( + configuration, + options, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated) => CreateCanvas( configuration, options, backend, bounds, surface), - WebGPUTextureFormat.Bgra8Unorm => CreateCanvas( + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( configuration, options, backend, bounds, surface), - WebGPUTextureFormat.Rgba8Snorm => CreateCanvas( + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated) => CreateCanvas( configuration, options, backend, bounds, surface), - WebGPUTextureFormat.Rgba16Float => CreateCanvas( + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated) => CreateCanvas( configuration, options, backend, bounds, surface) }; -#pragma warning restore CS8524 +#pragma warning restore CS8509, CS8524 + + /// + /// Creates a typed canvas over a WebGPU native surface with a shared text cache. + /// + /// The active processing configuration. + /// The drawing options for the canvas. + /// The reusable text drawing cache shared across canvases. + /// The drawing backend the canvas renders through. + /// The canvas bounds within the surface. + /// The WebGPU native surface backing the canvas. + /// The surface texture format and alpha representation that select the canvas pixel type. + /// The typed drawing canvas. + public static DrawingCanvas CreateCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + IDrawingBackend backend, + Rectangle bounds, + NativeSurface surface, + WebGPUTargetDescriptor targetDescriptor) + + // See the comment on the overload above for why CS8524 is suppressed. +#pragma warning disable CS8509, CS8524 + => (targetDescriptor.Format, targetDescriptor.AlphaRepresentation) switch + { + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface), + + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated) => CreateCanvas( + configuration, + options, + textCache, + backend, + bounds, + surface) + }; +#pragma warning restore CS8509, CS8524 /// /// Creates a typed frame over a WebGPU native surface. /// - internal static NativeCanvasFrame CreateFrame( + /// The pixel format matching the surface texture format. + /// The frame bounds within the surface. + /// The WebGPU native surface backing the frame. + /// The typed native canvas frame. + public static NativeCanvasFrame CreateFrame( Rectangle bounds, NativeSurface surface) where TPixel : unmanaged, IPixel @@ -65,6 +202,13 @@ internal static NativeCanvasFrame CreateFrame( /// /// Creates a typed drawing canvas over an already selected WebGPU frame format. /// + /// The pixel format matching the surface texture format. + /// The active processing configuration. + /// The drawing options for the canvas. + /// The drawing backend the canvas renders through. + /// The canvas bounds within the surface. + /// The WebGPU native surface backing the canvas. + /// The typed drawing canvas. private static DrawingCanvas CreateCanvas( Configuration configuration, DrawingOptions options, @@ -73,4 +217,25 @@ private static DrawingCanvas CreateCanvas( NativeSurface surface) where TPixel : unmanaged, IPixel => new(configuration, options, backend, CreateFrame(bounds, surface)); + + /// + /// Creates a typed drawing canvas over an already selected WebGPU frame format with a shared text cache. + /// + /// The pixel format matching the surface texture format. + /// The active processing configuration. + /// The drawing options for the canvas. + /// The reusable text drawing cache shared across canvases. + /// The drawing backend the canvas renders through. + /// The canvas bounds within the surface. + /// The WebGPU native surface backing the canvas. + /// The typed drawing canvas. + private static DrawingCanvas CreateCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + IDrawingBackend backend, + Rectangle bounds, + NativeSurface surface) + where TPixel : unmanaged, IPixel + => new(configuration, options, textCache, backend, CreateFrame(bounds, surface)); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs new file mode 100644 index 000000000..51c68dbe6 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUColorMatrixLayerEffect.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU transforms the layer with a shader; +/// a CPU backend applies the equivalent . +/// +public sealed class WebGPUColorMatrixLayerEffect : WebGPUShaderLayerEffect +{ + internal const string ShaderSource = """ + fn layer_effect(position: vec2) -> vec4 { + + // ImageSharp colour matrices operate on logical unassociated RGBA components. + let source = layer_load_unassociated(vec2(position)); + + // The translation row is supplied separately because WGSL exposes only a 4x4 matrix. + // Clamping matches ImageSharp's filter conversion before the result is stored. + let transformed = clamp( + source * imagesharp_uniforms.matrix + imagesharp_uniforms.offset, + vec4(0.0), + vec4(1.0)); + + // Effect working textures use associated alpha, so reassociate exactly once here. + return vec4(transformed.rgb * transformed.a, transformed.a); + } + """; + + internal static readonly WebGPUShaderUniformLayout UniformLayout = new( + [ + new WebGPUShaderUniform("matrix", WebGPUShaderUniformType.Matrix4x4, 1), + new WebGPUShaderUniform("offset", WebGPUShaderUniformType.Vector4, 1) + ]); + + /// + /// Initializes a new instance of the class. + /// + /// The colour matrix applied to the layer content. + public WebGPUColorMatrixLayerEffect(ColorMatrix matrix) + : base(new ColorMatrixLayerEffect(matrix), ShaderSource, UniformLayout) + { + this.Matrix = matrix; + + // ImageSharp stores its affine colour transform as a 5x4 matrix. The upper 4x4 block maps + // directly to WGSL's row-vector multiplication; M51-M54 form the separate translation row. + Matrix4x4 transform = new(matrix.M11, matrix.M12, matrix.M13, matrix.M14, matrix.M21, matrix.M22, matrix.M23, matrix.M24, matrix.M31, matrix.M32, matrix.M33, matrix.M34, matrix.M41, matrix.M42, matrix.M43, matrix.M44); + + // A pass retains an immutable uniform snapshot, so later instances cannot alter this matrix. + this.AddShaderPass(uniforms => + { + uniforms.SetMatrix4x4("matrix", transform); + uniforms.SetVector4("offset", new Vector4(matrix.M51, matrix.M52, matrix.M53, matrix.M54)); + }); + } + + /// + /// Gets the colour matrix applied to the layer content. + /// + public ColorMatrix Matrix { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeAlphaMode.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeAlphaMode.cs new file mode 100644 index 000000000..eb8f4146e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeAlphaMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Specifies how the native compositor interprets alpha in a WebGPU surface. +/// +public enum WebGPUCompositeAlphaMode +{ + /// + /// Allows WebGPU to select the surface composition mode. + /// + Auto, + + /// + /// Ignores the surface alpha channel and presents every pixel as opaque. + /// + Opaque, + + /// + /// Presents color components that have already been multiplied by alpha. + /// + Premultiplied = 2, +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs index 1d5ee20cf..caf6cfbfd 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUCompositeBindGroupLayoutFactory.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -15,6 +15,6 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// if the layout was created; otherwise . internal unsafe delegate bool WebGPUCompositeBindGroupLayoutFactory( WebGPU api, - Device* device, - out BindGroupLayout* bindGroupLayout, + WGPUDeviceImpl* device, + out WGPUBindGroupLayoutImpl* bindGroupLayout, out string? error); diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext.cs index aad6b2891..09eec1dd2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceContext.cs @@ -1,25 +1,19 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Internal WebGPU device/queue binding used by render targets and surface resources. +/// Owns the WebGPU device-scoped drawing state shared by render targets, surfaces, shader programs, and pipelines. /// -internal sealed class WebGPUDeviceContext : IDisposable +public sealed class WebGPUDeviceContext { private bool isDisposed; /// - /// Initializes a new instance of the class over the shared process-level WebGPU device. - /// - internal WebGPUDeviceContext() - : this(Configuration.Default) - { - } - - /// - /// Initializes a new instance of the class over the shared process-level WebGPU device. + /// Initializes a new instance of the class. /// /// The configuration instance to bind to the created backend. internal WebGPUDeviceContext(Configuration configuration) @@ -43,6 +37,11 @@ internal WebGPUDeviceContext(Configuration configuration) this.DeviceHandle = deviceHandle; this.QueueHandle = queueHandle; this.Configuration = configuration; + + // Device-scoped shared state owns the uncaptured-error callback. Install it now, + // matching the wrapped-handle constructor, so GPU errors raised before the first + // flush or readback are still reported instead of silently dropped. + _ = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), deviceHandle); } catch { @@ -52,50 +51,15 @@ internal WebGPUDeviceContext(Configuration configuration) } /// - /// Initializes a new instance of the class over externally-owned device and queue handles. - /// - /// The external WebGPU device handle. - /// The external WebGPU queue handle. - /// - /// These handles must originate from the same process WebGPU runtime used by ImageSharp.Drawing.WebGPU. - /// The context does not take ownership of them. - /// - internal WebGPUDeviceContext(nint deviceHandle, nint queueHandle) - : this(Configuration.Default, deviceHandle, queueHandle) - { - } - - /// - /// Initializes a new instance of the class over externally-owned device and queue handles. - /// - /// The configuration instance to bind to the created backend. - /// The external WebGPU device handle. - /// The external WebGPU queue handle. - /// - /// These handles must originate from the same process WebGPU runtime used by ImageSharp.Drawing.WebGPU. - /// The context does not take ownership of them. - /// - internal WebGPUDeviceContext(Configuration configuration, nint deviceHandle, nint queueHandle) - : this(configuration, CreateExternalDeviceHandle(deviceHandle), CreateExternalQueueHandle(queueHandle)) - { - } - - /// - /// Initializes a new instance of the class over wrapped device and queue handles using the default configuration. - /// - /// The wrapped WebGPU device handle. - /// The wrapped WebGPU queue handle. - internal WebGPUDeviceContext(WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle) - : this(Configuration.Default, deviceHandle, queueHandle) - { - } - - /// - /// Initializes a new instance of the class over externally-owned device and queue handles. + /// Initializes a new instance of the class over already-wrapped device and queue handles. /// /// The configuration instance to bind to the created backend. /// The wrapped WebGPU device handle. /// The wrapped WebGPU queue handle. + /// + /// The context stores the handles without taking ownership; native lifetime is controlled + /// by the handle wrappers themselves. + /// internal WebGPUDeviceContext(Configuration configuration, WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle) { Guard.NotNull(configuration, nameof(configuration)); @@ -116,13 +80,13 @@ internal WebGPUDeviceContext(Configuration configuration, WebGPUDeviceHandle dev /// /// Gets the configuration provided when the context was created. /// - internal Configuration Configuration { get; } + public Configuration Configuration { get; } /// /// Gets the WebGPU drawing backend owned by this context. /// Use this to inspect per-flush diagnostics for chunked rendering. /// - internal WebGPUDrawingBackend Backend { get; } + public WebGPUDrawingBackend Backend { get; } /// /// Gets the wrapped WebGPU device handle used by frames, canvases, and render-target allocation created from this context. @@ -135,95 +99,47 @@ internal WebGPUDeviceContext(Configuration configuration, WebGPUDeviceHandle dev internal WebGPUQueueHandle QueueHandle { get; } /// - /// Creates an owned offscreen WebGPU render target using the default RGBA8 texture format. - /// - /// The target width in pixels. - /// The target height in pixels. - /// An owned offscreen WebGPU render target. - internal WebGPURenderTarget CreateRenderTarget(int width, int height) - => this.CreateRenderTarget(WebGPUTextureFormat.Rgba8Unorm, width, height); - - /// - /// Creates an owned offscreen WebGPU render target for this context. + /// Compiles and caches every pipeline used by a shader effect on this WebGPU device. /// - /// The target texture format. - /// The target width in pixels. - /// The target height in pixels. - /// An owned offscreen WebGPU render target. - internal WebGPURenderTarget CreateRenderTarget( - WebGPUTextureFormat format, - int width, - int height) + /// The shader effect to compile. + /// + /// Call this before the effect's first rendered frame when shader compilation latency must not occur during presentation. + /// + public void Precompile(IWebGPUShaderEffect effect) { this.ThrowIfDisposed(); - Guard.MustBeGreaterThan(width, 0, nameof(width)); - Guard.MustBeGreaterThan(height, 0, nameof(height)); + Guard.NotNull(effect, nameof(effect)); - return WebGPURenderTarget.CreateFromContext(this, format, width, height); - } + if (effect is not IWebGPUShaderEffectSource effectSource) + { + throw new ArgumentException( + "Shader effects must derive from WebGPUShaderLayerEffect or WebGPUBackdropShaderLayerEffect.", + nameof(effect)); + } - /// - /// Creates a drawing canvas that renders directly into an externally-owned WebGPU texture. - /// - /// The external WebGPU texture handle. - /// The external WebGPU texture-view handle. - /// The texture format. - /// The frame width in pixels. - /// The frame height in pixels. - /// A drawing canvas targeting the external texture. - /// - /// The caller retains ownership of the texture and view; this context does not release them. - /// The texture must have been created with RenderAttachment | CopySrc | CopyDst | TextureBinding usage. - /// Dispose the returned canvas before the host calls wgpuSurfacePresent, then create a new canvas on the next frame. - /// - internal DrawingCanvas CreateCanvas( - nint textureHandle, - nint textureViewHandle, - WebGPUTextureFormat format, - int width, - int height) - => this.CreateCanvas( - new DrawingOptions(), - CreateExternalTextureHandle(textureHandle), - CreateExternalTextureViewHandle(textureViewHandle), - format, - width, - height); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), this.DeviceHandle); - /// - /// Creates a drawing canvas that renders directly into an externally-owned WebGPU texture. - /// - /// The initial drawing options. - /// The external WebGPU texture handle. - /// The external WebGPU texture-view handle. - /// The texture format. - /// The frame width in pixels. - /// The frame height in pixels. - /// A drawing canvas targeting the external texture. - /// - /// The caller retains ownership of the texture and view; this context does not release them. - /// The texture must have been created with RenderAttachment | CopySrc | CopyDst | TextureBinding usage. - /// Dispose the returned canvas before the host calls wgpuSurfacePresent, then create a new canvas on the next frame. - /// - internal DrawingCanvas CreateCanvas( - DrawingOptions options, - nint textureHandle, - nint textureViewHandle, - WebGPUTextureFormat format, - int width, - int height) - => this.CreateCanvas( - options, - CreateExternalTextureHandle(textureHandle), - CreateExternalTextureViewHandle(textureViewHandle), - format, - width, - height); + // Programs are specialized only for the source's numeric encoding and alpha association. + // Precompile all four semantic combinations once so this device-scoped operation is valid + // for every offscreen or presentation target without exposing internal texture descriptors. + ReadOnlySpan sourceDescriptors = + [ + new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), + new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit), + new(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.SignedUnit), + new(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.SignedUnit) + ]; + + foreach (WebGPUTargetDescriptor sourceDescriptor in sourceDescriptors) + { + deviceState.PrecompileEffect(effectSource, sourceDescriptor); + } + } /// /// Disposes the drawing backend owned by this context. /// - public void Dispose() + internal void Dispose() { if (this.isDisposed) { @@ -239,69 +155,4 @@ public void Dispose() /// internal void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); - - /// - /// Creates a drawing canvas over wrapped texture handles that are already in this assembly's ownership model. - /// - internal DrawingCanvas CreateCanvas( - DrawingOptions options, - WebGPUTextureHandle textureHandle, - WebGPUTextureViewHandle textureViewHandle, - WebGPUTextureFormat format, - int width, - int height) - { - Rectangle bounds = new(0, 0, width, height); - WebGPUNativeSurface surface = this.CreateSurface(textureHandle, textureViewHandle, format, width, height); - - return WebGPUCanvasFactory.CreateCanvas(this.Configuration, options, this.Backend, bounds, surface, format); - } - - /// - /// Creates the wrapped native surface over the supplied texture handles. - /// - private WebGPUNativeSurface CreateSurface( - WebGPUTextureHandle textureHandle, - WebGPUTextureViewHandle textureViewHandle, - WebGPUTextureFormat format, - int width, - int height) - { - this.ThrowIfDisposed(); - Guard.NotNull(textureHandle, nameof(textureHandle)); - Guard.NotNull(textureViewHandle, nameof(textureViewHandle)); - - return WebGPUNativeSurface.Create( - this.DeviceHandle, - this.QueueHandle, - textureHandle, - textureViewHandle, - format, - width, - height); - } - - /// - /// Wraps one externally-owned device handle without taking ownership. - /// - private static WebGPUDeviceHandle CreateExternalDeviceHandle(nint deviceHandle) - => new(deviceHandle, ownsHandle: false); - - /// - /// Wraps one externally-owned queue handle without taking ownership. - /// - private static WebGPUQueueHandle CreateExternalQueueHandle(nint queueHandle) - => new(queueHandle, ownsHandle: false); - - /// - /// Wraps one externally-owned texture handle without taking ownership. - /// - private static WebGPUTextureHandle CreateExternalTextureHandle(nint textureHandle) - => new(textureHandle, ownsHandle: false); - - /// - /// Wraps one externally-owned texture-view handle without taking ownership. - /// - private static WebGPUTextureViewHandle CreateExternalTextureViewHandle(nint textureViewHandle) - => new(textureViewHandle, ownsHandle: false); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceHandle.cs index 730cbbbab..35420f198 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDeviceHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -20,7 +20,7 @@ internal sealed unsafe class WebGPUDeviceHandle : WebGPUHandle /// when this wrapper owns the device and must release it; /// when the caller retains ownership. /// - internal WebGPUDeviceHandle(nint deviceHandle, bool ownsHandle) + public WebGPUDeviceHandle(nint deviceHandle, bool ownsHandle) : this(ownsHandle ? WebGPURuntime.GetApi() : null, deviceHandle, ownsHandle) { } @@ -37,7 +37,7 @@ internal WebGPUDeviceHandle(nint deviceHandle, bool ownsHandle) /// when this wrapper owns the device and must release it; /// when the caller retains ownership. /// - internal WebGPUDeviceHandle(WebGPU? api, nint deviceHandle, bool ownsHandle) + public WebGPUDeviceHandle(WebGPU? api, nint deviceHandle, bool ownsHandle) : base(deviceHandle, ownsHandle) => this.api = api; @@ -46,7 +46,7 @@ protected override bool ReleaseHandle() { try { - this.api?.DeviceRelease((Device*)this.handle); + this.api?.DeviceRelease((WGPUDeviceImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs index 1989ad5b4..4ad9bf936 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CompositePixels.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -11,59 +11,99 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// Pixel-format registration for composite session I/O. /// /// -/// is intentionally explicit and only includes one-to-one format mappings +/// is intentionally explicit and only includes one-to-one format mappings /// where the GPU texture format can round-trip the pixel payload without channel swizzle or custom conversion logic. /// Only formats that support storage texture binding (required by the compute compositor) are included. /// Formats that lack storage support are omitted and fall back to the CPU backend. /// public sealed partial class WebGPUDrawingBackend { - private static readonly CompositePixelRegistration[] CompositeRegistrations = + private static readonly CompositeTextureRegistration[] CompositeTextureRegistrations = [ - CompositePixelRegistration.Create(TextureFormat.Rgba8Snorm, TextureSampleType.Float, new("rgba8snorm", CompositeTextureEncodingKind.Snorm)), - CompositePixelRegistration.Create(TextureFormat.Rgba16float, TextureSampleType.Float, new("rgba16float", CompositeTextureEncodingKind.Float)), - CompositePixelRegistration.Create(TextureFormat.Rgba8Unorm, TextureSampleType.Float, new("rgba8unorm", CompositeTextureEncodingKind.Float)), - CompositePixelRegistration.Create(TextureFormat.Bgra8Unorm, TextureSampleType.Float, new("bgra8unorm", CompositeTextureEncodingKind.Float), FeatureName.Bgra8UnormStorage), + new(WebGPUTextureFormat.Rgba8Snorm, WGPUTextureFormat.RGBA8Snorm, new("rgba8snorm"), WGPUFeatureName.TextureFormatsTier1), + new(WebGPUTextureFormat.Rgba16Float, WGPUTextureFormat.RGBA16Float, new("rgba16float"), default), + new(WebGPUTextureFormat.Rgba8Unorm, WGPUTextureFormat.RGBA8Unorm, new("rgba8unorm"), default), + + // Bgra8Unorm is not storage-bindable in core WebGPU; it requires the optional + // Bgra8UnormStorage device feature, checked at render and readback time. + new(WebGPUTextureFormat.Bgra8Unorm, WGPUTextureFormat.BGRA8Unorm, new("bgra8unorm"), WGPUFeatureName.BGRA8UnormStorage), ]; - /// - /// Describes how one registered composite texture format encodes channel values in shader space. - /// - internal enum CompositeTextureEncodingKind - { - Float, - Snorm - } + private static readonly CompositePixelRegistration[] CompositePixelRegistrations = + [ + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.SignedUnit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.SignedUnit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit), + CompositePixelRegistration.Create(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit), + ]; /// - /// Resolves the WebGPU texture format identifier and any required device feature + /// Resolves the WebGPU target descriptor and any required device feature /// for . /// /// The requested pixel type. - /// Receives the mapped texture format identifier on success. + /// Receives the mapped target descriptor on success. /// /// Receives the device feature required for storage binding, or - /// when no special feature is needed. + /// the default value when no special feature is needed. /// /// /// when the pixel type has a registered GPU format mapping; otherwise . /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool TryGetCompositeTextureFormat(out WebGPUTextureFormat formatId, out FeatureName requiredFeature) + internal static bool TryGetCompositeTargetDescriptor(out WebGPUTargetDescriptor descriptor, out WGPUFeatureName requiredFeature) where TPixel : unmanaged, IPixel { if (!TryFind(typeof(TPixel), out CompositePixelRegistration r)) { - formatId = default; - requiredFeature = FeatureName.Undefined; + descriptor = default; + requiredFeature = default; return false; } - formatId = WebGPUTextureFormatMapper.FromNative(r.TextureFormat); - requiredFeature = r.RequiredFeature; + descriptor = r.Descriptor; + requiredFeature = FindTexture(r.Descriptor.Format).RequiredFeature; return true; } + /// + /// Creates the descriptor used by an ImageSharp-owned offscreen pixel buffer. + /// + /// The physical texture format. + /// The alpha representation stored by the target. + /// The offscreen target descriptor. + internal static WebGPUTargetDescriptor CreateOffscreenTargetDescriptor(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation) + { + // NormalizedByte4 exposes unit values by remapping its signed native components. + // Offscreen SNORM targets must retain that ImageSharp pixel contract. + WebGPUTargetNumericEncoding numericEncoding = format == WebGPUTextureFormat.Rgba8Snorm + ? WebGPUTargetNumericEncoding.SignedUnit + : WebGPUTargetNumericEncoding.Unit; + + return new WebGPUTargetDescriptor(format, alphaRepresentation, numericEncoding); + } + + /// + /// Creates the descriptor used by an externally-owned or presentable native texture. + /// + /// The physical texture format. + /// The alpha representation stored by the target. + /// The native target descriptor. + internal static WebGPUTargetDescriptor CreateNativeTargetDescriptor(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation) + { + // WebGPU float surface values are ordinary floating-point colors. SNORM remains signed + // by definition of the native texture format and therefore still requires unit remapping. + WebGPUTargetNumericEncoding numericEncoding = format == WebGPUTextureFormat.Rgba8Snorm + ? WebGPUTargetNumericEncoding.SignedUnit + : WebGPUTargetNumericEncoding.Unit; + + return new WebGPUTargetDescriptor(format, alphaRepresentation, numericEncoding); + } + /// /// Resolves native format information for one public WebGPU texture format. /// @@ -73,23 +113,32 @@ internal static bool TryGetCompositeTextureFormat(out WebGPUTextureForma [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void GetCompositeTextureFormatInfo( WebGPUTextureFormat format, - out TextureFormat textureFormat, - out FeatureName requiredFeature) + out WGPUTextureFormat textureFormat, + out WGPUFeatureName requiredFeature) { - textureFormat = WebGPUTextureFormatMapper.ToNative(format); - requiredFeature = Find(textureFormat).RequiredFeature; + CompositeTextureRegistration registration = FindTexture(format); + textureFormat = registration.TextureFormat; + requiredFeature = registration.RequiredFeature; } /// /// Resolves the shader-side read/write traits for a registered composite texture format. /// + /// The native texture format. Must be one of the registered formats. + /// The shader traits registered for the format. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static CompositeTextureShaderTraits GetCompositeTextureShaderTraits(TextureFormat textureFormat) - => Find(textureFormat).ShaderTraits; + internal static CompositeTextureShaderTraits GetCompositeTextureShaderTraits(WGPUTextureFormat textureFormat) + => FindTexture(textureFormat).ShaderTraits; + /// + /// Finds the registration for a CLR pixel type. + /// + /// The pixel CLR type to look up. + /// Receives the matching registration on success. + /// when the pixel type is registered; otherwise . private static bool TryFind(Type pixelType, out CompositePixelRegistration registration) { - foreach (CompositePixelRegistration r in CompositeRegistrations) + foreach (CompositePixelRegistration r in CompositePixelRegistrations) { if (r.PixelType == pixelType) { @@ -102,89 +151,119 @@ private static bool TryFind(Type pixelType, out CompositePixelRegistration regis return false; } - private static CompositePixelRegistration Find(TextureFormat textureFormat) - => Array.Find(CompositeRegistrations, r => r.TextureFormat == textureFormat); + /// + /// Finds the registration for a public texture format. Callers must pass a registered format. + /// + /// The public texture format to look up. + /// The matching texture registration. + private static CompositeTextureRegistration FindTexture(WebGPUTextureFormat format) + => Array.Find(CompositeTextureRegistrations, r => r.Format == format); + + /// + /// Finds the registration for a native texture format. Callers must pass a registered format. + /// + /// The native texture format to look up. + /// The matching texture registration. + private static CompositeTextureRegistration FindTexture(WGPUTextureFormat textureFormat) + => Array.Find(CompositeTextureRegistrations, r => r.TextureFormat == textureFormat); /// /// Shader-facing traits derived from one registered composite texture format. /// - internal readonly struct CompositeTextureShaderTraits(string outputFormat, CompositeTextureEncodingKind encodingKind) + /// The WGSL storage-texture format token used for writes. + internal readonly struct CompositeTextureShaderTraits(string outputFormat) { /// /// Gets the WGSL storage-texture format token used for writes. /// public string OutputFormat { get; } = outputFormat; - - /// - /// Gets the numeric encoding that the shader must apply when storing output texels. - /// - public CompositeTextureEncodingKind EncodingKind { get; } = encodingKind; } /// - /// Per-pixel registration payload consumed by GPU composition setup. + /// Physical texture registration consumed by GPU composition setup. /// - private readonly struct CompositePixelRegistration + private readonly struct CompositeTextureRegistration { /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - /// The registered pixel CLR type. + /// The public texture format. /// The matching WebGPU texture format. - /// The sampled texture type for this format. - /// Optional device feature required for storage binding support. /// Shader-facing read/write traits for this format. - public CompositePixelRegistration( - Type pixelType, - TextureFormat textureFormat, - TextureSampleType sampleType, - FeatureName requiredFeature, - CompositeTextureShaderTraits shaderTraits) + /// Optional device feature required for storage binding support. + public CompositeTextureRegistration( + WebGPUTextureFormat format, + WGPUTextureFormat textureFormat, + CompositeTextureShaderTraits shaderTraits, + WGPUFeatureName requiredFeature) { - this.PixelType = pixelType; + this.Format = format; this.TextureFormat = textureFormat; - this.SampleType = sampleType; - this.RequiredFeature = requiredFeature; this.ShaderTraits = shaderTraits; + this.RequiredFeature = requiredFeature; } /// - /// Gets the CLR pixel type registered for this mapping. + /// Gets the public texture format. /// - public Type PixelType { get; } + public WebGPUTextureFormat Format { get; } /// /// Gets the WebGPU texture format used for this pixel type. /// - public TextureFormat TextureFormat { get; } + public WGPUTextureFormat TextureFormat { get; } /// - /// Gets the sampled texture type used when reading this format. + /// Gets the shader-facing read/write traits for this format. /// - public TextureSampleType SampleType { get; } + public CompositeTextureShaderTraits ShaderTraits { get; } /// /// Gets the optional device feature required for storage binding support. - /// means the format is natively storable. /// - public FeatureName RequiredFeature { get; } + public WGPUFeatureName RequiredFeature { get; } + } + /// + /// Per-pixel registration payload consumed by GPU composition setup. + /// + private readonly struct CompositePixelRegistration + { /// - /// Gets the shader-facing read/write traits for this format. + /// Initializes a new instance of the struct. /// - public CompositeTextureShaderTraits ShaderTraits { get; } + /// The registered pixel CLR type. + /// The target descriptor matching the pixel type. + public CompositePixelRegistration(Type pixelType, WebGPUTargetDescriptor descriptor) + { + this.PixelType = pixelType; + this.Descriptor = descriptor; + } /// - /// Creates a registration record for with a required device feature. + /// Gets the CLR pixel type registered for this mapping. /// - /// The matching WebGPU texture format. - /// The sampled texture type for this format. - /// Shader-facing read/write traits for this format. - /// The device feature required for storage binding. + public Type PixelType { get; } + + /// + /// Gets the target descriptor registered for this mapping. + /// + public WebGPUTargetDescriptor Descriptor { get; } + + /// + /// Creates a registration record for . + /// + /// The pixel type to register. + /// The matching WebGPU texture format. + /// The alpha representation stored by the pixel type. + /// The pixel type's mapping between native and unit channel values. /// The initialized registration. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static CompositePixelRegistration Create(TextureFormat textureFormat, TextureSampleType sampleType, CompositeTextureShaderTraits shaderTraits, FeatureName requiredFeature = FeatureName.Undefined) + public static CompositePixelRegistration Create( + WebGPUTextureFormat format, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) where TPixel : unmanaged, IPixel - => new(typeof(TPixel), textureFormat, sampleType, requiredFeature, shaderTraits); + => new(typeof(TPixel), new WebGPUTargetDescriptor(format, alphaRepresentation, numericEncoding)); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CopyPixels.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CopyPixels.cs new file mode 100644 index 000000000..905e55f9b --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.CopyPixels.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// GPU frame-copy helpers. Copies run entirely on the GPU as texture-to-texture commands; +/// both frames must share the same device and queue and use the pixel type's native format. +/// +public sealed unsafe partial class WebGPUDrawingBackend +{ + /// + public void CopyPixels( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame target, + Rectangle sourceRectangle, + Point targetPoint) + where TPixel : unmanaged, IPixel + { + this.ThrowIfDisposed(); + Guard.NotNull(configuration, nameof(configuration)); + + if (!source.TryGetNativeSurface(out NativeSurface? sourceSurface) || sourceSurface is not WebGPUNativeSurface nativeSource) + { + throw new NotSupportedException("The source frame does not expose a WebGPU native surface."); + } + + if (!target.TryGetNativeSurface(out NativeSurface? targetSurface) || targetSurface is not WebGPUNativeSurface nativeTarget) + { + throw new NotSupportedException("The target frame does not expose a WebGPU native surface."); + } + + if (nativeSource.DeviceHandle.IsInvalid || + nativeSource.QueueHandle.IsInvalid || + nativeSource.TargetTextureHandle.IsInvalid || + nativeTarget.DeviceHandle.IsInvalid || + nativeTarget.QueueHandle.IsInvalid || + nativeTarget.TargetTextureHandle.IsInvalid) + { + throw new NotSupportedException("The source and target frames must expose valid WebGPU device, queue, and texture handles."); + } + + if (!TryGetCompositeTargetDescriptor(out WebGPUTargetDescriptor expectedTargetDescriptor, out _)) + { + throw new NotSupportedException($"The WebGPU backend does not support pixel format '{typeof(TPixel).Name}'."); + } + + if (nativeSource.TargetDescriptor != expectedTargetDescriptor || nativeTarget.TargetDescriptor != expectedTargetDescriptor) + { + throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' does not match one of the WebGPU target formats."); + } + + // Clip in two stages: first the request against the source bounds, then the shifted + // result against the target bounds. Out-of-range copies clip silently rather than throw. + Rectangle sourceBounds = new(0, 0, source.Bounds.Width, source.Bounds.Height); + Rectangle clippedSource = Rectangle.Intersect(sourceBounds, sourceRectangle); + if (clippedSource.Width <= 0 || clippedSource.Height <= 0) + { + return; + } + + Rectangle targetBounds = new(0, 0, target.Bounds.Width, target.Bounds.Height); + Rectangle targetRectangle = new( + targetPoint.X + clippedSource.X - sourceRectangle.X, + targetPoint.Y + clippedSource.Y - sourceRectangle.Y, + clippedSource.Width, + clippedSource.Height); + + Rectangle clippedTarget = Rectangle.Intersect(targetBounds, targetRectangle); + + if (clippedTarget.Width <= 0 || clippedTarget.Height <= 0) + { + return; + } + + // Propagate any target-side clipping back to the source origin so both rectangles + // describe the same clipped extent. + int sourceX = clippedSource.X + clippedTarget.X - targetRectangle.X; + int sourceY = clippedSource.Y + clippedTarget.Y - targetRectangle.Y; + int targetX = clippedTarget.X; + int targetY = clippedTarget.Y; + int width = clippedTarget.Width; + int height = clippedTarget.Height; + + // Frame bounds are logical canvas coordinates; native surfaces may be a view into + // a larger texture, so translate once before recording the texture copy. + int nativeSourceX = source.Bounds.X + nativeSource.TextureCoordinateOffset.X + sourceX; + int nativeSourceY = source.Bounds.Y + nativeSource.TextureCoordinateOffset.Y + sourceY; + int nativeTargetX = target.Bounds.X + nativeTarget.TextureCoordinateOffset.X + targetX; + int nativeTargetY = target.Bounds.Y + nativeTarget.TextureCoordinateOffset.Y + targetY; + + WebGPU api = WebGPURuntime.GetApi(); + using WebGPUHandle.HandleReference sourceDeviceReference = nativeSource.DeviceHandle.AcquireReference(); + using WebGPUHandle.HandleReference targetDeviceReference = nativeTarget.DeviceHandle.AcquireReference(); + using WebGPUHandle.HandleReference sourceQueueReference = nativeSource.QueueHandle.AcquireReference(); + using WebGPUHandle.HandleReference targetQueueReference = nativeTarget.QueueHandle.AcquireReference(); + using WebGPUHandle.HandleReference sourceTextureReference = nativeSource.TargetTextureHandle.AcquireReference(); + using WebGPUHandle.HandleReference targetTextureReference = nativeTarget.TargetTextureHandle.AcquireReference(); + + // CommandEncoderCopyTextureToTexture cannot cross devices or queues; a cross-device + // copy would need a CPU round trip, which this GPU-only path does not provide. + if (sourceDeviceReference.Handle != targetDeviceReference.Handle || sourceQueueReference.Handle != targetQueueReference.Handle) + { + throw new NotSupportedException("WebGPU frame pixel copies require source and target frames from the same device and queue."); + } + + WGPUCommandEncoderImpl* commandEncoder = null; + WGPUCommandBufferImpl* commandBuffer = null; + + try + { + WGPUDeviceImpl* device = (WGPUDeviceImpl*)targetDeviceReference.Handle; + WGPUCommandEncoderDescriptor encoderDescriptor = default; + commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); + if (commandEncoder is null) + { + throw new InvalidOperationException("The WebGPU device could not create a command encoder for the pixel copy."); + } + + WGPUTexelCopyTextureInfo textureSource = new() + { + texture = (WGPUTextureImpl*)sourceTextureReference.Handle, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)nativeSourceX, (uint)nativeSourceY, 0), + aspect = WGPUTextureAspect.All + }; + + WGPUTexelCopyTextureInfo textureTarget = new() + { + texture = (WGPUTextureImpl*)targetTextureReference.Handle, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)nativeTargetX, (uint)nativeTargetY, 0), + aspect = WGPUTextureAspect.All + }; + + WGPUExtent3D copySize = new((uint)width, (uint)height, 1); + api.CommandEncoderCopyTextureToTexture(commandEncoder, in textureSource, in textureTarget, in copySize); + + WGPUCommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + throw new InvalidOperationException("The WebGPU device could not finalize the pixel-copy command buffer."); + } + + api.QueueSubmit((WGPUQueueImpl*)targetQueueReference.Handle, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); + commandBuffer = null; + api.CommandEncoderRelease(commandEncoder); + commandEncoder = null; + } + finally + { + if (commandBuffer is not null) + { + api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + api.CommandEncoderRelease(commandEncoder); + } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Ordered.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Ordered.cs new file mode 100644 index 000000000..d612b79cf --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Ordered.cs @@ -0,0 +1,1459 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +#pragma warning disable SA1201 // Flush transaction types are kept next to the executor that owns their invariants. + +/// +/// Implements ordered transactional rendering for the WebGPU drawing backend. +/// +public sealed unsafe partial class WebGPUDrawingBackend +{ + /// + /// Executes the immutable ordered plan with allocator validation at Apply barriers, at the + /// final offscreen boundary, or through deferred presentation readbacks. + /// + /// The pixel format of the render target. + /// The active ImageSharp configuration. + /// The flush-scoped WebGPU resources and command state. + /// The externally owned root render target. + /// The retained scene and ordered execution plan. + /// The retained scene that receives deferred allocator growth. + /// Whether presentation allocator statuses can resolve after the frame returns. + /// The texture-format feature required by . + /// The scratch capacities carried across flushes. + /// The reusable staged-scene resource arena. + /// The reusable scheduling arena. + private void RenderOrderedScene( + Configuration configuration, + WebGPUFlushContext flushContext, + WebGPUSceneTarget rootTarget, + WebGPUEncodedScene encodedScene, + WebGPUDrawingBackendScene scene, + bool deferSchedulingStatus, + WGPUFeatureName requiredFeature, + ref WebGPUSceneBumpSizes currentBumpSizes, + ref WebGPUSceneResourceArena? resourceArena, + ref WebGPUSceneSchedulingArena? schedulingArena) + where TPixel : unmanaged, IPixel + { + using WebGPUOrderedExecutor executor = new( + this, + configuration, + flushContext, + rootTarget, + encodedScene, + scene, + deferSchedulingStatus, + requiredFeature, + currentBumpSizes, + resourceArena, + schedulingArena); + + try + { + executor.Execute(); + currentBumpSizes = executor.BumpSizes; + } + finally + { + // The executor replaces its arenas in place when a range outgrows them, disposing + // the instances these ref slots still name. The copy-back must run on the failure + // path too: otherwise the caller's finally returns the disposed originals to the + // reuse caches (a later release then frees their buffers twice) and the live + // replacements held only by the executor leak. + resourceArena = executor.ResourceArena; + schedulingArena = executor.SchedulingArena; + } + } + + /// + /// Owns one ordered flush's target checkpoints and either its synchronous transaction journal + /// or its deferred presentation status readbacks. + /// + /// The pixel format used to lay out and materialize Apply readbacks. + private sealed unsafe class WebGPUOrderedExecutor : IDisposable + where TPixel : unmanaged, IPixel + { + private readonly WebGPUDrawingBackend backend; + private readonly Configuration configuration; + private readonly WebGPUFlushContext flushContext; + private readonly WebGPUEncodedScene encodedScene; + private readonly WebGPUDrawingBackendScene scene; + private readonly bool deferSchedulingStatus; + private readonly WGPUFeatureName requiredFeature; + private readonly WebGPUOrderedTargetState[] targets; + private readonly WebGPUOrderedJournalEntry[] journal; + private readonly WebGPUSceneBumpSizes[] submittedBumpSizes; + private readonly uint[] submittedChunkTileHeights; + private readonly WebGPUApplyReadbackRegion[] applyRegions; + private readonly Image?[] applyImages; + private readonly WGPUBufferImpl* readbackBuffer; + private readonly nuint readbackByteLength; + private readonly ManualResetEventSlim? mapReady; + private readonly WebGPUBufferMapCallback? mapCallback; + private readonly WebGPUEffectRenderer? effectRenderer; + private WebGPUSceneResourceArena? resourceArena; + private WebGPUSceneSchedulingArena? schedulingArena; + private WGPUMapAsyncStatus mapStatus; + private ulong lastSubmissionIndex; + private int currentTargetId; + private int journalCount; + private int statusCount; + private bool disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The backend that owns shared capacity and chunk-hint state. + /// The active ImageSharp configuration. + /// The flush-scoped WebGPU command and resource context. + /// The externally owned root target. + /// The retained ordered scene. + /// The retained scene that receives deferred allocator growth. + /// Whether presentation allocator statuses can resolve after the frame returns. + /// The target-format feature required by . + /// The initial scheduling scratch capacities. + /// The rented resource arena, or . + /// The rented scheduling arena, or . + public WebGPUOrderedExecutor( + WebGPUDrawingBackend backend, + Configuration configuration, + WebGPUFlushContext flushContext, + WebGPUSceneTarget rootTarget, + WebGPUEncodedScene encodedScene, + WebGPUDrawingBackendScene scene, + bool deferSchedulingStatus, + WGPUFeatureName requiredFeature, + WebGPUSceneBumpSizes bumpSizes, + WebGPUSceneResourceArena? resourceArena, + WebGPUSceneSchedulingArena? schedulingArena) + { + this.backend = backend; + this.configuration = configuration; + this.flushContext = flushContext; + this.encodedScene = encodedScene; + this.scene = scene; + this.deferSchedulingStatus = deferSchedulingStatus; + this.requiredFeature = requiredFeature; + this.BumpSizes = bumpSizes; + this.resourceArena = resourceArena; + this.schedulingArena = schedulingArena; + this.targets = new WebGPUOrderedTargetState[encodedScene.OrderedTargetCount]; + this.journal = deferSchedulingStatus + ? [] + : new WebGPUOrderedJournalEntry[encodedScene.MaxPendingRangeCount]; + + this.submittedBumpSizes = new WebGPUSceneBumpSizes[encodedScene.MaxStatusCapacity]; + this.submittedChunkTileHeights = new uint[encodedScene.MaxStatusCapacity]; + this.applyRegions = new WebGPUApplyReadbackRegion[encodedScene.OrderedApplyCount]; + this.applyImages = new Image?[encodedScene.MaxApplyGroupCount]; + this.targets[0].Initialize(rootTarget); + + if (deferSchedulingStatus) + { + this.readbackBuffer = null; + this.readbackByteLength = 0; + this.mapReady = null; + this.mapCallback = null; + } + else + { + this.readbackByteLength = this.BuildReadbackLayout(); + WGPUBufferDescriptor descriptor = new() + { + usage = (ulong)(BufferUsage.CopyDst | BufferUsage.MapRead), + size = checked(this.readbackByteLength), + mappedAtCreation = 0U, + }; + + using WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference(); + this.readbackBuffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + if (this.readbackBuffer is null) + { + throw new InvalidOperationException("Failed to create the ordered WebGPU barrier readback buffer."); + } + + // The flush context releases the native buffer after every submitted command that + // references it has left managed ownership. + flushContext.TrackBuffer(this.readbackBuffer); + this.mapReady = new ManualResetEventSlim(false); + this.mapCallback = WebGPUBufferMapCallback.From(this.OnMapCompleted); + } + + if (encodedScene.OrderedShaderEffectCount != 0) + { + // The uniform buffer holds one disjoint region per pass of a single invocation, so + // its size follows the largest pass count among the retained effect operations. + int maximumPassCount = 0; + ReadOnlySpan operations = encodedScene.OrderedOperations; + for (int i = 0; i < operations.Length; i++) + { + if (operations[i].Kind == WebGPUSceneOperationKind.ShaderEffect) + { + maximumPassCount = Math.Max(maximumPassCount, operations[i].ShaderEffect!.Effect.GetShaderPasses().Length); + } + } + + try + { + this.effectRenderer = new WebGPUEffectRenderer( + flushContext, + encodedScene.MaximumShaderEffectWidth, + encodedScene.MaximumShaderEffectHeight, + encodedScene.MaximumShaderUniformByteLength, + maximumPassCount); + } + catch + { + // A ctor throw means the caller's using never binds, so Dispose cannot retire + // the callback owner and event created above. + this.mapCallback?.Dispose(); + this.mapReady?.Dispose(); + throw; + } + } + } + + /// + /// Gets the scratch capacities after all successful validation and replay attempts. + /// + public WebGPUSceneBumpSizes BumpSizes { get; private set; } + + /// + /// Gets the resource arena used by the completed ordered flush. + /// + public WebGPUSceneResourceArena? ResourceArena => this.resourceArena; + + /// + /// Gets the scheduling arena used by the completed ordered flush. + /// + public WebGPUSceneSchedulingArena? SchedulingArena => this.schedulingArena; + + /// + /// Executes every retained operation and commits the root target. Presentation-only + /// allocator validation completes through the backend's deferred status queue. + /// + public void Execute() + { + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + + for (int i = 0; i < operations.Length; i++) + { + WebGPUSceneOperation operation = operations[i]; + switch (operation.Kind) + { + case WebGPUSceneOperationKind.RenderRange: + this.SubmitRange( + operation.Range, + this.currentTargetId, + WebGPUOrderedExternalSourceKind.None, + null, + 0); + break; + + case WebGPUSceneOperationKind.BeginLayer: + this.BeginLayer(operation); + break; + + case WebGPUSceneOperationKind.EndLayer: + this.currentTargetId = operation.ParentTargetId; + this.SubmitRange( + operation.Range, + operation.ParentTargetId, + WebGPUOrderedExternalSourceKind.Target, + null, + operation.LayerTargetId); + this.targets[operation.LayerTargetId].MarkInactive(); + break; + + case WebGPUSceneOperationKind.Apply: + this.ResolveApplyBarrier(i, operation.ApplyGroupCount); + this.CommitTargets(); + this.ProcessApplyGroup(i, operation.ApplyGroupCount); + i += operation.ApplyGroupCount - 1; + break; + + case WebGPUSceneOperationKind.ShaderEffect: + if (!this.deferSchedulingStatus) + { + this.ResolveFinalBarrier(); + this.CommitTargets(); + } + + this.ProcessShaderEffect(operation.ShaderEffect!); + break; + } + } + + if (!this.deferSchedulingStatus) + { + this.ResolveFinalBarrier(); + } + + this.CommitTargets(); + } + + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.disposed = true; + this.DisposeApplyImages(this.applyImages.Length); + this.mapCallback?.Dispose(); + this.mapReady?.Dispose(); + } + + /// + /// Computes every generic pixel row once and returns the largest aligned barrier slice. + /// + /// The required readback buffer size in bytes. + private nuint BuildReadbackLayout() + { + int pixelSize = Unsafe.SizeOf(); + ulong maximumByteLength = checked((ulong)this.encodedScene.MaxStatusCapacity * (ulong)sizeof(GpuSceneBumpAllocators)); + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + + for (int i = 0; i < operations.Length; i++) + { + WebGPUSceneOperation operation = operations[i]; + if (operation.Kind != WebGPUSceneOperationKind.Apply || operation.ApplyGroupCount == 0) + { + continue; + } + + // WebGPU requires both texture-copy row pitches and texture-to-buffer offsets to + // be 256-byte aligned. Status records occupy the prefix before this aligned point. + ulong nextPixelOffset = AlignUp( + checked((ulong)operation.PendingStatusCapacity * (ulong)sizeof(GpuSceneBumpAllocators)), + 256); + ulong groupByteLength = nextPixelOffset; + + for (int groupIndex = 0; groupIndex < operation.ApplyGroupCount; groupIndex++) + { + WebGPUSceneOperation applyOperation = operations[i + groupIndex]; + WebGPUApplySceneItem apply = applyOperation.Apply!; + Rectangle requestedRead = apply.InputRect; + requestedRead.Offset(-apply.ReadOffset.X, -apply.ReadOffset.Y); + Rectangle clippedRead = Rectangle.Intersect(apply.DrawRange.TargetBounds, requestedRead); + int packedRowBytes = checked(clippedRead.Width * pixelSize); + int readbackRowBytes = clippedRead.Width == 0 ? 0 : checked((int)AlignUp((ulong)packedRowBytes, 256)); + ulong regionByteLength = checked((ulong)readbackRowBytes * (ulong)clippedRead.Height); + + this.applyRegions[applyOperation.ApplyIndex] = new WebGPUApplyReadbackRegion( + clippedRead, + clippedRead.X - requestedRead.X, + clippedRead.Y - requestedRead.Y, + nextPixelOffset, + packedRowBytes, + readbackRowBytes); + + nextPixelOffset = checked(nextPixelOffset + regionByteLength); + groupByteLength = Math.Max(groupByteLength, nextPixelOffset); + } + + maximumByteLength = Math.Max(maximumByteLength, groupByteLength); + i += operation.ApplyGroupCount - 1; + } + + // WebGPU buffers cannot be zero-sized. Ordered scenes with only empty ranges still + // retain one allocator-record-sized buffer so the ownership path remains uniform. + return checked((nuint)Math.Max(maximumByteLength, (ulong)sizeof(GpuSceneBumpAllocators))); + } + + /// + /// Creates and clears one stable scoped-layer target. + /// + /// The retained layer-entry operation. + private void BeginLayer(WebGPUSceneOperation operation) + { + if (!TryCreateCompositionTexture( + this.flushContext, + operation.LayerBounds.Width, + operation.LayerBounds.Height, + renderAttachment: true, + out WGPUTextureImpl* layerTexture, + out WGPUTextureViewImpl* layerTextureView, + out string? error)) + { + throw new InvalidOperationException(error); + } + + ClearTarget(this.flushContext, layerTextureView); + WebGPUSceneTarget layerTarget = new( + layerTexture, + layerTextureView, + operation.LayerBounds, + new Point(-operation.LayerBounds.X, -operation.LayerBounds.Y)); + + this.targets[operation.LayerTargetId].Initialize(layerTarget); + this.currentTargetId = operation.LayerTargetId; + } + + /// + /// Submits one range with synchronous transaction validation or deferred presentation validation. + /// + /// The retained range to submit. + /// The logical target receiving the range. + /// The source bound to the range's external texture slot. + /// The fixed Apply upload view, when applicable. + /// The layer target supplying a composite view, when applicable. + private void SubmitRange( + WebGPUSceneRange range, + int targetId, + WebGPUOrderedExternalSourceKind externalSourceKind, + WGPUTextureViewImpl* externalTextureView, + int externalTargetId) + { + if (range.FillCount == 0) + { + return; + } + + WebGPUOrderedJournalEntry newEntry = new( + range, + targetId, + externalSourceKind, + externalTextureView, + externalTargetId); + + if (this.deferSchedulingStatus) + { + this.SubmitRangeAttempt(ref newEntry); + return; + } + + ref WebGPUOrderedJournalEntry entry = ref this.journal[this.journalCount]; + entry = newEntry; + this.SubmitRangeAttempt(ref entry); + this.journalCount++; + } + + /// + /// Submits one initial or replayed range attempt and records or defers its status metadata. + /// + /// The stable journal entry describing the GPU draw. + private void SubmitRangeAttempt(ref WebGPUOrderedJournalEntry entry) + { + ref WebGPUOrderedTargetState targetState = ref this.targets[entry.TargetId]; + WebGPUSceneTarget backdropTarget = targetState.GetLatestTarget(); + targetState.GetNextOutput( + this.flushContext, + out WGPUTextureImpl* outputTexture, + out WGPUTextureViewImpl* outputTextureView, + out WebGPUOrderedTargetImage outputImage); + + WGPUTextureViewImpl* externalTextureView = entry.ExternalSourceKind switch + { + WebGPUOrderedExternalSourceKind.TextureView => entry.ExternalTextureView, + WebGPUOrderedExternalSourceKind.Target => this.targets[entry.ExternalTargetId].GetLatestTarget().TextureView, + _ => null + }; + + WebGPUStagedScene stagedScene = WebGPUSceneDispatch.CreateStagedScene( + this.flushContext, + this.encodedScene, + entry.Range, + this.requiredFeature, + this.BumpSizes, + externalTextureView, + ref this.resourceArena); + + nuint statusBufferCapacity = 0; + WGPUBufferImpl* statusReadbackBuffer = this.readbackBuffer; + bool statusBufferPassedToDispatch = false; + bool statusBufferOwnershipTransferred = false; + + if (this.deferSchedulingStatus) + { + statusBufferCapacity = checked( + (nuint)entry.Range.MaximumStatusRecordCount * (nuint)sizeof(GpuSceneBumpAllocators)); + statusReadbackBuffer = this.flushContext.DeviceState.RentStatusReadbackBuffer(statusBufferCapacity); + if (statusReadbackBuffer is null) + { + stagedScene.Dispose(); + throw new InvalidOperationException("Failed to create the deferred ordered scheduling-status readback buffer."); + } + } + + try + { + if (stagedScene.BindingLimitFailure.Buffer != WebGPUSceneDispatch.BindingLimitBuffer.None) + { + this.backend.DiagnosticLastFlushUsedChunking = true; + this.backend.lastChunkingBindingFailure = stagedScene.BindingLimitFailure.Buffer; + } + + int statusStart = this.deferSchedulingStatus ? 0 : this.statusCount; + statusBufferPassedToDispatch = this.deferSchedulingStatus; + bool submitted = WebGPUSceneDispatch.TrySubmitTransactionalStagedScene( + ref stagedScene, + backdropTarget, + outputTexture, + outputTextureView, + ref this.schedulingArena, + this.backend.GetChunkTileHeightHint(stagedScene.BindingLimitFailure.Buffer, entry.Range.TargetBounds.Size), + statusReadbackBuffer, + checked((nuint)statusStart * (nuint)sizeof(GpuSceneBumpAllocators)), + this.submittedBumpSizes.AsSpan(statusStart), + this.submittedChunkTileHeights.AsSpan(statusStart), + out int emittedStatusCount, + out uint fullTileHeight, + out uint successfulChunkTileHeight, + out ulong submissionIndex, + out string? error); + + if (!submitted) + { + throw new InvalidOperationException(error ?? "The transactional WebGPU range submission failed."); + } + + entry.SetStatus(statusStart, emittedStatusCount, fullTileHeight, stagedScene.BindingLimitFailure.IsExceeded); + this.lastSubmissionIndex = submissionIndex; + targetState.SetLatestImage(outputImage); + + if (this.deferSchedulingStatus) + { + WebGPUPendingSchedulingStatus pendingStatus = stagedScene.BindingLimitFailure.IsExceeded + ? new WebGPUPendingSchedulingStatus( + this.flushContext.Api, + this.flushContext.DeviceHandle, + this.flushContext.DeviceState, + statusReadbackBuffer, + statusBufferCapacity, + stagedScene.Config.BumpSizes, + this.submittedBumpSizes.AsSpan(0, emittedStatusCount), + this.submittedChunkTileHeights.AsSpan(0, emittedStatusCount), + fullTileHeight, + submissionIndex) + : new WebGPUPendingSchedulingStatus( + this.flushContext.Api, + this.flushContext.DeviceHandle, + this.flushContext.DeviceState, + statusReadbackBuffer, + statusBufferCapacity, + this.submittedBumpSizes[0], + submissionIndex); + + statusBufferOwnershipTransferred = true; + this.backend.EnqueuePendingSchedulingStatus(pendingStatus, this.scene, correctiveRender: null); + } + else + { + this.statusCount += emittedStatusCount; + } + + if (successfulChunkTileHeight != 0) + { + this.backend.UpdateChunkTileHeightHint( + stagedScene.BindingLimitFailure.Buffer, + entry.Range.TargetBounds.Size, + successfulChunkTileHeight); + } + } + finally + { + stagedScene.Dispose(); + + if (this.deferSchedulingStatus && !statusBufferOwnershipTransferred) + { + if (statusBufferPassedToDispatch) + { + // Dispatch may already have submitted commands that reference the buffer. + // Native command ownership permits releasing it with the flush resources, + // but it must not return to the reusable pool before those commands retire. + this.flushContext.TrackBuffer(statusReadbackBuffer); + } + else + { + this.flushContext.DeviceState.ReturnStatusReadbackBuffer(statusReadbackBuffer, statusBufferCapacity); + } + } + } + } + + /// + /// Validates pending statuses and captures every Apply source in one mapped buffer. + /// + /// The operation index of the Apply group head. + /// The number of independent Apply snapshots in the group. + private void ResolveApplyBarrier(int firstOperationIndex, int applyCount) + { + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + WebGPUSceneOperation groupHead = operations[firstOperationIndex]; + + for (int attempt = 0; attempt < MaxDynamicGrowthAttempts; attempt++) + { + ulong mappedByteLength = checked((ulong)groupHead.PendingStatusCapacity * (ulong)sizeof(GpuSceneBumpAllocators)); + bool recordedPixelCopy = false; + WebGPUSceneTarget sourceTarget = this.targets[this.currentTargetId].GetLatestTarget(); + + for (int groupIndex = 0; groupIndex < applyCount; groupIndex++) + { + WebGPUSceneOperation applyOperation = operations[firstOperationIndex + groupIndex]; + WebGPUApplyReadbackRegion region = this.applyRegions[applyOperation.ApplyIndex]; + mappedByteLength = Math.Max(mappedByteLength, region.EndOffset); + + if (region.ClippedSource.Width == 0 || region.ClippedSource.Height == 0) + { + continue; + } + + if (!this.flushContext.EnsureCommandEncoder()) + { + throw new InvalidOperationException("Failed to create the ordered Apply readback command encoder."); + } + + WGPUTexelCopyTextureInfo sourceCopy = new() + { + texture = sourceTarget.Texture, + mipLevel = 0, + origin = new WGPUOrigin3D( + (uint)(region.ClippedSource.X + sourceTarget.TextureOffset.X), + (uint)(region.ClippedSource.Y + sourceTarget.TextureOffset.Y), + 0), + aspect = WGPUTextureAspect.All + }; + + WGPUTexelCopyBufferInfo destinationCopy = new() + { + buffer = this.readbackBuffer, + layout = new WGPUTexelCopyBufferLayout + { + offset = region.BufferOffset, + bytesPerRow = (uint)region.ReadbackRowBytes, + rowsPerImage = (uint)region.ClippedSource.Height + } + }; + + WGPUExtent3D copySize = new((uint)region.ClippedSource.Width, (uint)region.ClippedSource.Height, 1); + this.flushContext.Api.CommandEncoderCopyTextureToBuffer( + this.flushContext.CommandEncoder, + in sourceCopy, + in destinationCopy, + in copySize); + + recordedPixelCopy = true; + } + + if (!recordedPixelCopy && this.statusCount == 0) + { + // A fully clipped Apply reads transparent pixels and has no GPU dependency. + // Materialize the correctly sized blank images without creating a fake submit. + this.CreateBlankApplyImages(firstOperationIndex, applyCount); + this.journalCount = 0; + return; + } + + ulong barrierSubmissionIndex = recordedPixelCopy + ? this.SubmitBarrierCommands() + : this.lastSubmissionIndex; + + void* mapped = this.MapReadback(checked((nuint)mappedByteLength), barrierSubmissionIndex); + + bool requiresReplay; + try + { + requiresReplay = this.ValidateMappedStatuses(mapped); + + if (!requiresReplay) + { + this.CopyApplyImagesFromMapped(firstOperationIndex, applyCount, mapped, checked((nuint)mappedByteLength)); + } + } + finally + { + this.flushContext.Api.BufferUnmap(this.readbackBuffer); + } + + if (requiresReplay) + { + // CopyDst commands cannot target a mapped buffer. Replay starts only after + // the failed barrier has been unmapped. + this.ReplayJournal(); + continue; + } + + this.journalCount = 0; + this.statusCount = 0; + return; + } + + throw new InvalidOperationException("The ordered WebGPU transaction exceeded the dynamic growth retry budget."); + } + + /// + /// Validates and commits the final status-only transaction. + /// + private void ResolveFinalBarrier() + { + if (this.journalCount == 0) + { + return; + } + + for (int attempt = 0; attempt < MaxDynamicGrowthAttempts; attempt++) + { + nuint mappedByteLength = checked((nuint)this.statusCount * (nuint)sizeof(GpuSceneBumpAllocators)); + void* mapped = this.MapReadback(mappedByteLength, this.lastSubmissionIndex); + + bool requiresReplay; + try + { + requiresReplay = this.ValidateMappedStatuses(mapped); + } + finally + { + this.flushContext.Api.BufferUnmap(this.readbackBuffer); + } + + if (requiresReplay) + { + this.ReplayJournal(); + continue; + } + + this.journalCount = 0; + this.statusCount = 0; + return; + } + + throw new InvalidOperationException("The ordered WebGPU transaction exceeded the dynamic growth retry budget."); + } + + /// + /// Maps the barrier buffer and waits only for the submission that owns its final bytes. + /// + /// The byte length required by the current barrier. + /// The exact submission that completes the barrier data. + /// The mapped read-only range. + private void* MapReadback(nuint mappedByteLength, ulong submissionIndex) + { + this.mapStatus = default; + this.mapReady!.Reset(); + this.flushContext.Api.BufferMapAsync( + this.readbackBuffer, + MapMode.Read, + 0, + mappedByteLength, + this.mapCallback!, + null); + + using WebGPUHandle.HandleReference deviceReference = this.flushContext.DeviceHandle.AcquireReference(); + if (!WaitForMapSignal(this.flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, this.mapReady!, submissionIndex) || + this.mapStatus != WGPUMapAsyncStatus.Success) + { + // Unmapping cancels a still-pending map before executor disposal retires its + // managed owner. The wrapper remains self-rooted until native delivers the + // cancellation callback, so native code cannot retain a dead delegate target. + this.flushContext.Api.BufferUnmap(this.readbackBuffer); + throw new InvalidOperationException($"Failed to map the ordered WebGPU barrier with status '{this.mapStatus}'."); + } + + void* mapped = this.flushContext.Api.BufferGetConstMappedRange(this.readbackBuffer, 0, mappedByteLength); + if (mapped is null) + { + this.flushContext.Api.BufferUnmap(this.readbackBuffer); + throw new InvalidOperationException("The ordered WebGPU barrier mapped successfully but returned no readable range."); + } + + return mapped; + } + + /// + /// Validates every journal status, updates all eight allocator high-water marks, and + /// reports whether the transaction must be replayed when any record overflowed. + /// + /// The mapped barrier prefix containing allocator records. + /// when the transaction was replayed; otherwise, . + private bool ValidateMappedStatuses(void* mapped) + { + ReadOnlySpan statuses = new(mapped, this.statusCount); + WebGPUSceneBumpSizes mergedSizes = this.BumpSizes; + bool requiresReplay = false; + + for (int i = 0; i < this.journalCount; i++) + { + WebGPUOrderedJournalEntry entry = this.journal[i]; + ReadOnlySpan entryStatuses = statuses.Slice(entry.StatusStart, entry.StatusCount); + ReadOnlySpan entrySubmittedSizes = this.submittedBumpSizes.AsSpan(entry.StatusStart, entry.StatusCount); + + if (entry.IsChunked) + { + bool entryRequiresGrowth = WebGPUSceneDispatch.ResolveChunkSchedulingStatuses( + entryStatuses, + entrySubmittedSizes, + this.submittedChunkTileHeights.AsSpan(entry.StatusStart, entry.StatusCount), + entry.FullTileHeight, + out WebGPUSceneBumpSizes entrySizes); + + requiresReplay |= entryRequiresGrowth; + mergedSizes = MaxBumpSizes(mergedSizes, entrySizes); + continue; + } + + GpuSceneBumpAllocators actual = entryStatuses[0]; + WebGPUSceneBumpSizes submitted = entrySubmittedSizes[0]; + WebGPUSceneBumpSizes actualSizes = new( + Math.Max(actual.Lines, submitted.Lines), + Math.Max(actual.Binning, submitted.Binning), + Math.Max(actual.PathRows, 1U), + Math.Max(actual.Tile, 1U), + Math.Max(actual.SegCounts, submitted.SegCounts), + Math.Max(actual.Segments, submitted.Segments), + Math.Max(actual.BlendSpill, submitted.BlendSpill), + Math.Max(actual.Ptcl, submitted.Ptcl)); + + if (WebGPUSceneDispatch.RequiresScratchReallocation(in actual, submitted)) + { + requiresReplay = true; + actualSizes = MaxBumpSizes(actualSizes, WebGPUSceneDispatch.GrowBumpSizes(submitted, in actual)); + } + + mergedSizes = MaxBumpSizes(mergedSizes, actualSizes); + } + + this.BumpSizes = mergedSizes; + return requiresReplay; + } + + /// + /// Replays only GPU ranges from committed target checkpoints after allocator growth. + /// Apply callbacks and their retained uploads are not repeated. + /// + private void ReplayJournal() + { + for (int i = 0; i < this.targets.Length; i++) + { + if (this.targets[i].IsInitialized) + { + this.targets[i].ResetToCommitted(); + } + } + + this.statusCount = 0; + for (int i = 0; i < this.journalCount; i++) + { + this.SubmitRangeAttempt(ref this.journal[i]); + } + } + + /// + /// Copies active target images into their durable root/layer checkpoints and submits all + /// commits together. Synchronous paths call this after validation; presentation paths + /// validate the submitted allocator records through the deferred status queue. + /// + private void CommitTargets() + { + bool recordedCommit = false; + + for (int i = 0; i < this.targets.Length; i++) + { + ref WebGPUOrderedTargetState target = ref this.targets[i]; + if (!target.IsInitialized || !target.IsActive || !target.RequiresCommit) + { + continue; + } + + if (!this.flushContext.EnsureCommandEncoder()) + { + throw new InvalidOperationException("Failed to create the ordered target commit encoder."); + } + + WebGPUSceneTarget latest = target.GetLatestTarget(); + WebGPUSceneTarget actual = target.ActualTarget; + CopyTextureRegion( + this.flushContext, + latest.Texture, + 0, + 0, + actual.Texture, + actual.Bounds.X + actual.TextureOffset.X, + actual.Bounds.Y + actual.TextureOffset.Y, + actual.Bounds.Width, + actual.Bounds.Height); + + recordedCommit = true; + } + + if (!recordedCommit) + { + return; + } + + if (!TrySubmitPendingCommands(this.flushContext)) + { + throw new InvalidOperationException("Failed to submit ordered target commits."); + } + + for (int i = 0; i < this.targets.Length; i++) + { + if (this.targets[i].IsInitialized && this.targets[i].IsActive) + { + this.targets[i].MarkCommitted(); + } + } + } + + /// + /// Runs one Apply group in source order and retains each upload until the following + /// transaction validates or replays its draw range. + /// + /// The Apply group head operation index. + /// The number of processors in the group. + private void ProcessApplyGroup(int firstOperationIndex, int applyCount) + { + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + int processedCount = 0; + + try + { + for (; processedCount < applyCount; processedCount++) + { + WebGPUApplySceneItem apply = operations[firstOperationIndex + processedCount].Apply!; + Image image = this.applyImages[processedCount]!; + image.Mutate(apply.Operation); + + if (!TryCreateCompositionTexture( + this.flushContext, + apply.InputRect.Width, + apply.InputRect.Height, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, + out string? error)) + { + throw new InvalidOperationException(error); + } + + using (WebGPUHandle.HandleReference queueReference = this.flushContext.QueueHandle.AcquireReference()) + { + WebGPUFlushContext.UploadTextureFromRegion( + this.flushContext.Api, + (WGPUQueueImpl*)queueReference.Handle, + texture, + image.Frames.RootFrame.PixelBuffer.GetRegion(), + this.configuration.MemoryAllocator); + } + + image.Dispose(); + this.applyImages[processedCount] = null; + this.SubmitRange( + apply.DrawRange, + this.currentTargetId, + WebGPUOrderedExternalSourceKind.TextureView, + textureView, + 0); + } + } + catch (Exception exception) + { + this.DisposeApplyImages(applyCount); + + try + { + // Current behavior makes every earlier successful Apply visible before a + // later processor exception escapes. Validate and commit those draws without + // invoking any processor a second time. + this.ResolveFinalBarrier(); + this.CommitTargets(); + } + catch + { + // The processor exception is the observable failure that initiated cleanup; + // a secondary device failure must not replace its stack or exception identity. + } + + ExceptionDispatchInfo.Capture(exception).Throw(); + throw; + } + } + + /// + /// Executes one native effect against the latest ordered source and journals its retained write-back range. + /// + /// The native effect operation. + private void ProcessShaderEffect(WebGPUShaderEffectSceneItem effect) + { + // A fully clipped write-back has no fill to draw. Skip the passes entirely: nothing + // samples the result, and SubmitRange would return without submitting, leaving the + // recorded passes in the shared encoder while the next invocation rewrites the + // uniform regions they reference. + if (effect.DrawRange.FillCount == 0) + { + return; + } + + WebGPUSceneTarget sourceTarget = this.targets[this.currentTargetId].GetLatestTarget(); + WGPUTextureViewImpl* effectTextureView = this.effectRenderer!.Render(effect, sourceTarget); + this.SubmitRange( + effect.DrawRange, + this.currentTargetId, + WebGPUOrderedExternalSourceKind.TextureView, + effectTextureView, + 0); + } + + /// + /// Creates transparent source images for a group whose requested reads are fully clipped. + /// + /// The Apply group head operation index. + /// The number of source images to create. + private void CreateBlankApplyImages(int firstOperationIndex, int applyCount) + { + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + for (int i = 0; i < applyCount; i++) + { + Rectangle inputRect = operations[firstOperationIndex + i].Apply!.InputRect; + this.applyImages[i] = new Image(this.configuration, inputRect.Width, inputRect.Height); + } + } + + /// + /// Materializes full Apply images from aligned, clipped rows while the barrier is mapped. + /// + /// The Apply group head operation index. + /// The number of source images to materialize. + /// The mapped barrier buffer. + /// The readable mapped byte length. + private void CopyApplyImagesFromMapped( + int firstOperationIndex, + int applyCount, + void* mapped, + nuint mappedByteLength) + { + ReadOnlySpan readback = new(mapped, checked((int)mappedByteLength)); + ReadOnlySpan operations = this.encodedScene.OrderedOperations; + + try + { + for (int i = 0; i < applyCount; i++) + { + WebGPUSceneOperation operation = operations[firstOperationIndex + i]; + WebGPUApplySceneItem apply = operation.Apply!; + WebGPUApplyReadbackRegion region = this.applyRegions[operation.ApplyIndex]; + Image image = new(this.configuration, apply.InputRect.Width, apply.InputRect.Height); + this.applyImages[i] = image; + + Buffer2DRegion destination = image.Frames.RootFrame.PixelBuffer.GetRegion(); + for (int y = 0; y < region.ClippedSource.Height; y++) + { + int sourceOffset = checked((int)region.BufferOffset + (y * region.ReadbackRowBytes)); + readback + .Slice(sourceOffset, region.PackedRowBytes) + .CopyTo(MemoryMarshal.AsBytes( + destination.DangerousGetRowSpan(region.DestinationY + y) + .Slice(region.DestinationX, region.ClippedSource.Width))); + } + } + } + catch + { + this.DisposeApplyImages(applyCount); + throw; + } + } + + /// + /// Disposes materialized Apply images and clears their reusable slots. + /// + /// The number of leading slots belonging to the current group. + private void DisposeApplyImages(int count) + { + for (int i = 0; i < count; i++) + { + this.applyImages[i]?.Dispose(); + this.applyImages[i] = null; + } + } + + /// + /// Submits the texture-copy commands that complete one mixed Apply barrier. + /// + /// The exact submission index owning the copied pixels. + private ulong SubmitBarrierCommands() + { + if (!TrySubmitWithIndex(this.flushContext, out ulong submissionIndex)) + { + throw new InvalidOperationException("Failed to submit the ordered Apply barrier."); + } + + return submissionIndex; + } + + /// + /// Receives the native map completion status for the currently active barrier. + /// + /// The native buffer-map completion status. + /// Unused native callback state. + private void OnMapCompleted(WGPUMapAsyncStatus status, void* userData) + { + _ = userData; + this.mapStatus = status; + this.mapReady!.Set(); + } + + /// + /// Aligns an unsigned byte offset upward to a required power-of-two boundary. + /// + /// The byte offset to align. + /// The required power-of-two alignment in bytes. + /// The aligned byte offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong AlignUp(ulong value, ulong alignment) + => checked((value + alignment - 1) & ~(alignment - 1)); + } + + /// + /// Identifies the image currently representing one logical ordered target. + /// + private enum WebGPUOrderedTargetImage + { + /// + /// The durable actual root or layer texture. + /// + Actual, + + /// + /// The target's first transaction scratch texture. + /// + ScratchA, + + /// + /// The target's second transaction scratch texture. + /// + ScratchB + } + + /// + /// Identifies the external texture binding retained by one replayable range. + /// + private enum WebGPUOrderedExternalSourceKind + { + /// + /// The range has no external texture binding. + /// + None, + + /// + /// The range samples a fixed Apply upload texture view. + /// + TextureView, + + /// + /// The range samples the latest logical image of another target. + /// + Target + } + + /// + /// Tracks durable and ping-pong images for one stable root or layer target identity. + /// + private struct WebGPUOrderedTargetState + { + private WGPUTextureImpl* scratchATexture; + private WGPUTextureViewImpl* scratchAView; + private WGPUTextureImpl* scratchBTexture; + private WGPUTextureViewImpl* scratchBView; + private WebGPUOrderedTargetImage latestImage; + + /// + /// Gets a value indicating whether this target identity has been materialized. + /// + public bool IsInitialized { get; private set; } + + /// + /// Gets a value indicating whether later operations can still require this target as a checkpoint. + /// + public bool IsActive { get; private set; } + + /// + /// Gets the durable actual target used as the replay checkpoint. + /// + public WebGPUSceneTarget ActualTarget { get; private set; } + + /// + /// Gets a value indicating whether the latest validated logical image still needs copying + /// into . + /// + public readonly bool RequiresCommit => this.latestImage != WebGPUOrderedTargetImage.Actual; + + /// + /// Initializes this identity from an externally owned root or flush-owned layer target. + /// + /// The durable target used for future replay. + public void Initialize(WebGPUSceneTarget actualTarget) + { + this.ActualTarget = actualTarget; + this.latestImage = WebGPUOrderedTargetImage.Actual; + this.IsInitialized = true; + this.IsActive = true; + } + + /// + /// Gets the target image that subsequent fine shading must sample. + /// + /// The latest logical target with the correct logical-to-texture offset. + public readonly WebGPUSceneTarget GetLatestTarget() + => this.latestImage switch + { + WebGPUOrderedTargetImage.ScratchA => new WebGPUSceneTarget( + this.scratchATexture, + this.scratchAView, + this.ActualTarget.Bounds, + new Point(-this.ActualTarget.Bounds.X, -this.ActualTarget.Bounds.Y)), + WebGPUOrderedTargetImage.ScratchB => new WebGPUSceneTarget( + this.scratchBTexture, + this.scratchBView, + this.ActualTarget.Bounds, + new Point(-this.ActualTarget.Bounds.X, -this.ActualTarget.Bounds.Y)), + _ => this.ActualTarget + }; + + /// + /// Selects and lazily creates the scratch texture that does not contain the current input image. + /// + /// The flush context that owns created scratch handles. + /// Receives the selected output texture. + /// Receives the selected output storage view. + /// Receives the selected scratch identity. + public void GetNextOutput( + WebGPUFlushContext flushContext, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, + out WebGPUOrderedTargetImage image) + { + image = this.latestImage == WebGPUOrderedTargetImage.ScratchA + ? WebGPUOrderedTargetImage.ScratchB + : WebGPUOrderedTargetImage.ScratchA; + + if (image == WebGPUOrderedTargetImage.ScratchA) + { + if (this.scratchATexture is null && + !TryCreateCompositionTexture( + flushContext, + this.ActualTarget.Bounds.Width, + this.ActualTarget.Bounds.Height, + out this.scratchATexture, + out this.scratchAView, + out string? error)) + { + throw new InvalidOperationException(error); + } + + texture = this.scratchATexture; + textureView = this.scratchAView; + return; + } + + if (this.scratchBTexture is null && + !TryCreateCompositionTexture( + flushContext, + this.ActualTarget.Bounds.Width, + this.ActualTarget.Bounds.Height, + out this.scratchBTexture, + out this.scratchBView, + out string? scratchError)) + { + throw new InvalidOperationException(scratchError); + } + + texture = this.scratchBTexture; + textureView = this.scratchBView; + } + + /// + /// Publishes the scratch image written by a successfully submitted range. + /// + /// The scratch identity containing the new logical output. + public void SetLatestImage(WebGPUOrderedTargetImage image) => this.latestImage = image; + + /// + /// Restores the logical image to the durable checkpoint before transaction replay. + /// + public void ResetToCommitted() => this.latestImage = WebGPUOrderedTargetImage.Actual; + + /// + /// Marks the latest validated image as durably copied into the actual target. + /// + public void MarkCommitted() => this.latestImage = WebGPUOrderedTargetImage.Actual; + + /// + /// Marks a completed layer target as no longer requiring commits after its composite range. + /// + public void MarkInactive() => this.IsActive = false; + } + + /// + /// Retains one GPU-only range so allocator growth can replay it without invoking Apply callbacks. + /// + private struct WebGPUOrderedJournalEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The retained range to replay. + /// The target identity receiving the range. + /// The range's external texture source. + /// The fixed Apply upload view. + /// The layer target supplying a composite view. + public WebGPUOrderedJournalEntry( + WebGPUSceneRange range, + int targetId, + WebGPUOrderedExternalSourceKind externalSourceKind, + WGPUTextureViewImpl* externalTextureView, + int externalTargetId) + { + this.Range = range; + this.TargetId = targetId; + this.ExternalSourceKind = externalSourceKind; + this.ExternalTextureView = externalTextureView; + this.ExternalTargetId = externalTargetId; + this.StatusStart = 0; + this.StatusCount = 0; + this.FullTileHeight = 0; + this.IsChunked = false; + } + + /// + /// Gets the retained encoded range. + /// + public WebGPUSceneRange Range { get; } + + /// + /// Gets the stable target identity receiving the range. + /// + public int TargetId { get; } + + /// + /// Gets the kind of external texture source retained by the range. + /// + public WebGPUOrderedExternalSourceKind ExternalSourceKind { get; } + + /// + /// Gets the fixed Apply upload texture view. + /// + public WGPUTextureViewImpl* ExternalTextureView { get; } + + /// + /// Gets the stable layer target identity supplying a composite texture. + /// + public int ExternalTargetId { get; } + + /// + /// Gets the first allocator record written by the latest range attempt. + /// + public int StatusStart { get; private set; } + + /// + /// Gets the number of allocator records written by the latest range attempt. + /// + public int StatusCount { get; private set; } + + /// + /// Gets the full range height in tile rows for chunk-status expansion. + /// + public uint FullTileHeight { get; private set; } + + /// + /// Gets a value indicating whether the latest range attempt used chunk-local capacities. + /// + public bool IsChunked { get; private set; } + + /// + /// Replaces status metadata after an initial or replayed submission. + /// + /// The first status record for this attempt. + /// The number of records emitted by this attempt. + /// The full range height in tile rows. + /// Whether the attempt used chunk-local capacities. + public void SetStatus(int statusStart, int statusCount, uint fullTileHeight, bool isChunked) + { + this.StatusStart = statusStart; + this.StatusCount = statusCount; + this.FullTileHeight = fullTileHeight; + this.IsChunked = isChunked; + } + } + + /// + /// Describes one aligned and clipped Apply pixel region inside the mixed barrier buffer. + /// + private readonly struct WebGPUApplyReadbackRegion + { + /// + /// Initializes a new instance of the struct. + /// + /// The source rectangle clipped to its logical target. + /// The destination X offset inside the full Apply image. + /// The destination Y offset inside the full Apply image. + /// The 256-byte-aligned buffer offset. + /// The meaningful pixel byte count in each row. + /// The 256-byte-aligned GPU row pitch. + public WebGPUApplyReadbackRegion( + Rectangle clippedSource, + int destinationX, + int destinationY, + ulong bufferOffset, + int packedRowBytes, + int readbackRowBytes) + { + this.ClippedSource = clippedSource; + this.DestinationX = destinationX; + this.DestinationY = destinationY; + this.BufferOffset = bufferOffset; + this.PackedRowBytes = packedRowBytes; + this.ReadbackRowBytes = readbackRowBytes; + } + + /// + /// Gets the clipped logical source rectangle. + /// + public Rectangle ClippedSource { get; } + + /// + /// Gets the destination X offset inside the full Apply image. + /// + public int DestinationX { get; } + + /// + /// Gets the destination Y offset inside the full Apply image. + /// + public int DestinationY { get; } + + /// + /// Gets the aligned byte offset inside the barrier buffer. + /// + public ulong BufferOffset { get; } + + /// + /// Gets the meaningful byte count in one pixel row. + /// + public int PackedRowBytes { get; } + + /// + /// Gets the GPU-aligned byte pitch between consecutive rows. + /// + public int ReadbackRowBytes { get; } + + /// + /// Gets the exclusive buffer end offset of this clipped pixel region. + /// + public ulong EndOffset + => checked(this.BufferOffset + ((ulong)this.ReadbackRowBytes * (ulong)this.ClippedSource.Height)); + } +} + +#pragma warning restore SA1201 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs index 337802d4f..f9311cfcb 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.Readback.cs @@ -4,19 +4,22 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Silk.NET.WebGPU; -using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// GPU readback helpers. +/// GPU readback helpers. Readback is strictly typed to the surface format by design: the +/// requested pixel type must match the target's native texture format exactly, otherwise the +/// operation throws. Callers that need a different pixel layout must read the native format +/// and convert on the CPU. /// public sealed unsafe partial class WebGPUDrawingBackend { + // Upper bound on the blocking wait for the buffer map-async callback. Prevents a lost or + // never-dispatched callback (for example after device loss) from hanging the caller forever. private const int ReadbackCallbackTimeoutMilliseconds = 5000; /// @@ -32,6 +35,11 @@ public void ReadRegion( Guard.NotNull(target, nameof(target)); Guard.NotNull(destination.Buffer, nameof(destination)); + // CPU reads observe content as final, so every deferred overflow readback must be + // resolved first: an overflowed offscreen flush is corrected by its parked re-render + // before the copy below is recorded, and queue ordering makes the copy see the fix. + this.HarvestPendingSchedulingStatuses(scene: null, drainAll: true); + // Readback is only available for native WebGPU targets with valid interop handles. if (!target.TryGetNativeSurface(out NativeSurface? nativeSurface) || nativeSurface is not WebGPUNativeSurface nativeTarget) { @@ -43,20 +51,22 @@ public void ReadRegion( throw new NotSupportedException("The target does not expose valid WebGPU device, queue, and texture handles for readback."); } - if (!TryGetCompositeTextureFormat(out WebGPUTextureFormat expectedFormat, out FeatureName requiredFeature)) + if (!TryGetCompositeTargetDescriptor(out WebGPUTargetDescriptor expectedTargetDescriptor, out WGPUFeatureName requiredFeature)) { throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' cannot be read back from a WebGPU target."); } - if (nativeTarget.TargetFormat != expectedFormat) + // Strict-format contract: no pixel conversion is performed on this path. The requested + // pixel type must be the surface's own native format. + if (nativeTarget.TargetDescriptor != expectedTargetDescriptor) { - throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' does not match WebGPU target format '{nativeTarget.TargetFormat}'."); + throw new NotSupportedException($"Pixel type '{typeof(TPixel).Name}' does not match the WebGPU target descriptor."); } // Convert canvas-local source coordinates to absolute native-surface coordinates. Rectangle absoluteSource = new( - target.Bounds.X + sourceRectangle.X, - target.Bounds.Y + sourceRectangle.Y, + target.Bounds.X + nativeTarget.TextureCoordinateOffset.X + sourceRectangle.X, + target.Bounds.Y + nativeTarget.TextureCoordinateOffset.Y + sourceRectangle.Y, sourceRectangle.Width, sourceRectangle.Height); @@ -69,21 +79,22 @@ public void ReadRegion( } WebGPU api = WebGPURuntime.GetApi(); - Wgpu wgpuExtension = WebGPURuntime.GetWgpuExtension(); using WebGPUHandle.HandleReference deviceReference = nativeTarget.DeviceHandle.AcquireReference(); using WebGPUHandle.HandleReference queueReference = nativeTarget.QueueHandle.AcquireReference(); using WebGPUHandle.HandleReference textureReference = nativeTarget.TargetTextureHandle.AcquireReference(); - Device* device = (Device*)deviceReference.Handle; + WGPUDeviceImpl* device = (WGPUDeviceImpl*)deviceReference.Handle; - if (requiredFeature != FeatureName.Undefined + // Feature gate: some formats (for example Bgra8Unorm) require an optional device + // feature before they can participate in composite I/O. + if (requiredFeature != default && !WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle).HasFeature(requiredFeature)) { throw new NotSupportedException($"The target device does not support WebGPU feature '{requiredFeature}' required to read back pixel type '{typeof(TPixel).Name}'."); } - Queue* queue = (Queue*)queueReference.Handle; - Texture* texture = (Texture*)textureReference.Handle; + WGPUQueueImpl* queue = (WGPUQueueImpl*)queueReference.Handle; + WGPUTextureImpl* texture = (WGPUTextureImpl*)textureReference.Handle; int pixelSizeInBytes = Unsafe.SizeOf(); int packedRowBytes = checked(source.Width * pixelSizeInBytes); @@ -92,20 +103,20 @@ public void ReadRegion( int readbackRowBytes = Align(packedRowBytes, 256); ulong readbackByteCount = checked((ulong)readbackRowBytes * (ulong)source.Height); - WgpuBuffer* readbackBuffer = null; - CommandEncoder* commandEncoder = null; - CommandBuffer* commandBuffer = null; + WGPUBufferImpl* readbackBuffer = null; + WGPUCommandEncoderImpl* commandEncoder = null; + WGPUCommandBufferImpl* commandBuffer = null; try { - BufferDescriptor bufferDescriptor = new() + WGPUBufferDescriptor bufferDescriptor = new() { - Usage = BufferUsage.CopyDst | BufferUsage.MapRead, - Size = readbackByteCount, - MappedAtCreation = false + usage = (ulong)(BufferUsage.CopyDst | BufferUsage.MapRead), + size = readbackByteCount, + mappedAtCreation = 0U, }; readbackBuffer = api.DeviceCreateBuffer(device, in bufferDescriptor); - CommandEncoderDescriptor encoderDescriptor = default; + WGPUCommandEncoderDescriptor encoderDescriptor = default; commandEncoder = api.DeviceCreateCommandEncoder(device, in encoderDescriptor); if (commandEncoder is null) { @@ -113,54 +124,55 @@ public void ReadRegion( } // Copy only the requested source rect from the target texture into the readback buffer. - ImageCopyTexture sourceCopy = new() + WGPUTexelCopyTextureInfo sourceCopy = new() { - Texture = texture, - MipLevel = 0, - Origin = new Origin3D((uint)source.X, (uint)source.Y, 0), - Aspect = TextureAspect.All + texture = texture, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)source.X, (uint)source.Y, 0), + aspect = WGPUTextureAspect.All }; - ImageCopyBuffer destinationCopy = new() + WGPUTexelCopyBufferInfo destinationCopy = new() { - Buffer = readbackBuffer, - Layout = new TextureDataLayout + buffer = readbackBuffer, + layout = new WGPUTexelCopyBufferLayout { - Offset = 0, - BytesPerRow = (uint)readbackRowBytes, - RowsPerImage = (uint)source.Height + offset = 0, + bytesPerRow = (uint)readbackRowBytes, + rowsPerImage = (uint)source.Height } }; - Extent3D copySize = new((uint)source.Width, (uint)source.Height, 1); + WGPUExtent3D copySize = new((uint)source.Width, (uint)source.Height, 1); api.CommandEncoderCopyTextureToBuffer(commandEncoder, in sourceCopy, in destinationCopy, in copySize); - CommandBufferDescriptor commandBufferDescriptor = default; + WGPUCommandBufferDescriptor commandBufferDescriptor = default; commandBuffer = api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); if (commandBuffer is null) { throw new InvalidOperationException("The WebGPU device could not finalize the readback command buffer."); } - api.QueueSubmit(queue, 1, ref commandBuffer); + ulong readbackSubmissionIndex = api.QueueSubmitForIndex(queue, 1, ref commandBuffer); + api.CommandBufferRelease(commandBuffer); commandBuffer = null; api.CommandEncoderRelease(commandEncoder); commandEncoder = null; // Map the GPU buffer and wait for completion before reading host-visible bytes. - BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + WGPUMapAsyncStatus mapStatus = default; using ManualResetEventSlim mapReady = new(false); - void Callback(BufferMapAsyncStatus status, void* userData) + void Callback(WGPUMapAsyncStatus status, void* userData) { _ = userData; mapStatus = status; mapReady.Set(); } - using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback.From(Callback); api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)readbackByteCount, callback, null); - if (!WaitForMapSignal(wgpuExtension, device, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + if (!WaitForMapSignal(api, device, mapReady, readbackSubmissionIndex) || mapStatus != WGPUMapAsyncStatus.Success) { throw new InvalidOperationException($"The WebGPU device could not map the readback buffer. Status: '{mapStatus}'."); } @@ -223,24 +235,29 @@ private static int Align(int value, int alignment) => ((value + alignment - 1) / alignment) * alignment; /// - /// Waits for the asynchronous readback map callback, pumping the native device when available. + /// Waits for the asynchronous readback map callback while pumping the native device. /// - /// The optional native WGPU extension used for device polling. + /// The WebGPU API used for device polling. /// The device that owns the mapped buffer. /// The event signaled by the map callback. + /// The queue submission index for this map attempt. /// when the callback completed before the timeout; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool WaitForMapSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + private static bool WaitForMapSignal(WebGPU api, WGPUDeviceImpl* device, ManualResetEventSlim signal, ulong submissionIndex) { - if (extension is null) - { - return signal.Wait(ReadbackCallbackTimeoutMilliseconds); - } - + // Poll the exact submission without allowing one native call to block past the managed + // timeout. Keeping the submission index is required when another thread can interleave a + // later submit; replacing it with a queue-wide poll would broaden this readback's target. Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < ReadbackCallbackTimeoutMilliseconds) { - _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + _ = api.DevicePoll(device, false, &submissionIndex); + + if (!signal.IsSet) + { + Thread.Yield(); + } } return signal.IsSet; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs index 41a1b35df..80d37d7b8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackend.cs @@ -2,7 +2,8 @@ // Licensed under the Six Labors Split License. using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; +using System.Runtime.ExceptionServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -42,6 +43,22 @@ public sealed unsafe partial class WebGPUDrawingBackend : IDrawingBackend, IDisp private WebGPUSceneSchedulingArena? cachedSchedulingArena; private WebGPUSceneResourceArena? cachedResourceArena; + // Deferred scheduling-status readbacks from recent flushes. The backend is the owner + // because it outlives the per-frame canvases and scenes: parking a pending map on a + // per-frame scene would force a blocking resolve at that scene's disposal, putting the + // CPU-GPU sync right back into the frame. The queue is harvested wait-free at the start + // of each flush; entries are only force-resolved when the queue exceeds its bound. + // Offscreen entries carry the context for a corrective re-render on overflow and hold a + // deferred-render reference that keeps their scene's GPU payload alive until harvested. + private readonly object pendingStatusSync = new(); + private readonly List pendingSchedulingStatuses = []; + + // Bounds the deferred-readback queue. The GPU typically completes a frame's map by the + // next flush, so the queue holds roughly one entry per flush in the current frame; the + // bound only bites when the GPU falls several frames behind, where a blocking resolve + // doubles as backpressure. + private const int MaxPendingSchedulingStatuses = 8; + // Advisory first-guess state for repeated oversized eager scenes. Parallel renders may // race to update it; every hinted chunk is still validated before dispatch, so a stale // or cross-scene value can only affect the first shrink attempt, not correctness. @@ -68,12 +85,22 @@ public WebGPUDrawingBackend() /// internal bool DiagnosticLastFlushUsedChunking { get; private set; } + /// + /// Gets or sets the upper bound used when validating staged-scene storage bindings. + /// + /// + /// The default leaves the device-reported limit unchanged. Tests can lower the bound on one + /// backend instance to exercise chunked rendering without changing shared device state. + /// + internal nuint ScratchBufferBindingSizeLimit { get; set; } = nuint.MaxValue; + /// /// Gets the binding category that selected chunked rendering for the last WebGPU flush. /// /// /// This value describes only the most recent flush on this backend instance. When the most recent flush did not use - /// chunked rendering, this property returns None. + /// chunked rendering, this property returns None. A corrective re-render triggered by a deferred overflow + /// readback also updates this value, so it can change after the originating flush call has returned. /// internal string DiagnosticLastChunkingBindingFailure => this.lastChunkingBindingFailure.ToString(); @@ -86,13 +113,25 @@ public DrawingBackendScene CreateScene( { this.ThrowIfDisposed(); - if (!WebGPUSceneEncoder.TryEncode( + // Batches containing Apply need the ordered encoder: Apply reads pixels back mid-scene, + // so the operations before and after it must stay in submission order. + bool encoded = commandBatch.HasApply + ? WebGPUSceneEncoder.TryEncodeOrdered( commandBatch, targetBounds, configuration.MemoryAllocator, configuration.MaxDegreeOfParallelism, out WebGPUEncodedScene encodedScene, - out string? error)) + out string? error) + : WebGPUSceneEncoder.TryEncode( + commandBatch, + targetBounds, + configuration.MemoryAllocator, + configuration.MaxDegreeOfParallelism, + out encodedScene, + out error); + + if (!encoded) { throw new InvalidOperationException(error); } @@ -117,10 +156,10 @@ public void RenderScene( if (scene is not WebGPUDrawingBackendScene webGPUScene) { - throw new InvalidOperationException("The retained scene is not a WebGPU drawing backend scene."); + throw new InvalidOperationException("The scene is not compatible with the WebGPU drawing backend."); } - if (!TryGetCompositeTextureFormat(out WebGPUTextureFormat formatId, out FeatureName requiredFeature)) + if (!TryGetCompositeTargetDescriptor(out WebGPUTargetDescriptor pixelDescriptor, out WGPUFeatureName requiredFeature)) { throw new NotSupportedException($"The WebGPU backend does not support pixel format '{typeof(TPixel).Name}'."); } @@ -129,112 +168,276 @@ public void RenderScene( // boundary and keep staging focused on dispatch data. _ = nativeTarget.TryGetNativeSurface(out NativeSurface? nativeSurface); WebGPUNativeSurface webGPUTarget = (WebGPUNativeSurface)nativeSurface!; - TextureFormat textureFormat = WebGPUTextureFormatMapper.ToNative(webGPUTarget.TargetFormat); - if (webGPUTarget.TargetFormat != formatId) + WebGPUTargetDescriptor targetDescriptor = webGPUTarget.TargetDescriptor; + if (targetDescriptor.Format != pixelDescriptor.Format || + targetDescriptor.AlphaRepresentation != pixelDescriptor.AlphaRepresentation) { - throw new InvalidOperationException("The target texture format does not match the retained WebGPU scene pixel format."); + throw new InvalidOperationException("The WebGPU target descriptor does not match the drawing backend scene pixel format."); } if (nativeTarget.Bounds != webGPUScene.Bounds) { - throw new InvalidOperationException("The target bounds do not match the retained WebGPU scene bounds."); + throw new InvalidOperationException("The target bounds do not match the WebGPU drawing backend scene bounds."); } this.DiagnosticLastFlushUsedChunking = false; this.lastChunkingBindingFailure = WebGPUSceneDispatch.BindingLimitBuffer.None; + // Harvest deferred overflow readbacks from earlier presentation flushes before reading + // capacities: completed maps resolve wait-free, and any observed overflow grows the + // sizes this render is about to use. + this.HarvestPendingSchedulingStatuses(webGPUScene, drainAll: false); + WebGPUSceneBumpSizes currentBumpSizes = webGPUScene.BumpSizes; WebGPUSceneResourceArena? resourceArena = null; WebGPUSceneSchedulingArena? schedulingArena = null; try { - WebGPUEncodedScene? encodedScene = webGPUScene.EncodedScene; + // Ordered Apply scenes execute a flat operation plan with explicit layer transitions; + // plain scenes take the single staged-dispatch path below. + WebGPUEncodedScene encodedScene = webGPUScene.EncodedScene; + if (encodedScene.HasOperations) + { + // Ordered ranges share one arena pair across the transaction. Rent the retained + // high-water buffers here so repeated flushes do not recreate every GPU buffer. + resourceArena = webGPUScene.RentResourceArena() ?? this.RentResourceArena(); + schedulingArena = webGPUScene.RentSchedulingArena() ?? this.RentSchedulingArena(); + + using WebGPUFlushContext flushContext = WebGPUFlushContext.Create( + nativeTarget, + targetDescriptor, + requiredFeature, + configuration.MemoryAllocator, + this.ScratchBufferBindingSizeLimit); + + using WebGPUHandle.HandleReference targetTextureReference = webGPUTarget.TargetTextureHandle.AcquireReference(); + using WebGPUHandle.HandleReference targetTextureViewReference = webGPUTarget.TargetTextureViewHandle.AcquireReference(); + + WebGPUSceneTarget rootTarget = new( + (WGPUTextureImpl*)targetTextureReference.Handle, + (WGPUTextureViewImpl*)targetTextureViewReference.Handle, + nativeTarget.Bounds, + webGPUTarget.TextureCoordinateOffset); + + if (!TryCreateSampleableBackdrop(flushContext, rootTarget, out WebGPUSceneTarget sampleableRootTarget, out string? backdropError)) + { + throw new InvalidOperationException(backdropError); + } + + // Presentation targets redraw continuously, so allocator status can resolve through + // the existing deferred queue without stalling each shader frame. CPU Apply and + // offscreen targets retain synchronous validation because their pixels must be + // complete before control returns to user code. + bool deferOrderedStatus = webGPUTarget.IsPresentationSurface && encodedScene.OrderedApplyCount == 0; + this.RenderOrderedScene( + configuration, + flushContext, + sampleableRootTarget, + encodedScene, + webGPUScene, + deferOrderedStatus, + requiredFeature, + ref currentBumpSizes, + ref resourceArena, + ref schedulingArena); + + if (flushContext.RequiresPresentationCopies) + { + if (!flushContext.EnsureCommandEncoder()) + { + throw new InvalidOperationException("Failed to create a command encoder for the ordered presentation copy."); + } + + CopyTextureRegion( + flushContext, + sampleableRootTarget.Texture, + sampleableRootTarget.Bounds.X + sampleableRootTarget.TextureOffset.X, + sampleableRootTarget.Bounds.Y + sampleableRootTarget.TextureOffset.Y, + rootTarget.Texture, + rootTarget.Bounds.X + rootTarget.TextureOffset.X, + rootTarget.Bounds.Y + rootTarget.TextureOffset.Y, + rootTarget.Bounds.Width, + rootTarget.Bounds.Height); + + if (!TrySubmitPendingCommands(flushContext)) + { + throw new InvalidOperationException("Failed to submit the ordered presentation copy."); + } + } + + webGPUScene.UpdateBumpSizes(currentBumpSizes); + + // Max-merge rather than assign: the backend value is a high-water mark shared by + // every future scene, and a small scene must not shrink it back into rediscovery. + this.bumpSizes = MaxBumpSizes(this.bumpSizes, currentBumpSizes); + return; + } - if (encodedScene is not null && encodedScene.FillCount != 0) + if (encodedScene.FillCount != 0) { // Scene arenas are rented once for the render. If a concurrent render // owns them, the backend cache supplies independent scratch buffers. resourceArena ??= webGPUScene.RentResourceArena() ?? this.RentResourceArena(); schedulingArena ??= webGPUScene.RentSchedulingArena() ?? this.RentSchedulingArena(); - bool renderCompleted = false; + this.RenderEncodedSceneWithGrowth( + configuration, + webGPUTarget, + nativeTarget.Bounds, + targetDescriptor, + requiredFeature, + webGPUScene, + deferOverflowCheck: true, + ref currentBumpSizes, + ref resourceArena, + ref schedulingArena); + } + + webGPUScene.UpdateBumpSizes(currentBumpSizes); - // Retry loop: scratch allocators start small and the GPU reports actual demand. - // The retained scene keeps the largest observed size so later renders avoid - // rediscovering the same growth. - for (int attempt = 0; attempt < MaxDynamicGrowthAttempts; attempt++) + // Max-merge rather than assign; see the ordered-scene path above. + this.bumpSizes = MaxBumpSizes(this.bumpSizes, currentBumpSizes); + } + finally + { + webGPUScene.ReturnArenas(resourceArena, schedulingArena, this); + } + } + + /// + /// Renders a plain (non-ordered) encoded scene through the staged pipeline with the dynamic + /// scratch-growth retry loop. All texture targets defer the scratch-overflow readback so the + /// flush never blocks on the GPU: presentation surfaces are corrected by the next frame's + /// redraw, while offscreen targets park a corrective re-render that a later harvest invokes + /// on the rare overflow, so no incomplete content can persist. + /// + /// The pixel format of the flush target. + /// The active processing configuration. + /// The native surface holding the target texture handles. + /// The target bounds for the flush. + /// The target texture format and alpha representation. + /// The device feature required by the target format, or the default value. + /// The retained scene being rendered. + /// Whether the overflow readback may be deferred; corrective re-renders pass . + /// The scratch capacities carried across attempts. + /// The rented scene resource arena. + /// The rented scheduling arena. + private void RenderEncodedSceneWithGrowth( + Configuration configuration, + WebGPUNativeSurface webGPUTarget, + Rectangle targetBounds, + WebGPUTargetDescriptor targetDescriptor, + WGPUFeatureName requiredFeature, + WebGPUDrawingBackendScene webGPUScene, + bool deferOverflowCheck, + ref WebGPUSceneBumpSizes currentBumpSizes, + ref WebGPUSceneResourceArena? resourceArena, + ref WebGPUSceneSchedulingArena? schedulingArena) + where TPixel : unmanaged, IPixel + { + WebGPUEncodedScene encodedScene = webGPUScene.EncodedScene; + bool renderCompleted = false; + + // Retry loop: scratch allocators start small and the GPU reports actual demand. + // The retained scene keeps the largest observed size so later renders avoid + // rediscovering the same growth. + for (int attempt = 0; attempt < MaxDynamicGrowthAttempts; attempt++) + { + WebGPUStagedScene stagedScene = WebGPUSceneDispatch.CreateStagedScene( + configuration, + webGPUTarget, + targetBounds, + encodedScene, + targetDescriptor, + requiredFeature, + currentBumpSizes, + this.ScratchBufferBindingSizeLimit, + ref resourceArena); + + try + { + if (stagedScene.BindingLimitFailure.Buffer != WebGPUSceneDispatch.BindingLimitBuffer.None) + { + this.DiagnosticLastFlushUsedChunking = true; + this.lastChunkingBindingFailure = stagedScene.BindingLimitFailure.Buffer; + } + + bool renderSucceeded = WebGPUSceneDispatch.TryRenderStagedScene( + ref stagedScene, + ref schedulingArena, + this.GetChunkTileHeightHint(stagedScene.BindingLimitFailure.Buffer, encodedScene.TargetSize), + deferOverflowCheck, + out bool requiresGrowth, + out WebGPUSceneBumpSizes grownBumpSizes, + out uint successfulChunkTileHeight, + out WebGPUPendingSchedulingStatus? pendingStatus, + out string? error); + + if (renderSucceeded) { - WebGPUStagedScene stagedScene = WebGPUSceneDispatch.CreateStagedScene( - configuration, - nativeTarget, - encodedScene, - textureFormat, - requiredFeature, - currentBumpSizes, - ref resourceArena); - - try + currentBumpSizes = MaxBumpSizes(currentBumpSizes, grownBumpSizes); + + // A deferred render parks its readback on the backend, which outlives + // per-frame scenes; a later flush harvests it and applies any observed + // growth. Offscreen targets additionally park a corrective re-render: + // their content is not redrawn every frame, so a rare overflow must be + // repaired rather than waiting for the next redraw. + if (pendingStatus is not null) { - if (stagedScene.BindingLimitFailure.Buffer != WebGPUSceneDispatch.BindingLimitBuffer.None) - { - this.DiagnosticLastFlushUsedChunking = true; - this.lastChunkingBindingFailure = stagedScene.BindingLimitFailure.Buffer; - } - - bool renderSucceeded = WebGPUSceneDispatch.TryRenderStagedScene( - ref stagedScene, - ref schedulingArena, - this.GetChunkTileHeightHint(stagedScene.BindingLimitFailure.Buffer, encodedScene.TargetSize), - out bool requiresGrowth, - out WebGPUSceneBumpSizes grownBumpSizes, - out uint successfulChunkTileHeight, - out string? error); - - if (renderSucceeded) - { - currentBumpSizes = MaxBumpSizes(currentBumpSizes, grownBumpSizes); - - if (successfulChunkTileHeight != 0) - { - this.UpdateChunkTileHeightHint( - stagedScene.BindingLimitFailure.Buffer, - encodedScene.TargetSize, - successfulChunkTileHeight); - } - - renderCompleted = true; - break; - } - - if (requiresGrowth) - { - currentBumpSizes = MaxBumpSizes(currentBumpSizes, grownBumpSizes); - continue; - } - - throw new InvalidOperationException(error ?? "The staged WebGPU scene dispatch failed."); + Action? correctiveRender = webGPUTarget.IsPresentationSurface + ? null + : () => this.RenderCorrective(configuration, webGPUTarget, targetBounds, targetDescriptor, requiredFeature, webGPUScene); + this.EnqueuePendingSchedulingStatus(pendingStatus, webGPUScene, correctiveRender); } - finally + + if (successfulChunkTileHeight != 0) { - stagedScene.Dispose(); + this.UpdateChunkTileHeightHint( + stagedScene.BindingLimitFailure.Buffer, + encodedScene.TargetSize, + successfulChunkTileHeight); } + + renderCompleted = true; + break; } - if (!renderCompleted) + if (requiresGrowth) { - throw new InvalidOperationException("The staged WebGPU scene exceeded the current dynamic growth retry budget."); + currentBumpSizes = MaxBumpSizes(currentBumpSizes, grownBumpSizes); + continue; } + + throw new InvalidOperationException(error ?? "The staged WebGPU scene dispatch failed."); + } + finally + { + stagedScene.Dispose(); } + } - webGPUScene.UpdateBumpSizes(currentBumpSizes); - this.bumpSizes = currentBumpSizes; + if (!renderCompleted) + { + throw new InvalidOperationException("The staged WebGPU scene exceeded the current dynamic growth retry budget."); } - finally + } + + /// + /// Clears the given render target to transparent black by opening a render pass with a + /// clear load action and immediately ending it. + /// + /// The flush-scoped WebGPU device, queue, and encoder state. + /// The texture view to clear. + private static void ClearTarget(WebGPUFlushContext flushContext, WGPUTextureViewImpl* targetView) + { + if (!flushContext.EnsureCommandEncoder() || !flushContext.BeginRenderPass(targetView, loadExisting: false)) { - webGPUScene.ReturnArenas(resourceArena, schedulingArena, this); + throw new InvalidOperationException("Failed to clear the WebGPU layer target."); } + + flushContext.EndRenderPassIfOpen(); } /// @@ -303,18 +506,22 @@ private void UpdateChunkTileHeightHint( /// /// Rents the cached scene resource arena for a render, leaving the backend cache empty. /// + /// The cached arena, or when the cache slot is empty. internal WebGPUSceneResourceArena? RentResourceArena() => Interlocked.Exchange(ref this.cachedResourceArena, null); /// /// Rents the cached scheduling arena for a render, leaving the backend cache empty. /// + /// The cached arena, or when the cache slot is empty. internal WebGPUSceneSchedulingArena? RentSchedulingArena() => Interlocked.Exchange(ref this.cachedSchedulingArena, null); /// /// Returns reusable arenas to this backend cache. /// + /// The scene resource arena to cache, or when none was rented. + /// The scheduling arena to cache, or when none was rented. internal void ReturnArenas( WebGPUSceneResourceArena? resourceArena, WebGPUSceneSchedulingArena? schedulingArena) @@ -341,34 +548,161 @@ internal void ReturnArenas( // must be released immediately instead of being left for final backend disposal. WebGPUSceneResourceArena.Dispose(Interlocked.Exchange(ref this.cachedResourceArena, resourceArena)); } + + // Dispose can drain the cache between the guard above and these stores; sweep again + // so an arena parked after that drain cannot outlive the backend. + if (this.isDisposed) + { + WebGPUSceneSchedulingArena.Dispose(Interlocked.Exchange(ref this.cachedSchedulingArena, null)); + WebGPUSceneResourceArena.Dispose(Interlocked.Exchange(ref this.cachedResourceArena, null)); + } } /// - /// Creates one transient composition texture that can be rendered to, sampled from, and copied. + /// Creates one transient composition texture that can be sampled from, storage-bound, and copied. + /// The texture and view are tracked by the flush context, which owns their release. /// + /// The flush-scoped WebGPU device, queue, and encoder state. + /// The texture width in pixels. + /// The texture height in pixels. + /// Receives the created texture on success. + /// Receives the created texture view on success. + /// Receives the failure reason when creation fails. + /// when the texture and view were created; otherwise . internal static bool TryCreateCompositionTexture( WebGPUFlushContext flushContext, int width, int height, - out Texture* texture, - out TextureView* textureView, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, + out string? error) + => TryCreateCompositionTexture(flushContext, width, height, renderAttachment: false, out texture, out textureView, out error); + + /// + /// Creates a sampleable copy of a presentation target, or returns an offscreen target unchanged. + /// + /// The flush context that owns the target and any copied texture. + /// The target whose current contents provide the fine-shader backdrop. + /// Receives a target that can be sampled by the fine shader. + /// Receives the failure reason when the presentation copy cannot be recorded. + /// when a sampleable backdrop is available; otherwise . + internal static bool TryCreateSampleableBackdrop( + WebGPUFlushContext flushContext, + WebGPUSceneTarget target, + out WebGPUSceneTarget backdropTarget, + out string? error) + { + if (!flushContext.RequiresPresentationCopies) + { + backdropTarget = target; + error = null; + return true; + } + + if (!TryCreateCompositionTexture( + flushContext, + target.Bounds.Width, + target.Bounds.Height, + out WGPUTextureImpl* backdropTexture, + out WGPUTextureViewImpl* backdropTextureView, + out error)) + { + backdropTarget = default; + return false; + } + + if (!flushContext.EnsureCommandEncoder()) + { + backdropTarget = default; + error = "Failed to create a command encoder for the presentation backdrop copy."; + return false; + } + + // Swapchain capabilities do not guarantee TextureBinding. Copy into a backend-owned + // composition texture before the fine shader binds the existing pixels as its backdrop. + CopyTextureRegion( + flushContext, + target.Texture, + target.Bounds.X + target.TextureOffset.X, + target.Bounds.Y + target.TextureOffset.Y, + backdropTexture, + 0, + 0, + target.Bounds.Width, + target.Bounds.Height); + + backdropTarget = new WebGPUSceneTarget( + backdropTexture, + backdropTextureView, + target.Bounds, + new Point(-target.Bounds.X, -target.Bounds.Y)); + + error = null; + return true; + } + + /// + /// Creates one transient composition texture that can be sampled from, storage-bound, and copied. + /// When is the texture can also be used + /// as a render-pass target, which scoped layers require for clearing. The texture and view are + /// tracked by the flush context, which owns their release. + /// + /// The flush-scoped WebGPU device, queue, and encoder state. + /// The texture width in pixels. + /// The texture height in pixels. + /// Whether the texture also needs render-attachment usage. + /// Receives the created texture on success. + /// Receives the created texture view on success. + /// Receives the failure reason when creation fails. + /// when the texture and view were created; otherwise . + private static bool TryCreateCompositionTexture( + WebGPUFlushContext flushContext, + int width, + int height, + bool renderAttachment, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, out string? error) { textureView = null; + TextureUsage usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst; + + if (renderAttachment) + { + usage |= TextureUsage.RenderAttachment; + } - TextureDescriptor textureDescriptor = new() + // Prefer a device-pooled texture from an earlier flush. Every consumer addresses these + // textures in texel space with explicit extents, so an entry larger than requested is + // safe, and reuse skips both driver allocation and wgpu's lazy zero-initialization. + if (flushContext.DeviceState.TryRentPooledTexture( + flushContext.TextureFormat, + (ulong)usage, + (uint)width, + (uint)height, + out texture, + out textureView, + out uint rentedWidth, + out uint rentedHeight)) { - Usage = TextureUsage.TextureBinding | TextureUsage.StorageBinding | TextureUsage.CopySrc | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = flushContext.TextureFormat, - MipLevelCount = 1, - SampleCount = 1 + flushContext.TrackPooledTexture(texture, textureView, flushContext.TextureFormat, (ulong)usage, rentedWidth, rentedHeight); + error = null; + return true; + } + + WGPUTextureDescriptor textureDescriptor = new() + { + usage = (ulong)usage, + dimension = WGPUTextureDimension._2D, + size = new WGPUExtent3D((uint)width, (uint)height, 1), + format = flushContext.TextureFormat, + mipLevelCount = 1, + sampleCount = 1 }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - texture = flushContext.Api.DeviceCreateTexture((Device*)deviceReference.Handle, in textureDescriptor); + texture = flushContext.Api.DeviceCreateTexture((WGPUDeviceImpl*)deviceReference.Handle, in textureDescriptor); } if (texture is null) @@ -377,18 +711,18 @@ internal static bool TryCreateCompositionTexture( return false; } - TextureViewDescriptor textureViewDescriptor = new() + WGPUTextureViewDescriptor textureViewDescriptor = new() { - Format = flushContext.TextureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All + format = flushContext.TextureFormat, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All }; - textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + textureView = flushContext.Api.TextureCreateView(texture, &textureViewDescriptor); if (textureView is null) { flushContext.Api.TextureRelease(texture); @@ -397,65 +731,101 @@ internal static bool TryCreateCompositionTexture( return false; } - flushContext.TrackTexture(texture); - flushContext.TrackTextureView(textureView); + // Ownership transfers to the flush context here; it returns both handles to the device + // pool when disposed, so the next flush rents them instead of recreating. + flushContext.TrackPooledTexture(texture, textureView, flushContext.TextureFormat, (ulong)usage, (uint)width, (uint)height); error = null; return true; } /// - /// Copies one texture region from source to destination texture. + /// Records one texture-to-texture region copy on the flush context's current command encoder. + /// The copy executes when that encoder is next submitted. /// + /// The flush-scoped WebGPU device, queue, and encoder state. + /// The texture copied from. + /// The source origin X in texels. + /// The source origin Y in texels. + /// The texture copied to. + /// The destination origin X in texels. + /// The destination origin Y in texels. + /// The copy width in texels. + /// The copy height in texels. [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static void CopyTextureRegion( WebGPUFlushContext flushContext, - Texture* sourceTexture, + WGPUTextureImpl* sourceTexture, int sourceOriginX, int sourceOriginY, - Texture* destinationTexture, + WGPUTextureImpl* destinationTexture, int destinationOriginX, int destinationOriginY, int width, int height) { - ImageCopyTexture source = new() + WGPUTexelCopyTextureInfo source = new() { - Texture = sourceTexture, - MipLevel = 0, - Origin = new Origin3D((uint)sourceOriginX, (uint)sourceOriginY, 0), - Aspect = TextureAspect.All + texture = sourceTexture, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)sourceOriginX, (uint)sourceOriginY, 0), + aspect = WGPUTextureAspect.All }; - ImageCopyTexture destination = new() + WGPUTexelCopyTextureInfo destination = new() { - Texture = destinationTexture, - MipLevel = 0, - Origin = new Origin3D((uint)destinationOriginX, (uint)destinationOriginY, 0), - Aspect = TextureAspect.All + texture = destinationTexture, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)destinationOriginX, (uint)destinationOriginY, 0), + aspect = WGPUTextureAspect.All }; - Extent3D copySize = new((uint)width, (uint)height, 1); + WGPUExtent3D copySize = new((uint)width, (uint)height, 1); flushContext.Api.CommandEncoderCopyTextureToTexture(flushContext.CommandEncoder, in source, in destination, in copySize); } /// - /// Submits the current command encoder, if any. + /// Finishes and submits the flush context's current command encoder, if any. /// - internal static bool TrySubmit(WebGPUFlushContext flushContext) + /// The flush-scoped WebGPU device, queue, and encoder state. + /// + /// when there was nothing to submit or the submit succeeded; otherwise . + /// + internal static bool TrySubmitPendingCommands(WebGPUFlushContext flushContext) { - CommandEncoder* commandEncoder = flushContext.CommandEncoder; - if (commandEncoder is null) + if (flushContext.CommandEncoder is null) { return true; } + return TrySubmitWithIndex(flushContext, out _); + } + + /// + /// Finishes and submits the flush context's current command encoder and returns its submission index. + /// + /// The flush-scoped WebGPU device, queue, and encoder state. + /// Receives the queue submission index for the submitted command buffer. + /// + /// when the command buffer was submitted; otherwise . + /// + internal static bool TrySubmitWithIndex(WebGPUFlushContext flushContext, out ulong submissionIndex) + { + WGPUCommandEncoderImpl* commandEncoder = flushContext.CommandEncoder; + submissionIndex = default; + + if (commandEncoder is null) + { + return false; + } + + // An encoder cannot be finished while a pass is still recording. flushContext.EndComputePassIfOpen(); flushContext.EndRenderPassIfOpen(); - CommandBuffer* commandBuffer = null; + WGPUCommandBufferImpl* commandBuffer = null; try { - CommandBufferDescriptor descriptor = default; + WGPUCommandBufferDescriptor descriptor = default; commandBuffer = flushContext.Api.CommandEncoderFinish(commandEncoder, in descriptor); if (commandBuffer is null) { @@ -464,7 +834,8 @@ internal static bool TrySubmit(WebGPUFlushContext flushContext) using (WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference()) { - flushContext.Api.QueueSubmit((Queue*)queueReference.Handle, 1, ref commandBuffer); + WGPUQueueImpl* queue = (WGPUQueueImpl*)queueReference.Handle; + submissionIndex = flushContext.Api.QueueSubmitForIndex(queue, 1, ref commandBuffer); } flushContext.Api.CommandBufferRelease(commandBuffer); @@ -496,8 +867,188 @@ public void Dispose() this.lastChunkingBindingFailure = WebGPUSceneDispatch.BindingLimitBuffer.None; this.isDisposed = true; - WebGPUSceneSchedulingArena.Dispose(Interlocked.Exchange(ref this.cachedSchedulingArena, null)); - WebGPUSceneResourceArena.Dispose(Interlocked.Exchange(ref this.cachedResourceArena, null)); + // Retire deferred readbacks before their buffers. Each map is resolved or canceled here; + // a callback that native WebGPU still owes remains self-rooted after a timeout and is + // prevented from entering the disposed readback owner. The arena drain must run even + // when the harvest throws, or both cached arenas leak with no retry possible. + try + { + this.HarvestPendingSchedulingStatuses(null, true); + } + finally + { + WebGPUSceneSchedulingArena.Dispose(Interlocked.Exchange(ref this.cachedSchedulingArena, null)); + WebGPUSceneResourceArena.Dispose(Interlocked.Exchange(ref this.cachedResourceArena, null)); + } + } + + /// + /// Parks the deferred scheduling-status readback of a just-submitted flush on this backend + /// for a later flush to harvest. Offscreen targets pass a corrective re-render and hold a + /// deferred-render reference on their scene so the correction remains possible until the + /// readback resolves; presentation surfaces are corrected by their next redraw and pass + /// . + /// + /// The pending readback to park. + /// The retained scene the flush rendered. + /// The corrective re-render to invoke on overflow, or for presentation flushes. + private void EnqueuePendingSchedulingStatus( + WebGPUPendingSchedulingStatus pendingStatus, + WebGPUDrawingBackendScene scene, + Action? correctiveRender) + { + if (correctiveRender is not null) + { + scene.AddDeferredRenderReference(); + } + + PendingSchedulingStatusEntry entry = new( + pendingStatus, + correctiveRender is null ? null : scene, + correctiveRender); + + lock (this.pendingStatusSync) + { + this.pendingSchedulingStatuses.Add(entry); + } + } + + /// + /// Resolves parked deferred readbacks. Completed maps resolve wait-free; unfinished maps + /// stay parked unless the queue exceeds its bound (or is set), + /// where the oldest entries are resolved blocking as GPU backpressure. Observed demand is + /// merged into the backend capacities and the scene about to render, and offscreen entries + /// that overflowed are re-rendered with the grown capacities so no incomplete content can + /// persist in a texture the app may cache. + /// + /// The scene about to render, or during teardown or readback drains. + /// Whether to resolve every parked entry regardless of readiness. + private void HarvestPendingSchedulingStatuses(WebGPUDrawingBackendScene? scene, bool drainAll) + { + List? due = null; + lock (this.pendingStatusSync) + { + if (this.pendingSchedulingStatuses.Count == 0) + { + return; + } + + // Map callbacks only advance while the device is pumped; one non-blocking poll + // delivers every callback that has already completed on the GPU timeline. + this.pendingSchedulingStatuses[0].Status.PollDevice(); + + int overflow = this.pendingSchedulingStatuses.Count - MaxPendingSchedulingStatuses; + for (int i = this.pendingSchedulingStatuses.Count - 1; i >= 0; i--) + { + PendingSchedulingStatusEntry entry = this.pendingSchedulingStatuses[i]; + + // Entries are ordered oldest-first, so indexes below the overflow count are + // exactly the oldest entries beyond the queue bound. + if (drainAll || entry.Status.IsReady || i < overflow) + { + (due ??= []).Add(entry); + this.pendingSchedulingStatuses.RemoveAt(i); + } + } + } + + if (due is null) + { + return; + } + + // Entries were already removed from the queue above, so a failure on one entry must not + // abandon the rest: every remaining status would leak its readback buffer and its scene + // reference, leaving the scene's deferred teardown permanently blocked. The first + // failure is rethrown after every entry has been retired. + Exception? firstFailure = null; + foreach (PendingSchedulingStatusEntry entry in due) + { + try + { + bool overflowed = WebGPUSceneDispatch.ResolveDeferredSchedulingStatus(entry.Status, out WebGPUSceneBumpSizes resolvedBumpSizes); + scene?.UpdateBumpSizes(resolvedBumpSizes); + entry.Scene?.UpdateBumpSizes(resolvedBumpSizes); + this.bumpSizes = MaxBumpSizes(this.bumpSizes, resolvedBumpSizes); + + if (overflowed) + { + entry.CorrectiveRender?.Invoke(); + } + } + catch (Exception failure) + { + firstFailure ??= failure; + entry.Status.Dispose(); + } + finally + { + entry.Scene?.ReleaseDeferredRenderReference(); + } + } + + if (firstFailure is not null) + { + ExceptionDispatchInfo.Capture(firstFailure).Throw(); + } + } + + /// + /// Re-renders an offscreen flush whose deferred readback reported a scratch overflow. + /// Runs the staged pipeline synchronously with the grown capacities so the corrected + /// content replaces the incomplete render before anything can observe it as final. A + /// target whose owner was disposed in the meantime is skipped: its content is gone too. + /// + /// The pixel format of the flush target. + /// The processing configuration of the original flush. + /// The native surface holding the target texture handles. + /// The target bounds of the original flush. + /// The target texture format and alpha representation. + /// The device feature required by the target format, or the default value. + /// The retained scene to replay, kept alive by a deferred-render reference. + private void RenderCorrective( + Configuration configuration, + WebGPUNativeSurface webGPUTarget, + Rectangle targetBounds, + WebGPUTargetDescriptor targetDescriptor, + WGPUFeatureName requiredFeature, + WebGPUDrawingBackendScene webGPUScene) + where TPixel : unmanaged, IPixel + { + WebGPUSceneBumpSizes currentBumpSizes = webGPUScene.BumpSizes; + WebGPUSceneResourceArena? resourceArena = webGPUScene.RentResourceArena() ?? this.RentResourceArena(); + WebGPUSceneSchedulingArena? schedulingArena = webGPUScene.RentSchedulingArena() ?? this.RentSchedulingArena(); + + try + { + this.RenderEncodedSceneWithGrowth( + configuration, + webGPUTarget, + targetBounds, + targetDescriptor, + requiredFeature, + webGPUScene, + deferOverflowCheck: false, + ref currentBumpSizes, + ref resourceArena, + ref schedulingArena); + + webGPUScene.UpdateBumpSizes(currentBumpSizes); + this.bumpSizes = MaxBumpSizes(this.bumpSizes, currentBumpSizes); + } + catch (ObjectDisposedException) + { + // The target texture handles were released between the flush and the harvest; + // the content the correction would repaint no longer exists. + } + catch (InvalidOperationException) + { + // Handle acquisition or device validation failed for the retired target; skip. + } + finally + { + webGPUScene.ReturnArenas(resourceArena, schedulingArena, this); + } } /// @@ -506,4 +1057,43 @@ public void Dispose() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + + /// + /// One parked deferred readback and the corrective context for its flush. + /// + private readonly struct PendingSchedulingStatusEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The deferred scheduling-status readback. + /// The rendered scene, held alive via a deferred-render reference; for presentation flushes. + /// The corrective re-render invoked when the readback reports overflow; for presentation flushes. + public PendingSchedulingStatusEntry( + WebGPUPendingSchedulingStatus status, + WebGPUDrawingBackendScene? scene, + Action? correctiveRender) + { + this.Status = status; + this.Scene = scene; + this.CorrectiveRender = correctiveRender; + } + + /// + /// Gets the deferred scheduling-status readback. + /// + public WebGPUPendingSchedulingStatus Status { get; } + + /// + /// Gets the rendered scene, held alive via a deferred-render reference; + /// for presentation flushes. + /// + public WebGPUDrawingBackendScene? Scene { get; } + + /// + /// Gets the corrective re-render invoked when the readback reports overflow; + /// for presentation flushes. + /// + public Action? CorrectiveRender { get; } + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackendScene.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackendScene.cs index 754f5ce47..7c6fcbdf0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackendScene.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDrawingBackendScene.cs @@ -6,6 +6,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Retained scene created by the WebGPU drawing backend. /// +/// +/// A retained scene can be rendered multiple times, including concurrently. The scene caches +/// reusable GPU arenas and monotonically growing scratch capacities between renders so repeat +/// renders skip the dynamic-growth discovery passes. +/// public sealed class WebGPUDrawingBackendScene : DrawingBackendScene { // These arenas contain mutable GPU scratch/resource buffers. They are cached on the @@ -15,6 +20,14 @@ public sealed class WebGPUDrawingBackendScene : DrawingBackendScene private WebGPUSceneResourceArena? resourceArena; private WebGPUSceneSchedulingArena? schedulingArena; + // Deferred-render references keep the encoded GPU payload alive past user disposal while + // the backend may still need it for a corrective re-render (a deferred overflow readback + // is outstanding). Teardown runs when disposal has been requested and the last reference + // releases; the tornDown guard makes the actual teardown single-shot under races. + private int deferredRenderReferences; + private int teardownRequested; + private int tornDown; + // Volatile works on int, so the uint scratch capacities are stored bit-for-bit in // signed fields. The values are compared and restored as uint in the accessors. // Each counter is monotonic: concurrent renders may race to report usage, but the @@ -47,9 +60,9 @@ internal WebGPUDrawingBackendScene( } /// - /// Gets the retained encoded scene when this is a leaf scene. + /// Gets the retained encoded scene. /// - internal WebGPUEncodedScene? EncodedScene { get; } + internal WebGPUEncodedScene EncodedScene { get; } /// /// Gets the scratch capacities for the scene. @@ -71,8 +84,10 @@ internal WebGPUSceneBumpSizes BumpSizes internal WebGPUDrawingBackend? ArenaOwner { get; set; } /// - /// Updates the scratch capacities retained by this scene. + /// Updates the scratch capacities retained by this scene. Each counter only ever grows; + /// values smaller than the current retained maximum are ignored. /// + /// The scratch capacities observed by a completed render. internal void UpdateBumpSizes(WebGPUSceneBumpSizes bumpSizes) { UpdateBumpSize(ref this.bumpLines, bumpSizes.Lines); @@ -86,20 +101,46 @@ internal void UpdateBumpSizes(WebGPUSceneBumpSizes bumpSizes) } /// - /// Rents reusable scene resource buffers for one render. + /// Adds a deferred-render reference that keeps this scene's encoded GPU payload alive past + /// user disposal, so a corrective re-render for an outstanding deferred overflow readback + /// can still replay it. /// + internal void AddDeferredRenderReference() + => Interlocked.Increment(ref this.deferredRenderReferences); + + /// + /// Releases a deferred-render reference. When disposal has already been requested and this + /// was the last reference, the actual teardown runs now. + /// + internal void ReleaseDeferredRenderReference() + { + if (Interlocked.Decrement(ref this.deferredRenderReferences) == 0 + && Volatile.Read(ref this.teardownRequested) == 1) + { + this.TeardownCore(); + } + } + + /// + /// Rents reusable scene resource buffers for one render, leaving the scene slot empty. + /// + /// The cached arena, or when the slot is empty. internal WebGPUSceneResourceArena? RentResourceArena() => Interlocked.Exchange(ref this.resourceArena, null); /// - /// Rents reusable scheduling scratch buffers for one render. + /// Rents reusable scheduling scratch buffers for one render, leaving the scene slot empty. /// + /// The cached arena, or when the slot is empty. internal WebGPUSceneSchedulingArena? RentSchedulingArena() => Interlocked.Exchange(ref this.schedulingArena, null); /// /// Returns reusable arenas after one render. /// + /// The scene resource arena to return, or when none was rented. + /// The scheduling arena to return, or when none was rented. + /// The backend that receives displaced arenas and this scene's arenas on disposal. internal void ReturnArenas( WebGPUSceneResourceArena? resourceArena, WebGPUSceneSchedulingArena? schedulingArena, @@ -131,11 +172,21 @@ internal void ReturnArenas( { arenaOwner.ReturnArenas(displacedResourceArena, displacedSchedulingArena); } + + // A return that races scene disposal would strand the arenas on a dead scene and + // starve the backend cache into recreating GPU buffers every flush; pull them back + // out and hand them to the backend instead. + if (Volatile.Read(ref this.teardownRequested) == 1) + { + this.ReleaseCachedArenas(); + } } /// /// Updates one retained scratch-capacity counter without allowing a concurrent render to shrink it. /// + /// The counter field storing the uint capacity bit-for-bit as int. + /// The newly observed capacity. private static void UpdateBumpSize(ref int target, uint value) { while (true) @@ -164,20 +215,37 @@ private static void UpdateBumpSize(ref int target, uint value) /// protected override void DisposeCore() { - if (this.EncodedScene is not null && - !ReferenceEquals(this.EncodedScene, WebGPUEncodedScene.Empty)) + Volatile.Write(ref this.teardownRequested, 1); + + // Cached arenas return to the backend immediately even when encoded-payload teardown + // is deferred: corrective re-renders rent fresh arenas, and keeping these parked on a + // disposed scene starves the backend cache into recreating GPU buffers every flush. + this.ReleaseCachedArenas(); + + // Encoded-payload teardown is deferred while the backend still holds deferred-render + // references for outstanding overflow readbacks; the last release runs it instead. + if (Volatile.Read(ref this.deferredRenderReferences) == 0) { - this.EncodedScene.Dispose(); + this.TeardownCore(); } + } - // Disposal uses the same rent path as rendering so it cannot release an arena - // currently rented by another render. A scene should still not be disposed while - // user code intends to keep rendering it, but this prevents cached arena slots - // from being shared or double-released during ordinary teardown races. + /// + /// Returns any cached arenas to the backend cache, or disposes them when no backend has + /// been recorded. Uses the same rent path as rendering so it cannot release an arena + /// currently rented by another render, and is safe to call repeatedly. + /// + private void ReleaseCachedArenas() + { WebGPUDrawingBackend? arenaOwner = this.ArenaOwner; WebGPUSceneResourceArena? resourceArena = this.RentResourceArena(); WebGPUSceneSchedulingArena? schedulingArena = this.RentSchedulingArena(); + if (resourceArena is null && schedulingArena is null) + { + return; + } + if (arenaOwner is not null) { arenaOwner.ReturnArenas(resourceArena, schedulingArena); @@ -187,7 +255,27 @@ protected override void DisposeCore() WebGPUSceneSchedulingArena.Dispose(schedulingArena); WebGPUSceneResourceArena.Dispose(resourceArena); } + } + + /// + /// Releases the encoded GPU payload exactly once, after both disposal has been requested + /// and no deferred-render references remain. Also sweeps arenas a corrective re-render + /// may have parked after disposal returned the original set. + /// + private void TeardownCore() + { + // Disposal request and reference release can race to this point; only one wins. + if (Interlocked.Exchange(ref this.tornDown, 1) == 1) + { + return; + } + + if (!ReferenceEquals(this.EncodedScene, WebGPUEncodedScene.Empty)) + { + this.EncodedScene.Dispose(); + } + this.ReleaseCachedArenas(); this.ArenaOwner = null; } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUDropShadowLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUDropShadowLayerEffect.cs new file mode 100644 index 000000000..7b32beb21 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUDropShadowLayerEffect.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU creates the shadow with shader passes; +/// a CPU backend applies the equivalent . +/// +public sealed class WebGPUDropShadowLayerEffect : WebGPUShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The shadow offset, in pixels. + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// The shadow colour. + public WebGPUDropShadowLayerEffect(Point offset, float sigma, Color color) + : base( + new DropShadowLayerEffect(offset, sigma, color), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + this.AddPasses(sigma, color); + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + /// Adds the tint and blur passes which form the shadow image. + /// + /// The Gaussian blur sigma, in pixels. + /// The shadow colour. + private void AddPasses(float sigma, Color color) + { + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + // Zero RGB rows replace source colour with the constant shadow colour. Scaling alpha by + // the colour alpha retains the source silhouette at the requested shadow opacity. + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + WebGPUColorMatrixLayerEffect tintEffect = new(tint); + ReadOnlySpan tintPasses = tintEffect.GetShaderPasses(); + + if (sigma == 0) + { + this.AddShaderPasses(tintPasses); + return; + } + + WebGPUGaussianBlurLayerEffect blurEffect = new(sigma); + ReadOnlySpan blurPasses = blurEffect.GetShaderPasses(); + + // Tinting before blur prevents source colours bleeding into the shadow. The inherited + // write-back metadata applies Offset after these passes, so it is not a shader uniform. + this.AddShaderPasses(tintPasses); + this.AddShaderPasses(blurPasses); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUEffectPipelineKey.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUEffectPipelineKey.cs new file mode 100644 index 000000000..8ac4b56dd --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUEffectPipelineKey.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies one exact generated layer-effect pipeline without using user labels as cache keys. +/// +internal readonly struct WebGPUEffectPipelineKey : IEquatable +{ + private readonly string source; + private readonly int uniformByteLength; + private readonly WGPUTextureFormat outputFormat; + private readonly int hashCode; + + /// + /// Initializes a new instance of the struct. + /// + /// The exact generated shader module. + /// The byte length of its user uniform binding. + /// The render target format written by the shader. + public WebGPUEffectPipelineKey(WebGPUShaderModuleSource moduleSource, int uniformByteLength, WGPUTextureFormat outputFormat) + { + this.source = moduleSource.Source; + this.uniformByteLength = uniformByteLength; + this.outputFormat = outputFormat; + this.hashCode = HashCode.Combine(moduleSource.PrecomputedHashCode, uniformByteLength, outputFormat); + } + + /// + public bool Equals(WebGPUEffectPipelineKey other) + => this.uniformByteLength == other.uniformByteLength && + this.outputFormat == other.outputFormat && + string.Equals(this.source, other.source, StringComparison.Ordinal); + + /// + public override bool Equals(object? obj) + => obj is WebGPUEffectPipelineKey other && this.Equals(other); + + /// + public override int GetHashCode() => this.hashCode; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUEffectRenderer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUEffectRenderer.cs new file mode 100644 index 000000000..2ef55709d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUEffectRenderer.cs @@ -0,0 +1,311 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Executes retained shader effects using flush-owned working textures and uniform storage. +/// +internal sealed unsafe class WebGPUEffectRenderer +{ + private const ulong UserUniformOffset = 256; + + private readonly WebGPUFlushContext flushContext; + private readonly WGPUTextureViewImpl* scratchAView; + private readonly WGPUTextureViewImpl* scratchBView; + private readonly WGPUBufferImpl* uniformBuffer; + private readonly ulong passUniformStride; + + /// + /// Initializes a new instance of the class. + /// + /// The flush that owns all invocation resources. + /// The largest effect input width in the retained scene. + /// The largest effect input height in the retained scene. + /// The largest user uniform binding in the retained scene. + /// The largest pass count of any single effect invocation in the retained scene. + public WebGPUEffectRenderer( + WebGPUFlushContext flushContext, + int maximumWidth, + int maximumHeight, + int maximumUniformByteLength, + int maximumPassCount) + { + this.flushContext = flushContext; + + _ = this.CreateWorkingTexture(maximumWidth, maximumHeight, out this.scratchAView); + _ = this.CreateWorkingTexture(maximumWidth, maximumHeight, out this.scratchBView); + + // Every pass owns a disjoint 256-byte-aligned region: [framework | user uniforms]. Distinct + // offsets mean queued uniform writes for a later pass can never replace bytes an earlier + // recorded pass still references, so one invocation needs no intermediate queue submission. + // 256 satisfies WebGPU's maximum permitted minUniformBufferOffsetAlignment. + this.passUniformStride = UserUniformOffset + AlignUp((ulong)Math.Max(16, maximumUniformByteLength), 256); + + // The buffer comes from the device pool: its content is rewritten by every invocation, so + // reuse across flushes is safe and skips a per-flush native buffer creation. + nuint uniformByteLength = checked((nuint)(this.passUniformStride * (ulong)Math.Max(1, maximumPassCount))); + this.uniformBuffer = flushContext.DeviceState.RentEffectUniformBuffer(uniformByteLength); + if (this.uniformBuffer is null) + { + throw new InvalidOperationException("Failed to create the WebGPU layer-effect uniform buffer."); + } + + flushContext.TrackPooledUniformBuffer(this.uniformBuffer, uniformByteLength); + } + + /// + /// Executes one retained shader effect and returns the working texture containing its result. + /// + /// The retained effect, source rectangle, and write-back metadata. + /// The latest logical target sampled by the effect. + /// The working texture view containing associated Rgba16Float pixels. + public WGPUTextureViewImpl* Render(WebGPUShaderEffectSceneItem effect, WebGPUSceneTarget sourceTarget) + { + // The ordered executor only ever supplies sampleable sources: the root target is replaced + // by its sampleable copy before execution begins, and every scratch or layer target is a + // composition texture created with TextureBinding usage. Copying here again would add one + // full-target texture and copy per effect invocation for presentation surfaces. + WebGPUSceneTarget sampleableSource = sourceTarget; + + Rectangle requestedRead = effect.InputRect; + requestedRead.Offset(-effect.ReadOffset.X, -effect.ReadOffset.Y); + Rectangle clippedRead = Rectangle.Intersect(sampleableSource.Bounds, requestedRead); + Point sourceOrigin = new( + requestedRead.X + sampleableSource.TextureOffset.X, + requestedRead.Y + sampleableSource.TextureOffset.Y); + Point validMinimum = new( + clippedRead.X - requestedRead.X, + clippedRead.Y - requestedRead.Y); + Point validMaximum = new( + validMinimum.X + clippedRead.Width, + validMinimum.Y + clippedRead.Height); + + WebGPUShaderFrameworkUniforms framework = new( + sourceOrigin, + validMinimum, + validMaximum, + effect.InputRect.Size); + + WGPUTextureViewImpl* sourceView = sampleableSource.TextureView; + WebGPUTargetDescriptor sourceDescriptor = this.flushContext.TargetDescriptor; + ReadOnlySpan shaderPasses = effect.Effect.GetShaderPasses(); + + for (int i = 0; i < shaderPasses.Length; i++) + { + WGPUTextureViewImpl* outputView = sourceView == this.scratchAView ? this.scratchBView : this.scratchAView; + this.RenderShaderPass(shaderPasses[i], i, sourceView, sourceDescriptor, in framework, outputView, effect.InputRect.Size); + + sourceView = outputView; + sourceDescriptor = WebGPUShaderEffectWorkingTexture.Descriptor; + + // Every pass after the first reads a complete working image whose local origin is zero. + Point inputMaximum = new(effect.InputRect.Width, effect.InputRect.Height); + framework = new WebGPUShaderFrameworkUniforms(Point.Empty, Point.Empty, inputMaximum, effect.InputRect.Size); + } + + return sourceView; + } + + /// + /// Encodes one layer-effect fragment-shader pass. + /// + private void RenderShaderPass( + WebGPUShaderPass shaderPass, + int passIndex, + WGPUTextureViewImpl* sourceView, + WebGPUTargetDescriptor sourceDescriptor, + in WebGPUShaderFrameworkUniforms framework, + WGPUTextureViewImpl* outputView, + Size outputSize) + { + WebGPUShaderProgram program = shaderPass.Program; + WebGPUShaderModuleSource moduleSource = program.GetModuleSource( + sourceDescriptor, + shaderPass.XBorderMode, + shaderPass.YBorderMode); + + // The render-pipeline target must exactly match the working-texture attachment. Fragment + // arithmetic remains f32; the RGBA16Float attachment performs the documented binary16 store + // between passes. Keep this native format synchronized with WebGPUShaderEffectWorkingTexture. + if (!this.flushContext.DeviceState.TryGetOrCreateEffectPipeline( + moduleSource, + program.UniformLayout.ByteLength, + WGPUTextureFormat.RGBA16Float, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPURenderPipelineImpl* pipeline, + out string? error)) + { + throw new InvalidOperationException(error); + } + + // Each pass writes into its own disjoint region, so these queued writes cannot disturb + // the bytes referenced by an earlier pass recorded in the still-pending flush encoder. + ulong passUniformOffset = checked(this.passUniformStride * (ulong)passIndex); + + using WebGPUHandle.HandleReference queueReference = this.flushContext.QueueHandle.AcquireReference(); + WGPUQueueImpl* queue = (WGPUQueueImpl*)queueReference.Handle; + WebGPUShaderFrameworkUniforms frameworkValue = framework; + this.flushContext.Api.QueueWriteBuffer( + queue, + this.uniformBuffer, + passUniformOffset, + &frameworkValue, + (nuint)WebGPUShaderFrameworkUniforms.ByteLength); + + ReadOnlySpan uniformData = shaderPass.Uniforms.Data; + fixed (byte* uniformPointer = uniformData) + { + this.flushContext.Api.QueueWriteBuffer( + queue, + this.uniformBuffer, + passUniformOffset + UserUniformOffset, + uniformPointer, + (nuint)uniformData.Length); + } + + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[4]; + entries[0] = new WGPUBindGroupEntry + { + binding = 0, + textureView = sourceView + }; + entries[1] = new WGPUBindGroupEntry + { + binding = 1, + buffer = this.uniformBuffer, + offset = passUniformOffset, + size = WebGPUShaderFrameworkUniforms.ByteLength + }; + entries[2] = new WGPUBindGroupEntry + { + binding = 2, + buffer = this.uniformBuffer, + offset = passUniformOffset + UserUniformOffset, + size = checked((ulong)uniformData.Length) + }; + entries[3] = new WGPUBindGroupEntry + { + binding = 3, + sampler = this.flushContext.DeviceState.EffectFilteringSampler + }; + + WGPUBindGroupDescriptor bindGroupDescriptor = new() + { + layout = bindGroupLayout, + entryCount = 4, + entries = entries + }; + + using WebGPUHandle.HandleReference deviceReference = this.flushContext.DeviceHandle.AcquireReference(); + WGPUBindGroupImpl* bindGroup = this.flushContext.Api.DeviceCreateBindGroup((WGPUDeviceImpl*)deviceReference.Handle, in bindGroupDescriptor); + if (bindGroup is null) + { + throw new InvalidOperationException("Failed to create the WebGPU layer-effect bind group."); + } + + this.flushContext.TrackBindGroup(bindGroup); + + if (!this.flushContext.EnsureCommandEncoder() || + !this.flushContext.BeginRenderPass(outputView, WebGPUShaderEffectWorkingTexture.Descriptor, loadExisting: false)) + { + throw new InvalidOperationException("Failed to begin the WebGPU layer-effect render pass."); + } + + WGPURenderPassEncoderImpl* renderPass = this.flushContext.PassEncoder; + this.flushContext.Api.RenderPassEncoderSetViewport(renderPass, 0, 0, outputSize.Width, outputSize.Height, 0, 1); + this.flushContext.Api.RenderPassEncoderSetScissorRect(renderPass, 0, 0, (uint)outputSize.Width, (uint)outputSize.Height); + this.flushContext.Api.RenderPassEncoderSetPipeline(renderPass, pipeline); + this.flushContext.Api.RenderPassEncoderSetBindGroup(renderPass, 0, bindGroup); + this.flushContext.Api.RenderPassEncoderDraw(renderPass, 3, 1, 0, 0); + this.flushContext.EndRenderPassIfOpen(); + + // No submission here: every pass owns a disjoint uniform region, so the recorded pass + // stays in the shared flush encoder and rides the next range submission. Queue ordering + // still executes the uniform writes above before that submission's commands. + } + + /// + /// Rounds a byte length up to the next multiple of a power-of-two alignment. + /// + /// The byte length to align. + /// The power-of-two alignment. + /// The aligned byte length. + private static ulong AlignUp(ulong value, ulong alignment) + => (value + alignment - 1) & ~(alignment - 1); + + /// + /// Creates one associated Rgba16Float scratch texture and transfers ownership to the flush context. + /// + /// + /// The renderer creates two textures and alternates between them for consecutive passes. At + /// 8 bytes per pixel each, they occupy 16 bytes per pixel in total. Using Rgba32Float would + /// raise that total to 32 bytes per pixel and double the read/write bandwidth of every pass. + /// See for the corresponding precision + /// tradeoff and logical alpha representation. + /// + private WGPUTextureImpl* CreateWorkingTexture(int width, int height, out WGPUTextureViewImpl* textureView) + { + const TextureUsage usage = TextureUsage.TextureBinding | TextureUsage.RenderAttachment; + + // Prefer a device-pooled working texture from an earlier flush. A larger entry is safe: + // each pass renders through an explicit viewport/scissor, layer_load addresses texels + // within the framework valid bounds, and layer_sample normalizes by textureDimensions. + if (this.flushContext.DeviceState.TryRentPooledTexture( + WGPUTextureFormat.RGBA16Float, + (ulong)usage, + (uint)width, + (uint)height, + out WGPUTextureImpl* rentedTexture, + out textureView, + out uint rentedWidth, + out uint rentedHeight)) + { + this.flushContext.TrackPooledTexture(rentedTexture, textureView, WGPUTextureFormat.RGBA16Float, (ulong)usage, rentedWidth, rentedHeight); + return rentedTexture; + } + + // The native texture and view formats must match both the effect pipeline and the logical + // descriptor used when the resulting texture is sampled by another pass or written back. + WGPUTextureDescriptor textureDescriptor = new() + { + usage = (ulong)usage, + dimension = WGPUTextureDimension._2D, + size = new WGPUExtent3D((uint)width, (uint)height, 1), + format = WGPUTextureFormat.RGBA16Float, + mipLevelCount = 1, + sampleCount = 1 + }; + + using WebGPUHandle.HandleReference deviceReference = this.flushContext.DeviceHandle.AcquireReference(); + WGPUTextureImpl* texture = this.flushContext.Api.DeviceCreateTexture((WGPUDeviceImpl*)deviceReference.Handle, in textureDescriptor); + if (texture is null) + { + throw new InvalidOperationException("Failed to create a WebGPU layer-effect working texture."); + } + + WGPUTextureViewDescriptor textureViewDescriptor = new() + { + format = WGPUTextureFormat.RGBA16Float, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All + }; + + textureView = this.flushContext.Api.TextureCreateView(texture, &textureViewDescriptor); + if (textureView is null) + { + this.flushContext.Api.TextureRelease(texture); + throw new InvalidOperationException("Failed to create a WebGPU layer-effect working texture view."); + } + + // Returned to the device pool with the flush so later flushes rent instead of recreating. + this.flushContext.TrackPooledTexture(texture, textureView, WGPUTextureFormat.RGBA16Float, (ulong)usage, (uint)width, (uint)height); + return texture; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironment.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironment.cs index bcce845e7..af5733f19 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironment.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironment.cs @@ -1,14 +1,25 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Provides explicit support probes for the library-managed WebGPU environment. /// Use this type when you want to check availability or compute pipeline support before constructing WebGPU objects. /// -public static class WebGPUEnvironment +public static unsafe class WebGPUEnvironment { + // The native callback is registered at most once per process; later sink changes only + // swap the managed sink field the callback reads. + private static bool nativeLogCallbackRegistered; + + // Read on the native log-callback thread; a null sink turns each log line into a no-op. + private static Action? nativeLogSink; + /// /// Gets or sets the options used when the library-managed WebGPU environment is initialized. /// @@ -28,12 +39,79 @@ public static class WebGPUEnvironment public static Action? UncapturedError { get; set; } /// - /// Probes whether the library-managed WebGPU device and queue are available. + /// Routes the native wgpu-native log stream (including the validation errors that precede a + /// lost device) to a managed sink. Unlike , this surfaces errors + /// that abort the process through the binding's submit panic before the uncaptured callback runs. + /// + /// The managed sink invoked for each native log line, or to disable. + /// + /// The sink can be invoked from native wgpu threads; keep it short and non-blocking. + /// Passing only mutes the managed sink; a previously registered + /// native callback stays installed and simply drops its messages. + /// The first call that installs a sink also fixes the native log level at + /// the warning level; that level is not lowered again by later calls. + /// + public static void EnableNativeLogging(Action? sink) + { + nativeLogSink = sink; + if (sink is null) + { + return; + } + + // Register the static unmanaged entry point once. HandleNativeLog reads the sink field on + // every invocation, so swapping the sink needs no native callback reconfiguration. + if (nativeLogCallbackRegistered) + { + return; + } + + WebGPU api = WebGPURuntime.GetApi(); + api.SetLogCallback(&HandleNativeLog, null); + api.SetLogLevel(WGPULogLevel.Warn); + nativeLogCallbackRegistered = true; + } + + /// + /// Marshals one native wgpu log line to the managed sink. + /// + /// The native log level. + /// The native UTF-8 log message, or . + /// Unused native user-data pointer. + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void HandleNativeLog(WGPULogLevel level, WGPUStringView message, void* userData) + { + Action? sink = nativeLogSink; + if (sink is null) + { + return; + } + + string text = message.ToManagedString(); + try + { + sink($"[wgpu {level}] {text}"); + } + catch + { + // The native wgpu callback has no managed caller to receive exceptions. + } + } + + /// + /// Probes whether the required wgpu-native entry points and the library-managed WebGPU device and queue are available. /// /// - /// when the library-managed WebGPU device and queue are available; + /// when the required wgpu-native entry points and the library-managed + /// WebGPU device and queue are available; /// otherwise, the stable failure code describing why the probe failed. /// + /// + /// Returns on Windows when the packaged + /// DirectX Shader Compiler runtime is unavailable. + /// Returns when the core WebGPU API loads + /// but a wgpu-native extension entry point required by the backend is unavailable. + /// public static WebGPUEnvironmentError ProbeAvailability() => WebGPURuntime.ProbeAvailability(); @@ -50,6 +128,8 @@ public static WebGPUEnvironmentError ProbeComputePipelineSupport() /// /// Reports one uncaptured native WebGPU error to the configured public callback. /// + /// The public error classification. + /// The error message reported by the native runtime. internal static void ReportUncapturedError(WebGPUErrorType errorType, string message) { Action? callback = UncapturedError; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironmentError.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironmentError.cs index 8f7d3e229..1376d3af8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironmentError.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUEnvironmentError.cs @@ -18,7 +18,7 @@ public enum WebGPUEnvironmentError Success = 0, /// - /// The shared WebGPU API loader or required extension could not be initialized. + /// The shared WebGPU API loader could not be initialized. /// ApiInitializationFailed, @@ -37,6 +37,12 @@ public enum WebGPUEnvironmentError /// AdapterRequestFailed, + /// + /// The only available adapter is a software rasterizer. Software adapters are not supported + /// by this backend; the CPU drawing backend serves GPU-less environments instead. + /// + SoftwareAdapterUnsupported, + /// /// The device request callback did not complete before the timeout expired. /// @@ -66,4 +72,14 @@ public enum WebGPUEnvironmentError /// The isolated compute-pipeline probe process terminated before it could report a normal result. /// ComputePipelineProbeProcessFailed, + + /// + /// The required WGPU extension is unavailable. + /// + WgpuExtensionUnavailable, + + /// + /// The DirectX Shader Compiler required by the Windows WebGPU backend is unavailable. + /// + DxcUnavailable } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUErrorTypeMapper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUErrorTypeMapper.cs new file mode 100644 index 000000000..bb9b43faf --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUErrorTypeMapper.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Maps native WebGPU error classifications to the public error model. +/// +internal static class WebGPUErrorTypeMapper +{ + /// + /// Maps a native error classification to its public equivalent. + /// + /// The native error classification. + /// The public error classification. + public static WebGPUErrorType ToPublic(WGPUErrorType errorType) + => errorType switch + { + WGPUErrorType.NoError => WebGPUErrorType.NoError, + WGPUErrorType.Validation => WebGPUErrorType.Validation, + WGPUErrorType.OutOfMemory => WebGPUErrorType.OutOfMemory, + WGPUErrorType.Internal => WebGPUErrorType.Internal, + _ => WebGPUErrorType.Unknown + }; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurface.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurface.cs index aa7c3c7c2..5e812f0de 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurface.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurface.cs @@ -13,7 +13,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; public sealed class WebGPUExternalSurface : IDisposable { private readonly WebGPUSurfaceResources resources; - private readonly WebGPUPresentMode presentMode; + private readonly WebGPUSurfaceSession? ownedSession; + + // Last size forwarded by the host via Resize. A zero-area value pauses acquisition while + // the native surface retains its last valid configuration for the next nonzero resize. private Size framebufferSize; private bool isDisposed; @@ -59,43 +62,99 @@ public WebGPUExternalSurface( Guard.MustBeGreaterThan(framebufferSize.Width, 0, nameof(framebufferSize)); Guard.MustBeGreaterThan(framebufferSize.Height, 0, nameof(framebufferSize)); - this.presentMode = options.PresentMode; + this.framebufferSize = framebufferSize; + WebGPUSurfaceSession session = new(configuration); + + try + { + this.resources = WebGPUSurfaceResources.Create( + session, + host, + options.Format, + options.AlphaMode, + options.PresentMode, + this.framebufferSize); + + this.ownedSession = session; + } + catch + { + session.Dispose(); + throw; + } + } + + /// + /// Initializes a new instance of the class that borrows the device resources owned by . + /// + /// The session shared with related presentation surfaces. + /// The native surface host that the WebGPU surface will attach to. + /// The initial framebuffer size in pixels. + /// The external surface options. + internal WebGPUExternalSurface( + WebGPUSurfaceSession session, + WebGPUSurfaceHost host, + Size framebufferSize, + WebGPUExternalSurfaceOptions options) + { this.framebufferSize = framebufferSize; this.resources = WebGPUSurfaceResources.Create( - configuration, - new SilkNativeSurfaceAdapter(host), + session, + host, options.Format, - this.presentMode, + options.AlphaMode, + options.PresentMode, this.framebufferSize); } + /// + /// Gets the WebGPU device context used to render this surface. + /// + /// The owning surface session controls the returned context's lifetime. + public WebGPUDeviceContext DeviceContext + { + get + { + this.ThrowIfDisposed(); + + return this.resources.DeviceContext; + } + } + /// /// Notifies the external surface that the drawable framebuffer has resized and reconfigures the swapchain when the - /// size changes. + /// size changes. Zero-area sizes (minimize, mid-layout) pause frame acquisition while the last valid native + /// configuration is retained. /// /// The new framebuffer size in pixels. public void Resize(Size framebufferSize) { this.ThrowIfDisposed(); - if (framebufferSize.Width <= 0 || framebufferSize.Height <= 0) + if (framebufferSize == this.framebufferSize) { return; } - if (framebufferSize == this.framebufferSize) + this.framebufferSize = framebufferSize; + + if (framebufferSize.Width <= 0 || framebufferSize.Height <= 0) { return; } - this.framebufferSize = framebufferSize; - this.resources.ConfigureSurface(this.presentMode, this.framebufferSize); + this.resources.ConfigureSurface(this.resources.PresentMode, this.framebufferSize); } /// - /// Tries to acquire the next drawable frame. + /// Tries to acquire the next drawable frame using default drawing options. /// /// Receives the acquired frame on success. /// when a frame is available; otherwise . + /// + /// A result means no drawable frame is available right now, for example because the + /// surface was lost, outdated, timed out, or has a zero-sized framebuffer. + /// Dispose the returned frame when you are done with it to present it and release its per-frame resources. + /// public bool TryAcquireFrame([NotNullWhen(true)] out WebGPUSurfaceFrame? frame) => this.TryAcquireFrameCore(new DrawingOptions(), out frame); @@ -105,10 +164,18 @@ public bool TryAcquireFrame([NotNullWhen(true)] out WebGPUSurfaceFrame? frame) /// The drawing options for the acquired frame. /// Receives the acquired frame on success. /// when a frame is available; otherwise . + /// + /// A result means no drawable frame is available right now, for example because the + /// surface was lost, outdated, timed out, or has a zero-sized framebuffer. + /// Dispose the returned frame when you are done with it to present it and release its per-frame resources. + /// public bool TryAcquireFrame(DrawingOptions options, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) => this.TryAcquireFrameCore(options, out frame); - /// + /// + /// Releases the WebGPU surface stack. The native host handles supplied through + /// remain owned by the caller and are not released. + /// public void Dispose() { if (this.isDisposed) @@ -117,21 +184,32 @@ public void Dispose() } this.resources.Dispose(); + this.ownedSession?.Dispose(); this.isDisposed = true; } + /// + /// Acquires the next drawable frame from the shared surface resources using the last size reported via + /// . + /// + /// The drawing options for the acquired frame. + /// Receives the acquired frame on success. + /// when a frame is available; otherwise . private bool TryAcquireFrameCore( DrawingOptions options, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) { this.ThrowIfDisposed(); return this.resources.TryAcquireFrame( - this.presentMode, + this.resources.PresentMode, this.framebufferSize, options, out frame); } + /// + /// Throws when this surface has been disposed. + /// private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurfaceOptions.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurfaceOptions.cs index 8614520c5..0a10fd94a 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurfaceOptions.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUExternalSurfaceOptions.cs @@ -15,10 +15,24 @@ public sealed class WebGPUExternalSurfaceOptions /// /// Gets or sets how completed frames are queued for presentation to the display. /// + /// + /// When the requested mode is unavailable, is used. + /// public WebGPUPresentMode PresentMode { get; set; } = WebGPUPresentMode.Fifo; /// /// Gets or sets the swapchain texture format used by acquired frames. /// + /// + /// The requested format is used when available. Otherwise, a compatible format is selected automatically. + /// public WebGPUTextureFormat Format { get; set; } = WebGPUTextureFormat.Rgba8Unorm; + + /// + /// Gets or sets how the native compositor interprets the surface alpha channel. + /// + /// + /// The requested mode is used when available. Otherwise, a compatible mode is selected automatically. + /// + public WebGPUCompositeAlphaMode AlphaMode { get; set; } = WebGPUCompositeAlphaMode.Auto; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs index f3e0c7ee3..8989249d7 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUFlushContext.cs @@ -4,11 +4,9 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Silk.NET.WebGPU; -using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -28,8 +26,12 @@ internal enum CompositePipelineBlendMode /// internal sealed unsafe class WebGPUFlushContext : IDisposable { + // Transient GPU objects created during this flush, stored as nint because pointer types + // cannot be list type arguments. All of them are released in Dispose after the open + // passes end, so command encoding never references a freed object. private readonly List transientBindGroups = []; private readonly List transientBuffers = []; + private readonly List transientSamplers = []; private readonly List transientTextureViews = []; private readonly List transientTextures = []; @@ -38,36 +40,71 @@ internal sealed unsafe class WebGPUFlushContext : IDisposable // Handles are released when this flush context is disposed. private readonly Dictionary cachedSourceTextureViews = new(ReferenceEqualityComparer.Instance); + // Device-pooled resources rented for this flush. Unlike the transient lists above these are + // returned to the device pool at disposal instead of released, so the next flush can reuse + // the same native objects without recreation or lazy zero-initialization. + private readonly List pooledTextures = []; + private readonly List pooledUniformBuffers = []; + + // One gradient-ramp texture serves every staged range of the flush's encoded scene. The view + // is owned through pooledTextures; this cache only avoids duplicate staging. + private object? cachedGradientScene; + private nint cachedGradientTextureView; + private bool disposed; + /// + /// Initializes a new instance of the class. + /// Use ; this constructor only stores already-validated state. + /// + /// The WebGPU API facade for this flush. + /// The safe handle for the device. + /// The safe handle for the queue. + /// The safe handle for the target texture. + /// The safe handle for the target texture view. + /// The target bounds for this flush. + /// The offset from logical target coordinates to texture coordinates. + /// The target texture format and alpha representation. + /// The native target texture format. + /// Whether the target texture belongs to a presentation surface. + /// Whether the target must be copied through an ImageSharp-owned texture before it can be sampled or storage-bound. + /// The allocator for temporary CPU staging buffers. + /// The device-scoped shared caches and reusable resources. + /// The backend-specific upper bound for staged-scene storage bindings. private WebGPUFlushContext( WebGPU api, - Wgpu wgpuExtension, WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle, WebGPUTextureHandle targetTextureHandle, WebGPUTextureViewHandle targetTextureViewHandle, in Rectangle targetBounds, - TextureFormat textureFormat, + Point targetTextureOffset, + WebGPUTargetDescriptor targetDescriptor, + WGPUTextureFormat textureFormat, + bool isPresentationSurface, + bool requiresPresentationCopies, MemoryAllocator memoryAllocator, - WebGPURuntime.DeviceSharedState deviceState) + WebGPURuntime.DeviceSharedState deviceState, + nuint scratchBufferBindingSizeLimit) { this.Api = api; - this.WgpuExtension = wgpuExtension; this.DeviceHandle = deviceHandle; this.QueueHandle = queueHandle; this.TargetTextureHandle = targetTextureHandle; this.TargetTextureViewHandle = targetTextureViewHandle; this.TargetBounds = targetBounds; + this.TargetTextureOffset = targetTextureOffset; + this.TargetDescriptor = targetDescriptor; this.TextureFormat = textureFormat; + this.IsPresentationSurface = isPresentationSurface; + this.RequiresPresentationCopies = requiresPresentationCopies; this.MemoryAllocator = memoryAllocator; this.DeviceState = deviceState; - } - /// - /// Gets the wgpu-native extension used to poll asynchronous callbacks. - /// - public Wgpu WgpuExtension { get; } + // The device owns the real API limit. A backend instance may only make that limit more + // restrictive, which lets integration tests exercise chunking without mutating shared state. + this.ScratchBufferBindingSizeLimit = Math.Min(deviceState.MaxStorageBufferBindingSize, scratchBufferBindingSizeLimit); + } /// /// Gets the WebGPU API facade for this flush. @@ -107,10 +144,30 @@ private WebGPUFlushContext( /// public Rectangle TargetBounds { get; } + /// + /// Gets the offset applied when mapping logical target coordinates to target texture coordinates. + /// + public Point TargetTextureOffset { get; } + + /// + /// Gets the target texture format and alpha representation for this flush. + /// + public WebGPUTargetDescriptor TargetDescriptor { get; } + /// /// Gets the target texture format for this flush. /// - public TextureFormat TextureFormat { get; } + public WGPUTextureFormat TextureFormat { get; } + + /// + /// Gets a value indicating whether the target texture belongs to a presentation surface. + /// + public bool IsPresentationSurface { get; } + + /// + /// Gets a value indicating whether the target must be copied through an ImageSharp-owned texture before it can be sampled or storage-bound. + /// + public bool RequiresPresentationCopies { get; } /// /// Gets the allocator used for temporary CPU staging buffers in this flush context. @@ -122,10 +179,15 @@ private WebGPUFlushContext( /// public WebGPURuntime.DeviceSharedState DeviceState { get; } + /// + /// Gets the effective storage-binding ceiling for staged-scene scratch buffers. + /// + public nuint ScratchBufferBindingSizeLimit { get; } + /// /// Gets the shared instance-data buffer used for parameter uploads. /// - public WgpuBuffer* InstanceBuffer { get; private set; } + public WGPUBufferImpl* InstanceBuffer { get; private set; } /// /// Gets the instance buffer capacity in bytes. @@ -133,62 +195,102 @@ private WebGPUFlushContext( public nuint InstanceBufferCapacity { get; private set; } /// - /// Gets or sets the current write offset into . + /// Gets the current write offset into . /// - public nuint InstanceBufferWriteOffset { get; internal set; } + public nuint InstanceBufferWriteOffset { get; private set; } /// /// Gets or sets the active command encoder. /// - public CommandEncoder* CommandEncoder { get; set; } + public WGPUCommandEncoderImpl* CommandEncoder { get; set; } /// /// Gets the currently open render pass encoder, if any. /// - public RenderPassEncoder* PassEncoder { get; private set; } + public WGPURenderPassEncoderImpl* PassEncoder { get; private set; } /// /// Gets the currently open compute pass encoder, if any. /// - public ComputePassEncoder* ComputePassEncoder { get; private set; } + public WGPUComputePassEncoderImpl* ComputePassEncoder { get; private set; } /// /// Creates a flush context for a native WebGPU surface. /// + /// The pixel format of the target frame. /// The target frame. - /// The expected GPU texture format. + /// The expected GPU texture format and alpha representation. /// /// A device feature required by the pixel type for storage binding, or - /// when no special feature is needed. + /// the default value when no special feature is needed. /// /// The memory allocator for staging buffers. + /// The backend-specific upper bound for staged-scene storage bindings. /// The flush context. public static WebGPUFlushContext Create( NativeCanvasFrame frame, - TextureFormat expectedTextureFormat, - FeatureName requiredFeature, - MemoryAllocator memoryAllocator) + WebGPUTargetDescriptor expectedTargetDescriptor, + WGPUFeatureName requiredFeature, + MemoryAllocator memoryAllocator, + nuint scratchBufferBindingSizeLimit) where TPixel : unmanaged, IPixel { // The native-frame overload is used after WebGPU target selection has already succeeded, // so this casts once at the backend boundary and keeps the concrete target data together. _ = frame.TryGetNativeSurface(out NativeSurface? nativeSurface); - WebGPUNativeSurface nativeTarget = (WebGPUNativeSurface)nativeSurface!; + return Create( + (WebGPUNativeSurface)nativeSurface!, + frame.Bounds, + expectedTargetDescriptor, + requiredFeature, + memoryAllocator, + scratchBufferBindingSizeLimit); + } + + /// + /// Creates a flush context directly over a native WebGPU surface and target bounds. Used by + /// corrective re-renders, which outlive the per-flush canvas frame and therefore cannot go + /// through the frame overload. + /// + /// The native surface holding the target texture handles. + /// The target bounds for the flush. + /// The expected GPU texture format and alpha representation. + /// + /// A device feature required by the pixel type for storage binding, or + /// the default value when no special feature is needed. + /// + /// The memory allocator for staging buffers. + /// The backend-specific upper bound for staged-scene storage bindings. + /// The flush context. + public static WebGPUFlushContext Create( + WebGPUNativeSurface nativeTarget, + Rectangle bounds, + WebGPUTargetDescriptor expectedTargetDescriptor, + WGPUFeatureName requiredFeature, + MemoryAllocator memoryAllocator, + nuint scratchBufferBindingSizeLimit) + { WebGPU api = WebGPURuntime.GetApi(); - TextureFormat textureFormat = WebGPUTextureFormatMapper.ToNative(nativeTarget.TargetFormat); - Rectangle bounds = frame.Bounds; + WebGPUDrawingBackend.GetCompositeTextureFormatInfo(nativeTarget.TargetDescriptor.Format, out WGPUTextureFormat textureFormat, out _); Rectangle nativeBounds = new(0, 0, nativeTarget.Width, nativeTarget.Height); + Point targetTextureOffset = nativeTarget.TextureCoordinateOffset; + Rectangle textureBounds = new( + bounds.X + targetTextureOffset.X, + bounds.Y + targetTextureOffset.Y, + bounds.Width, + bounds.Height); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, nativeTarget.DeviceHandle); if (nativeTarget.DeviceHandle.IsInvalid || nativeTarget.QueueHandle.IsInvalid || nativeTarget.TargetTextureViewHandle.IsInvalid || - textureFormat != expectedTextureFormat) + nativeTarget.TargetDescriptor != expectedTargetDescriptor) { throw new InvalidOperationException("The native WebGPU target does not match the flush context requirements."); } - if (requiredFeature != FeatureName.Undefined && !deviceState.HasFeature(requiredFeature)) + if (requiredFeature != default && !deviceState.HasFeature(requiredFeature)) { throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}'."); } @@ -196,22 +298,26 @@ public static WebGPUFlushContext Create( // Region frames expose bounds relative to their parent target. The flush context must preserve // that absolute slice so later scene encoding, dispatch planning, and texture copies address // the correct sub-rectangle of the native surface instead of silently expanding back to full-frame. - if (!nativeBounds.Contains(bounds)) + if (!nativeBounds.Contains(textureBounds)) { throw new InvalidOperationException("The native WebGPU target bounds do not contain the flush bounds."); } return new WebGPUFlushContext( api, - WebGPURuntime.GetWgpuExtension(), nativeTarget.DeviceHandle, nativeTarget.QueueHandle, nativeTarget.TargetTextureHandle, nativeTarget.TargetTextureViewHandle, in bounds, + targetTextureOffset, + nativeTarget.TargetDescriptor, textureFormat, + nativeTarget.IsPresentationSurface, + nativeTarget.RequiresPresentationCopies, memoryAllocator, - deviceState); + deviceState, + scratchBufferBindingSizeLimit); } /// @@ -219,6 +325,10 @@ public static WebGPUFlushContext Create( /// /// The required number of bytes for the current flush. /// The minimum allocation size to enforce when creating a new buffer. + /// + /// Growing replaces the buffer; previous contents are discarded, so callers must re-upload + /// any data they still need. + /// public void EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapacityBytes) { if (this.InstanceBuffer is not null && this.InstanceBufferCapacity >= requiredBytes) @@ -234,14 +344,14 @@ public void EnsureInstanceBufferCapacity(nuint requiredBytes, nuint minimumCapac } nuint targetSize = requiredBytes > minimumCapacityBytes ? requiredBytes : minimumCapacityBytes; - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = targetSize + usage = (ulong)(BufferUsage.Storage | BufferUsage.CopyDst), + size = targetSize }; using WebGPUHandle.HandleReference deviceReference = this.DeviceHandle.AcquireReference(); - this.InstanceBuffer = this.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); + this.InstanceBuffer = this.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); this.InstanceBufferCapacity = targetSize; } @@ -256,22 +366,42 @@ public bool EnsureCommandEncoder() return true; } - CommandEncoderDescriptor descriptor = default; + WGPUCommandEncoderDescriptor descriptor = default; using WebGPUHandle.HandleReference deviceReference = this.DeviceHandle.AcquireReference(); - this.CommandEncoder = this.Api.DeviceCreateCommandEncoder((Device*)deviceReference.Handle, in descriptor); + this.CommandEncoder = this.Api.DeviceCreateCommandEncoder((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); return this.CommandEncoder is not null; } /// - /// Begins a render pass that targets the specified texture view. + /// Begins a render pass that targets the specified texture view, clearing existing contents. /// - public bool BeginRenderPass(TextureView* targetView) + /// The texture view that receives the pass output. + /// if a render pass is open; otherwise . + public bool BeginRenderPass(WGPUTextureViewImpl* targetView) => this.BeginRenderPass(targetView, loadExisting: false); /// /// Begins a render pass that targets the specified texture view, optionally preserving existing contents. /// - public bool BeginRenderPass(TextureView* targetView, bool loadExisting) + /// The texture view that receives the pass output. + /// to load the existing texture contents; to clear. + /// if a render pass is open; otherwise . + /// + /// When a render pass is already open it is reused as-is; and + /// are ignored in that case. Fails when no command encoder + /// exists or a compute pass is open, because passes cannot be nested on one encoder. + /// + public bool BeginRenderPass(WGPUTextureViewImpl* targetView, bool loadExisting) + => this.BeginRenderPass(targetView, this.TargetDescriptor, loadExisting); + + /// + /// Begins a render pass using the physical encoding of the supplied target representation. + /// + /// The texture view that receives the pass output. + /// The representation stored by . + /// to load existing contents; to clear. + /// if a render pass is open; otherwise . + public bool BeginRenderPass(WGPUTextureViewImpl* targetView, WebGPUTargetDescriptor targetDescriptor, bool loadExisting) { if (this.PassEncoder is not null) { @@ -283,19 +413,27 @@ public bool BeginRenderPass(TextureView* targetView, bool loadExisting) return false; } - RenderPassColorAttachment colorAttachment = new() + WGPURenderPassColorAttachment colorAttachment = new() { - View = targetView, - ResolveTarget = null, - LoadOp = loadExisting ? LoadOp.Load : LoadOp.Clear, - StoreOp = StoreOp.Store, - ClearValue = default + view = targetView, + + // A 2D attachment must use WebGPU's undefined sentinel; zero selects slice 0 and is valid only for 3D views. + depthSlice = uint.MaxValue, + resolveTarget = null, + loadOp = loadExisting ? WGPULoadOp.Load : WGPULoadOp.Clear, + storeOp = WGPUStoreOp.Store, + + // WebGPU clear values use the attachment's physical encoding. Physical -1 is the + // logical transparent zero for ImageSharp formats mapped through a signed-unit target. + clearValue = targetDescriptor.NumericEncoding == WebGPUTargetNumericEncoding.SignedUnit + ? new WGPUColor { r = -1D, g = -1D, b = -1D, a = -1D } + : default }; - RenderPassDescriptor renderPassDescriptor = new() + WGPURenderPassDescriptor renderPassDescriptor = new() { - ColorAttachmentCount = 1, - ColorAttachments = &colorAttachment + colorAttachmentCount = 1, + colorAttachments = &colorAttachment }; this.PassEncoder = this.Api.CommandEncoderBeginRenderPass(this.CommandEncoder, in renderPassDescriptor); @@ -333,7 +471,7 @@ public bool BeginComputePass() return false; } - ComputePassDescriptor descriptor = default; + WGPUComputePassDescriptor descriptor = default; this.ComputePassEncoder = this.Api.CommandEncoderBeginComputePass(this.CommandEncoder, in descriptor); return this.ComputePassEncoder is not null; } @@ -357,7 +495,7 @@ public void EndComputePassIfOpen() /// Tracks a transient bind group allocated during this flush. /// /// The bind group to track. - public void TrackBindGroup(BindGroup* bindGroup) + public void TrackBindGroup(WGPUBindGroupImpl* bindGroup) { if (bindGroup is not null) { @@ -368,7 +506,8 @@ public void TrackBindGroup(BindGroup* bindGroup) /// /// Tracks a transient buffer allocated during this flush. /// - public void TrackBuffer(WgpuBuffer* buffer) + /// The buffer to track. + public void TrackBuffer(WGPUBufferImpl* buffer) { if (buffer is not null) { @@ -376,10 +515,23 @@ public void TrackBuffer(WgpuBuffer* buffer) } } + /// + /// Tracks a transient sampler allocated during this flush. + /// + /// The sampler to track. + public void TrackSampler(WGPUSamplerImpl* sampler) + { + if (sampler is not null) + { + this.transientSamplers.Add((nint)sampler); + } + } + /// /// Tracks a transient texture view allocated during this flush. /// - public void TrackTextureView(TextureView* textureView) + /// The texture view to track. + public void TrackTextureView(WGPUTextureViewImpl* textureView) { if (textureView is not null) { @@ -390,7 +542,8 @@ public void TrackTextureView(TextureView* textureView) /// /// Tracks a transient texture allocated during this flush. /// - public void TrackTexture(Texture* texture) + /// The texture to track. + public void TrackTexture(WGPUTextureImpl* texture) { if (texture is not null) { @@ -398,17 +551,55 @@ public void TrackTexture(Texture* texture) } } + /// + /// Tracks a device-pooled texture rented for this flush. It is returned to the device pool, + /// not released, when this flush context is disposed. + /// + /// The rented texture. + /// The full view rented or created with . + /// The format the texture was created with. + /// The exact usage bits the texture was created with. + /// The created width in texels. + /// The created height in texels. + public void TrackPooledTexture( + WGPUTextureImpl* texture, + WGPUTextureViewImpl* textureView, + WGPUTextureFormat format, + ulong usage, + uint width, + uint height) + { + if (texture is not null) + { + this.pooledTextures.Add(new PooledTextureRental((nint)texture, (nint)textureView, format, usage, width, height)); + } + } + + /// + /// Tracks a device-pooled layer-effect uniform buffer rented for this flush. It is returned + /// to the device pool, not released, when this flush context is disposed. + /// + /// The rented buffer. + /// The byte capacity of . + public void TrackPooledUniformBuffer(WGPUBufferImpl* buffer, nuint byteLength) + { + if (buffer is not null) + { + this.pooledUniformBuffers.Add(new PooledBufferRental((nint)buffer, byteLength)); + } + } + /// /// Tries to resolve a cached source texture view for an input image. /// /// The source image key. /// When this method returns , contains the cached texture view. /// if a cached texture view exists; otherwise . - public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureView) + public bool TryGetCachedSourceTextureView(Image image, out WGPUTextureViewImpl* textureView) { if (this.cachedSourceTextureViews.TryGetValue(image, out nint handle) && handle != 0) { - textureView = (TextureView*)handle; + textureView = (WGPUTextureViewImpl*)handle; return true; } @@ -421,9 +612,39 @@ public bool TryGetCachedSourceTextureView(Image image, out TextureView* textureV /// /// The source image key. /// The texture view to cache. - public void CacheSourceTextureView(Image image, TextureView* textureView) + public void CacheSourceTextureView(Image image, WGPUTextureViewImpl* textureView) => this.cachedSourceTextureViews[image] = (nint)textureView; + /// + /// Tries to resolve the uploaded gradient-ramp texture view for the flush's encoded scene. + /// + /// The encoded scene whose gradient rows were uploaded. + /// When this method returns , contains the cached view. + /// when the scene's gradient texture was already staged this flush. + public bool TryGetCachedGradientTextureView(object scene, out WGPUTextureViewImpl* textureView) + { + if (ReferenceEquals(this.cachedGradientScene, scene) && this.cachedGradientTextureView != 0) + { + textureView = (WGPUTextureViewImpl*)this.cachedGradientTextureView; + return true; + } + + textureView = null; + return false; + } + + /// + /// Caches the uploaded gradient-ramp texture view so later ranges of the same scene bind it + /// without creating and uploading another texture. + /// + /// The encoded scene whose gradient rows were uploaded. + /// The staged gradient texture view. + public void CacheGradientTextureView(object scene, WGPUTextureViewImpl* textureView) + { + this.cachedGradientScene = scene; + this.cachedGradientTextureView = (nint)textureView; + } + /// /// Releases transient GPU resources owned by this flush context. /// @@ -434,6 +655,13 @@ public void Dispose() return; } + // The flag is set before any release so a throw mid-teardown cannot leave a + // re-runnable context: a second Dispose over the still-populated lists would + // release every tracked object twice and double-return pooled rentals. + this.disposed = true; + + // Ordering: end any open passes before releasing the encoder they were recorded on, + // then release the encoder before the transient objects it may still reference. this.EndComputePassIfOpen(); this.EndRenderPassIfOpen(); @@ -454,33 +682,61 @@ public void Dispose() for (int i = 0; i < this.transientBindGroups.Count; i++) { - this.Api.BindGroupRelease((BindGroup*)this.transientBindGroups[i]); + this.Api.BindGroupRelease((WGPUBindGroupImpl*)this.transientBindGroups[i]); } for (int i = 0; i < this.transientBuffers.Count; i++) { - this.Api.BufferRelease((WgpuBuffer*)this.transientBuffers[i]); + this.Api.BufferRelease((WGPUBufferImpl*)this.transientBuffers[i]); + } + + for (int i = 0; i < this.transientSamplers.Count; i++) + { + this.Api.SamplerRelease((WGPUSamplerImpl*)this.transientSamplers[i]); } for (int i = 0; i < this.transientTextureViews.Count; i++) { - this.Api.TextureViewRelease((TextureView*)this.transientTextureViews[i]); + this.Api.TextureViewRelease((WGPUTextureViewImpl*)this.transientTextureViews[i]); } for (int i = 0; i < this.transientTextures.Count; i++) { - this.Api.TextureRelease((Texture*)this.transientTextures[i]); + this.Api.TextureRelease((WGPUTextureImpl*)this.transientTextures[i]); } this.transientBindGroups.Clear(); this.transientBuffers.Clear(); + this.transientSamplers.Clear(); this.transientTextureViews.Clear(); this.transientTextures.Clear(); // Cache entries point to transient texture views that are released above. this.cachedSourceTextureViews.Clear(); - this.disposed = true; + // Rented pooled resources go back to the device pool after the commands that used them + // were submitted; queue ordering makes reuse by a later flush safe, and the pool itself + // releases entries once its retention bound is reached. + for (int i = 0; i < this.pooledTextures.Count; i++) + { + PooledTextureRental rental = this.pooledTextures[i]; + this.DeviceState.ReturnPooledTexture( + (WGPUTextureImpl*)rental.Texture, + (WGPUTextureViewImpl*)rental.View, + rental.Format, + rental.Usage, + rental.Width, + rental.Height); + } + + for (int i = 0; i < this.pooledUniformBuffers.Count; i++) + { + PooledBufferRental rental = this.pooledUniformBuffers[i]; + this.DeviceState.ReturnEffectUniformBuffer((WGPUBufferImpl*)rental.Buffer, rental.ByteLength); + } + + this.pooledTextures.Clear(); + this.pooledUniformBuffers.Clear(); } /// @@ -492,10 +748,10 @@ public void Dispose() /// The destination texture. /// The CPU-side source region to upload. /// The allocator used when a packed staging copy is required. - internal static void UploadTextureFromRegion( + public static void UploadTextureFromRegion( WebGPU api, - Queue* queue, - Texture* destinationTexture, + WGPUQueueImpl* queue, + WGPUTextureImpl* destinationTexture, Buffer2DRegion sourceRegion, MemoryAllocator memoryAllocator) where TPixel : unmanaged @@ -513,10 +769,10 @@ internal static void UploadTextureFromRegion( /// The destination X coordinate in the texture. /// The destination Y coordinate in the texture. /// The destination array layer. - internal static void UploadTextureFromRegion( + public static void UploadTextureFromRegion( WebGPU api, - Queue* queue, - Texture* destinationTexture, + WGPUQueueImpl* queue, + WGPUTextureImpl* destinationTexture, Buffer2DRegion sourceRegion, MemoryAllocator memoryAllocator, uint destinationX, @@ -525,15 +781,15 @@ internal static void UploadTextureFromRegion( where TPixel : unmanaged { int pixelSizeInBytes = Unsafe.SizeOf(); - ImageCopyTexture destination = new() + WGPUTexelCopyTextureInfo destination = new() { - Texture = destinationTexture, - MipLevel = 0, - Origin = new Origin3D(destinationX, destinationY, destinationLayer), - Aspect = TextureAspect.All + texture = destinationTexture, + mipLevel = 0, + origin = new WGPUOrigin3D(destinationX, destinationY, destinationLayer), + aspect = WGPUTextureAspect.All }; - Extent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); + WGPUExtent3D writeSize = new((uint)sourceRegion.Width, (uint)sourceRegion.Height, 1); int rowBytes = checked(sourceRegion.Width * pixelSizeInBytes); uint alignedRowBytes = AlignTo256((uint)rowBytes); @@ -551,11 +807,11 @@ internal static void UploadTextureFromRegion( int uploadByteCount = checked((int)directByteCount); nuint uploadByteCountNuint = checked((nuint)uploadByteCount); - TextureDataLayout layout = new() + WGPUTexelCopyBufferLayout layout = new() { - Offset = 0, - BytesPerRow = (uint)sourceStrideBytes, - RowsPerImage = (uint)sourceRegion.Height + offset = 0, + bytesPerRow = (uint)sourceStrideBytes, + rowsPerImage = (uint)sourceRegion.Height }; Span sourceBytes = MemoryMarshal.AsBytes(sourceMemory.Span).Slice(startByteOffset, uploadByteCount); @@ -578,11 +834,11 @@ internal static void UploadTextureFromRegion( MemoryMarshal.AsBytes(sourceRow)[..rowBytes].CopyTo(packedData.Slice(y * alignedRowBytesInt, rowBytes)); } - TextureDataLayout packedLayout = new() + WGPUTexelCopyBufferLayout packedLayout = new() { - Offset = 0, - BytesPerRow = alignedRowBytes, - RowsPerImage = (uint)sourceRegion.Height + offset = 0, + bytesPerRow = alignedRowBytes, + rowsPerImage = (uint)sourceRegion.Height }; fixed (byte* uploadPtr = packedData) @@ -598,4 +854,86 @@ internal static void UploadTextureFromRegion( /// The aligned byte count. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignTo256(uint value) => (value + 255U) & ~255U; + + /// + /// Stores one rented device-pooled texture with the creation parameters its return requires. + /// + private readonly struct PooledTextureRental + { + /// + /// Initializes a new instance of the struct. + /// + /// The native texture pointer stored as an integer. + /// The native full-texture view pointer stored as an integer. + /// The format the texture was created with. + /// The exact usage bits the texture was created with. + /// The created width in texels. + /// The created height in texels. + public PooledTextureRental(nint texture, nint view, WGPUTextureFormat format, ulong usage, uint width, uint height) + { + this.Texture = texture; + this.View = view; + this.Format = format; + this.Usage = usage; + this.Width = width; + this.Height = height; + } + + /// + /// Gets the native texture pointer stored as an integer. + /// + public nint Texture { get; } + + /// + /// Gets the native full-texture view pointer stored as an integer. + /// + public nint View { get; } + + /// + /// Gets the format the texture was created with. + /// + public WGPUTextureFormat Format { get; } + + /// + /// Gets the exact usage bits the texture was created with. + /// + public ulong Usage { get; } + + /// + /// Gets the created width in texels. + /// + public uint Width { get; } + + /// + /// Gets the created height in texels. + /// + public uint Height { get; } + } + + /// + /// Stores one rented device-pooled buffer with its capacity. + /// + private readonly struct PooledBufferRental + { + /// + /// Initializes a new instance of the struct. + /// + /// The native buffer pointer stored as an integer. + /// The byte capacity of . + public PooledBufferRental(nint buffer, nuint byteLength) + { + this.Buffer = buffer; + this.ByteLength = byteLength; + } + + /// + /// Gets the native buffer pointer stored as an integer. + /// + public nint Buffer { get; } + + /// + /// Gets the byte capacity of . + /// + public nuint ByteLength { get; } + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUGaussianBlurLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUGaussianBlurLayerEffect.cs new file mode 100644 index 000000000..bd549b245 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUGaussianBlurLayerEffect.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU performs the Gaussian blur with +/// separable shader passes; a CPU backend applies the equivalent . +/// +public sealed class WebGPUGaussianBlurLayerEffect : WebGPUShaderLayerEffect +{ + internal const string ShaderSource = """ + fn layer_effect(position: vec2) -> vec4 { + + // The unpaired center tap is sampled exactly at the output position. + var result = layer_sample(position) * imagesharp_uniforms.center_weight; + var first_weight = imagesharp_uniforms.center_weight * imagesharp_uniforms.weight_step; + var second_ratio = imagesharp_uniforms.weight_step * imagesharp_uniforms.weight_step * imagesharp_uniforms.weight_step; + let ratio_squared = imagesharp_uniforms.weight_step * imagesharp_uniforms.weight_step; + let ratio_growth = ratio_squared * ratio_squared; + + for (var distance = 1u; distance <= imagesharp_uniforms.radius; distance += 2u) { + + // G(d + 1) / G(d) is q^(2d + 1), where q = exp(-1 / (2 sigma^2)). + // The recurrence keeps the program independent of sigma and avoids evaluating + // exp() for every output pixel while retaining bilinear paired-tap sampling. + let second_weight = select(0.0, first_weight * second_ratio, distance < imagesharp_uniforms.radius); + let combined_weight = first_weight + second_weight; + let sample_distance = + ((f32(distance) * first_weight) + (f32(distance + 1u) * second_weight)) / combined_weight; + let offset = imagesharp_uniforms.direction * sample_distance; + result += (layer_sample(position - offset) + layer_sample(position + offset)) * combined_weight; + + // Advancing by two taps multiplies by q^(2d + 1) and q^(2d + 3). + // The ratio for the following pair grows by q^4. + first_weight = second_weight * second_ratio * ratio_squared; + second_ratio *= ratio_growth; + } + + return result; + } + """; + + internal static readonly WebGPUShaderUniformLayout UniformLayout = new( + [ + new WebGPUShaderUniform("direction", WebGPUShaderUniformType.Vector2, 1), + new WebGPUShaderUniform("center_weight", WebGPUShaderUniformType.Float32, 1), + new WebGPUShaderUniform("radius", WebGPUShaderUniformType.UInt32, 1), + new WebGPUShaderUniform("weight_step", WebGPUShaderUniformType.Float32, 1) + ]); + + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels; zero leaves the content unchanged. + /// + /// Thrown when is negative. + /// + public WebGPUGaussianBlurLayerEffect(float sigma) + : base(new BlurLayerEffect(sigma), ShaderSource, UniformLayout) + { + this.Sigma = sigma; + + // ImageSharp truncates its Gaussian at three standard deviations. The CPU-computed center + // weight preserves the same normalized kernel, while q lets the shader reconstruct every + // remaining weight through the exact Gaussian recurrence without sigma-specific WGSL. + int radius = (int)MathF.Ceiling(sigma * 3F); + DenseMatrix kernel = CreateKernel(radius, sigma); + float weightStep = radius == 0 ? 1F : MathF.Exp(-1F / (2F * sigma * sigma)); + + // Gaussian convolution is separable: the horizontal result convolved vertically with the + // same one-dimensional kernel is exactly the corresponding two-dimensional Gaussian. + // GaussianBlurProcessor uses Repeat on both axes: samples beyond the requested processing + // rectangle repeat its nearest edge pixel instead of introducing transparent texels. + this.AddShaderPass(BorderWrappingMode.Repeat, BorderWrappingMode.Repeat, uniforms => + { + uniforms.SetVector2("direction", Vector2.UnitX); + uniforms.SetFloat32("center_weight", kernel[0, radius]); + uniforms.SetUInt32("radius", (uint)radius); + uniforms.SetFloat32("weight_step", weightStep); + }); + + this.AddShaderPass(BorderWrappingMode.Repeat, BorderWrappingMode.Repeat, uniforms => + { + uniforms.SetVector2("direction", Vector2.UnitY); + uniforms.SetFloat32("center_weight", kernel[0, radius]); + uniforms.SetUInt32("radius", (uint)radius); + uniforms.SetFloat32("weight_step", weightStep); + }); + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Creates the normalized one-dimensional Gaussian convolution kernel. + /// + /// The number of sampled pixels on either side of the center. + /// The Gaussian standard deviation, in pixels. + /// A one-row matrix containing the kernel from negative radius through positive radius. + private static DenseMatrix CreateKernel(int radius, float sigma) + { + if (radius == 0) + { + // A zero-radius convolution is the identity. This also avoids evaluating G(0) with a + // zero sigma, whose formula contains divisions by zero. + DenseMatrix identity = new(1, 1); + identity[0, 0] = 1F; + return identity; + } + + int size = checked((radius * 2) + 1); + + // A single row is the complete kernel because Gaussian convolution is separable. Both + // shader passes consume these same normalized weights along their respective axes. + DenseMatrix kernel = new(size, 1); + Span weights = kernel.Span; + float sum = 0F; + float midpoint = (size - 1) / 2F; + + // This is the same discrete Gaussian G(x) used by ImageSharp's Gaussian blur processor. + // Keeping the unnormalized weights first permits one normalization pass after their sum is known. + for (int i = 0; i < size; i++) + { + float x = i - midpoint; + const float numerator = 1F; + float denominator = MathF.Sqrt(2 * MathF.PI) * sigma; + float exponentNumerator = -x * x; + float exponentDenominator = 2 * (sigma * sigma); + float left = numerator / denominator; + float right = MathF.Exp(exponentNumerator / exponentDenominator); + float weight = left * right; + sum += weight; + weights[i] = weight; + } + + // Unit-sum weights preserve a constant image and prevent either pass changing brightness. + for (int i = 0; i < size; i++) + { + weights[i] /= sum; + } + + return kernel; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUGlowLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUGlowLayerEffect.cs new file mode 100644 index 000000000..068c5024e --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUGlowLayerEffect.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU creates the glow with shader passes; +/// a CPU backend applies the equivalent . +/// +public sealed class WebGPUGlowLayerEffect : WebGPUShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels. + /// The glow colour. + public WebGPUGlowLayerEffect(float sigma, Color color) + : base( + new GlowLayerEffect(sigma, color), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Sigma = sigma; + this.Color = color; + this.AddPasses(sigma, color); + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the glow colour. + /// + public Color Color { get; } + + /// + /// Adds the tint and blur passes which form the glow image. + /// + /// The Gaussian blur sigma, in pixels. + /// The glow colour. + private void AddPasses(float sigma, Color color) + { + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + // Zero RGB rows replace source colour with the constant glow colour. Scaling alpha by the + // colour alpha retains the source silhouette at the requested glow opacity. + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + WebGPUColorMatrixLayerEffect tintEffect = new(tint); + WebGPUGaussianBlurLayerEffect blurEffect = new(sigma); + ReadOnlySpan tintPasses = tintEffect.GetShaderPasses(); + ReadOnlySpan blurPasses = blurEffect.GetShaderPasses(); + + // Tint before blur so the expanded pixels contain only the requested glow colour. Glow + // requires sigma greater than zero, so every valid instance has both pass sequences. + this.AddShaderPasses(tintPasses); + this.AddShaderPasses(blurPasses); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUHandle.cs index e378acfc8..7be1fe2f2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUHandle.cs @@ -51,7 +51,7 @@ protected WebGPUHandle(nint handle, bool ownsHandle) /// Callers should keep the returned token in the narrowest possible scope and dispose it /// as soon as the native call sequence that uses ends. /// - internal HandleReference AcquireReference() + public HandleReference AcquireReference() { bool addRefSucceeded = false; this.DangerousAddRef(ref addRefSucceeded); @@ -79,13 +79,21 @@ internal HandleReference AcquireReference() /// without carrying a separate success flag beside the handle. Disposing it releases the /// reference acquired by . Modeled after /// : a lightweight scoped value that should not be - /// copied or stored beyond the immediate native-call scope. + /// copied or stored beyond the immediate native-call scope. Disposal only nulls the owner + /// on the copy being disposed, so disposing two copies releases the reference twice and + /// corrupts the safe-handle refcount. /// internal struct HandleReference : IDisposable { + // Nulled by Dispose so a second Dispose on the same copy is a no-op. private WebGPUHandle? owner; - internal HandleReference(WebGPUHandle owner, nint handle) + /// + /// Initializes a new instance of the struct. + /// + /// The safe handle whose reference count was incremented by . + /// The raw native handle kept alive by this reference. + public HandleReference(WebGPUHandle owner, nint handle) { this.owner = owner; this.Handle = handle; diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUInnerShadowLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUInnerShadowLayerEffect.cs new file mode 100644 index 000000000..7e148d79d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUInnerShadowLayerEffect.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides a shader-accelerated counterpart to . +/// +/// +/// This effect can be used with both WebGPU and CPU drawing backends. WebGPU creates the inner shadow with shader +/// passes; a CPU backend applies the equivalent . +/// +public sealed class WebGPUInnerShadowLayerEffect : WebGPUShaderLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The shadow offset, in pixels. + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// The shadow colour. + public WebGPUInnerShadowLayerEffect(Point offset, float sigma, Color color) + : base( + new InnerShadowLayerEffect(offset, sigma, color), + WebGPUColorMatrixLayerEffect.ShaderSource, + WebGPUColorMatrixLayerEffect.UniformLayout) + { + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + this.AddPasses(sigma, color); + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + /// Adds the inverse-mask tint and blur passes which form the inner shadow image. + /// + /// The Gaussian blur sigma, in pixels. + /// The shadow colour. + private void AddPasses(float sigma, Color color) + { + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Unassociated); + + // M44 = -a and M54 = a produce a * (1 - source alpha), creating the coloured inverse + // silhouette which will feather into the content when blurred. + ColorMatrix tint = default; + tint.M44 = -vector.W; + tint.M54 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + WebGPUColorMatrixLayerEffect tintEffect = new(tint); + ReadOnlySpan tintPasses = tintEffect.GetShaderPasses(); + + if (sigma == 0) + { + this.AddShaderPasses(tintPasses); + return; + } + + WebGPUGaussianBlurLayerEffect blurEffect = new(sigma); + ReadOnlySpan blurPasses = blurEffect.GetShaderPasses(); + + // Blur the inverse mask after tinting. The inherited write-back metadata applies Offset + // and SrcAtop clipping after these passes, so neither concern belongs in shader uniforms. + this.AddShaderPasses(tintPasses); + this.AddShaderPasses(blurPasses); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUInstanceHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUInstanceHandle.cs index e8c924775..aa2def9b6 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUInstanceHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUInstanceHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -12,19 +12,6 @@ internal sealed unsafe class WebGPUInstanceHandle : WebGPUHandle { private readonly WebGPU? api; - /// - /// Initializes a new instance of the class. - /// - /// The WebGPU instance handle value. - /// - /// when this wrapper owns the instance and must release it; - /// when the caller retains ownership. - /// - internal WebGPUInstanceHandle(nint instanceHandle, bool ownsHandle) - : this(ownsHandle ? WebGPURuntime.GetApi() : null, instanceHandle, ownsHandle) - { - } - /// /// Initializes a new instance of the class. /// @@ -37,7 +24,7 @@ internal WebGPUInstanceHandle(nint instanceHandle, bool ownsHandle) /// when this wrapper owns the instance and must release it; /// when the caller retains ownership. /// - internal WebGPUInstanceHandle(WebGPU? api, nint instanceHandle, bool ownsHandle) + public WebGPUInstanceHandle(WebGPU? api, nint instanceHandle, bool ownsHandle) : base(instanceHandle, ownsHandle) => this.api = api; @@ -46,7 +33,7 @@ protected override bool ReleaseHandle() { try { - this.api?.InstanceRelease((Instance*)this.handle); + this.api?.InstanceRelease((WGPUInstanceImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs index 4c1d3cafc..a368b4918 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUNativeSurface.cs @@ -1,13 +1,17 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// WebGPU native drawing surface exposed through the backend-agnostic base type. /// +/// +/// The surface never releases the wrapped handles; its creator (for example or a +/// surface frame) owns them and must keep them valid for the lifetime of the surface. +/// internal sealed class WebGPUNativeSurface : NativeSurface { /// @@ -17,17 +21,23 @@ internal sealed class WebGPUNativeSurface : NativeSurface /// The wrapped queue handle used to submit work against the target texture. /// The wrapped target texture handle. /// The wrapped target texture-view handle. - /// The target texture format. - /// The surface width in pixels. - /// The surface height in pixels. + /// The target texture format and alpha representation. + /// The full backing texture width in pixels. + /// The full backing texture height in pixels. + /// The offset added when converting canvas-local coordinates to absolute texture coordinates. + /// Whether the target texture is presented to screen after the flush that renders it. + /// Whether the target lacks the sampling and storage usages required by the drawing pipeline. public WebGPUNativeSurface( WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle, WebGPUTextureHandle targetTextureHandle, WebGPUTextureViewHandle targetTextureViewHandle, - WebGPUTextureFormat targetFormat, + WebGPUTargetDescriptor targetDescriptor, int width, - int height) + int height, + Point textureCoordinateOffset, + bool isPresentationSurface, + bool requiresPresentationCopies) { Guard.NotNull(deviceHandle, nameof(deviceHandle)); Guard.NotNull(queueHandle, nameof(queueHandle)); @@ -38,9 +48,12 @@ public WebGPUNativeSurface( this.QueueHandle = queueHandle; this.TargetTextureHandle = targetTextureHandle; this.TargetTextureViewHandle = targetTextureViewHandle; - this.TargetFormat = targetFormat; + this.TargetDescriptor = targetDescriptor; this.Width = width; this.Height = height; + this.TextureCoordinateOffset = textureCoordinateOffset; + this.IsPresentationSurface = isPresentationSurface; + this.RequiresPresentationCopies = requiresPresentationCopies; } /// @@ -64,41 +77,70 @@ public WebGPUNativeSurface( public WebGPUTextureViewHandle TargetTextureViewHandle { get; } /// - /// Gets the native render target texture format identifier. + /// Gets the target texture format and alpha representation. /// - public WebGPUTextureFormat TargetFormat { get; } + public WebGPUTargetDescriptor TargetDescriptor { get; } /// - /// Gets the surface width in pixels. + /// Gets the full backing texture width in pixels. Absolute texture coordinates are clipped against + /// (0, 0, , ). /// public int Width { get; } /// - /// Gets the surface height in pixels. + /// Gets the full backing texture height in pixels. Absolute texture coordinates are clipped against + /// (0, 0, , ). /// public int Height { get; } + /// + /// Gets the offset added when converting canvas-local coordinates to absolute texture coordinates: + /// absolute texel = frame bounds origin + this offset + canvas-local coordinate. + /// Non-zero when the canvas addresses a sub-region of the x texture + /// rather than starting at its origin. + /// + public Point TextureCoordinateOffset { get; } + + /// + /// Gets a value indicating whether the target texture is presented to screen after the flush + /// that renders it. Presentation targets are re-rendered every frame, so the backend may defer + /// the scratch-overflow check for the flush instead of blocking on the GPU: a rare overflowed + /// frame presents once with incomplete coverage and the next frame renders with grown buffers. + /// Non-presentation targets (offscreen render targets, image readback) keep the synchronous + /// check so their output is always complete when the flush returns. + /// + public bool IsPresentationSurface { get; } + + /// + /// Gets a value indicating whether rendering must copy through an ImageSharp-owned texture because the target cannot be sampled or storage-bound directly. + /// + public bool RequiresPresentationCopies { get; } + /// /// Allocates a WebGPU render target and creates a native surface over the owned texture handles. /// /// The WebGPU API instance used to allocate native resources. /// The wrapped WGPUDevice* handle. /// The wrapped WGPUQueue* handle. - /// The target texture format. + /// The target texture format and alpha representation. /// The texture width in pixels. /// The texture height in pixels. - /// Receives the allocated wrapped WGPUTexture* handle. - /// Receives the allocated wrapped WGPUTextureView* handle. + /// Receives the allocated wrapped WGPUTexture* handle. The caller owns it and must dispose it. + /// Receives the allocated wrapped WGPUTextureView* handle. The caller owns it and must dispose it. + /// The offset added when converting canvas-local coordinates to absolute texture coordinates. + /// Whether the target texture is presented to screen after the flush that renders it. /// The native surface wrapping the allocated texture. - internal static unsafe WebGPUNativeSurface Create( + public static unsafe WebGPUNativeSurface Create( WebGPU api, WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle, - WebGPUTextureFormat format, + WebGPUTargetDescriptor targetDescriptor, int width, int height, out WebGPUTextureHandle textureHandle, - out WebGPUTextureViewHandle textureViewHandle) + out WebGPUTextureViewHandle textureViewHandle, + Point textureCoordinateOffset, + bool isPresentationSurface) { if (deviceHandle.IsInvalid) { @@ -113,45 +155,45 @@ internal static unsafe WebGPUNativeSurface Create( Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - WebGPUDrawingBackend.GetCompositeTextureFormatInfo(format, out TextureFormat textureFormat, out FeatureName requiredFeature); + WebGPUDrawingBackend.GetCompositeTextureFormatInfo(targetDescriptor.Format, out WGPUTextureFormat textureFormat, out WGPUFeatureName requiredFeature); using WebGPUHandle.HandleReference deviceReference = deviceHandle.AcquireReference(); - Device* device = (Device*)deviceReference.Handle; - if (requiredFeature != FeatureName.Undefined && + WGPUDeviceImpl* device = (WGPUDeviceImpl*)deviceReference.Handle; + if (requiredFeature != default && !WebGPURuntime.GetOrCreateDeviceState(api, deviceHandle).HasFeature(requiredFeature)) { - throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}' for texture format '{format}'."); + throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}' for texture format '{targetDescriptor.Format}'."); } - TextureDescriptor textureDescriptor = new() + WGPUTextureDescriptor textureDescriptor = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = textureFormat, - MipLevelCount = 1, - SampleCount = 1, + usage = (ulong)(TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding | TextureUsage.StorageBinding), + dimension = WGPUTextureDimension._2D, + size = new WGPUExtent3D((uint)width, (uint)height, 1), + format = textureFormat, + mipLevelCount = 1, + sampleCount = 1, }; - Texture* texture = api.DeviceCreateTexture(device, in textureDescriptor); + WGPUTextureImpl* texture = api.DeviceCreateTexture(device, in textureDescriptor); if (texture is null) { throw new InvalidOperationException("The WebGPU device could not create a render-target texture."); } - TextureViewDescriptor textureViewDescriptor = new() + WGPUTextureViewDescriptor textureViewDescriptor = new() { - Format = textureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All, + format = textureFormat, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All, }; - TextureView* textureView = api.TextureCreateView(texture, in textureViewDescriptor); + WGPUTextureViewImpl* textureView = api.TextureCreateView(texture, &textureViewDescriptor); if (textureView is null) { api.TextureRelease(texture); @@ -164,14 +206,18 @@ internal static unsafe WebGPUNativeSurface Create( { createdTextureHandle = new WebGPUTextureHandle(api, (nint)texture, ownsHandle: true); createdTextureViewHandle = new WebGPUTextureViewHandle(api, (nint)textureView, ownsHandle: true); + WebGPUNativeSurface surface = Create( deviceHandle, queueHandle, createdTextureHandle, createdTextureViewHandle, - format, + targetDescriptor, width, - height); + height, + textureCoordinateOffset, + isPresentationSurface, + requiresPresentationCopies: false); textureHandle = createdTextureHandle; textureViewHandle = createdTextureViewHandle; @@ -182,6 +228,7 @@ internal static unsafe WebGPUNativeSurface Create( createdTextureViewHandle?.Dispose(); createdTextureHandle?.Dispose(); + // Raw pointers are released directly only when wrapping into an owning handle never happened. if (createdTextureViewHandle is null) { api.TextureViewRelease(textureView); @@ -197,16 +244,31 @@ internal static unsafe WebGPUNativeSurface Create( } /// - /// Creates a native surface over wrapped WebGPU texture handles. + /// Creates a native surface over wrapped WebGPU texture handles after validating them. + /// The caller retains ownership of every handle. /// - internal static WebGPUNativeSurface Create( + /// The wrapped device handle that owns the target texture. + /// The wrapped queue handle used to submit work against the target texture. + /// The wrapped target texture handle. + /// The wrapped target texture-view handle. + /// The target texture format and alpha representation. + /// The full backing texture width in pixels. + /// The full backing texture height in pixels. + /// The offset added when converting canvas-local coordinates to absolute texture coordinates. + /// Whether the target texture is presented to screen after the flush that renders it. + /// Whether the target lacks the sampling and storage usages required by the drawing pipeline. + /// The native surface wrapping the supplied handles. + public static WebGPUNativeSurface Create( WebGPUDeviceHandle deviceHandle, WebGPUQueueHandle queueHandle, WebGPUTextureHandle targetTextureHandle, WebGPUTextureViewHandle targetTextureViewHandle, - WebGPUTextureFormat targetFormat, + WebGPUTargetDescriptor targetDescriptor, int width, - int height) + int height, + Point textureCoordinateOffset, + bool isPresentationSurface, + bool requiresPresentationCopies) { Guard.NotNull(deviceHandle, nameof(deviceHandle)); Guard.NotNull(queueHandle, nameof(queueHandle)); @@ -221,8 +283,11 @@ internal static WebGPUNativeSurface Create( queueHandle, targetTextureHandle, targetTextureViewHandle, - targetFormat, + targetDescriptor, width, - height); + height, + textureCoordinateOffset, + isPresentationSurface, + requiresPresentationCopies); } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUPendingSchedulingStatus.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUPendingSchedulingStatus.cs new file mode 100644 index 000000000..932ed085a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUPendingSchedulingStatus.cs @@ -0,0 +1,495 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// A deferred scheduling-status readback for one presented flush. Presentation flushes submit the +/// full frame (scheduling, fine shading, status copy, and target copy) in one submission and start +/// the asynchronous readback map without waiting for the GPU, removing the per-frame CPU-GPU sync +/// from the present path. The owning retained scene resolves the pending status at the start of +/// its next flush (or on disposal): by then the GPU has long finished, so the map wait is +/// effectively free, and any observed scratch overflow grows the scene's cached capacities so the +/// following frames render fully provisioned. +/// +/// +/// The instance owns a dedicated map-readable buffer rather than borrowing the pooled scheduling +/// arena's readback buffer: the arena returns to its pool when the flush ends, and reusing a +/// buffer with a map still pending is undefined behavior. The map callback wrapper roots itself +/// from native registration until WebGPU delivers the invocation; after a resolution timeout it +/// suppresses access to this disposed owner while still preserving the unmanaged function target. +/// +internal sealed unsafe class WebGPUPendingSchedulingStatus : IDisposable +{ + /// + /// The API facade used to read and release the readback buffer. + /// + private readonly WebGPU api; + + /// + /// The wrapped device handle that owns the readback buffer. + /// + private readonly WebGPUDeviceHandle deviceHandle; + + /// + /// The device-scoped shared state whose pool recycles the readback buffer once it is + /// safely unmapped. + /// + private readonly WebGPURuntime.DeviceSharedState deviceState; + + /// + /// The dedicated map-readable buffer holding the copied bump-allocator counters. + /// + private WGPUBufferImpl* readbackBuffer; + + /// + /// The mapped byte length requested when the asynchronous map was started. + /// + private readonly nuint readbackByteLength; + + /// + /// The physical byte capacity of the rented readback buffer. The buffer is returned to the + /// pool under this size rather than the mapped length, which for a chunked flush is smaller + /// (one record per chunk versus one per tile row), so the oversized buffer is filed under its + /// true capacity and stays reusable for later equally large rents. + /// + private readonly nuint bufferByteCapacity; + + /// + /// The self-rooting map callback wrapper that tracks the invocation accepted by native WebGPU. + /// + private readonly WebGPUBufferMapCallback callback; + + /// + /// Set by the map callback once the buffer contents are readable. + /// + private readonly ManualResetEventSlim mapReady = new(false); + + /// + /// The map status reported by the callback. + /// + private WGPUMapAsyncStatus mapStatus; + + /// + /// The submission index tied to this readback submission. + /// + private readonly ulong submissionIndex; + + /// + /// Whether resolution has already run. + /// + private bool resolved; + + /// + /// Whether owned callback and synchronization resources have already been released. + /// + private bool disposed; + + /// + /// Initializes a new instance of the class and + /// starts the asynchronous map of the already-submitted readback copy. + /// + /// The API facade used to read and release the readback buffer. + /// The wrapped device handle that owns the readback buffer. + /// The device-scoped shared state whose pool recycles the buffer. + /// The dedicated map-readable buffer; ownership transfers to this instance. + /// The scratch capacities the deferred flush rendered with. + /// The queue submission index for this deferred status copy. + public WebGPUPendingSchedulingStatus( + WebGPU api, + WebGPUDeviceHandle deviceHandle, + WebGPURuntime.DeviceSharedState deviceState, + WGPUBufferImpl* readbackBuffer, + WebGPUSceneBumpSizes submittedBumpSizes, + ulong submissionIndex) + : this( + api, + deviceHandle, + deviceState, + readbackBuffer, + (nuint)sizeof(GpuSceneBumpAllocators), + (nuint)sizeof(GpuSceneBumpAllocators), + submittedBumpSizes, + null, + null, + 0U, + submissionIndex) + { + } + + /// + /// Initializes a new instance of the class for + /// one allocator record stored in a larger pooled readback buffer. + /// + /// The API facade used to read and release the readback buffer. + /// The wrapped device handle that owns the readback buffer. + /// The device-scoped shared state whose pool recycles the buffer. + /// The dedicated map-readable buffer; ownership transfers to this instance. + /// The physical byte capacity of . + /// The scratch capacities the deferred flush rendered with. + /// The queue submission index for this deferred status copy. + public WebGPUPendingSchedulingStatus( + WebGPU api, + WebGPUDeviceHandle deviceHandle, + WebGPURuntime.DeviceSharedState deviceState, + WGPUBufferImpl* readbackBuffer, + nuint bufferByteCapacity, + WebGPUSceneBumpSizes submittedBumpSizes, + ulong submissionIndex) + : this( + api, + deviceHandle, + deviceState, + readbackBuffer, + (nuint)sizeof(GpuSceneBumpAllocators), + bufferByteCapacity, + submittedBumpSizes, + null, + null, + 0U, + submissionIndex) + { + } + + /// + /// Initializes a new instance of the class for + /// a chunked staged-scene flush and starts the asynchronous map of all chunk status records. + /// + /// The API facade used to read and release the readback buffer. + /// The wrapped device handle that owns the readback buffer. + /// The device-scoped shared state whose pool recycles the buffer. + /// The dedicated map-readable buffer; ownership transfers to this instance. + /// The physical byte capacity of , used when it is returned to the pool. + /// The full-scene scratch capacities the deferred flush rendered from. + /// The scratch capacities each chunk rendered with. + /// The tile-row height of each chunk. + /// The full tile-row height of the chunked target range. + /// The queue submission index for this deferred status copy. + public WebGPUPendingSchedulingStatus( + WebGPU api, + WebGPUDeviceHandle deviceHandle, + WebGPURuntime.DeviceSharedState deviceState, + WGPUBufferImpl* readbackBuffer, + nuint bufferByteCapacity, + WebGPUSceneBumpSizes submittedBumpSizes, + ReadOnlySpan chunkBumpSizes, + ReadOnlySpan chunkTileHeights, + uint fullTileHeight, + ulong submissionIndex) + : this( + api, + deviceHandle, + deviceState, + readbackBuffer, + checked((nuint)chunkBumpSizes.Length * (nuint)sizeof(GpuSceneBumpAllocators)), + bufferByteCapacity, + submittedBumpSizes, + chunkBumpSizes.ToArray(), + chunkTileHeights.ToArray(), + fullTileHeight, + submissionIndex) + { + } + + /// + /// Initializes a new instance of the class and + /// starts the asynchronous map of the already-submitted readback copy. + /// + /// The API facade used to read and release the readback buffer. + /// The wrapped device handle that owns the readback buffer. + /// The device-scoped shared state whose pool recycles the buffer. + /// The dedicated map-readable buffer; ownership transfers to this instance. + /// The byte length to map from . + /// The physical byte capacity of , used when it is returned to the pool. + /// The scratch capacities the deferred flush rendered with. + /// The scratch capacities each chunk rendered with. + /// The tile-row height of each chunk. + /// The full tile-row height of the chunked target range. + /// The queue submission index for this deferred status copy. + private WebGPUPendingSchedulingStatus( + WebGPU api, + WebGPUDeviceHandle deviceHandle, + WebGPURuntime.DeviceSharedState deviceState, + WGPUBufferImpl* readbackBuffer, + nuint readbackByteLength, + nuint bufferByteCapacity, + WebGPUSceneBumpSizes submittedBumpSizes, + WebGPUSceneBumpSizes[]? chunkBumpSizes, + uint[]? chunkTileHeights, + uint fullTileHeight, + ulong submissionIndex) + { + this.api = api; + this.deviceHandle = deviceHandle; + this.deviceState = deviceState; + this.readbackBuffer = readbackBuffer; + this.readbackByteLength = readbackByteLength; + this.bufferByteCapacity = bufferByteCapacity; + this.submissionIndex = submissionIndex; + this.SubmittedBumpSizes = submittedBumpSizes; + this.ChunkBumpSizes = chunkBumpSizes; + this.ChunkTileHeights = chunkTileHeights; + this.FullTileHeight = fullTileHeight; + + this.callback = WebGPUBufferMapCallback.From(this.OnMapped); + this.api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, readbackByteLength, this.callback, null); + } + + /// + /// Gets the scratch capacities the deferred flush rendered with. Comparing the resolved + /// counters against these sizes tells the caller whether the frame overflowed. + /// + public WebGPUSceneBumpSizes SubmittedBumpSizes { get; } + + /// + /// Gets the scratch capacities each chunk rendered with, or for a + /// non-chunked deferred flush. + /// + public WebGPUSceneBumpSizes[]? ChunkBumpSizes { get; } + + /// + /// Gets the tile-row height of each chunk, or for a non-chunked + /// deferred flush. + /// + public uint[]? ChunkTileHeights { get; } + + /// + /// Gets the full tile-row height of the chunked target range. + /// + public uint FullTileHeight { get; } + + /// + /// Gets a value indicating whether this status describes a chunked staged-scene flush. + /// + public bool IsChunked => this.ChunkBumpSizes is not null; + + /// + /// Gets a value indicating whether the map callback has fired, making + /// wait-free. Callbacks only advance while the device is pumped, so callers should + /// once before checking a batch of pending statuses. + /// + public bool IsReady => this.mapReady.IsSet; + + /// + /// Pumps the device once without waiting so any completed map callbacks are delivered. + /// + public void PollDevice() + { + if (this.resolved) + { + return; + } + + using WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference(); + ulong submissionIndex = this.submissionIndex; + _ = this.api.DevicePoll((WGPUDeviceImpl*)deviceReference.Handle, false, &submissionIndex); + } + + /// + /// Resolves the deferred readback: waits (bounded) for the map callback, reads the + /// bump-allocator counters, and releases the readback buffer. + /// + /// Receives the counters reported by the GPU when successful. + /// + /// when the counters were read; when the map + /// failed or timed out, in which case the caller keeps its current capacities. + /// + public bool TryResolve(out GpuSceneBumpAllocators bumpAllocators) + { + bumpAllocators = default; + if (this.resolved || this.readbackBuffer is null) + { + return false; + } + + this.resolved = true; + + bool signaled; + using (WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference()) + { + signaled = WaitForMapSignal( + this.api, + (WGPUDeviceImpl*)deviceReference.Handle, + this.mapReady, + this.submissionIndex); + } + + if (!signaled || this.mapStatus != WGPUMapAsyncStatus.Success) + { + this.ReleaseBuffer(unmap: false); + return false; + } + + void* mapped = this.api.BufferGetConstMappedRange(this.readbackBuffer, 0, (nuint)sizeof(GpuSceneBumpAllocators)); + if (mapped is null) + { + this.ReleaseBuffer(unmap: true); + return false; + } + + bumpAllocators = Unsafe.Read(mapped); + this.ReleaseBuffer(unmap: true); + return true; + } + + /// + /// Resolves the deferred chunked readback: waits (bounded) for the map callback, reads all + /// chunk-local bump-allocator counters, and releases the readback buffer. + /// + /// Receives the counters reported by the GPU for each chunk when successful. + /// + /// when the counters were read; when the map + /// failed or timed out, in which case the caller keeps its current capacities. + /// + public bool TryResolveChunked(out GpuSceneBumpAllocators[]? bumpAllocators) + { + bumpAllocators = null; + if (this.resolved || this.readbackBuffer is null || this.ChunkBumpSizes is null) + { + return false; + } + + this.resolved = true; + + bool signaled; + using (WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference()) + { + signaled = WaitForMapSignal( + this.api, + (WGPUDeviceImpl*)deviceReference.Handle, + this.mapReady, + this.submissionIndex); + } + + if (!signaled || this.mapStatus != WGPUMapAsyncStatus.Success) + { + this.ReleaseBuffer(unmap: false); + return false; + } + + void* mapped = this.api.BufferGetConstMappedRange(this.readbackBuffer, 0, this.readbackByteLength); + if (mapped is null) + { + this.ReleaseBuffer(unmap: true); + return false; + } + + bumpAllocators = new GpuSceneBumpAllocators[this.ChunkBumpSizes.Length]; + ReadOnlySpan statuses = new(mapped, bumpAllocators.Length); + statuses.CopyTo(bumpAllocators); + this.ReleaseBuffer(unmap: true); + return true; + } + + /// + /// Disposes the pending status. When still unresolved this waits (bounded) for the map + /// callback, then retires the managed owner and releases the readback buffer. + /// + public void Dispose() + { + if (this.disposed) + { + return; + } + + // Mark disposal before retiring the callback owner so repeated calls cannot release the + // readback resources twice. The wrapper retains its own root if native still owes a call. + this.disposed = true; + + try + { + if (!this.resolved) + { + // Give the accepted map a bounded opportunity to finish before releasing its buffer. + // A timeout is still safe because retiring the wrapper below suppresses a later call + // into this owner while its self-root preserves the native function target. + _ = this.TryResolve(out _); + } + } + finally + { + // A resolve throw (device poll or map-range failure) must not skip the retirement: + // the flag above already made this the only disposal pass, so the buffer, callback + // owner, and event would otherwise leak for the process lifetime. + this.ReleaseBuffer(unmap: false); + this.callback.Dispose(); + this.mapReady.Dispose(); + } + } + + /// + /// The buffer map callback; records the status and signals resolution. + /// + /// The map status reported by the implementation. + /// Unused user data pointer. + private void OnMapped(WGPUMapAsyncStatus status, void* userData) + { + _ = userData; + this.mapStatus = status; + this.mapReady.Set(); + } + + /// + /// Retires the owned readback buffer exactly once. A buffer that completed its map cycle + /// is unmapped and recycled through the device pool; a buffer whose map may still be + /// pending (failed or timed-out wait) is released outright, because recycling a buffer + /// with an outstanding map is undefined behavior. + /// + /// Whether the buffer completed its map and must be unmapped first. + private void ReleaseBuffer(bool unmap) + { + if (this.readbackBuffer is null) + { + return; + } + + if (unmap) + { + this.api.BufferUnmap(this.readbackBuffer); + this.deviceState.ReturnStatusReadbackBuffer(this.readbackBuffer, this.bufferByteCapacity); + } + else + { + this.api.BufferRelease(this.readbackBuffer); + } + + this.readbackBuffer = null; + } + + /// + /// Pumps the WebGPU device while waiting for the map callback to signal completion. + /// Mirrors the synchronous flush path's wait so a lost device cannot hang the caller. + /// + /// The WebGPU API used to advance callback delivery. + /// The device that owns the mapped readback buffer. + /// The event that the map callback sets when the copy is ready to read. + /// The queue submission index used for this readback. + /// when the callback completed before the timeout; otherwise, . + private static bool WaitForMapSignal( + WebGPU api, + WGPUDeviceImpl* device, + ManualResetEventSlim signal, + ulong submissionIndex) + { + // Keep polling scoped to this readback's submission, but never ask one native poll to wait. + // A blocking DevicePoll can overrun the managed five-second bound before control returns + // to the stopwatch check. + Stopwatch stopwatch = Stopwatch.StartNew(); + + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < 5000) + { + _ = api.DevicePoll(device, false, &submissionIndex); + + if (!signal.IsSet) + { + _ = Thread.Yield(); + } + } + + return signal.IsSet; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUPresentationRenderer.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUPresentationRenderer.cs new file mode 100644 index 000000000..7908fe083 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUPresentationRenderer.cs @@ -0,0 +1,272 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Presents an ImageSharp-owned texture through a WebGPU surface texture. +/// +internal sealed unsafe class WebGPUPresentationRenderer : IDisposable +{ + private const string PipelineKey = "surface/presentation"; + + private readonly WebGPU api; + private readonly WebGPUDeviceHandle deviceHandle; + private readonly WebGPUQueueHandle queueHandle; + private readonly WebGPUTargetDescriptor targetDescriptor; + private readonly WebGPUTextureHandle sourceTextureHandle; + private readonly uint width; + private readonly uint height; + private readonly bool copyToSurface; + private readonly WGPURenderPipelineImpl* pipeline; + private WGPUBindGroupImpl* bindGroup; + private bool isDisposed; + + /// + /// Initializes a new instance of the class for a persistent source target. + /// + /// The WebGPU API facade. + /// The device context that owns the source and destination textures. + /// The persistent ImageSharp-owned target presented by this renderer. + /// Whether the surface supports receiving a native texture copy. + public WebGPUPresentationRenderer(WebGPU api, WebGPUDeviceContext deviceContext, WebGPURenderTarget source, bool copyToSurface) + { + this.api = api; + this.deviceHandle = deviceContext.DeviceHandle; + this.queueHandle = deviceContext.QueueHandle; + this.targetDescriptor = source.Surface.TargetDescriptor; + this.sourceTextureHandle = source.TextureHandle; + this.width = (uint)source.Width; + this.height = (uint)source.Height; + this.copyToSurface = copyToSurface; + + // Copy-capable surfaces need no shader resources. The source and destination have the + // same negotiated format and extent, so WebGPU's native texture copy is the shortest path. + if (copyToSurface) + { + return; + } + + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(api, this.deviceHandle); + WGPUTextureFormat textureFormat = WebGPUTextureFormatMapper.ToNative(this.targetDescriptor.Format); + + if (!deviceState.TryGetOrCreateCompositePipeline( + PipelineKey, + PresentationShader.ShaderCode, + PresentationShader.TryCreateBindGroupLayout, + textureFormat, + CompositePipelineBlendMode.None, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPURenderPipelineImpl* createdPipeline, + out string? error)) + { + throw new InvalidOperationException(error); + } + + this.pipeline = createdPipeline; + + using WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference(); + using WebGPUHandle.HandleReference sourceTextureViewReference = source.TextureViewHandle.AcquireReference(); + + WGPUBindGroupEntry entry = new() + { + binding = 0, + textureView = (WGPUTextureViewImpl*)sourceTextureViewReference.Handle + }; + + WGPUBindGroupDescriptor bindGroupDescriptor = new() + { + layout = bindGroupLayout, + entryCount = 1, + entries = &entry + }; + + this.bindGroup = api.DeviceCreateBindGroup((WGPUDeviceImpl*)deviceReference.Handle, in bindGroupDescriptor); + if (this.bindGroup is null) + { + throw new InvalidOperationException("Failed to create the WebGPU presentation bind group."); + } + } + + /// + /// Transfers the bound source texture into a presentable destination texture. + /// + /// The acquired surface texture that receives a native copy when supported. + /// The acquired surface texture view that receives the frame. + public void Present(WebGPUTextureHandle destinationTextureHandle, WebGPUTextureViewHandle destinationTextureViewHandle) + { + ObjectDisposedException.ThrowIf(this.isDisposed, this); + + if (this.copyToSurface) + { + this.CopyToSurface(destinationTextureHandle); + return; + } + + WGPUCommandEncoderImpl* commandEncoder = null; + WGPURenderPassEncoderImpl* renderPass = null; + WGPUCommandBufferImpl* commandBuffer = null; + + try + { + using WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference(); + using WebGPUHandle.HandleReference queueReference = this.queueHandle.AcquireReference(); + using WebGPUHandle.HandleReference destinationTextureViewReference = destinationTextureViewHandle.AcquireReference(); + + WGPUCommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.api.DeviceCreateCommandEncoder((WGPUDeviceImpl*)deviceReference.Handle, in commandEncoderDescriptor); + if (commandEncoder is null) + { + throw new InvalidOperationException("Failed to create the WebGPU presentation command encoder."); + } + + WGPURenderPassColorAttachment colorAttachment = new() + { + view = (WGPUTextureViewImpl*)destinationTextureViewReference.Handle, + depthSlice = uint.MaxValue, + loadOp = WGPULoadOp.Clear, + storeOp = WGPUStoreOp.Store, + + // The fullscreen triangle overwrites every texel. This format-correct clear still + // preserves logical transparency if clipping rules ever leave an edge uncovered. + clearValue = this.targetDescriptor.NumericEncoding == WebGPUTargetNumericEncoding.SignedUnit + ? new WGPUColor { r = -1D, g = -1D, b = -1D, a = -1D } + : default + }; + + WGPURenderPassDescriptor renderPassDescriptor = new() + { + colorAttachmentCount = 1, + colorAttachments = &colorAttachment + }; + + renderPass = this.api.CommandEncoderBeginRenderPass(commandEncoder, in renderPassDescriptor); + if (renderPass is null) + { + throw new InvalidOperationException("Failed to begin the WebGPU presentation render pass."); + } + + this.api.RenderPassEncoderSetPipeline(renderPass, this.pipeline); + this.api.RenderPassEncoderSetBindGroup(renderPass, 0, this.bindGroup); + this.api.RenderPassEncoderDraw(renderPass, 3, 1, 0, 0); + this.api.RenderPassEncoderEnd(renderPass); + this.api.RenderPassEncoderRelease(renderPass); + renderPass = null; + + WGPUCommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + throw new InvalidOperationException("Failed to finish the WebGPU presentation command buffer."); + } + + this.api.QueueSubmit((WGPUQueueImpl*)queueReference.Handle, 1, ref commandBuffer); + } + finally + { + if (renderPass is not null) + { + this.api.RenderPassEncoderEnd(renderPass); + this.api.RenderPassEncoderRelease(renderPass); + } + + if (commandBuffer is not null) + { + this.api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.api.CommandEncoderRelease(commandEncoder); + } + } + } + + /// + /// Copies the persistent source texture into a copy-capable surface texture. + /// + /// The acquired surface texture. + private void CopyToSurface(WebGPUTextureHandle destinationTextureHandle) + { + WGPUCommandEncoderImpl* commandEncoder = null; + WGPUCommandBufferImpl* commandBuffer = null; + + try + { + using WebGPUHandle.HandleReference deviceReference = this.deviceHandle.AcquireReference(); + using WebGPUHandle.HandleReference queueReference = this.queueHandle.AcquireReference(); + using WebGPUHandle.HandleReference sourceTextureReference = this.sourceTextureHandle.AcquireReference(); + using WebGPUHandle.HandleReference destinationTextureReference = destinationTextureHandle.AcquireReference(); + + WGPUCommandEncoderDescriptor commandEncoderDescriptor = default; + commandEncoder = this.api.DeviceCreateCommandEncoder((WGPUDeviceImpl*)deviceReference.Handle, in commandEncoderDescriptor); + if (commandEncoder is null) + { + throw new InvalidOperationException("Failed to create the WebGPU presentation copy command encoder."); + } + + WGPUTexelCopyTextureInfo source = new() + { + texture = (WGPUTextureImpl*)sourceTextureReference.Handle, + mipLevel = 0, + origin = default, + aspect = WGPUTextureAspect.All + }; + + WGPUTexelCopyTextureInfo destination = new() + { + texture = (WGPUTextureImpl*)destinationTextureReference.Handle, + mipLevel = 0, + origin = default, + aspect = WGPUTextureAspect.All + }; + + WGPUExtent3D copySize = new(this.width, this.height, 1); + this.api.CommandEncoderCopyTextureToTexture(commandEncoder, in source, in destination, in copySize); + + WGPUCommandBufferDescriptor commandBufferDescriptor = default; + commandBuffer = this.api.CommandEncoderFinish(commandEncoder, in commandBufferDescriptor); + if (commandBuffer is null) + { + throw new InvalidOperationException("Failed to finish the WebGPU presentation copy command buffer."); + } + + // Canvas disposal submits first. Queue ordering therefore completes the render before + // this copy without a CPU wait or an additional synchronization primitive. + this.api.QueueSubmit((WGPUQueueImpl*)queueReference.Handle, 1, ref commandBuffer); + } + finally + { + if (commandBuffer is not null) + { + this.api.CommandBufferRelease(commandBuffer); + } + + if (commandEncoder is not null) + { + this.api.CommandEncoderRelease(commandEncoder); + } + } + } + + /// + /// Releases the source-texture bind group owned by this renderer. + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + if (this.bindGroup is not null) + { + this.api.BindGroupRelease(this.bindGroup); + this.bindGroup = null; + } + + this.isDisposed = true; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUQueueHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUQueueHandle.cs index 228f5f0bd..a6966706e 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUQueueHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUQueueHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -20,7 +20,7 @@ internal sealed unsafe class WebGPUQueueHandle : WebGPUHandle /// when this wrapper owns the queue and must release it; /// when the caller retains ownership. /// - internal WebGPUQueueHandle(nint queueHandle, bool ownsHandle) + public WebGPUQueueHandle(nint queueHandle, bool ownsHandle) : this(ownsHandle ? WebGPURuntime.GetApi() : null, queueHandle, ownsHandle) { } @@ -37,7 +37,7 @@ internal WebGPUQueueHandle(nint queueHandle, bool ownsHandle) /// when this wrapper owns the queue and must release it; /// when the caller retains ownership. /// - internal WebGPUQueueHandle(WebGPU? api, nint queueHandle, bool ownsHandle) + public WebGPUQueueHandle(WebGPU? api, nint queueHandle, bool ownsHandle) : base(queueHandle, ownsHandle) => this.api = api; @@ -46,7 +46,7 @@ protected override bool ReleaseHandle() { try { - this.api?.QueueRelease((Queue*)this.handle); + this.api?.QueueRelease((WGPUQueueImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs index f589cbcf7..4a6f3daa0 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURenderTarget.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -16,6 +15,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; public sealed class WebGPURenderTarget : IDisposable { private readonly WebGPUDeviceContext deviceContext; + + // False when the context is shared, e.g. targets created via CreateRenderTarget or from a + // surface frame; those must not tear down the context their siblings still use. private readonly bool ownsDeviceContext; private bool isDisposed; @@ -25,7 +27,7 @@ public sealed class WebGPURenderTarget : IDisposable /// The target width in pixels. /// The target height in pixels. public WebGPURenderTarget(int width, int height) - : this(Configuration.Default, WebGPUTextureFormat.Rgba8Unorm, width, height) + : this(Configuration.Default, WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, width, height) { } @@ -39,7 +41,19 @@ public WebGPURenderTarget( WebGPUTextureFormat format, int width, int height) - : this(Configuration.Default, format, width, height) + : this(Configuration.Default, format, PixelAlphaRepresentation.Unassociated, width, height) + { + } + + /// + /// Initializes a new instance of the class using the shared process-level device. + /// + /// The target texture format. + /// The alpha representation stored by the target. + /// The target width in pixels. + /// The target height in pixels. + public WebGPURenderTarget(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, int width, int height) + : this(Configuration.Default, format, alphaRepresentation, width, height) { } @@ -50,7 +64,7 @@ public WebGPURenderTarget( /// The target width in pixels. /// The target height in pixels. public WebGPURenderTarget(Configuration configuration, int width, int height) - : this(configuration, WebGPUTextureFormat.Rgba8Unorm, width, height) + : this(configuration, WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, width, height) { } @@ -66,16 +80,40 @@ public WebGPURenderTarget( WebGPUTextureFormat format, int width, int height) - : this(new WebGPUDeviceContext(configuration), true, format, width, height) + : this(configuration, format, PixelAlphaRepresentation.Unassociated, width, height) + { + } + + /// + /// Initializes a new instance of the class using the shared process-level device. + /// + /// The configuration instance to bind to the created backend. + /// The target texture format. + /// The alpha representation stored by the target. + /// The target width in pixels. + /// The target height in pixels. + public WebGPURenderTarget(Configuration configuration, WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, int width, int height) + : this(new WebGPUDeviceContext(configuration), true, WebGPUDrawingBackend.CreateOffscreenTargetDescriptor(format, alphaRepresentation), width, height, isPresentationSurface: false) { } - private WebGPURenderTarget( + /// + /// Initializes a new instance of the class over an existing device context, + /// allocating the backing texture and view on that context's device. + /// + /// The device context that owns the device and queue used by this target. + /// Whether this target disposes when it is disposed. + /// The target texture format and alpha representation. + /// The target width in pixels. + /// The target height in pixels. + /// Whether this target supplies pixels to a presentation surface. + internal WebGPURenderTarget( WebGPUDeviceContext deviceContext, bool ownsDeviceContext, - WebGPUTextureFormat format, + WebGPUTargetDescriptor targetDescriptor, int width, - int height) + int height, + bool isPresentationSurface) { this.deviceContext = deviceContext; this.ownsDeviceContext = ownsDeviceContext; @@ -89,16 +127,19 @@ private WebGPURenderTarget( api, deviceContext.DeviceHandle, deviceContext.QueueHandle, - format, + targetDescriptor, width, height, out WebGPUTextureHandle textureHandle, - out WebGPUTextureViewHandle textureViewHandle); + out WebGPUTextureViewHandle textureViewHandle, + textureCoordinateOffset: default, + isPresentationSurface); this.TextureHandle = textureHandle; this.TextureViewHandle = textureViewHandle; this.Surface = surface; - this.Format = format; + this.Format = targetDescriptor.Format; + this.AlphaRepresentation = targetDescriptor.AlphaRepresentation; this.Bounds = new Rectangle(0, 0, width, height); } catch @@ -117,6 +158,19 @@ private WebGPURenderTarget( /// internal WebGPUDrawingBackend Backend => this.deviceContext.Backend; + /// + /// Gets the device context used by this render target. + /// + public WebGPUDeviceContext DeviceContext + { + get + { + this.ThrowIfDisposed(); + + return this.deviceContext; + } + } + /// /// Gets the native surface backing this render target. /// Most callers should use or instead. @@ -143,6 +197,11 @@ private WebGPURenderTarget( /// public WebGPUTextureFormat Format { get; } + /// + /// Gets the alpha representation stored by the target. + /// + public PixelAlphaRepresentation AlphaRepresentation { get; } + /// /// Gets the owned wrapped texture handle behind this render target. /// @@ -153,6 +212,26 @@ private WebGPURenderTarget( /// internal WebGPUTextureViewHandle TextureViewHandle { get; } + /// + /// Creates an empty render target with the same texture format as this target. + /// + /// The target width in pixels. + /// The target height in pixels. + /// The created render target. + /// + /// The created target does not contain a copy of this target's pixels. + /// This target must remain undisposed while the created target is in use. + /// + public WebGPURenderTarget CreateRenderTarget(int width, int height) + { + this.ThrowIfDisposed(); + this.deviceContext.ThrowIfDisposed(); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + return new WebGPURenderTarget(this.deviceContext, false, this.Surface.TargetDescriptor, width, height, isPresentationSurface: false); + } + /// /// Creates a drawing canvas over this render target. /// @@ -176,28 +255,59 @@ public DrawingCanvas CreateCanvas(DrawingOptions options) this.deviceContext.Backend, this.Bounds, this.Surface, - this.Format); + this.Surface.TargetDescriptor); } /// - /// Reads the current GPU texture contents back into a new CPU image. + /// Creates a drawing canvas over this render target. /// - /// The readback image. + /// The initial drawing options. + /// The text drawing cache used by this canvas instance. + /// A drawing canvas targeting this render target. + public DrawingCanvas CreateCanvas(DrawingOptions options, DrawingTextCache textCache) + { + this.ThrowIfDisposed(); + this.deviceContext.ThrowIfDisposed(); + Guard.NotNull(textCache, nameof(textCache)); + + return WebGPUCanvasFactory.CreateCanvas( + this.deviceContext.Configuration, + options, + textCache, + this.deviceContext.Backend, + this.Bounds, + this.Surface, + this.Surface.TargetDescriptor); + } + + /// + /// Reads the current GPU texture contents back into a new CPU image whose pixel type matches and . + /// + /// + /// The readback image whose pixel type has the target's channel layout, numeric encoding, and alpha representation. + /// public Image ReadbackImage() -#pragma warning disable CS8524 - => this.Format switch +#pragma warning disable CS8509, CS8524 // Exhaustive in practice: construction normalizes alpha and validates the format. + => (this.Format, this.AlphaRepresentation) switch { - WebGPUTextureFormat.Rgba8Unorm => this.ReadbackImage(), - WebGPUTextureFormat.Bgra8Unorm => this.ReadbackImage(), - WebGPUTextureFormat.Rgba8Snorm => this.ReadbackImage(), - WebGPUTextureFormat.Rgba16Float => this.ReadbackImage() + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated) => this.ReadbackImage(), + (WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated) => this.ReadbackImage(), + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Unassociated) => this.ReadbackImage(), + (WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated) => this.ReadbackImage(), + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated) => this.ReadbackImage(), + (WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated) => this.ReadbackImage(), + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated) => this.ReadbackImage(), + (WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated) => this.ReadbackImage() }; -#pragma warning restore CS8524 +#pragma warning restore CS8509, CS8524 /// /// Reads the current GPU texture contents back into a new CPU image. /// - /// The destination image pixel format. + /// + /// The destination image pixel format. Must match and ; the backend read throws + /// on a mismatch. Use to dispatch by format. + /// /// The readback image. public Image ReadbackImage() where TPixel : unmanaged, IPixel @@ -222,7 +332,10 @@ public Image ReadbackImage() /// /// Reads the current GPU texture contents back into an existing CPU buffer region. /// - /// The destination image pixel format. + /// + /// The destination image pixel format. Must match and ; the backend read throws + /// on a mismatch. + /// /// The destination buffer region that receives the readback pixels. public void ReadbackInto(Buffer2DRegion destination) where TPixel : unmanaged, IPixel @@ -248,7 +361,8 @@ public void ReadbackInto(Buffer2DRegion destination) } /// - /// Releases the owned texture view and texture. + /// Releases the owned texture view and texture, and the device context when this target created it. + /// Targets created from a shared context leave that context untouched. /// public void Dispose() { @@ -269,15 +383,8 @@ public void Dispose() } /// - /// Allocates an owned render target for the specified context, format, and size. + /// Throws when this render target has been disposed. /// - internal static WebGPURenderTarget CreateFromContext( - WebGPUDeviceContext deviceContext, - WebGPUTextureFormat format, - int width, - int height) - => new(deviceContext, false, format, width, height); - private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.Effects.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.Effects.cs new file mode 100644 index 000000000..65ee398d0 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.Effects.cs @@ -0,0 +1,441 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics; +using System.Text; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +internal static unsafe partial class WebGPURuntime +{ + internal sealed partial class DeviceSharedState + { + /// + /// Gets or creates the exact render pipeline used by one generated layer-effect module. + /// + /// The complete generated WGSL module and source mapping. + /// The byte length of the user uniform binding. + /// The render target format written by the fragment shader. + /// Receives the cached bind-group layout. + /// Receives the cached render pipeline. + /// Receives the native resource-creation failure. + /// when the pipeline is available; otherwise . + public bool TryGetOrCreateEffectPipeline( + WebGPUShaderModuleSource moduleSource, + int uniformByteLength, + WGPUTextureFormat outputFormat, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPURenderPipelineImpl* pipeline, + out string? error) + { + bindGroupLayout = null; + pipeline = null; + + ObjectDisposedException.ThrowIf(this.disposed, this); + + WebGPUEffectPipelineKey key = new(moduleSource, uniformByteLength, outputFormat); + + // The dictionary prevents duplicate infrastructure objects, while the per-entry lock + // serializes native creation for this exact key without blocking unrelated programs. + WebGPUEffectPipelineInfrastructure infrastructure = this.effectPipelines.GetOrAdd( + key, + static _ => new WebGPUEffectPipelineInfrastructure()); + + lock (infrastructure) + { + if (infrastructure.Pipeline is not null) + { + bindGroupLayout = infrastructure.BindGroupLayout; + pipeline = infrastructure.Pipeline; + error = null; + return true; + } + + if (!this.TryCreateEffectPipelineInfrastructure( + moduleSource, + uniformByteLength, + outputFormat, + out WGPUBindGroupLayoutImpl* createdBindGroupLayout, + out WGPUPipelineLayoutImpl* createdPipelineLayout, + out WGPUShaderModuleImpl* createdShaderModule, + out WGPURenderPipelineImpl* createdPipeline, + out error)) + { + return false; + } + + infrastructure.BindGroupLayout = createdBindGroupLayout; + infrastructure.PipelineLayout = createdPipelineLayout; + infrastructure.ShaderModule = createdShaderModule; + infrastructure.Pipeline = createdPipeline; + + bindGroupLayout = createdBindGroupLayout; + pipeline = createdPipeline; + return true; + } + } + + /// + /// Compiles every pipeline used by one shader effect for the supplied initial source representation. + /// + /// The effect whose ordered passes are compiled. + /// The representation sampled by the first pass. + public void PrecompileEffect(IWebGPUShaderEffectSource effect, WebGPUTargetDescriptor sourceDescriptor) + { + ReadOnlySpan passes = effect.GetShaderPasses(); + + for (int i = 0; i < passes.Length; i++) + { + WebGPUShaderPass pass = passes[i]; + WebGPUShaderProgram program = pass.Program; + WebGPUShaderModuleSource moduleSource = program.GetModuleSource( + sourceDescriptor, + pass.XBorderMode, + pass.YBorderMode); + + if (!this.TryGetOrCreateEffectPipeline( + moduleSource, + program.UniformLayout.ByteLength, + WGPUTextureFormat.RGBA16Float, + out _, + out _, + out string? error)) + { + throw new InvalidOperationException(error); + } + + // The first pass samples the drawing target. Every later pass samples the + // associated Rgba16Float working texture produced by its predecessor. + sourceDescriptor = WebGPUShaderEffectWorkingTexture.Descriptor; + } + } + + /// + /// Creates every device-owned object behind one effect pipeline and releases partial state on failure. + /// + private bool TryCreateEffectPipelineInfrastructure( + WebGPUShaderModuleSource moduleSource, + int uniformByteLength, + WGPUTextureFormat outputFormat, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUPipelineLayoutImpl* pipelineLayout, + out WGPUShaderModuleImpl* shaderModule, + out WGPURenderPipelineImpl* pipeline, + out string? error) + { + bindGroupLayout = this.CreateEffectBindGroupLayout(uniformByteLength); + pipelineLayout = null; + shaderModule = null; + pipeline = null; + + if (bindGroupLayout is null) + { + error = "Failed to create the WebGPU layer-effect bind-group layout."; + return false; + } + + WGPUBindGroupLayoutImpl** bindGroupLayouts = stackalloc WGPUBindGroupLayoutImpl*[1]; + bindGroupLayouts[0] = bindGroupLayout; + WGPUPipelineLayoutDescriptor pipelineLayoutDescriptor = new() + { + bindGroupLayoutCount = 1, + bindGroupLayouts = bindGroupLayouts + }; + + pipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); + if (pipelineLayout is null) + { + this.Api.BindGroupLayoutRelease(bindGroupLayout); + bindGroupLayout = null; + error = "Failed to create the WebGPU layer-effect pipeline layout."; + return false; + } + + if (!this.TryCreateValidatedShaderModule(moduleSource, out shaderModule, out error)) + { + this.Api.PipelineLayoutRelease(pipelineLayout); + this.Api.BindGroupLayoutRelease(bindGroupLayout); + pipelineLayout = null; + bindGroupLayout = null; + return false; + } + + pipeline = this.CreateCompositePipeline(pipelineLayout, shaderModule, outputFormat, CompositePipelineBlendMode.None); + if (pipeline is null) + { + this.Api.ShaderModuleRelease(shaderModule); + this.Api.PipelineLayoutRelease(pipelineLayout); + this.Api.BindGroupLayoutRelease(bindGroupLayout); + shaderModule = null; + pipelineLayout = null; + bindGroupLayout = null; + error = "Failed to create the WebGPU layer-effect render pipeline."; + return false; + } + + error = null; + return true; + } + + /// + /// Creates the fixed source/framework/user/sampler binding layout shared by one exact shader module. + /// + private WGPUBindGroupLayoutImpl* CreateEffectBindGroupLayout(int uniformByteLength) + { + WGPUBindGroupLayoutEntry* entries = stackalloc WGPUBindGroupLayoutEntry[4]; + entries[0] = new WGPUBindGroupLayoutEntry + { + binding = 0, + visibility = (ulong)ShaderStage.Fragment, + texture = new WGPUTextureBindingLayout + { + sampleType = WGPUTextureSampleType.Float, + viewDimension = WGPUTextureViewDimension._2D, + multisampled = 0U + } + }; + entries[1] = new WGPUBindGroupLayoutEntry + { + binding = 1, + visibility = (ulong)ShaderStage.Fragment, + buffer = new WGPUBufferBindingLayout + { + type = WGPUBufferBindingType.Uniform, + minBindingSize = WebGPUShaderFrameworkUniforms.ByteLength + } + }; + entries[2] = new WGPUBindGroupLayoutEntry + { + binding = 2, + visibility = (ulong)ShaderStage.Fragment, + buffer = new WGPUBufferBindingLayout + { + type = WGPUBufferBindingType.Uniform, + minBindingSize = checked((ulong)uniformByteLength) + } + }; + entries[3] = new WGPUBindGroupLayoutEntry + { + binding = 3, + visibility = (ulong)ShaderStage.Fragment, + sampler = new WGPUSamplerBindingLayout + { + type = WGPUSamplerBindingType.Filtering + } + }; + + WGPUBindGroupLayoutDescriptor descriptor = new() + { + entryCount = 4, + entries = entries + }; + + return this.Api.DeviceCreateBindGroupLayout(this.Device, in descriptor); + } + + /// + /// Creates a module and rejects it when native WGSL compilation reports an error. + /// + private bool TryCreateValidatedShaderModule(WebGPUShaderModuleSource moduleSource, out WGPUShaderModuleImpl* shaderModule, out string? error) + { + WGPUPopErrorScopeStatus scopeStatus = WGPUPopErrorScopeStatus.CallbackCancelled; + WGPUErrorType errorType = WGPUErrorType.Unknown; + string? diagnosticMessage = null; + Exception? callbackException = null; + + // wgpu-native 29 exports wgpuShaderModuleGetCompilationInfo but deliberately leaves + // it unimplemented. A validation error scope is the supported path for reporting an + // invalid WGSL module without allowing the error to escape as an uncaptured failure. + this.Api.DevicePushErrorScope(this.Device, WGPUErrorFilter.Validation); + shaderModule = this.CreateShaderModule(moduleSource.Utf8Source); + + // PopErrorScope may complete asynchronously even in AllowSpontaneous mode. Copy its + // borrowed message during the callback and poll the device until completion. + using ManualResetEventSlim callbackReady = new(false); + using WebGPUPopErrorScopeCallback callback = WebGPUPopErrorScopeCallback.From((status, type, message, _) => + { + scopeStatus = status; + errorType = type; + + try + { + diagnosticMessage = message.ToManagedString(); + } + catch (Exception ex) + { + // Exceptions cannot cross the unmanaged callback boundary. Preserve the + // failure until control has returned to the managed compilation path. + callbackException = ex; + } + finally + { + callbackReady.Set(); + } + }); + + this.Api.DevicePopErrorScope(this.Device, callback, null); + + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!callbackReady.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) + { + _ = this.Api.DevicePoll(this.Device, false, null); + + if (!callbackReady.IsSet) + { + Thread.Yield(); + } + } + + callback.Dispose(); + + // Disposal synchronizes with an in-flight callback. Recheck the signal afterwards so + // completion at the timeout boundary is not incorrectly reported as a timeout. + if (!callbackReady.IsSet) + { + if (shaderModule is not null) + { + this.Api.ShaderModuleRelease(shaderModule); + } + + shaderModule = null; + error = "Timed out while validating the WebGPU layer-effect shader module."; + return false; + } + + if (callbackException is not null) + { + if (shaderModule is not null) + { + this.Api.ShaderModuleRelease(shaderModule); + } + + shaderModule = null; + throw new InvalidOperationException("Failed to copy the WebGPU layer-effect shader validation message.", callbackException); + } + + if (scopeStatus != WGPUPopErrorScopeStatus.Success) + { + if (shaderModule is not null) + { + this.Api.ShaderModuleRelease(shaderModule); + } + + shaderModule = null; + error = $"WebGPU returned '{scopeStatus}' while validating the layer-effect shader module."; + return false; + } + + if (errorType != WGPUErrorType.NoError) + { + if (shaderModule is not null) + { + this.Api.ShaderModuleRelease(shaderModule); + } + + shaderModule = null; + WebGPUShaderDiagnostic[] diagnostics = + [ + new WebGPUShaderDiagnostic( + WebGPUShaderDiagnosticSeverity.Error, + diagnosticMessage ?? $"WebGPU shader validation failed with '{errorType}'.", + 0, + 0) + ]; + + throw new WebGPUShaderCompilationException(CreateCompilationErrorMessage(diagnostics), diagnostics); + } + + if (shaderModule is null) + { + error = "Failed to create the WebGPU layer-effect shader module."; + return false; + } + + error = null; + return true; + } + + /// + /// Formats compiler errors while retaining the structured diagnostics on the exception. + /// + private static string CreateCompilationErrorMessage(ReadOnlySpan diagnostics) + { + StringBuilder builder = new("WebGPU layer-effect shader compilation failed."); + for (int i = 0; i < diagnostics.Length; i++) + { + WebGPUShaderDiagnostic diagnostic = diagnostics[i]; + builder.AppendLine(); + builder.Append(diagnostic.Severity); + if (diagnostic.Line > 0) + { + builder.Append(" at line ").Append(diagnostic.Line); + if (diagnostic.Column > 0) + { + builder.Append(", column ").Append(diagnostic.Column); + } + } + + builder.Append(": ").Append(diagnostic.Message); + } + + return builder.ToString(); + } + + /// + /// Releases every device-owned object retained for one exact effect pipeline. + /// + private void ReleaseEffectPipelineInfrastructure(WebGPUEffectPipelineInfrastructure infrastructure) + { + if (infrastructure.Pipeline is not null) + { + this.Api.RenderPipelineRelease(infrastructure.Pipeline); + infrastructure.Pipeline = null; + } + + if (infrastructure.PipelineLayout is not null) + { + this.Api.PipelineLayoutRelease(infrastructure.PipelineLayout); + infrastructure.PipelineLayout = null; + } + + if (infrastructure.ShaderModule is not null) + { + this.Api.ShaderModuleRelease(infrastructure.ShaderModule); + infrastructure.ShaderModule = null; + } + + if (infrastructure.BindGroupLayout is not null) + { + this.Api.BindGroupLayoutRelease(infrastructure.BindGroupLayout); + infrastructure.BindGroupLayout = null; + } + } + + /// + /// Device-owned objects behind one exact effect pipeline. The instance is also its creation lock. + /// + private sealed class WebGPUEffectPipelineInfrastructure + { + /// + /// Gets or sets the shader bind-group layout. + /// + public WGPUBindGroupLayoutImpl* BindGroupLayout { get; set; } + + /// + /// Gets or sets the render pipeline layout. + /// + public WGPUPipelineLayoutImpl* PipelineLayout { get; set; } + + /// + /// Gets or sets the compiled shader module. + /// + public WGPUShaderModuleImpl* ShaderModule { get; set; } + + /// + /// Gets or sets the render pipeline. + /// + public WGPURenderPipelineImpl* Pipeline { get; set; } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs index 6b9b47af5..6b4e37a75 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.DeviceSharedState.cs @@ -2,14 +2,22 @@ // Licensed under the Six Labors Split License. using System.Collections.Concurrent; -using System.Runtime.InteropServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal static unsafe partial class WebGPURuntime { + /// + /// Maps raw native device pointers to their shared state. Keyed by the raw pointer so + /// distinct wrapper handles over the same native device resolve to one shared state. + /// private static readonly ConcurrentDictionary DeviceStateCache = new(); + + /// + /// Serializes creation and teardown of cache entries so a state cannot be created + /// concurrently with disposing the cache. + /// private static readonly object DeviceStateCacheSync = new(); /// @@ -18,7 +26,7 @@ internal static unsafe partial class WebGPURuntime /// The WebGPU API facade used to manage native resources. /// The device key and owner for the shared state. /// The shared device state instance for . - internal static DeviceSharedState GetOrCreateDeviceState(WebGPU api, WebGPUDeviceHandle deviceHandle) + public static DeviceSharedState GetOrCreateDeviceState(WebGPU api, WebGPUDeviceHandle deviceHandle) { nint cacheKey = deviceHandle.DangerousGetHandle(); @@ -31,12 +39,20 @@ internal static DeviceSharedState GetOrCreateDeviceState(WebGPU api, WebGPUDevic DeviceSharedState created = new(api, deviceHandle); DeviceStateCache[cacheKey] = created; + + // First-ever compilation of the staged pipeline set costs multi-second driver work + // on a cold driver shader cache. Warming in the background at device creation moves + // that cost off the first flush; the pipeline caches are thread-safe, so an early + // flush simply blocks on the specific pipelines it needs. + WebGPUSceneDispatch.BeginPipelineWarmup(created); return created; } } /// - /// Disposes all cached device-scoped shared state. + /// Disposes all cached device-scoped shared state. Called from process-exit teardown before + /// the runtime-owned device handles are disposed, because each state holds a reference on + /// its device handle. /// private static void ClearDeviceStateCache() { @@ -51,35 +67,212 @@ private static void ClearDeviceStateCache() } } + /// + /// Disposes and removes the cached shared state for a single device. Called when a device is + /// being torn down, such as after device-loss recovery replaces it, so the device's cached + /// pipelines are released and the reference the state holds on the device handle is dropped + /// now, instead of lingering in the pointer-keyed cache until process exit. + /// + /// + /// The state must be disposed while its device is still live, because disposal releases + /// pipelines and unregisters the error callback through the device. The caller therefore + /// invokes this before disposing the device handle. No-op when the device was never flushed + /// and so never created a cache entry. + /// + /// + /// The raw device pointer that keyed the entry, as returned by the device handle's + /// . + /// + public static void RemoveDeviceState(nint deviceKey) + { + lock (DeviceStateCacheSync) + { + if (DeviceStateCache.TryRemove(deviceKey, out DeviceSharedState? state)) + { + state.Dispose(); + } + } + } + + /// + /// Marks the cached state for a native device as lost. + /// + /// The raw device pointer reported by the native callback. + public static void MarkDeviceLost(nint deviceKey) + { + if (DeviceStateCache.TryGetValue(deviceKey, out DeviceSharedState? state)) + { + state.MarkLost(); + } + } + /// /// Shared device-scoped caches for pipelines and pipeline layouts. /// - internal sealed class DeviceSharedState : IDisposable + internal sealed partial class DeviceSharedState : IDisposable { + /// + /// Fallback storage-buffer binding ceiling (WebGPU's guaranteed minimum of 128 MiB) + /// used when the device limit cannot be queried. + /// private const nuint DefaultMaxStorageBufferBindingSize = 128U * 1024U * 1024U; + + /// + /// Cached graphics-pipeline families keyed by pipeline key. Creation within one family + /// is serialized by locking the family instance itself. + /// private readonly ConcurrentDictionary compositePipelines = new(StringComparer.Ordinal); + + /// + /// Cached compute-pipeline families keyed by pipeline key. Creation within one family + /// is serialized by locking the family instance itself. + /// private readonly ConcurrentDictionary compositeComputePipelines = new(StringComparer.Ordinal); - private readonly HashSet deviceFeatures; + + /// + /// Cached layer-effect pipelines keyed by their exact generated source and binding contract. + /// + private readonly ConcurrentDictionary effectPipelines = new(); + + /// + /// Pool of map-readable status buffers reused by deferred overflow readbacks. One buffer + /// is needed per deferred flush; creating it fresh each time is a measurable per-frame + /// driver cost, so retired buffers are recycled here. + /// + private readonly Stack statusReadbackBuffers = new(); + + /// + /// Guards ; flushes on different threads can rent + /// and return concurrently. + /// + private readonly object statusReadbackSync = new(); + + /// + /// Pool of full-size transient textures (ordered ping-pong targets, presentation backdrop + /// copies, and layer-effect working textures) reused across flushes. Creating and lazily + /// zero-initializing these multi-megabyte textures every frame is a measurable per-frame + /// driver and GPU cost, so retired textures are recycled here with their views. + /// + private readonly List pooledTextures = new(); + + /// + /// Guards ; flushes on different threads can rent and return + /// concurrently. + /// + private readonly object pooledTextureSync = new(); + + /// + /// Pool of layer-effect uniform buffers reused across flushes. + /// + private readonly Stack effectUniformBuffers = new(); + + /// + /// Guards . + /// + private readonly object effectUniformSync = new(); + + /// + /// Immutable single-pixel fallback textures keyed by format and pixel bits. Scenes bind + /// these when they contain no gradients or images; their content never changes, so they + /// live for the device lifetime instead of being recreated per staged range. + /// + private readonly Dictionary singlePixelTextures = new(); + + /// + /// Guards . + /// + private readonly object singlePixelSync = new(); + + /// + /// Signals that the one-time background pipeline warm-up no longer uses this state. + /// + private readonly ManualResetEventSlim pipelineWarmupCompleted = new(false); + + /// + /// Upper bound on pooled status readback buffers; returns beyond it release instead. + /// + private const int MaxPooledStatusReadbackBuffers = 16; + + /// + /// Upper bound on pooled transient textures; returns beyond it release instead. The bound + /// caps retained GPU memory when a scene stops using ordered targets or effects. + /// + private const int MaxPooledTextures = 12; + + /// + /// Upper bound on pooled layer-effect uniform buffers. + /// + private const int MaxPooledEffectUniformBuffers = 4; + + /// + /// Snapshot of the device features taken at construction time. + /// + private readonly HashSet deviceFeatures; + + /// + /// Holds one reference on the device handle for the lifetime of this state so the + /// cached pipelines can never outlive the device pointer they were created from. + /// private WebGPUHandle.HandleReference deviceReference; - private readonly PfnErrorCallback uncapturedErrorCallback; + + /// + /// Tracks whether has run. + /// private bool disposed; - internal DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle) + /// + /// Set atomically by the native device-lost callback, which may run on any runtime thread. + /// + private int isLost; + + /// + /// Initializes a new instance of the class, acquiring a + /// device reference and snapshotting the device features and limits. + /// + /// The WebGPU API facade used to manage native resources. + /// The device this state is scoped to. + public DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle) { this.Api = api; this.deviceReference = deviceHandle.AcquireReference(); try { - this.uncapturedErrorCallback = PfnErrorCallback.From(HandleUncapturedError); - this.Device = (Device*)this.deviceReference.Handle; - this.Api.DeviceSetUncapturedErrorCallback(this.Device, this.uncapturedErrorCallback, null); + this.Device = (WGPUDeviceImpl*)this.deviceReference.Handle; this.deviceFeatures = EnumerateDeviceFeatures(api, this.Device); this.MaxStorageBufferBindingSize = QueryMaxStorageBufferBindingSize(api, this.Device); + + // Every effect module exposes the same filtered layer-sampling binding. Keep one + // immutable sampler per device so effects do not allocate and release identical + // native sampler objects for each pass or flush. + WGPUSamplerDescriptor effectFilteringSamplerDescriptor = new() + { + addressModeU = WGPUAddressMode.ClampToEdge, + addressModeV = WGPUAddressMode.ClampToEdge, + addressModeW = WGPUAddressMode.ClampToEdge, + magFilter = WGPUFilterMode.Linear, + minFilter = WGPUFilterMode.Linear, + mipmapFilter = WGPUMipmapFilterMode.Nearest, + lodMinClamp = 0, + lodMaxClamp = 0, + maxAnisotropy = 1 + }; + + this.EffectFilteringSampler = api.DeviceCreateSampler(this.Device, in effectFilteringSamplerDescriptor); + if (this.EffectFilteringSampler is null) + { + throw new InvalidOperationException("Failed to create the WebGPU layer-effect filtering sampler."); + } } catch { - this.uncapturedErrorCallback.Dispose(); + // Construction failed part-way; release the device reference so the handle + // refcount is not left permanently elevated. + if (this.EffectFilteringSampler is not null) + { + api.SamplerRelease(this.EffectFilteringSampler); + } + this.deviceReference.Dispose(); throw; } @@ -95,89 +288,464 @@ internal DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle) /// private static ReadOnlySpan CompositeFragmentEntryPoint => "fs_main\0"u8; - /// - /// Gets the synchronization object used for shared state mutation. - /// - public object SyncRoot { get; } = new(); - /// /// Gets the WebGPU API instance used by this shared state. /// public WebGPU Api { get; } /// - /// Gets the device associated with this shared state. + /// Gets the device associated with this shared state. The pointer stays valid for the + /// lifetime of this instance because pins the handle open. /// - public Device* Device { get; } + public WGPUDeviceImpl* Device { get; } /// - /// Gets the maximum size, in bytes, that this device can bind as one storage buffer. + /// Gets the maximum size, in bytes, of one usable storage scratch buffer on this device: the + /// smaller of the queried maxStorageBufferBindingSize and maxBufferSize. /// /// - /// The staged scene uses this queried device limit instead of WebGPU's guaranteed minimum so - /// large scenes are only rejected when the active device really cannot bind the requested buffer. + /// A scratch buffer must be both creatable (within maxBufferSize) and bindable in full (within + /// maxStorageBufferBindingSize), so the effective ceiling that drives the chunking decision is the + /// minimum of both. Using the (often larger) binding size alone lets a scene skip chunking and then + /// fail buffer creation, surfacing as an invalid-buffer validation error and a blank frame. /// public nuint MaxStorageBufferBindingSize { get; } + /// + /// Gets the linear filtering sampler shared by layer-effect shader passes on this device. + /// + public WGPUSamplerImpl* EffectFilteringSampler { get; private set; } + + /// + /// Gets a value indicating whether the native runtime has reported this device as lost. + /// + public bool IsLost => Volatile.Read(ref this.isLost) != 0; + /// /// Returns whether the device has the specified feature. /// /// The feature to check. /// when the device has the feature; otherwise . - public bool HasFeature(FeatureName feature) + public bool HasFeature(WGPUFeatureName feature) => this.deviceFeatures.Contains(feature); /// - /// Forwards uncaptured native WebGPU errors through the public environment callback. + /// Marks the native device as lost. /// - private static void HandleUncapturedError(ErrorType errorType, byte* message, void* userData) + public void MarkLost() + => Volatile.Write(ref this.isLost, 1); + + /// + /// Signals that background pipeline warm-up has stopped using this state. + /// + public void CompletePipelineWarmup() + => this.pipelineWarmupCompleted.Set(); + + /// + /// Rents a pooled map-readable status buffer, or creates one when the pool has no buffer + /// large enough for the requested status data. + /// + /// The required scheduling-status byte length. + /// The rented buffer, or when creation failed. + public WGPUBufferImpl* RentStatusReadbackBuffer(nuint byteLength) { - _ = userData; + lock (this.statusReadbackSync) + { + Span retained = stackalloc StatusReadbackBufferEntry[MaxPooledStatusReadbackBuffers]; + int retainedIndex = 0; + + while (this.statusReadbackBuffers.Count > 0) + { + StatusReadbackBufferEntry entry = this.statusReadbackBuffers.Pop(); + if (entry.ByteLength >= byteLength) + { + while (retainedIndex > 0) + { + this.statusReadbackBuffers.Push(retained[--retainedIndex]); + } + + return (WGPUBufferImpl*)entry.Buffer; + } + + retained[retainedIndex++] = entry; + } + + while (retainedIndex > 0) + { + this.statusReadbackBuffers.Push(retained[--retainedIndex]); + } + } + + WGPUBufferDescriptor descriptor = new() + { + usage = (ulong)(BufferUsage.CopyDst | BufferUsage.MapRead), + size = byteLength, + mappedAtCreation = 0U, + }; + + return this.Api.DeviceCreateBuffer(this.Device, in descriptor); + } + + /// + /// Returns a status readback buffer to the pool. The buffer must be unmapped with no + /// map pending; a buffer whose map state is unknown must be released, not returned. + /// + /// The buffer to recycle. + /// The byte capacity of . + public void ReturnStatusReadbackBuffer(WGPUBufferImpl* buffer, nuint byteLength) + { + if (buffer is null) + { + return; + } + + lock (this.statusReadbackSync) + { + if (!this.disposed && this.statusReadbackBuffers.Count < MaxPooledStatusReadbackBuffers) + { + this.statusReadbackBuffers.Push(new StatusReadbackBufferEntry((nint)buffer, byteLength)); + return; + } + } + + this.Api.BufferRelease(buffer); + } + + /// + /// Rents a pooled transient texture whose format and usage match exactly and whose + /// dimensions are at least the requested size. + /// + /// + /// Oversized reuse is safe for every transient-texture consumer: fine shading, layer + /// effects, and presentation blits address these textures in texel space or normalize by + /// the queried textureDimensions, never by an assumed logical size. + /// + /// The required texture format. + /// The exact usage bits the texture must have been created with. + /// The minimum width in texels. + /// The minimum height in texels. + /// Receives the rented texture on success. + /// Receives the rented texture's full view on success. + /// Receives the rented texture's created width. + /// Receives the rented texture's created height. + /// when a pooled texture satisfied the request. + public bool TryRentPooledTexture( + WGPUTextureFormat format, + ulong usage, + uint width, + uint height, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, + out uint entryWidth, + out uint entryHeight) + { + lock (this.pooledTextureSync) + { + for (int i = 0; i < this.pooledTextures.Count; i++) + { + PooledTextureEntry entry = this.pooledTextures[i]; + if (entry.Format == format && entry.Usage == usage && entry.Width >= width && entry.Height >= height) + { + this.pooledTextures.RemoveAt(i); + texture = (WGPUTextureImpl*)entry.Texture; + textureView = (WGPUTextureViewImpl*)entry.View; + entryWidth = entry.Width; + entryHeight = entry.Height; + return true; + } + } + } + + texture = null; + textureView = null; + entryWidth = 0; + entryHeight = 0; + return false; + } + + /// + /// Returns a transient texture and its view to the pool, or releases both when the pool + /// is full or this state has been disposed. + /// + /// + /// Returning while previously submitted commands still reference the texture is safe: + /// reuse happens through later submissions on the same queue, which execute after the + /// earlier commands retire, and native command buffers hold their own references. + /// + /// The texture to recycle. + /// The full view created with . + /// The texture format it was created with. + /// The exact usage bits it was created with. + /// The created width in texels. + /// The created height in texels. + public void ReturnPooledTexture( + WGPUTextureImpl* texture, + WGPUTextureViewImpl* textureView, + WGPUTextureFormat format, + ulong usage, + uint width, + uint height) + { + if (texture is null) + { + return; + } - string errorMessage = message is null - ? string.Empty - : Marshal.PtrToStringUTF8((nint)message) ?? string.Empty; + // Entries strictly dominated by the returning texture can never be preferred over it, + // so releasing them here keeps superseded sizes from accumulating after a resize. + // Equal-size entries are kept: ping-pong targets rent two identical textures at once. + Span released = stackalloc nint[2 * MaxPooledTextures]; + int releasedCount = 0; - WebGPUEnvironment.ReportUncapturedError(ToPublicErrorType(errorType), errorMessage); + lock (this.pooledTextureSync) + { + if (!this.disposed) + { + for (int i = this.pooledTextures.Count - 1; i >= 0; i--) + { + PooledTextureEntry entry = this.pooledTextures[i]; + if (entry.Format == format && + entry.Usage == usage && + entry.Width <= width && + entry.Height <= height && + (entry.Width < width || entry.Height < height)) + { + released[releasedCount++] = entry.View; + released[releasedCount++] = entry.Texture; + this.pooledTextures.RemoveAt(i); + } + } + } + + if (!this.disposed && this.pooledTextures.Count < MaxPooledTextures) + { + this.pooledTextures.Add(new PooledTextureEntry((nint)texture, (nint)textureView, format, usage, width, height)); + texture = null; + } + } + + // Native releases happen outside the lock; the entries are already unreachable. + for (int i = 0; i < releasedCount; i += 2) + { + if (released[i] != 0) + { + this.Api.TextureViewRelease((WGPUTextureViewImpl*)released[i]); + } + + this.Api.TextureRelease((WGPUTextureImpl*)released[i + 1]); + } + + if (texture is not null) + { + if (textureView is not null) + { + this.Api.TextureViewRelease(textureView); + } + + this.Api.TextureRelease(texture); + } } /// - /// Maps Silk's native error enum to the public API enum without exposing Silk types. + /// Rents a pooled layer-effect uniform buffer, or creates one when the pool has no + /// buffer of at least the requested capacity. /// - private static WebGPUErrorType ToPublicErrorType(ErrorType errorType) - => errorType switch + /// The required uniform capacity in bytes. + /// The rented buffer, or when creation failed. + public WGPUBufferImpl* RentEffectUniformBuffer(nuint byteLength) + { + lock (this.effectUniformSync) + { + Span retained = stackalloc StatusReadbackBufferEntry[MaxPooledEffectUniformBuffers]; + int retainedIndex = 0; + + while (this.effectUniformBuffers.Count > 0) + { + StatusReadbackBufferEntry entry = this.effectUniformBuffers.Pop(); + if (entry.ByteLength >= byteLength) + { + while (retainedIndex > 0) + { + this.effectUniformBuffers.Push(retained[--retainedIndex]); + } + + return (WGPUBufferImpl*)entry.Buffer; + } + + retained[retainedIndex++] = entry; + } + + while (retainedIndex > 0) + { + this.effectUniformBuffers.Push(retained[--retainedIndex]); + } + } + + WGPUBufferDescriptor descriptor = new() { - ErrorType.NoError => WebGPUErrorType.NoError, - ErrorType.Validation => WebGPUErrorType.Validation, - ErrorType.OutOfMemory => WebGPUErrorType.OutOfMemory, - ErrorType.Internal => WebGPUErrorType.Internal, - ErrorType.DeviceLost => WebGPUErrorType.DeviceLost, - _ => WebGPUErrorType.Unknown + usage = (ulong)(BufferUsage.Uniform | BufferUsage.CopyDst), + size = byteLength }; + return this.Api.DeviceCreateBuffer(this.Device, in descriptor); + } + + /// + /// Returns a layer-effect uniform buffer to the pool, or releases it when the pool is + /// full or this state has been disposed. + /// + /// The buffer to recycle. + /// The byte capacity of . + public void ReturnEffectUniformBuffer(WGPUBufferImpl* buffer, nuint byteLength) + { + if (buffer is null) + { + return; + } + + lock (this.effectUniformSync) + { + if (!this.disposed && this.effectUniformBuffers.Count < MaxPooledEffectUniformBuffers) + { + this.effectUniformBuffers.Push(new StatusReadbackBufferEntry((nint)buffer, byteLength)); + return; + } + } + + this.Api.BufferRelease(buffer); + } + + /// + /// Gets or creates the immutable single-pixel fallback texture for the supplied format + /// and raw pixel bits. The returned view is owned by this state for the device lifetime + /// and must not be released or flush-tracked by callers. + /// + /// The queue used to upload the pixel on first creation. + /// The texture format. + /// The raw little-endian pixel bytes, at most 16. + /// Receives the cached texture view on success. + /// when the fallback texture is available. + public bool TryGetOrCreateSinglePixelTexture( + WGPUQueueImpl* queue, + WGPUTextureFormat format, + ReadOnlySpan pixelBits, + out WGPUTextureViewImpl* textureView) + { + Span keyBits = stackalloc byte[16]; + keyBits.Clear(); + pixelBits.CopyTo(keyBits); + SinglePixelTextureKey key = new( + format, + BitConverter.ToUInt64(keyBits), + BitConverter.ToUInt64(keyBits[8..]), + pixelBits.Length); + + lock (this.singlePixelSync) + { + ObjectDisposedException.ThrowIf(this.disposed, this); + + if (this.singlePixelTextures.TryGetValue(key, out SinglePixelTextureEntry entry)) + { + textureView = (WGPUTextureViewImpl*)entry.View; + return true; + } + + WGPUTextureDescriptor textureDescriptor = new() + { + usage = (ulong)(TextureUsage.TextureBinding | TextureUsage.CopyDst), + dimension = WGPUTextureDimension._2D, + size = new WGPUExtent3D(1, 1, 1), + format = format, + mipLevelCount = 1, + sampleCount = 1 + }; + + WGPUTextureImpl* texture = this.Api.DeviceCreateTexture(this.Device, in textureDescriptor); + if (texture is null) + { + textureView = null; + return false; + } + + WGPUTextureViewDescriptor viewDescriptor = new() + { + format = format, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All + }; + + WGPUTextureViewImpl* view = this.Api.TextureCreateView(texture, &viewDescriptor); + if (view is null) + { + this.Api.TextureRelease(texture); + textureView = null; + return false; + } + + WGPUTexelCopyTextureInfo destination = new() + { + texture = texture, + mipLevel = 0, + origin = new WGPUOrigin3D(0, 0, 0), + aspect = WGPUTextureAspect.All + }; + + WGPUTexelCopyBufferLayout layout = new() + { + offset = 0, + bytesPerRow = (uint)pixelBits.Length, + rowsPerImage = 1 + }; + + WGPUExtent3D extent = new(1, 1, 1); + fixed (byte* pixelPtr = pixelBits) + { + this.Api.QueueWriteTexture(queue, in destination, pixelPtr, (nuint)pixelBits.Length, in layout, in extent); + } + + this.singlePixelTextures[key] = new SinglePixelTextureEntry((nint)texture, (nint)view); + textureView = view; + return true; + } + } + /// /// Snapshots the feature set currently reported by the native device. /// - private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* device) + /// The WebGPU API facade. + /// The device to query. + /// The set of features the device reports; empty when the device is or reports none. + private static HashSet EnumerateDeviceFeatures(WebGPU api, WGPUDeviceImpl* device) { if (device is null) { return []; } - int count = (int)api.DeviceEnumerateFeatures(device, (FeatureName*)null); - if (count <= 0) + WGPUSupportedFeatures supportedFeatures = default; + api.DeviceGetFeatures(device, &supportedFeatures); + if (supportedFeatures.featureCount == 0) { return []; } - FeatureName* features = stackalloc FeatureName[count]; - api.DeviceEnumerateFeatures(device, features); - - HashSet result = new(count); - for (int i = 0; i < count; i++) + HashSet result = new((int)supportedFeatures.featureCount); + try + { + for (nuint i = 0; i < supportedFeatures.featureCount; i++) + { + result.Add(supportedFeatures.features[i]); + } + } + finally { - result.Add(features[i]); + // The feature array belongs to the native query result and must be returned through + // the matching free-members function after the managed snapshot has been copied. + api.SupportedFeaturesFreeMembers(supportedFeatures); } return result; @@ -186,21 +754,32 @@ private static HashSet EnumerateDeviceFeatures(WebGPU api, Device* /// /// Queries the device's storage-buffer binding ceiling, falling back to WebGPU's guaranteed minimum when unavailable. /// - private static nuint QueryMaxStorageBufferBindingSize(WebGPU api, Device* device) + /// The WebGPU API facade. + /// The device to query. + /// The maximum storage-buffer binding size in bytes. + private static nuint QueryMaxStorageBufferBindingSize(WebGPU api, WGPUDeviceImpl* device) { if (device is null) { return DefaultMaxStorageBufferBindingSize; } - SupportedLimits supportedLimits = default; - if (!api.DeviceGetLimits(device, ref supportedLimits)) + WGPULimits supportedLimits = default; + if (api.DeviceGetLimits(device, &supportedLimits) != WGPUStatus.Success) { return DefaultMaxStorageBufferBindingSize; } - ulong reported = supportedLimits.Limits.MaxStorageBufferBindingSize; - if (reported == 0 || reported > nuint.MaxValue) + // A scratch buffer must be both creatable (<= maxBufferSize) and bindable in full + // (<= maxStorageBufferBindingSize). maxBufferSize is frequently the smaller of the two + // (256 MiB by default versus a multi-gigabyte binding size), so the effective per-buffer + // ceiling that the chunking decision must respect is the minimum of both. Using the binding + // size alone lets a large scene skip chunking and then fail buffer creation, which surfaces + // as an invalid-buffer validation error and a blank frame. + ulong binding = supportedLimits.maxStorageBufferBindingSize; + ulong bufferSize = supportedLimits.maxBufferSize; + ulong reported = Math.Min(binding == 0 ? ulong.MaxValue : binding, bufferSize == 0 ? ulong.MaxValue : bufferSize); + if (reported == 0 || reported == ulong.MaxValue || reported > nuint.MaxValue) { return DefaultMaxStorageBufferBindingSize; } @@ -209,16 +788,27 @@ private static nuint QueryMaxStorageBufferBindingSize(WebGPU api, Device* device } /// - /// Gets or creates a graphics pipeline used for composite rendering. + /// Gets or creates a graphics pipeline used for composite rendering. Pipeline variants + /// are cached per (texture format, blend mode) within the family identified by + /// . /// + /// The stable key identifying the pipeline family. + /// Null-terminated WGSL source, used only when the family's shader module has not been created yet. + /// Creates the family's bind-group layout on first use. + /// The color-target format for the requested variant. + /// The blend mode for the requested variant. + /// Receives the family's shared bind-group layout on success. + /// Receives the cached or newly created render pipeline on success. + /// Receives the failure description when creation fails. + /// when the pipeline is available; otherwise . public bool TryGetOrCreateCompositePipeline( string pipelineKey, ReadOnlySpan shaderCode, WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, - TextureFormat textureFormat, + WGPUTextureFormat textureFormat, CompositePipelineBlendMode blendMode, - out BindGroupLayout* bindGroupLayout, - out RenderPipeline* pipeline, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPURenderPipelineImpl* pipeline, out string? error) { bindGroupLayout = null; @@ -239,9 +829,9 @@ infrastructure.PipelineLayout is null || if (!this.TryCreateCompositeInfrastructure( shaderCode, bindGroupLayoutFactory, - out BindGroupLayout* createdBindGroupLayout, - out PipelineLayout* createdPipelineLayout, - out ShaderModule* createdShaderModule, + out WGPUBindGroupLayoutImpl* createdBindGroupLayout, + out WGPUPipelineLayoutImpl* createdPipelineLayout, + out WGPUShaderModuleImpl* createdShaderModule, out error)) { return false; @@ -253,15 +843,15 @@ infrastructure.PipelineLayout is null || } bindGroupLayout = infrastructure.BindGroupLayout; - (TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode) variantKey = (textureFormat, blendMode); + (WGPUTextureFormat TextureFormat, CompositePipelineBlendMode BlendMode) variantKey = (textureFormat, blendMode); if (infrastructure.Pipelines.TryGetValue(variantKey, out nint cachedPipelineHandle) && cachedPipelineHandle != 0) { - pipeline = (RenderPipeline*)cachedPipelineHandle; + pipeline = (WGPURenderPipelineImpl*)cachedPipelineHandle; error = null; return true; } - RenderPipeline* createdPipeline = this.CreateCompositePipeline( + WGPURenderPipelineImpl* createdPipeline = this.CreateCompositePipeline( infrastructure.PipelineLayout, infrastructure.ShaderModule, textureFormat, @@ -280,15 +870,26 @@ infrastructure.PipelineLayout is null || } /// - /// Gets or creates a compute pipeline used for composite execution. + /// Gets or creates a compute pipeline used for composite execution. One pipeline is + /// cached per family, so and + /// are only consumed on first creation; later calls must pass the same values for the + /// same . /// + /// The stable key identifying the pipeline family. + /// Null-terminated WGSL source, used only when the family's shader module has not been created yet. + /// Null-terminated compute entry-point name, used only on first creation. + /// Creates the family's bind-group layout on first use. + /// Receives the family's shared bind-group layout on success. + /// Receives the cached or newly created compute pipeline on success. + /// Receives the failure description when creation fails. + /// when the pipeline is available; otherwise . public bool TryGetOrCreateCompositeComputePipeline( string pipelineKey, ReadOnlySpan shaderCode, ReadOnlySpan entryPoint, WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUComputePipelineImpl* pipeline, out string? error) { bindGroupLayout = null; @@ -309,9 +910,9 @@ infrastructure.PipelineLayout is null || if (!this.TryCreateCompositeInfrastructure( shaderCode, bindGroupLayoutFactory, - out BindGroupLayout* createdBindGroupLayout, - out PipelineLayout* createdPipelineLayout, - out ShaderModule* createdShaderModule, + out WGPUBindGroupLayoutImpl* createdBindGroupLayout, + out WGPUPipelineLayoutImpl* createdPipelineLayout, + out WGPUShaderModuleImpl* createdShaderModule, out error)) { return false; @@ -330,7 +931,7 @@ infrastructure.PipelineLayout is null || return true; } - ComputePipeline* createdPipeline = this.CreateCompositeComputePipeline( + WGPUComputePipelineImpl* createdPipeline = this.CreateCompositeComputePipeline( infrastructure.PipelineLayout, infrastructure.ShaderModule, entryPoint); @@ -348,7 +949,9 @@ infrastructure.PipelineLayout is null || } /// - /// Releases all cached pipelines owned by this state. + /// Releases all cached pipelines owned by this state, unregisters the uncaptured-error + /// callback, and drops the device reference. Invoked under the runtime's cache lock via + /// ; not safe to run concurrently with pipeline creation. /// public void Dispose() { @@ -357,6 +960,15 @@ public void Dispose() return; } + // Warm-up creates entries through the same pipeline caches as a foreground flush. + // Wait before enumerating and releasing them so teardown never races their creation. + this.pipelineWarmupCompleted.Wait(); + + // Mark disposed before draining the pools: every Return* method checks the flag + // under its pool lock and releases instead of pooling, so a return racing this + // teardown cannot re-add an entry to an already-drained pool and leak it. + this.disposed = true; + foreach (CompositePipelineInfrastructure infrastructure in this.compositePipelines.Values) { this.ReleaseCompositeInfrastructure(infrastructure); @@ -371,23 +983,85 @@ public void Dispose() this.compositeComputePipelines.Clear(); - // Clear the native callback slot before freeing Silk's delegate thunk. - this.Api.DeviceSetUncapturedErrorCallback(this.Device, default, null); - this.uncapturedErrorCallback.Dispose(); + foreach (WebGPUEffectPipelineInfrastructure infrastructure in this.effectPipelines.Values) + { + this.ReleaseEffectPipelineInfrastructure(infrastructure); + } + + this.effectPipelines.Clear(); + + this.Api.SamplerRelease(this.EffectFilteringSampler); + this.EffectFilteringSampler = null; + lock (this.statusReadbackSync) + { + while (this.statusReadbackBuffers.Count > 0) + { + this.Api.BufferRelease((WGPUBufferImpl*)this.statusReadbackBuffers.Pop().Buffer); + } + } + + lock (this.pooledTextureSync) + { + for (int i = 0; i < this.pooledTextures.Count; i++) + { + PooledTextureEntry entry = this.pooledTextures[i]; + if (entry.View != 0) + { + this.Api.TextureViewRelease((WGPUTextureViewImpl*)entry.View); + } + + this.Api.TextureRelease((WGPUTextureImpl*)entry.Texture); + } + + this.pooledTextures.Clear(); + } + + lock (this.effectUniformSync) + { + while (this.effectUniformBuffers.Count > 0) + { + this.Api.BufferRelease((WGPUBufferImpl*)this.effectUniformBuffers.Pop().Buffer); + } + } + + lock (this.singlePixelSync) + { + foreach (SinglePixelTextureEntry entry in this.singlePixelTextures.Values) + { + if (entry.View != 0) + { + this.Api.TextureViewRelease((WGPUTextureViewImpl*)entry.View); + } + + this.Api.TextureRelease((WGPUTextureImpl*)entry.Texture); + } + + this.singlePixelTextures.Clear(); + } + + // Drop the device reference last: every release above calls into the device. this.deviceReference.Dispose(); - this.disposed = true; + this.pipelineWarmupCompleted.Dispose(); } /// /// Creates the shared bind-group layout, pipeline layout, and shader module for one cached pipeline family. + /// On failure every partially created object is released before returning. /// + /// Null-terminated WGSL source for the family's shader module. + /// Creates the family's bind-group layout. + /// Receives the created bind-group layout on success. + /// Receives the created pipeline layout on success. + /// Receives the created shader module on success. + /// Receives the failure description when creation fails. + /// when all three objects were created; otherwise . private bool TryCreateCompositeInfrastructure( ReadOnlySpan shaderCode, WebGPUCompositeBindGroupLayoutFactory bindGroupLayoutFactory, - out BindGroupLayout* bindGroupLayout, - out PipelineLayout* pipelineLayout, - out ShaderModule* shaderModule, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUPipelineLayoutImpl* pipelineLayout, + out WGPUShaderModuleImpl* shaderModule, out string? error) { bindGroupLayout = null; @@ -399,12 +1073,12 @@ private bool TryCreateCompositeInfrastructure( return false; } - BindGroupLayout** bindGroupLayouts = stackalloc BindGroupLayout*[1]; + WGPUBindGroupLayoutImpl** bindGroupLayouts = stackalloc WGPUBindGroupLayoutImpl*[1]; bindGroupLayouts[0] = bindGroupLayout; - PipelineLayoutDescriptor pipelineLayoutDescriptor = new() + WGPUPipelineLayoutDescriptor pipelineLayoutDescriptor = new() { - BindGroupLayoutCount = 1, - BindGroupLayouts = bindGroupLayouts + bindGroupLayoutCount = 1, + bindGroupLayouts = bindGroupLayouts }; pipelineLayout = this.Api.DeviceCreatePipelineLayout(this.Device, in pipelineLayoutDescriptor); @@ -432,10 +1106,15 @@ private bool TryCreateCompositeInfrastructure( /// /// Creates one graphics pipeline variant for the specified output format and blend mode. /// - private RenderPipeline* CreateCompositePipeline( - PipelineLayout* pipelineLayout, - ShaderModule* shaderModule, - TextureFormat textureFormat, + /// The family's shared pipeline layout. + /// The family's shared shader module. + /// The color-target format for the variant. + /// The blend mode for the variant. + /// The created render pipeline, or on failure. + private WGPURenderPipelineImpl* CreateCompositePipeline( + WGPUPipelineLayoutImpl* pipelineLayout, + WGPUShaderModuleImpl* shaderModule, + WGPUTextureFormat textureFormat, CompositePipelineBlendMode blendMode) { ReadOnlySpan vertexEntryPoint = CompositeVertexEntryPoint; @@ -458,58 +1137,65 @@ private bool TryCreateCompositeInfrastructure( /// /// Creates the underlying render pipeline once the shared shader module and entry points are fixed. /// - private RenderPipeline* CreateCompositePipelineCore( - PipelineLayout* pipelineLayout, - ShaderModule* shaderModule, + /// The family's shared pipeline layout. + /// The family's shared shader module. + /// Pinned pointer to the null-terminated vertex entry-point name. + /// Pinned pointer to the null-terminated fragment entry-point name. + /// The color-target format for the variant. + /// The blend mode for the variant. + /// The created render pipeline, or on failure. + private WGPURenderPipelineImpl* CreateCompositePipelineCore( + WGPUPipelineLayoutImpl* pipelineLayout, + WGPUShaderModuleImpl* shaderModule, byte* vertexEntryPointPtr, byte* fragmentEntryPointPtr, - TextureFormat textureFormat, + WGPUTextureFormat textureFormat, CompositePipelineBlendMode blendMode) { _ = blendMode; - VertexState vertexState = new() + WGPUVertexState vertexState = new() { - Module = shaderModule, - EntryPoint = vertexEntryPointPtr, - BufferCount = 0, - Buffers = null + module = shaderModule, + entryPoint = vertexEntryPointPtr, + bufferCount = 0, + buffers = null }; - ColorTargetState* colorTargets = stackalloc ColorTargetState[1]; - colorTargets[0] = new ColorTargetState + WGPUColorTargetState* colorTargets = stackalloc WGPUColorTargetState[1]; + colorTargets[0] = new WGPUColorTargetState { - Format = textureFormat, - Blend = null, - WriteMask = ColorWriteMask.All + format = textureFormat, + blend = null, + writeMask = (ulong)ColorWriteMask.All }; - FragmentState fragmentState = new() + WGPUFragmentState fragmentState = new() { - Module = shaderModule, - EntryPoint = fragmentEntryPointPtr, - TargetCount = 1, - Targets = colorTargets + module = shaderModule, + entryPoint = fragmentEntryPointPtr, + targetCount = 1, + targets = colorTargets }; - RenderPipelineDescriptor descriptor = new() + WGPURenderPipelineDescriptor descriptor = new() { - Layout = pipelineLayout, - Vertex = vertexState, - Primitive = new PrimitiveState + layout = pipelineLayout, + vertex = vertexState, + primitive = new WGPUPrimitiveState { - Topology = PrimitiveTopology.TriangleList, - StripIndexFormat = IndexFormat.Undefined, - FrontFace = FrontFace.Ccw, - CullMode = CullMode.None + topology = WGPUPrimitiveTopology.TriangleList, + stripIndexFormat = WGPUIndexFormat.Undefined, + frontFace = WGPUFrontFace.CCW, + cullMode = WGPUCullMode.None }, - DepthStencil = null, - Multisample = new MultisampleState + depthStencil = null, + multisample = new WGPUMultisampleState { - Count = 1, - Mask = uint.MaxValue, - AlphaToCoverageEnabled = false + count = 1, + mask = uint.MaxValue, + alphaToCoverageEnabled = 0U, }, - Fragment = &fragmentState + fragment = &fragmentState }; return this.Api.DeviceCreateRenderPipeline(this.Device, in descriptor); @@ -518,23 +1204,27 @@ private bool TryCreateCompositeInfrastructure( /// /// Creates the compute pipeline used by one cached composite compute shader. /// - private ComputePipeline* CreateCompositeComputePipeline( - PipelineLayout* pipelineLayout, - ShaderModule* shaderModule, + /// The family's shared pipeline layout. + /// The family's shared shader module. + /// Null-terminated compute entry-point name. + /// The created compute pipeline, or on failure. + private WGPUComputePipelineImpl* CreateCompositeComputePipeline( + WGPUPipelineLayoutImpl* pipelineLayout, + WGPUShaderModuleImpl* shaderModule, ReadOnlySpan entryPoint) { fixed (byte* entryPointPtr = entryPoint) { - ProgrammableStageDescriptor computeState = new() + WGPUComputeState computeState = new() { - Module = shaderModule, - EntryPoint = entryPointPtr + module = shaderModule, + entryPoint = entryPointPtr }; - ComputePipelineDescriptor descriptor = new() + WGPUComputePipelineDescriptor descriptor = new() { - Layout = pipelineLayout, - Compute = computeState + layout = pipelineLayout, + compute = computeState }; return this.Api.DeviceCreateComputePipeline(this.Device, in descriptor); @@ -544,19 +1234,21 @@ private bool TryCreateCompositeInfrastructure( /// /// Creates a shader module from null-terminated WGSL source bytes. /// - private ShaderModule* CreateShaderModule(ReadOnlySpan shaderCode) + /// Null-terminated WGSL source bytes. + /// The created shader module, or on failure. + private WGPUShaderModuleImpl* CreateShaderModule(ReadOnlySpan shaderCode) { fixed (byte* shaderCodePtr = shaderCode) { - ShaderModuleWGSLDescriptor wgslDescriptor = new() + WGPUShaderSourceWGSL wgslDescriptor = new() { - Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, - Code = shaderCodePtr + chain = new WGPUChainedStruct { sType = WGPUSType.ShaderSourceWGSL }, + code = shaderCodePtr }; - ShaderModuleDescriptor shaderDescriptor = new() + WGPUShaderModuleDescriptor shaderDescriptor = new() { - NextInChain = (ChainedStruct*)&wgslDescriptor + nextInChain = (WGPUChainedStruct*)&wgslDescriptor }; return this.Api.DeviceCreateShaderModule(this.Device, in shaderDescriptor); @@ -566,13 +1258,14 @@ private bool TryCreateCompositeInfrastructure( /// /// Releases one cached graphics-pipeline family and every render pipeline variant it owns. /// + /// The pipeline family to release. private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infrastructure) { foreach (nint pipelineHandle in infrastructure.Pipelines.Values) { if (pipelineHandle != 0) { - this.Api.RenderPipelineRelease((RenderPipeline*)pipelineHandle); + this.Api.RenderPipelineRelease((WGPURenderPipelineImpl*)pipelineHandle); } } @@ -600,6 +1293,7 @@ private void ReleaseCompositeInfrastructure(CompositePipelineInfrastructure infr /// /// Releases one cached compute-pipeline family and the shared resources behind it. /// + /// The pipeline family to release. private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfrastructure infrastructure) { if (infrastructure.Pipeline is not null) @@ -628,28 +1322,225 @@ private void ReleaseCompositeComputeInfrastructure(CompositeComputePipelineInfra } /// - /// Shared render-pipeline infrastructure for compositing variants. + /// Stores one pooled transient texture with the creation parameters that gate its reuse. /// - private sealed class CompositePipelineInfrastructure + private readonly struct PooledTextureEntry { - public Dictionary<(TextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; + /// + /// Initializes a new instance of the struct. + /// + /// The native texture pointer stored as an integer. + /// The native full-texture view pointer stored as an integer. + /// The texture format it was created with. + /// The exact usage bits it was created with. + /// The created width in texels. + /// The created height in texels. + public PooledTextureEntry(nint texture, nint view, WGPUTextureFormat format, ulong usage, uint width, uint height) + { + this.Texture = texture; + this.View = view; + this.Format = format; + this.Usage = usage; + this.Width = width; + this.Height = height; + } - public BindGroupLayout* BindGroupLayout { get; set; } + /// + /// Gets the native texture pointer stored as an integer. + /// + public nint Texture { get; } + + /// + /// Gets the native full-texture view pointer stored as an integer. + /// + public nint View { get; } + + /// + /// Gets the texture format the texture was created with. + /// + public WGPUTextureFormat Format { get; } + + /// + /// Gets the exact usage bits the texture was created with. + /// + public ulong Usage { get; } + + /// + /// Gets the created width in texels. + /// + public uint Width { get; } + + /// + /// Gets the created height in texels. + /// + public uint Height { get; } + } - public PipelineLayout* PipelineLayout { get; set; } + /// + /// Identifies one immutable single-pixel fallback texture by format and raw pixel bits. + /// + private readonly struct SinglePixelTextureKey : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The texture format. + /// The first eight raw pixel bytes. + /// The second eight raw pixel bytes, zero-padded. + /// The raw pixel byte length. + public SinglePixelTextureKey(WGPUTextureFormat format, ulong pixelLow, ulong pixelHigh, int pixelByteLength) + { + this.Format = format; + this.PixelLow = pixelLow; + this.PixelHigh = pixelHigh; + this.PixelByteLength = pixelByteLength; + } - public ShaderModule* ShaderModule { get; set; } + /// + /// Gets the texture format. + /// + public WGPUTextureFormat Format { get; } + + /// + /// Gets the first eight raw pixel bytes. + /// + public ulong PixelLow { get; } + + /// + /// Gets the second eight raw pixel bytes, zero-padded. + /// + public ulong PixelHigh { get; } + + /// + /// Gets the raw pixel byte length. + /// + public int PixelByteLength { get; } + + /// + public bool Equals(SinglePixelTextureKey other) + => this.Format == other.Format && + this.PixelLow == other.PixelLow && + this.PixelHigh == other.PixelHigh && + this.PixelByteLength == other.PixelByteLength; + + /// + public override bool Equals(object? obj) + => obj is SinglePixelTextureKey other && this.Equals(other); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Format, this.PixelLow, this.PixelHigh, this.PixelByteLength); } - private sealed class CompositeComputePipelineInfrastructure + /// + /// Stores one immutable single-pixel fallback texture and its view. + /// + private readonly struct SinglePixelTextureEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The native texture pointer stored as an integer. + /// The native view pointer stored as an integer. + public SinglePixelTextureEntry(nint texture, nint view) + { + this.Texture = texture; + this.View = view; + } + + /// + /// Gets the native texture pointer stored as an integer. + /// + public nint Texture { get; } + + /// + /// Gets the native view pointer stored as an integer. + /// + public nint View { get; } + } + + /// + /// Stores a pooled status-readback buffer with its byte capacity so larger chunked + /// readbacks never receive a smaller single-status buffer. + /// + private readonly struct StatusReadbackBufferEntry { - public BindGroupLayout* BindGroupLayout { get; set; } + /// + /// Initializes a new instance of the struct. + /// + /// The native buffer pointer stored as an integer. + /// The byte capacity of . + public StatusReadbackBufferEntry(nint buffer, nuint byteLength) + { + this.Buffer = buffer; + this.ByteLength = byteLength; + } + + /// + /// Gets the native buffer pointer stored as an integer. + /// + public nint Buffer { get; } - public PipelineLayout* PipelineLayout { get; set; } + /// + /// Gets the byte capacity of . + /// + public nuint ByteLength { get; } + } - public ShaderModule* ShaderModule { get; set; } + /// + /// Shared render-pipeline infrastructure for compositing variants. Instances double as + /// the per-family lock object that serializes lazy creation of their contents. + /// + private sealed class CompositePipelineInfrastructure + { + /// + /// Gets the cached pipeline variants keyed by (texture format, blend mode). + /// Values are stored as because pointer types cannot be + /// dictionary type arguments. + /// + public Dictionary<(WGPUTextureFormat TextureFormat, CompositePipelineBlendMode BlendMode), nint> Pipelines { get; } = []; + + /// + /// Gets or sets the bind-group layout shared by all variants in this family. + /// + public WGPUBindGroupLayoutImpl* BindGroupLayout { get; set; } + + /// + /// Gets or sets the pipeline layout shared by all variants in this family. + /// + public WGPUPipelineLayoutImpl* PipelineLayout { get; set; } + + /// + /// Gets or sets the shader module shared by all variants in this family. + /// + public WGPUShaderModuleImpl* ShaderModule { get; set; } + } - public ComputePipeline* Pipeline { get; set; } + /// + /// Shared compute-pipeline infrastructure for one cached compute shader. Instances + /// double as the per-family lock object that serializes lazy creation of their contents. + /// + private sealed class CompositeComputePipelineInfrastructure + { + /// + /// Gets or sets the bind-group layout for this family. + /// + public WGPUBindGroupLayoutImpl* BindGroupLayout { get; set; } + + /// + /// Gets or sets the pipeline layout for this family. + /// + public WGPUPipelineLayoutImpl* PipelineLayout { get; set; } + + /// + /// Gets or sets the shader module for this family. + /// + public WGPUShaderModuleImpl* ShaderModule { get; set; } + + /// + /// Gets or sets the single cached compute pipeline for this family. + /// + public WGPUComputePipelineImpl* Pipeline { get; set; } } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs index c4430d585..4bb2960b9 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs @@ -1,8 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; -using Silk.NET.WebGPU.Extensions.WGPU; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using FilePath = System.IO.Path; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -11,9 +15,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// /// -/// This type owns the process-level Silk API loader, its -/// shared extension, and a lazily provisioned default -/// device/queue pair used by the GPU backend when no native surface is available. +/// This type owns the process-level API facade and a lazily provisioned +/// default device/queue pair used by the GPU backend when no native surface is available. /// /// /// Backends use to access the shared WebGPU loader and @@ -25,10 +28,18 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal static unsafe partial class WebGPURuntime { + /// + /// Serializes probe execution and guards the cached probe results. + /// Probes hold this lock while provisioning runs, so a probe may acquire + /// while holding . The global lock + /// order is therefore first, then ; + /// every path that needs both must acquire them in that order. + /// private static readonly object ProbeSync = new(); /// - /// Synchronizes all runtime state transitions. + /// Synchronizes all runtime state transitions: API/extension loading, the cached + /// default device/queue pair, and process-exit teardown. /// private static readonly object Sync = new(); @@ -38,26 +49,38 @@ internal static unsafe partial class WebGPURuntime private static WebGPU? api; /// - /// Shared wgpu-native extension facade. + /// Lazily provisioned device handle for CPU-backed frames. Owned by the runtime and + /// disposed during process-exit teardown; callers must never dispose it. /// - private static Wgpu? wgpuExtension; + private static WebGPUDeviceHandle? autoDeviceHandle; /// - /// Lazily provisioned device handle for CPU-backed frames. + /// Lazily provisioned queue handle for CPU-backed frames. Owned by the runtime and + /// disposed during process-exit teardown; callers must never dispose it. /// - private static WebGPUDeviceHandle? autoDeviceHandle; + private static WebGPUQueueHandle? autoQueueHandle; /// - /// Lazily provisioned queue handle for CPU-backed frames. + /// Process-shared WebGPU instance used by every native surface. Owned by the runtime and + /// released during process-exit teardown; callers must never release it. /// - private static WebGPUQueueHandle? autoQueueHandle; + private static WGPUInstanceImpl* sharedInstance; /// - /// Tracks whether the process-exit hook has been installed. + /// Tracks whether the process-exit hook has been installed. Guarded by . /// private static bool processExitHooked; + /// + /// Cached result of ; until the first + /// probe runs. Guarded by . + /// private static WebGPUEnvironmentError? availabilityProbeResult; + + /// + /// Cached result of ; until + /// the first probe runs. Guarded by . + /// private static WebGPUEnvironmentError? computePipelineProbeResult; /// @@ -85,21 +108,44 @@ public static WebGPU GetApi() } /// - /// Gets the shared wgpu-native extension facade, initializing the runtime on first use. + /// Gets the process-shared WebGPU instance, creating it on first use. /// - /// The shared wgpu-native extension facade. - /// Thrown when the WebGPU API cannot be initialized. - public static Wgpu GetWgpuExtension() + /// + /// wgpu-native keeps a single global backend registry, so every native surface shares one + /// instance for the process lifetime. Creating and destroying an instance per surface (per + /// window) churns that global state and, under rapid window reopen, leaves a freshly created + /// surface invalid, which wgpu reports by aborting the process. The runtime owns the instance + /// and releases it during process-exit teardown; callers must never release it. + /// + /// The shared native instance pointer. + /// Thrown when the WebGPU API or instance cannot be initialized. + public static WGPUInstanceImpl* GetOrCreateSharedInstance() { lock (Sync) { EnsureInitialized(); - if (wgpuExtension is null) + if (api is null) { - throw new InvalidOperationException("WebGPU.TryGetDeviceExtension for Wgpu failed."); + throw new InvalidOperationException("WebGPU.GetApi returned null."); } - return wgpuExtension; + if (sharedInstance is null) + { + if (!TryGetDxcPaths(out _, out _)) + { + throw new InvalidOperationException("The packaged DirectX Shader Compiler runtime is unavailable."); + } + + WGPUInstanceImpl* created = CreateConfiguredInstance(api); + if (created is null) + { + throw new InvalidOperationException("WebGPU instance creation failed."); + } + + sharedInstance = created; + } + + return sharedInstance; } } @@ -111,14 +157,20 @@ public static Wgpu GetWgpuExtension() /// Receives the queue pointer on success. /// Receives the stable failure code on error. /// when handles are available; otherwise . - internal static bool TryGetOrCreateDevice( + /// + /// Thread safe; all provisioning happens under . The returned handles stay + /// owned by the runtime and are disposed at process exit, so callers must not dispose them. + /// + public static bool TryGetOrCreateDevice( out WebGPUDeviceHandle? device, out WebGPUQueueHandle? queue, out WebGPUEnvironmentError errorCode) { lock (Sync) { - // Fast path: return cached handles. + // Handles are published only after runtime initialization has acquired the required + // extension. Teardown clears the handles and extension together under this same lock, + // so cached handles already prove the complete environment invariant. if (autoDeviceHandle is not null && autoQueueHandle is not null) { device = autoDeviceHandle; @@ -127,6 +179,14 @@ internal static bool TryGetOrCreateDevice( return true; } + if (!TryGetDxcPaths(out _, out _)) + { + device = null; + queue = null; + errorCode = WebGPUEnvironmentError.DxcUnavailable; + return false; + } + try { EnsureInitialized(); @@ -149,7 +209,7 @@ internal static bool TryGetOrCreateDevice( // Provision: instance -> adapter -> device -> queue. // The instance and adapter are transient; only the device and queue are cached. - Instance* instance = api.CreateInstance((InstanceDescriptor*)null); + WGPUInstanceImpl* instance = CreateConfiguredInstance(api); if (instance is null) { device = null; @@ -158,13 +218,13 @@ internal static bool TryGetOrCreateDevice( return false; } - Adapter* adapter = null; - Device* requestedDevice = null; - Queue* requestedQueue = null; + WGPUAdapterImpl* adapter = null; + WGPUDeviceImpl* requestedDevice = null; + WGPUQueueImpl* requestedQueue = null; bool initialized = false; try { - if (!TryRequestAdapter(api, instance, out adapter, out errorCode)) + if (!TryRequestAdapter(api, instance, null, out adapter, out errorCode)) { device = null; queue = null; @@ -187,6 +247,21 @@ internal static bool TryGetOrCreateDevice( return false; } + // DevicePoll is a wgpu-native extension entry point required by every asynchronous + // readback path. Probe it before publishing the device so availability cannot report + // success against an incompatible native library found by the operating system. + try + { + _ = api.DevicePoll(requestedDevice, false, null); + } + catch (EntryPointNotFoundException) + { + device = null; + queue = null; + errorCode = WebGPUEnvironmentError.WgpuExtensionUnavailable; + return false; + } + // Cache for subsequent calls. autoDeviceHandle = new WebGPUDeviceHandle(api, (nint)requestedDevice, ownsHandle: true); autoQueueHandle = new WebGPUQueueHandle(api, (nint)requestedQueue, ownsHandle: true); @@ -224,17 +299,21 @@ internal static bool TryGetOrCreateDevice( } /// - /// Probes whether the current process can initialize WebGPU and provision a device/queue pair. + /// Probes whether the current process can initialize WebGPU with the required WGPU extension + /// and provision a device/queue pair. /// - /// when basic WebGPU device acquisition succeeds; otherwise the failure code. + /// + /// when the required WGPU extension and basic WebGPU device acquisition + /// are available; otherwise, the failure code. + /// /// - /// This is the broad availability check. It answers only "can this process get far enough to open WebGPU at all?" - /// and deliberately stops before shader-module or pipeline creation. Callers that only need to know whether native - /// WebGPU interop exists should use this probe. Callers that need the staged compute backend must additionally use + /// This is the broad availability check. It answers only whether the process can initialize the required native + /// WebGPU entry points and acquire a device and queue. It deliberately stops before shader-module or pipeline creation. + /// Callers that only need to know whether native WebGPU interop exists should use this probe. Callers that need the staged compute backend must additionally use /// , because successful device acquisition does not guarantee /// that compute-pipeline creation is actually usable on the active runtime/driver stack. /// - internal static WebGPUEnvironmentError ProbeAvailability() + public static WebGPUEnvironmentError ProbeAvailability() { lock (ProbeSync) { @@ -273,7 +352,7 @@ internal static WebGPUEnvironmentError ProbeAvailability() /// validation and isolates the actual pipeline creation in a remote process when possible so a native failure becomes /// a probe result instead of taking down the caller. /// - internal static WebGPUEnvironmentError ProbeComputePipelineSupport() + public static WebGPUEnvironmentError ProbeComputePipelineSupport() { lock (ProbeSync) { @@ -289,6 +368,9 @@ internal static WebGPUEnvironmentError ProbeComputePipelineSupport() return computePipelineProbeResult.Value; } + // Without process isolation the pipeline-creation attempt could crash natively and + // take the caller down with it, so skip the risky step and report success based on + // the availability probe alone. if (!RemoteExecutor.IsSupported) { computePipelineProbeResult = WebGPUEnvironmentError.Success; @@ -307,42 +389,45 @@ internal static WebGPUEnvironmentError ProbeComputePipelineSupport() } /// - /// Executes one isolated compute-pipeline creation probe for . + /// Executes one isolated compute-pipeline creation probe for + /// . /// /// - /// 0 when compute-pipeline creation succeeded; 1 when the probe completed and reported failure. - /// Any other value means the isolated probe process terminated before the probe could return normally. + /// 0 when pipeline creation succeeded; 1 when the probe completed and reported + /// failure. Any other value means the isolated probe process terminated before the probe + /// could return normally. /// - internal static int RunComputePipelineSupportProbe() + public static int RunComputePipelineSupportProbe() { try { - if (!TryGetOrCreateDevice(out WebGPUDeviceHandle? deviceHandle, out _, out _) - || deviceHandle is null) + if (!TryGetOrCreateDevice(out WebGPUDeviceHandle? deviceHandle, out WebGPUQueueHandle? queueHandle, out _) + || deviceHandle is null + || queueHandle is null) { return 1; } WebGPU api = GetApi(); using WebGPUHandle.HandleReference deviceReference = deviceHandle.AcquireReference(); - Device* device = (Device*)deviceReference.Handle; + WGPUDeviceImpl* device = (WGPUDeviceImpl*)deviceReference.Handle; ReadOnlySpan probeShader = "@compute @workgroup_size(1) fn main() {}\0"u8; fixed (byte* shaderCodePtr = probeShader) { - ShaderModuleWGSLDescriptor wgslDescriptor = new() + WGPUShaderSourceWGSL wgslDescriptor = new() { - Chain = new ChainedStruct { SType = SType.ShaderModuleWgslDescriptor }, - Code = shaderCodePtr + chain = new WGPUChainedStruct { sType = WGPUSType.ShaderSourceWGSL }, + code = shaderCodePtr }; - ShaderModuleDescriptor shaderDescriptor = new() + WGPUShaderModuleDescriptor shaderDescriptor = new() { - NextInChain = (ChainedStruct*)&wgslDescriptor + nextInChain = (WGPUChainedStruct*)&wgslDescriptor }; - ShaderModule* shaderModule = api.DeviceCreateShaderModule(device, in shaderDescriptor); + WGPUShaderModuleImpl* shaderModule = api.DeviceCreateShaderModule(device, in shaderDescriptor); if (shaderModule is null) { return 1; @@ -353,19 +438,19 @@ internal static int RunComputePipelineSupportProbe() ReadOnlySpan entryPoint = "main\0"u8; fixed (byte* entryPointPtr = entryPoint) { - ProgrammableStageDescriptor computeStage = new() + WGPUComputeState computeStage = new() { - Module = shaderModule, - EntryPoint = entryPointPtr + module = shaderModule, + entryPoint = entryPointPtr }; - PipelineLayoutDescriptor layoutDescriptor = new() + WGPUPipelineLayoutDescriptor layoutDescriptor = new() { - BindGroupLayoutCount = 0, - BindGroupLayouts = null + bindGroupLayoutCount = 0, + bindGroupLayouts = null }; - PipelineLayout* pipelineLayout = api.DeviceCreatePipelineLayout(device, in layoutDescriptor); + WGPUPipelineLayoutImpl* pipelineLayout = api.DeviceCreatePipelineLayout(device, in layoutDescriptor); if (pipelineLayout is null) { return 1; @@ -373,13 +458,13 @@ internal static int RunComputePipelineSupportProbe() try { - ComputePipelineDescriptor pipelineDescriptor = new() + WGPUComputePipelineDescriptor pipelineDescriptor = new() { - Layout = pipelineLayout, - Compute = computeStage + layout = pipelineLayout, + compute = computeStage }; - ComputePipeline* pipeline = api.DeviceCreateComputePipeline(device, in pipelineDescriptor); + WGPUComputePipelineImpl* pipeline = api.DeviceCreateComputePipeline(device, in pipelineDescriptor); if (pipeline is null) { return 1; @@ -406,18 +491,34 @@ internal static int RunComputePipelineSupportProbe() } } + /// + /// Releases all runtime-owned GPU state and cached probe results. The process-wide loader + /// wrappers remain rooted until operating-system process teardown. + /// Callers must hold . + /// private static void DisposeRuntimeCore() { + // Device shared state holds references on the device handles, so it must be released + // first; otherwise disposing the auto handles below could not drain their refcounts + // and the native device would stay open. ClearDeviceStateCache(); if (api is not null) { autoQueueHandle?.Dispose(); autoDeviceHandle?.Dispose(); + + // Released last among GPU objects: every surface that borrowed it is disposed by now, + // and non-owning surface instance handles never release it. + if (sharedInstance is not null) + { + api.InstanceRelease(sharedInstance); + } } autoDeviceHandle = null; autoQueueHandle = null; + sharedInstance = null; lock (ProbeSync) { @@ -425,62 +526,140 @@ private static void DisposeRuntimeCore() computePipelineProbeResult = null; } - // By the time process-exit teardown reaches the shared loader wrappers, the runtime may - // already be unwinding native loader state underneath Silk. The cached device/queue and - // all runtime-owned GPU state have already been released above, so these dispose failures - // no longer represent leaked WebGPU objects; they only mean the loader is already torn - // down or no longer in a state where Silk can unload it cleanly. We must still null the - // references so any later re-entry in the same process cannot observe stale wrappers. - try - { - wgpuExtension?.Dispose(); - } - catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) - { - } - finally + // DllImport keeps the native module loaded until normal process teardown. It must not be + // explicitly unloaded while a platform window can still dispatch native callbacks. + } + + /// + /// Loads the shared API facade and installs the process-exit teardown hook. + /// Callers must hold ; the exit hook re-acquires the lock when it fires. + /// + private static void EnsureInitialized() + { + if (!processExitHooked) { - wgpuExtension = null; + AppDomain.CurrentDomain.ProcessExit += (_, _) => + { + // Acquire in the global lock order (ProbeSync, then Sync). The probes hold + // ProbeSync and then enter Sync via TryGetOrCreateDevice, so taking Sync + // first here would deadlock against a probe racing process exit. The inner + // lock (ProbeSync) in DisposeRuntimeCore is then a reentrant acquisition. + lock (ProbeSync) + { + lock (Sync) + { + DisposeRuntimeCore(); + } + } + }; + + processExitHooked = true; } - try + api ??= WebGPU.GetApi(); + if (api is null) { - api?.Dispose(); + throw new InvalidOperationException("WebGPU.GetApi returned null."); } - catch (Exception ex) when (ex is ObjectDisposedException or InvalidOperationException) + } + + /// + /// Creates an instance configured to use the packaged DirectX Shader Compiler on Windows. + /// + /// The WebGPU API used to create the instance. + /// The created instance, or when creation fails. + private static WGPUInstanceImpl* CreateConfiguredInstance(WebGPU webGpu) + { + WGPUInstanceDescriptor descriptor = default; + if (!OperatingSystem.IsWindows()) { + return webGpu.CreateInstance(&descriptor); } - finally + + _ = TryGetDxcPaths(out string dxcPath, out _); + byte[] dxcPathBytes = Encoding.UTF8.GetBytes(dxcPath + '\0'); + + fixed (byte* dxcPathPointer = dxcPathBytes) { - api = null; + // DXC applies only to DX12, so select that backend explicitly. This also prevents + // unused Windows backends from reporting failed HWND capability probes before the + // runtime selects the valid DX12 adapter. + WGPUInstanceExtras extras = new() + { + chain = new WGPUChainedStruct + { + sType = (WGPUSType)WGPUNativeSType.WGPUSType_InstanceExtras + }, + backends = WebGPUNative.WGPUInstanceBackend_DX12, + dx12ShaderCompiler = WGPUDx12Compiler.Dxc, + dxcPath = dxcPathPointer, + + // A direct HWND swapchain is always opaque. The visual path makes wgpu create + // a DirectComposition visual for that HWND and supports alpha-aware presentation. + dx12PresentationSystem = WGPUDx12SwapchainKind.DxgiFromVisual + }; + + descriptor.nextInChain = (WGPUChainedStruct*)&extras; + return webGpu.CreateInstance(&descriptor); } } - private static void EnsureInitialized() + /// + /// Locates the packaged DirectX Shader Compiler files required by the Windows backend. + /// + /// Receives the path to dxcompiler.dll. + /// Receives the path to dxil.dll. + /// when both files are available, or on non-Windows platforms. + private static bool TryGetDxcPaths(out string dxcPath, out string dxilPath) { - if (!processExitHooked) + dxcPath = string.Empty; + dxilPath = string.Empty; + if (!OperatingSystem.IsWindows()) { - AppDomain.CurrentDomain.ProcessExit += (_, _) => + return true; + } + + // wgpu receives the compiler as a file location rather than binding it as an import, + // so resolve it through the host's native-library search. That search already honors + // every deployment layout: flat outputs, the NuGet runtimes layout, and + // self-contained publishes. + if (!NativeLibrary.TryLoad("dxcompiler.dll", typeof(WebGPURuntime).Assembly, null, out IntPtr module)) + { + // A Native AOT shared library hosted by a foreign process probes relative to that + // host, so fall back to the directory of this library's own image. + if (NativeModuleLocator.TryGetModuleDirectory(out string moduleDirectory)) { - lock (Sync) + dxcPath = FilePath.Combine(moduleDirectory, "dxcompiler.dll"); + dxilPath = FilePath.Combine(moduleDirectory, "dxil.dll"); + if (File.Exists(dxcPath) && File.Exists(dxilPath)) { - DisposeRuntimeCore(); + return true; } - }; + } - processExitHooked = true; + dxcPath = string.Empty; + dxilPath = string.Empty; + return false; } - api ??= WebGPU.GetApi(); - if (api is null) + try { - throw new InvalidOperationException("WebGPU.GetApi returned null."); + dxcPath = NativeModuleLocator.GetModuleFilePath(module); + } + finally + { + NativeLibrary.Free(module); } - if (wgpuExtension is null && !api.TryGetDeviceExtension(null, out wgpuExtension)) + if (dxcPath.Length == 0) { - throw new InvalidOperationException("WebGPU.TryGetDeviceExtension for Wgpu failed."); + return false; } + + // The compiler loads dxil.dll from its own directory to sign shaders, so both files + // ship side by side in every layout. + dxilPath = FilePath.Combine(FilePath.GetDirectoryName(dxcPath)!, "dxil.dll"); + return File.Exists(dxilPath); } /// @@ -488,20 +667,23 @@ private static void EnsureInitialized() /// /// The environment failure code. /// The exception message describing that failure. - internal static string CreateEnvironmentExceptionMessage(WebGPUEnvironmentError errorCode) + public static string CreateEnvironmentExceptionMessage(WebGPUEnvironmentError errorCode) => errorCode switch { WebGPUEnvironmentError.Success => "The WebGPU operation did not report an error.", WebGPUEnvironmentError.ApiInitializationFailed => "Failed to initialize the WebGPU runtime.", + WebGPUEnvironmentError.DxcUnavailable => "The packaged DirectX Shader Compiler runtime is unavailable.", WebGPUEnvironmentError.InstanceCreationFailed => "The WebGPU runtime could not create an instance.", WebGPUEnvironmentError.AdapterRequestTimedOut => "Timed out while waiting for the WebGPU adapter request callback.", WebGPUEnvironmentError.AdapterRequestFailed => "The WebGPU runtime failed to acquire a WebGPU adapter.", + WebGPUEnvironmentError.SoftwareAdapterUnsupported => "The only available WebGPU adapter is a software rasterizer, which this backend does not support; use the CPU drawing backend instead.", WebGPUEnvironmentError.DeviceRequestTimedOut => "Timed out while waiting for the WebGPU device request callback.", WebGPUEnvironmentError.DeviceRequestFailed => "The WebGPU runtime failed to acquire a WebGPU device.", WebGPUEnvironmentError.QueueAcquisitionFailed => "The WebGPU runtime acquired a device but could not retrieve its default queue.", WebGPUEnvironmentError.DeviceAcquisitionFailed => "The WebGPU runtime failed to provision a WebGPU device and queue.", WebGPUEnvironmentError.ComputePipelineCreationFailed => "The isolated WebGPU compute-pipeline probe reported failure.", WebGPUEnvironmentError.ComputePipelineProbeProcessFailed => "The isolated WebGPU compute-pipeline probe process terminated before it could report a result.", + WebGPUEnvironmentError.WgpuExtensionUnavailable => "The required WGPU extension is unavailable.", _ => "The WebGPU runtime failed for an unknown reason." }; @@ -510,38 +692,59 @@ internal static string CreateEnvironmentExceptionMessage(WebGPUEnvironmentError /// /// The WebGPU API wrapper. /// The instance that issues the request. + /// The presentation surface the adapter must support, or for an offscreen adapter. /// Receives the returned adapter on success. /// Receives the stable failure code when the request fails. /// when an adapter was acquired; otherwise, . - private static bool TryRequestAdapter( + public static bool TryRequestAdapter( WebGPU api, - Instance* instance, - out Adapter* adapter, + WGPUInstanceImpl* instance, + WGPUSurfaceImpl* compatibleSurface, + out WGPUAdapterImpl* adapter, out WebGPUEnvironmentError errorCode) { - RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; - Adapter* callbackAdapter = null; + WGPURequestAdapterStatus callbackStatus = default; + WGPUAdapterImpl* callbackAdapter = null; using ManualResetEventSlim callbackReady = new(false); // The native callback completes on the runtime's thread model, so the managed side stores // the result into locals and then resumes once the signal is set or the request times out. - void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) + void Callback(WGPURequestAdapterStatus status, WGPUAdapterImpl* adapterPtr, WGPUStringView message, void* userData) { callbackStatus = status; callbackAdapter = adapterPtr; callbackReady.Set(); } - using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); + void ReleaseAbandonedResult(WGPURequestAdapterStatus status, WGPUAdapterImpl* adapterPtr, WGPUStringView message, void* userData) + { + _ = status; + _ = message; + _ = userData; + + if (adapterPtr is not null) + { + // AllowSpontaneous callbacks can run from a native WebGPU call stack, where the + // checked-in header forbids re-entrant WebGPU calls. Release a late owned result + // on the thread pool after the callback has returned to native code. + _ = ThreadPool.UnsafeQueueUserWorkItem( + static state => state.Api.AdapterRelease((WGPUAdapterImpl*)state.Handle), + (Api: api, Handle: (nint)adapterPtr), + preferLocal: false); + } + } + + using WebGPURequestAdapterCallback callbackPtr = WebGPURequestAdapterCallback.From(Callback, ReleaseAbandonedResult); WebGPUEnvironmentOptions environmentOptions = WebGPUEnvironment.Options; - RequestAdapterOptions options = new() + WGPURequestAdapterOptions options = new() { - PowerPreference = environmentOptions.PowerPreference switch + compatibleSurface = compatibleSurface, + powerPreference = environmentOptions.PowerPreference switch { - WebGPUPowerPreference.Default => PowerPreference.Undefined, - WebGPUPowerPreference.LowPower => PowerPreference.LowPower, - WebGPUPowerPreference.HighPerformance => PowerPreference.HighPerformance, + WebGPUPowerPreference.Default => WGPUPowerPreference.Undefined, + WebGPUPowerPreference.LowPower => WGPUPowerPreference.LowPower, + WebGPUPowerPreference.HighPerformance => WGPUPowerPreference.HighPerformance, _ => throw new InvalidOperationException("The WebGPU power preference mapping is incomplete.") } }; @@ -549,18 +752,48 @@ void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, v api.InstanceRequestAdapter(instance, in options, callbackPtr, null); if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) { + // Retire the owner before disposing its signal. Dispose waits for a callback already + // in progress; rechecking the signal then distinguishes that race from a genuinely + // outstanding native request whose eventual result must use the abandonment path. + callbackPtr.Dispose(); + if (callbackReady.IsSet) + { + adapter = callbackAdapter; + errorCode = callbackStatus == WGPURequestAdapterStatus.Success && callbackAdapter is not null + ? WebGPUEnvironmentError.Success + : WebGPUEnvironmentError.AdapterRequestFailed; + return errorCode == WebGPUEnvironmentError.Success; + } + adapter = null; errorCode = WebGPUEnvironmentError.AdapterRequestTimedOut; return false; } adapter = callbackAdapter; - if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) + if (callbackStatus != WGPURequestAdapterStatus.Success || callbackAdapter is null) { errorCode = WebGPUEnvironmentError.AdapterRequestFailed; return false; } + // A software rasterizer cannot run this backend's shader pipeline; GPU-less machines are + // served by the CPU drawing backend. Rejecting at adapter selection keeps the check free + // of device creation, submissions, and readbacks. + WGPUAdapterInfo adapterInfo = default; + if (api.AdapterGetInfo(callbackAdapter, &adapterInfo) == WGPUStatus.Success) + { + WGPUAdapterType adapterType = adapterInfo.adapterType; + api.AdapterInfoFreeMembers(adapterInfo); + if (adapterType == WGPUAdapterType.CPU) + { + api.AdapterRelease(callbackAdapter); + adapter = null; + errorCode = WebGPUEnvironmentError.SoftwareAdapterUnsupported; + return false; + } + } + errorCode = WebGPUEnvironmentError.Success; return true; } @@ -573,67 +806,109 @@ void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, v /// Receives the returned device on success. /// Receives the stable failure code when the request fails. /// when a device was acquired; otherwise, . - private static bool TryRequestDevice( + public static bool TryRequestDevice( WebGPU api, - Adapter* adapter, - out Device* device, + WGPUAdapterImpl* adapter, + out WGPUDeviceImpl* device, out WebGPUEnvironmentError errorCode) { - RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; - Device* callbackDevice = null; + WGPURequestDeviceStatus callbackStatus = default; + WGPUDeviceImpl* callbackDevice = null; using ManualResetEventSlim callbackReady = new(false); // Device creation is also callback-driven, so the request writes into locals and then // the caller continues once the callback signals completion. - void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) + void Callback(WGPURequestDeviceStatus status, WGPUDeviceImpl* devicePtr, WGPUStringView message, void* userData) { callbackStatus = status; callbackDevice = devicePtr; callbackReady.Set(); } - using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); + void ReleaseAbandonedResult(WGPURequestDeviceStatus status, WGPUDeviceImpl* devicePtr, WGPUStringView message, void* userData) + { + _ = status; + _ = message; + _ = userData; + + if (devicePtr is not null) + { + // The native result carries ownership even though the managed request has timed + // out. Defer its release until after this spontaneous callback returns because + // webgpu.h explicitly forbids re-entrant API calls from such a callback. + _ = ThreadPool.UnsafeQueueUserWorkItem( + static state => state.Api.DeviceRelease((WGPUDeviceImpl*)state.Handle), + (Api: api, Handle: (nint)devicePtr), + preferLocal: false); + } + } + + using WebGPURequestDeviceCallback callbackPtr = WebGPURequestDeviceCallback.From(Callback, ReleaseAbandonedResult); // Auto-provision a device when no native surface provides one. // Request optional storage features that are available on this adapter. // The compute compositor needs storage binding on the transient output texture, // and some formats (e.g. Bgra8Unorm) require explicit device features. - Span requestedFeatures = stackalloc FeatureName[1]; + Span requestedFeatures = stackalloc WGPUFeatureName[2]; int requestedCount = 0; - if (api.AdapterHasFeature(adapter, FeatureName.Bgra8UnormStorage)) + if (api.AdapterHasFeature(adapter, WGPUFeatureName.BGRA8UnormStorage)) + { + requestedFeatures[requestedCount++] = WGPUFeatureName.BGRA8UnormStorage; + } + + if (api.AdapterHasFeature(adapter, WGPUFeatureName.TextureFormatsTier1)) { - requestedFeatures[requestedCount++] = FeatureName.Bgra8UnormStorage; + requestedFeatures[requestedCount++] = WGPUFeatureName.TextureFormatsTier1; } - DeviceDescriptor descriptor; - if (requestedCount > 0) + // Raise only the storage-buffer binding and total buffer-size ceilings to the adapter maximum so + // a large scene fits in a single storage binding instead of falling back to chunked (multi-pass) + // rendering. Every other limit stays at its WebGPU default. Without this the device inherits the + // default 128 MiB maxStorageBufferBindingSize, which a dense stroke batch's segment buffer exceeds. + WGPULimits requiredLimits = BuildStorageBindingLimits(api, adapter); + + fixed (WGPUFeatureName* featuresPtr = requestedFeatures) { - fixed (FeatureName* featuresPtr = requestedFeatures) + WGPUDeviceDescriptor descriptor = new() { - descriptor = new DeviceDescriptor + requiredLimits = &requiredLimits, + requiredFeatureCount = (nuint)requestedCount, + requiredFeatures = requestedCount > 0 ? featuresPtr : null, + deviceLostCallbackInfo = new() { - RequiredFeatureCount = (uint)requestedCount, - RequiredFeatures = featuresPtr, - }; + mode = WGPUCallbackMode.AllowSpontaneous, + callback = &HandleDeviceLost + }, + uncapturedErrorCallbackInfo = new() + { + callback = &HandleUncapturedError + } + }; - api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); - } - } - else - { - descriptor = default; api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); } if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) { + // Synchronize with a callback that crossed the timeout boundary before deciding + // whether the result was observed or must be abandoned later. + callbackPtr.Dispose(); + if (callbackReady.IsSet) + { + device = callbackDevice; + errorCode = callbackStatus == WGPURequestDeviceStatus.Success && callbackDevice is not null + ? WebGPUEnvironmentError.Success + : WebGPUEnvironmentError.DeviceRequestFailed; + return errorCode == WebGPUEnvironmentError.Success; + } + device = null; errorCode = WebGPUEnvironmentError.DeviceRequestTimedOut; return false; } device = callbackDevice; - if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) + if (callbackStatus != WGPURequestDeviceStatus.Success || callbackDevice is null) { errorCode = WebGPUEnvironmentError.DeviceRequestFailed; return false; @@ -642,4 +917,113 @@ void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void errorCode = WebGPUEnvironmentError.Success; return true; } + + /// + /// Builds a device limit request that raises only the storage-buffer binding size and the total + /// buffer size to the adapter's maximum, leaving every other limit at its WebGPU default. + /// + /// + /// Requesting the adapter's full limit set perturbs alignment and per-stage limits and corrupts + /// resource bindings, so all fields except the two storage ceilings are left at the undefined + /// sentinel, which instructs the implementation to keep the default value for that limit. + /// + /// The WebGPU API used to query the adapter. + /// The adapter whose maximum storage limits are requested. + /// The populated to attach to the device descriptor. + public static WGPULimits BuildStorageBindingLimits(WebGPU api, WGPUAdapterImpl* adapter) + { + WGPULimits adapterLimits = default; + _ = api.AdapterGetLimits(adapter, &adapterLimits); + + // WebGPU treats these sentinel values as "leave this limit at its default" when a required-limits + // block is supplied, so only the two storage ceilings below deviate from the device defaults. + const uint keepU32 = uint.MaxValue; + const ulong keepU64 = ulong.MaxValue; + + WGPULimits limits = new() + { + maxTextureDimension1D = keepU32, + maxTextureDimension2D = keepU32, + maxTextureDimension3D = keepU32, + maxTextureArrayLayers = keepU32, + maxBindGroups = keepU32, + maxBindGroupsPlusVertexBuffers = keepU32, + maxBindingsPerBindGroup = keepU32, + maxDynamicUniformBuffersPerPipelineLayout = keepU32, + maxDynamicStorageBuffersPerPipelineLayout = keepU32, + maxSampledTexturesPerShaderStage = keepU32, + maxSamplersPerShaderStage = keepU32, + maxStorageBuffersPerShaderStage = keepU32, + maxStorageTexturesPerShaderStage = keepU32, + maxUniformBuffersPerShaderStage = keepU32, + maxUniformBufferBindingSize = keepU64, + maxStorageBufferBindingSize = adapterLimits.maxStorageBufferBindingSize, + minUniformBufferOffsetAlignment = keepU32, + minStorageBufferOffsetAlignment = keepU32, + maxVertexBuffers = keepU32, + maxBufferSize = adapterLimits.maxBufferSize, + maxVertexAttributes = keepU32, + maxVertexBufferArrayStride = keepU32, + maxInterStageShaderVariables = keepU32, + maxColorAttachments = keepU32, + maxColorAttachmentBytesPerSample = keepU32, + maxComputeWorkgroupStorageSize = keepU32, + maxComputeInvocationsPerWorkgroup = keepU32, + maxComputeWorkgroupSizeX = keepU32, + maxComputeWorkgroupSizeY = keepU32, + maxComputeWorkgroupSizeZ = keepU32, + maxComputeWorkgroupsPerDimension = keepU32, + maxImmediateSize = keepU32, + }; + + return limits; + } + + /// + /// Reports a native device-lost callback through the managed WebGPU error callback. + /// + /// The address of the device reported by WebGPU. + /// The native device-lost reason. + /// The diagnostic message supplied by the runtime. + /// The first unused native user-data pointer. + /// The second unused native user-data pointer. + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void HandleDeviceLost( + WGPUDeviceImpl** device, + WGPUDeviceLostReason reason, + WGPUStringView message, + void* userData1, + void* userData2) + { + if (device is not null && *device is not null) + { + MarkDeviceLost((nint)(*device)); + } + + _ = userData1; + _ = userData2; + WebGPUEnvironment.ReportUncapturedError(WebGPUErrorType.DeviceLost, $"Device lost ({reason}): {message.ToManagedString()}"); + } + + /// + /// Reports a native uncaptured-error callback through the managed WebGPU error callback. + /// + /// The address of the device reporting the error. + /// The native error type. + /// The diagnostic message supplied by the runtime. + /// The first unused native user-data pointer. + /// The second unused native user-data pointer. + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void HandleUncapturedError( + WGPUDeviceImpl** device, + WGPUErrorType type, + WGPUStringView message, + void* userData1, + void* userData2) + { + _ = device; + _ = userData1; + _ = userData2; + WebGPUEnvironment.ReportUncapturedError(WebGPUErrorTypeMapper.ToPublic(type), message.ToManagedString()); + } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs index 856b02102..863509921 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneConfig.cs @@ -68,6 +68,31 @@ public static WebGPUSceneConfig Create(WebGPUEncodedScene scene, WebGPUSceneBump WebGPUSceneBufferSizes.Create(scene, bumpSizes, chunkWindow), bumpSizes, chunkWindow); + + /// + /// Creates the dispatch and buffer plan for one encoded scene range. + /// + /// The encoded scene whose shared buffers are being rendered. + /// The range inside to render. + /// The current dynamic scratch capacities for the staged pipeline. + /// A flush-scoped configuration describing dispatch sizes, buffer sizes, and scratch capacities. + public static WebGPUSceneConfig Create(WebGPUEncodedScene scene, WebGPUSceneRange range, WebGPUSceneBumpSizes bumpSizes) + => Create(scene, range, bumpSizes, WebGPUSceneChunkWindow.FullScene(range)); + + /// + /// Creates the dispatch and buffer plan for one encoded scene range and one tile-row render window. + /// + /// The encoded scene whose shared buffers are being rendered. + /// The range inside to render. + /// The current dynamic scratch capacities for the staged pipeline. + /// The tile-row window rendered by this staged-scene attempt. + /// A flush-scoped configuration describing dispatch sizes, buffer sizes, and scratch capacities. + public static WebGPUSceneConfig Create(WebGPUEncodedScene scene, WebGPUSceneRange range, WebGPUSceneBumpSizes bumpSizes, WebGPUSceneChunkWindow chunkWindow) + => new( + WebGPUSceneWorkgroupCounts.Create(range, chunkWindow), + WebGPUSceneBufferSizes.Create(scene, range, bumpSizes, chunkWindow), + bumpSizes, + chunkWindow); } /// @@ -113,6 +138,28 @@ public static WebGPUSceneChunkWindow FullScene(WebGPUEncodedScene scene) uint tileHeight = checked((uint)scene.TileCountY); return new WebGPUSceneChunkWindow(0U, tileHeight, tileHeight); } + + /// + /// Creates the full-range render window. + /// + /// The encoded range whose full tile height is being rendered. + /// The tile window spanning the entire encoded range. + public static WebGPUSceneChunkWindow FullScene(WebGPUSceneRange range) + { + // Tiles are 16x16 pixels (TILE_WIDTH/TILE_HEIGHT in Shared/config.wgsl). + uint tileHeight = DivideRoundUp(checked((uint)range.TargetBounds.Height), 16U); + return new WebGPUSceneChunkWindow(0U, tileHeight, tileHeight); + } + + /// + /// Rounds up integer division for tile sizing. + /// + /// The dividend. + /// The divisor. + /// The quotient rounded up to the next whole unit. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint DivideRoundUp(uint value, uint divisor) + => (value + divisor - 1U) / divisor; } /// @@ -201,6 +248,7 @@ public WebGPUSceneBumpSizes( /// which are sized to accommodate its reference scenes, including paris-30k, without a /// grow-and-retry cycle on the first flush. /// + /// The initial scratch capacities. public static WebGPUSceneBumpSizes Initial() => new( 1U << 21, // Lines @@ -430,6 +478,9 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU uint lineCount = checked((uint)scene.LineCount); uint clipCount = checked((uint)scene.ClipCount); uint pathTagCount = checked((uint)scene.PathTagByteCount); + + // Each pathtag_reduce workgroup consumes 256 u32 tag words (1024 tag bytes), + // so the tag byte stream is padded to that granularity before dividing. uint pathTagPadded = AlignUp(pathTagCount, 4U * 256U); // The pathtag scan has a small and large variant. We choose once here so every later @@ -438,8 +489,14 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU bool useLargePathScan = pathTagWgs > 256U; uint reducedSize = useLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; uint drawObjectWgs = DivideRoundUp(drawObjectCount, 256U); + + // draw_leaf.wgsl distributes all draw blocks over at most 256 workgroups + // (each loops over its share of blocks), so reduce/leaf dispatches cap at 256. uint drawMonoidWgs = Math.Min(drawObjectWgs, 256U); uint flattenWgs = DivideRoundUp(pathTagCount, 256U); + + // Vello's sizing: clip_leaf processes the final (possibly partial) partition + // itself, so clip_reduce only covers the full partitions preceding it. uint clipReduceWgs = clipCount == 0U ? 0U : (clipCount - 1U) / 256U; uint clipWgs = DivideRoundUp(clipCount, 256U); uint pathWgs = DivideRoundUp(pathCount, 256U); @@ -460,7 +517,7 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU 256U, reducedSize / 256U, pathTagWgs, - drawObjectWgs, + pathWgs, flattenWgs, drawMonoidWgs, drawMonoidWgs, @@ -481,9 +538,73 @@ public static WebGPUSceneWorkgroupCounts Create(WebGPUEncodedScene scene, WebGPU chunkWindow.TileHeight); } + /// + /// Computes the workgroup counts required to run the staged-scene pipeline for one encoded range. + /// + /// The encoded range whose dispatch sizes are being planned. + /// The tile-row window rendered by this staged-scene attempt. + /// The per-pass workgroup counts for this encoded range and chunk window. + public static WebGPUSceneWorkgroupCounts Create(WebGPUSceneRange range, WebGPUSceneChunkWindow chunkWindow) + { + uint drawObjectCount = checked((uint)range.DrawTagCount); + uint pathCount = checked((uint)range.PathCount); + uint lineCount = checked((uint)range.LineCount); + uint clipCount = checked((uint)range.ClipCount); + uint pathTagCount = checked((uint)range.PathTagByteCount); + + // Same dispatch math as the full-scene overload above; see the comments + // there for the pathtag padding, draw cap, and clip reduce rationale. + uint pathTagPadded = AlignUp(pathTagCount, 4U * 256U); + uint pathTagWgs = pathTagPadded / (4U * 256U); + bool useLargePathScan = pathTagWgs > 256U; + uint reducedSize = useLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; + uint drawObjectWgs = DivideRoundUp(drawObjectCount, 256U); + uint drawMonoidWgs = Math.Min(drawObjectWgs, 256U); + uint flattenWgs = DivideRoundUp(pathTagCount, 256U); + uint clipReduceWgs = clipCount == 0U ? 0U : (clipCount - 1U) / 256U; + uint clipWgs = DivideRoundUp(clipCount, 256U); + uint pathWgs = DivideRoundUp(pathCount, 256U); + uint tileCountX = checked((uint)((range.TargetBounds.Width + 15) / 16)); + uint tileCountY = DivideRoundUp(checked((uint)range.TargetBounds.Height), 16U); + uint widthInBins = DivideRoundUp(tileCountX, 16U); + uint heightInBins = DivideRoundUp(chunkWindow.TileBufferHeight, 16U); + uint fullHeightInBins = DivideRoundUp(tileCountY, 16U); + uint totalBins = checked(widthInBins * fullHeightInBins); + uint binChunks = DivideRoundUp(totalBins, 256U); + + return new WebGPUSceneWorkgroupCounts( + useLargePathScan, + pathTagWgs, + 256U, + reducedSize / 256U, + pathTagWgs, + pathWgs, + flattenWgs, + drawMonoidWgs, + drawMonoidWgs, + clipReduceWgs, + clipWgs, + BinningComputeShader.GetDispatchX(drawObjectCount), + binChunks, + PathRowAllocComputeShader.GetDispatchX(pathCount), + TileAllocComputeShader.GetDispatchX(pathCount), + PathCountSetupComputeShader.GetDispatchX(), + PathCountComputeShader.GetDispatchX(lineCount), + BackdropComputeShader.GetDispatchX(pathCount), + widthInBins, + heightInBins, + PathTilingSetupComputeShader.GetDispatchX(), + 1, + tileCountX, + chunkWindow.TileHeight); + } + /// /// Rounds up integer division for dispatch sizing. /// + /// The dividend. + /// The divisor. + /// The quotient rounded up to the next whole unit. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint DivideRoundUp(uint value, uint divisor) => (value + divisor - 1U) / divisor; @@ -491,6 +612,9 @@ private static uint DivideRoundUp(uint value, uint divisor) /// /// Rounds up to the next multiple of . /// + /// The value to round. + /// The alignment; must be a power of two. + /// The rounded value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignUp(uint value, uint alignment) => value + (uint)(-(int)value & (alignment - 1U)); @@ -501,6 +625,34 @@ private static uint AlignUp(uint value, uint alignment) /// internal readonly struct WebGPUSceneBufferSizes { + /// + /// Initializes a new instance of the struct. + /// + /// The first pathtag-reduction scratch buffer size. + /// The second pathtag-reduction scratch buffer size. + /// The pathtag scan scratch buffer size. + /// The final pathtag monoid buffer size. + /// The per-path bounding-box buffer size. + /// The draw reduction buffer size. + /// The final draw monoid buffer size. + /// The scene info-word buffer size. + /// The clip input buffer size. + /// The clip element buffer size. + /// The clip bic buffer size. + /// The clip bounding-box buffer size. + /// The draw bounding-box buffer size. + /// The bump allocator block size. + /// The per-path scheduling buffer size. + /// The flattened line buffer size. + /// The bin-header buffer size. + /// The bin-data scratch buffer size. + /// The indirect dispatch-count buffer size. + /// The sparse path-row buffer size. + /// The path-tile buffer size. + /// The segment-count buffer size. + /// The segment buffer size. + /// The blend-spill buffer size. + /// The PTCL buffer size. public WebGPUSceneBufferSizes( WebGPUSceneBufferSize pathReduced, WebGPUSceneBufferSize pathReduced2, @@ -525,7 +677,7 @@ public WebGPUSceneBufferSizes( WebGPUSceneBufferSize pathTiles, WebGPUSceneBufferSize segCounts, WebGPUSceneBufferSize segments, - WebGPUSceneBufferSize blendSpill, + WebGPUSceneBufferSize blendSpill, WebGPUSceneBufferSize ptcl) { this.PathReduced = pathReduced; @@ -673,7 +825,7 @@ public WebGPUSceneBufferSizes( /// /// Gets the size of the blend-spill buffer. /// - public WebGPUSceneBufferSize BlendSpill { get; } + public WebGPUSceneBufferSize BlendSpill { get; } /// /// Gets the size of the PTCL buffer. @@ -695,6 +847,8 @@ public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUScen uint pathReducedCount = reducedSize; uint pathReduced2Count = 256U; uint pathReducedScanCount = reducedSize; + + // One TagMonoid per u32 tag word; the scan writes whole 256-element partitions. uint pathMonoidCount = pathTagWgs * 256U; uint pathBboxCount = checked((uint)scene.PathCount); uint drawObjectCount = checked((uint)scene.DrawTagCount); @@ -703,6 +857,8 @@ public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUScen uint infoCount = checked((uint)scene.InfoBufferWordCount); uint clipInputCount = checked((uint)scene.ClipCount); uint clipElementCount = checked((uint)scene.ClipCount); + + // One Bic per full 256-element clip partition (Vello's n_clip / 256 sizing). uint clipBicCount = clipInputCount / 256U; uint clipBboxCount = clipInputCount; uint drawBboxCount = drawObjectCount; @@ -712,7 +868,12 @@ public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUScen // the Y dispatch tiles the full bin grid in 256-bin chunks. The header // buffer therefore reserves 256 slots per (draw-partition, bin-chunk). uint binHeaderCount = checked(drawObjectPartitions * workgroupCounts.BinningY * 256U); + + // Path records are padded to whole 256-thread allocation workgroups. uint pathCount = AlignUp(checked((uint)scene.PathCount), 256U); + + // Every tile gets PtclInitialAlloc words up front; the bump-allocated tail + // (config.ptcl_dyn_start in Shared/config.wgsl) begins after that fixed region. uint ptclBootstrapCount = checked((uint)scene.TileCountX * chunkWindow.TileBufferHeight * WebGPUSceneDispatch.PtclInitialAlloc); uint ptclCount = checked(bumpSizes.Ptcl + ptclBootstrapCount); @@ -740,13 +901,68 @@ public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUScen WebGPUSceneBufferSize.Create(bumpSizes.PathTiles), WebGPUSceneBufferSize.Create(bumpSizes.SegCounts), WebGPUSceneBufferSize.Create(bumpSizes.Segments), - WebGPUSceneBufferSize.Create(bumpSizes.BlendSpill), + WebGPUSceneBufferSize.Create(bumpSizes.BlendSpill), + WebGPUSceneBufferSize.Create(ptclCount)); + } + + /// + /// Computes the GPU buffer sizes required by the current staged-scene pipeline for one encoded range. + /// + /// The encoded scene whose shared buffers are being rendered. + /// The range inside to render. + /// The current dynamic scratch capacities for the staged pipeline. + /// The tile-row window rendered by this staged-scene attempt. + /// The planned GPU buffer sizes for this encoded range and chunk window. + public static WebGPUSceneBufferSizes Create(WebGPUEncodedScene scene, WebGPUSceneRange range, WebGPUSceneBumpSizes bumpSizes, WebGPUSceneChunkWindow chunkWindow) + { + WebGPUSceneWorkgroupCounts workgroupCounts = WebGPUSceneWorkgroupCounts.Create(range, chunkWindow); + uint pathTagWgs = workgroupCounts.PathReduceX; + uint reducedSize = workgroupCounts.UseLargePathScan ? AlignUp(pathTagWgs, 256U) : pathTagWgs; + uint drawObjectCount = checked((uint)range.DrawTagCount); + uint drawObjectPartitions = BinningComputeShader.GetDispatchX(drawObjectCount); + + // See the full-scene overload above for the bin-header, path padding, + // and ptcl bootstrap sizing rationale. + uint binHeaderCount = checked(drawObjectPartitions * workgroupCounts.BinningY * 256U); + uint pathCount = AlignUp(checked((uint)range.PathCount), 256U); + uint tileCountX = checked((uint)((range.TargetBounds.Width + 15) / 16)); + uint ptclBootstrapCount = checked(tileCountX * chunkWindow.TileBufferHeight * WebGPUSceneDispatch.PtclInitialAlloc); + uint ptclCount = checked(bumpSizes.Ptcl + ptclBootstrapCount); + + return new WebGPUSceneBufferSizes( + WebGPUSceneBufferSize.Create(reducedSize), + WebGPUSceneBufferSize.Create(256U), + WebGPUSceneBufferSize.Create(reducedSize), + WebGPUSceneBufferSize.Create(pathTagWgs * 256U), + WebGPUSceneBufferSize.Create(checked((uint)range.PathCount)), + WebGPUSceneBufferSize.Create(workgroupCounts.DrawReduceX), + WebGPUSceneBufferSize.Create(drawObjectCount), + WebGPUSceneBufferSize.Create(checked((uint)(range.InfoWordCount + scene.PathGradientDataWordCount))), + WebGPUSceneBufferSize.Create(checked((uint)range.ClipCount)), + WebGPUSceneBufferSize.Create(checked((uint)range.ClipCount)), + WebGPUSceneBufferSize.Create(checked((uint)range.ClipCount) / 256U), + WebGPUSceneBufferSize.Create(checked((uint)range.ClipCount)), + WebGPUSceneBufferSize.Create(drawObjectCount), + WebGPUSceneBufferSize.Create(1), + WebGPUSceneBufferSize.Create(pathCount), + WebGPUSceneBufferSize.Create(bumpSizes.Lines), + WebGPUSceneBufferSize.Create(binHeaderCount), + WebGPUSceneBufferSize.Create(bumpSizes.Binning), + WebGPUSceneBufferSize.Create(1), + WebGPUSceneBufferSize.Create(bumpSizes.PathRows), + WebGPUSceneBufferSize.Create(bumpSizes.PathTiles), + WebGPUSceneBufferSize.Create(bumpSizes.SegCounts), + WebGPUSceneBufferSize.Create(bumpSizes.Segments), + WebGPUSceneBufferSize.Create(bumpSizes.BlendSpill), WebGPUSceneBufferSize.Create(ptclCount)); } /// /// Rounds up to the next multiple of . /// + /// The value to round. + /// The alignment; must be a power of two. + /// The rounded value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint AlignUp(uint value, uint alignment) => value + (uint)(-(int)value & (alignment - 1U)); @@ -755,9 +971,14 @@ private static uint AlignUp(uint value, uint alignment) /// /// Typed buffer size primitive mirroring Vello's exact-count planning style. /// +/// The unmanaged element type whose size determines the byte length. internal readonly struct WebGPUSceneBufferSize where T : unmanaged { + /// + /// Initializes a new instance of the struct. + /// + /// The requested element count; zero is clamped to one. private WebGPUSceneBufferSize(uint length) // Storage bindings must remain non-zero-sized for validation. @@ -780,6 +1001,8 @@ public nuint ByteLength /// /// Creates a buffer-size wrapper that preserves WebGPU's non-zero storage-binding requirement. /// + /// The requested element count; zero is clamped to one. + /// The buffer-size wrapper. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static WebGPUSceneBufferSize Create(uint length) => new(length); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs index 721e9fb7b..55a2aa98d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneDispatch.cs @@ -6,22 +6,38 @@ using System.Buffers; using System.Diagnostics; using System.Runtime.CompilerServices; -using Silk.NET.WebGPU; -using Silk.NET.WebGPU.Extensions.WGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Phase 1 shell for the staged WebGPU scene pipeline. +/// Drives the staged WebGPU scene pipeline: validates encoded scenes, plans buffer bindings, records +/// the Vello-derived compute stage sequence, and executes it against a flush target. /// +/// +/// +/// The stage order is fixed by data dependencies: prepare (bump reset), pathtag reduce/scan, +/// bbox_clear, flatten, draw reduce/leaf, clip reduce/leaf, binning, then the tile stages +/// (path_row_alloc, path_row_span, tile_alloc, path_count, backdrop, coarse, path_tiling) and +/// finally the fine shading pass. Scratch buffers use GPU bump allocators sized from cached +/// estimates; after each attempt the allocator counters are read back and, on overflow, the caller +/// retries with grown capacities (see 's bounded retry loop). +/// +/// +/// Scenes whose tile-dependent buffers exceed the device storage-buffer binding limit are rendered +/// in vertical tile-row chunks: the chunk-invariant stages run once and the chunk-local stages +/// replay per tile-row window. +/// +/// internal static class WebGPUSceneDispatch { // Fixed bootstrap PTCL reservation per tile. The coarse stage writes each tile's initial // command list into this prefix, while dynamic PTCL growth starts after the tileCount * 64 area. - internal const uint PtclInitialAlloc = 64U; + public const uint PtclInitialAlloc = 64U; + // Device pipeline-cache keys, one per staged-scene compute stage. The fine pipeline key is + // suffixed with the texture format because its shader is generated per output format. private const string PreparePipelineKey = "scene/prepare"; private const string PathtagReducePipelineKey = "scene/pathtag-reduce"; private const string PathtagReduce2PipelineKey = "scene/pathtag-reduce2"; @@ -45,9 +61,18 @@ internal static class WebGPUSceneDispatch private const string PathTilingSetupPipelineKey = "scene/path-tiling-setup"; private const string PathTilingPipelineKey = "scene/path-tiling"; private const string FineAreaPipelineKey = "scene/fine-area"; - private const string FineAliasedThresholdPipelineKey = "scene/fine-aliased-threshold"; private const string ChunkResetPipelineKey = "scene/chunk-reset"; + // Must match the WG_SIZE workgroup clip stack in clip_leaf.wgsl; the CPU-side clip stream + // 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; + /// /// Identifies the staged-scene storage binding that exceeded the device limit for one flush attempt. /// @@ -136,30 +161,55 @@ public BindingLimitFailure(BindingLimitBuffer buffer, nuint requiredBytes, nuint /// /// Builds flush-scoped GPU resources for a retained encoded scene. /// + /// + /// Validates the clip stream and planned binding sizes before creating any GPU resources. + /// A chunkable binding-limit overflow is not fatal here; it is recorded on the returned scene + /// so the render entry point can route the scene through the tile-row chunked path. + /// + /// The pixel format of the flush target. + /// The library configuration providing the memory allocator. + /// The native surface holding the target texture handles. + /// The target bounds for the flush. + /// The retained encoded scene to stage. + /// The texture format and alpha representation of the flush target. + /// The device feature required by the target format, if any. + /// The scratch capacities carried over from earlier flushes. + /// The backend-specific upper bound for staged-scene storage bindings. + /// The reusable scene resource arena slot for this flush. + /// The staged scene holding the flush context, config, and GPU resources. public static WebGPUStagedScene CreateStagedScene( Configuration configuration, - NativeCanvasFrame target, + WebGPUNativeSurface target, + Rectangle targetBounds, WebGPUEncodedScene encodedScene, - TextureFormat textureFormat, - FeatureName requiredFeature, + WebGPUTargetDescriptor targetDescriptor, + WGPUFeatureName requiredFeature, WebGPUSceneBumpSizes bumpSizes, + nuint scratchBufferBindingSizeLimit, ref WebGPUSceneResourceArena? resourceArena) where TPixel : unmanaged, IPixel { WebGPUFlushContext flushContext = WebGPUFlushContext.Create( target, - textureFormat, + targetBounds, + targetDescriptor, requiredFeature, - configuration.MemoryAllocator); + configuration.MemoryAllocator, + scratchBufferBindingSizeLimit); try { - WebGPUSceneBumpSizes seededBumpSizes = SeedSceneBumpSizes(bumpSizes, encodedScene); + WebGPUSceneBumpSizes seededBumpSizes = SeedSceneBumpSizes(bumpSizes, encodedScene, flushContext.ScratchBufferBindingSizeLimit); WebGPUSceneConfig config = WebGPUSceneConfig.Create(encodedScene, seededBumpSizes); uint baseColor = 0U; bool chunkingRequired = false; - if (!TryValidateBindingSizes(encodedScene, config, flushContext.DeviceState.MaxStorageBufferBindingSize, out BindingLimitFailure bindingLimitFailure, out string? error)) + if (!TryValidateEncodedClipStream(encodedScene, out string? error)) + { + throw new InvalidOperationException(error); + } + + if (!TryValidateBindingSizes(encodedScene, config, flushContext.ScratchBufferBindingSizeLimit, out BindingLimitFailure bindingLimitFailure, out error)) { if (!IsChunkableBindingFailure(bindingLimitFailure.Buffer)) { @@ -169,6 +219,8 @@ public static WebGPUStagedScene CreateStagedScene( chunkingRequired = true; } + // No fills means no GPU work; return a resource-less staged scene so callers can run + // the normal render path, which early-outs on the zero draw count. if (encodedScene.FillCount == 0) { return new WebGPUStagedScene( @@ -177,7 +229,8 @@ public static WebGPUStagedScene CreateStagedScene( config, default, chunkingRequired ? bindingLimitFailure : BindingLimitFailure.None, - ownsEncodedScene: false); + false, + true); } if (!WebGPUSceneResources.TryCreate(flushContext, encodedScene, config, baseColor, ref resourceArena, out WebGPUSceneResourceSet resources, out error)) @@ -191,7 +244,8 @@ public static WebGPUStagedScene CreateStagedScene( config, resources, chunkingRequired ? bindingLimitFailure : BindingLimitFailure.None, - ownsEncodedScene: false); + false, + true); } catch { @@ -200,6 +254,287 @@ public static WebGPUStagedScene CreateStagedScene( } } + /// + /// Builds flush-scoped GPU resources for one retained scene range. + /// + /// + /// Unlike the full-scene overload, the flush context is supplied by the caller and stays owned + /// by it; the returned staged scene never disposes it. + /// + /// The pixel format of the flush target. + /// The caller-owned flush context for the current render. + /// The retained encoded scene that owns the range. + /// The scene range to stage. + /// The device feature required by the target format, if any. + /// The scratch capacities carried over from earlier flushes. + /// The optional external texture view supplied by the caller, forwarded to resource creation. + /// The reusable scene resource arena slot for this flush. + /// The staged scene holding the config and GPU resources for the range. + public static unsafe WebGPUStagedScene CreateStagedScene( + WebGPUFlushContext flushContext, + WebGPUEncodedScene encodedScene, + WebGPUSceneRange range, + WGPUFeatureName requiredFeature, + WebGPUSceneBumpSizes bumpSizes, + WGPUTextureViewImpl* externalTextureView, + ref WebGPUSceneResourceArena? resourceArena) + where TPixel : unmanaged, IPixel + { + if (requiredFeature != default && !flushContext.DeviceState.HasFeature(requiredFeature)) + { + throw new NotSupportedException($"The WebGPU device does not support required feature '{requiredFeature}'."); + } + + WebGPUSceneBumpSizes seededBumpSizes = SeedSceneBumpSizes(bumpSizes, range, flushContext.ScratchBufferBindingSizeLimit); + WebGPUSceneConfig config = WebGPUSceneConfig.Create(encodedScene, range, seededBumpSizes); + uint baseColor = 0U; + bool chunkingRequired = false; + + if (!TryValidateEncodedClipStream(encodedScene, range, out string? error)) + { + throw new InvalidOperationException(error); + } + + if (!TryValidateBindingSizes(encodedScene, config, flushContext.ScratchBufferBindingSizeLimit, out BindingLimitFailure bindingLimitFailure, out error)) + { + if (!IsChunkableBindingFailure(bindingLimitFailure.Buffer)) + { + throw new InvalidOperationException(error ?? "The staged WebGPU scene exceeded the current binding limits."); + } + + chunkingRequired = true; + } + + if (range.FillCount == 0) + { + return new WebGPUStagedScene( + flushContext, + encodedScene, + range, + config, + default, + chunkingRequired ? bindingLimitFailure : BindingLimitFailure.None); + } + + if (!WebGPUSceneResources.TryCreate( + flushContext, + encodedScene, + range, + config, + baseColor, + externalTextureView, + ref resourceArena, + out WebGPUSceneResourceSet resources, + out error)) + { + throw new InvalidOperationException(error ?? "Failed to create WebGPU scene resources."); + } + + return new WebGPUStagedScene( + flushContext, + encodedScene, + range, + config, + resources, + chunkingRequired ? bindingLimitFailure : BindingLimitFailure.None); + } + + /// + /// Validates the full encoded scene clip-control stream before GPU staging. + /// + /// The scene whose draw-tag stream will be staged. + /// Receives the validation failure reason. + /// when the clip stream is safe for GPU scheduling. + private static bool TryValidateEncodedClipStream(WebGPUEncodedScene encodedScene, out string? error) + => TryValidateEncodedClipStream( + encodedScene, + 0, + encodedScene.DrawTagCount, + encodedScene.DrawDataWordCount, + encodedScene.InfoWordCount, + encodedScene.PathCount, + encodedScene.ClipCount, + "full scene", + out error); + + /// + /// Validates one encoded range clip-control stream before GPU staging. + /// + /// The scene that owns the packed draw-tag stream. + /// The staged range inside . + /// Receives the validation failure reason. + /// when the clip stream is safe for GPU scheduling. + private static bool TryValidateEncodedClipStream(WebGPUEncodedScene encodedScene, WebGPUSceneRange range, out string? error) + => TryValidateEncodedClipStream( + encodedScene, + range.DrawTagStart, + range.DrawTagCount, + range.DrawDataWordCount, + range.InfoWordCount, + range.PathCount, + range.ClipCount, + "scene range", + out error); + + /// + /// Validates the BeginClip/EndClip sequence consumed by clip_leaf.wgsl. + /// + /// The scene that owns the packed draw-tag stream. + /// The first draw-tag index in the stream slice. + /// The number of draw tags in the stream slice. + /// The draw-data word count declared for the stream slice. + /// The info word count declared for the stream slice. + /// The path count declared for the stream slice. + /// The expected number of clip-control draw tags in the stream slice. + /// A short label used in the validation message. + /// Receives the validation failure reason. + /// when the stream slice satisfies the shader clip-stack contract. + private static bool TryValidateEncodedClipStream( + WebGPUEncodedScene encodedScene, + int drawTagStart, + int drawTagCount, + int expectedDrawDataWordCount, + int expectedInfoWordCount, + int expectedPathCount, + int expectedClipCount, + string label, + out string? error) + { + if (drawTagStart < 0 || drawTagCount < 0 || drawTagStart + drawTagCount > encodedScene.DrawTagCount) + { + error = $"The WebGPU {label} draw-tag range [{drawTagStart}, {drawTagStart + drawTagCount}) is outside the encoded draw-tag stream length {encodedScene.DrawTagCount}."; + return false; + } + + ReadOnlySpan sceneWords = encodedScene.SceneData.Span; + long drawTagWordStart = encodedScene.Layout.DrawTagBase + drawTagStart; + long drawTagWordEnd = drawTagWordStart + drawTagCount; + if (drawTagWordStart < 0 || drawTagWordEnd > sceneWords.Length) + { + error = $"The WebGPU {label} draw-tag words [{drawTagWordStart}, {drawTagWordEnd}) are outside the packed scene word length {sceneWords.Length}."; + return false; + } + + ReadOnlySpan drawTags = sceneWords.Slice((int)drawTagWordStart, drawTagCount); + int depth = 0; + int clipCount = 0; + long pathIndex = 0; + long clipIndex = 0; + long sceneOffset = 0; + long infoOffset = 0; + Span clipPathStack = stackalloc long[MaxClipStackDepth]; + Span clipDrawStack = stackalloc long[MaxClipStackDepth]; + for (int i = 0; i < drawTags.Length; i++) + { + uint drawTag = drawTags[i]; + GpuSceneDrawMonoid drawMonoid = GpuSceneDrawTag.Map(drawTag); + if (drawTag != GpuSceneDrawTag.Nop && pathIndex >= expectedPathCount) + { + error = $"The WebGPU {label} draw tag {drawTagStart + i} references path {pathIndex}, but the stream declares {expectedPathCount} paths."; + return false; + } + + if (sceneOffset + drawMonoid.SceneOffset > expectedDrawDataWordCount) + { + error = $"The WebGPU {label} draw tag {drawTagStart + i} reads draw-data words [{sceneOffset}, {sceneOffset + drawMonoid.SceneOffset}), but the stream declares {expectedDrawDataWordCount} words."; + return false; + } + + if (infoOffset + drawMonoid.InfoOffset > expectedInfoWordCount) + { + error = $"The WebGPU {label} draw tag {drawTagStart + i} writes info words [{infoOffset}, {infoOffset + drawMonoid.InfoOffset}), but the stream declares {expectedInfoWordCount} words."; + return false; + } + + if (drawTag == GpuSceneDrawTag.BeginClip) + { + // clip_leaf.wgsl reconstructs clip parents with one 256-entry workgroup stack. + if (depth == MaxClipStackDepth) + { + error = $"The WebGPU {label} clip stack exceeds {MaxClipStackDepth} entries at draw tag {drawTagStart + i}."; + return false; + } + + clipPathStack[depth] = pathIndex; + clipDrawStack[depth] = i; + depth++; + clipCount++; + } + else if (drawTag == GpuSceneDrawTag.EndClip) + { + if (depth == 0) + { + error = $"The WebGPU {label} has an EndClip without a matching BeginClip at draw tag {drawTagStart + i}."; + return false; + } + + depth--; + + // This mirrors Vello's CPU clip_leaf stack: EndClip must pop a BeginClip whose + // path and draw indices remain valid inside the range-local GPU buffers. + long parentPathIndex = clipPathStack[depth]; + long parentDrawIndex = clipDrawStack[depth]; + if (parentPathIndex < 0 || parentPathIndex >= expectedPathCount) + { + error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to path {parentPathIndex}, but the stream declares {expectedPathCount} paths."; + return false; + } + + if (parentDrawIndex < 0 || parentDrawIndex >= drawTagCount) + { + error = $"The WebGPU {label} EndClip at draw tag {drawTagStart + i} resolves to draw tag {parentDrawIndex}, but the stream contains {drawTagCount} draw tags."; + return false; + } + + clipCount++; + } + + pathIndex += drawMonoid.PathIndex; + clipIndex += drawMonoid.ClipIndex; + sceneOffset += drawMonoid.SceneOffset; + infoOffset += drawMonoid.InfoOffset; + } + + if (depth != 0) + { + error = $"The WebGPU {label} closes with {depth} unmatched BeginClip records."; + return false; + } + + if (pathIndex != expectedPathCount) + { + error = $"The WebGPU {label} declares {expectedPathCount} paths but the draw-tag stream encodes {pathIndex}."; + return false; + } + + if (clipIndex != expectedClipCount) + { + error = $"The WebGPU {label} declares {expectedClipCount} clip records but the draw-tag monoid encodes {clipIndex}."; + return false; + } + + if (sceneOffset != expectedDrawDataWordCount) + { + error = $"The WebGPU {label} declares {expectedDrawDataWordCount} draw-data words but the draw-tag stream encodes {sceneOffset}."; + return false; + } + + if (infoOffset != expectedInfoWordCount) + { + error = $"The WebGPU {label} declares {expectedInfoWordCount} info words but the draw-tag stream encodes {infoOffset}."; + return false; + } + + if (clipCount != expectedClipCount) + { + error = $"The WebGPU {label} declares {expectedClipCount} clip records but contains {clipCount} BeginClip/EndClip draw tags."; + return false; + } + + error = null; + return true; + } + /// /// Checks whether one encoded scene can be bound to the current WebGPU staged pipeline. /// @@ -223,7 +558,7 @@ public static bool TryValidateBindingSizes( { bindingLimitFailure = BindingLimitFailure.None; WebGPUSceneBufferSizes bufferSizes = config.BufferSizes; - nuint infoBinDataByteLength = checked(GetBindingByteLength(encodedScene.InfoBufferWordCount) + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength); + nuint infoBinDataByteLength = checked(config.BufferSizes.Info.ByteLength + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength); if (!TryValidateBufferSize(GetBindingByteLength(1), "scene config", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) || !TryValidateBufferSize(GetBindingByteLength(encodedScene.SceneWordCount), "scene data", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) || !TryValidateBufferSize(bufferSizes.PathReduced.ByteLength, "path reduced", BindingLimitBuffer.None, maxStorageBufferBindingSize, out bindingLimitFailure, out error) || @@ -257,65 +592,14 @@ public static bool TryValidateBindingSizes( return true; } - /// - /// Checks the encoded-scene stats that must fit inside the staged pipeline's fixed scratch buffers. - /// - /// - /// These are the pipeline's current bump-allocator capacities. Unlike the API binding limit above, - /// these sizes are expected to become growable state as the robust dynamic-memory path is completed. - /// - /// The encoded scene whose aggregate scheduling counts are being checked. - /// The staged-scene buffer plan that provides the current scratch capacities. - /// Receives the validation failure reason when a required scratch buffer would overflow. - /// when the encoded scene fits inside the current scratch capacities; otherwise, . - public static bool TryValidateScratchCapacities( - WebGPUEncodedScene encodedScene, - WebGPUSceneConfig config, - out string? error) - { - WebGPUSceneBufferSizes bufferSizes = config.BufferSizes; - uint lineCount = checked((uint)encodedScene.LineCount); - uint pathRowEstimate = checked((uint)encodedScene.TotalPathRowCount); - uint pathTileFloor = checked((uint)Math.Max(encodedScene.TotalPathRowCount, encodedScene.PathCount)); - uint initialPtclWords = checked((uint)encodedScene.TileCountX * (uint)encodedScene.TileCountY * PtclInitialAlloc); - - if (lineCount > bufferSizes.Lines.Length) - { - error = $"The staged-scene line buffer reserves {bufferSizes.Lines.Length} entries, but this scene encodes {lineCount} lines."; - return false; - } - - if (lineCount > bufferSizes.SegCounts.Length) - { - error = $"The staged-scene segment-count buffer reserves {bufferSizes.SegCounts.Length} entries, but this scene needs at least {lineCount}."; - return false; - } - - if (pathRowEstimate > bufferSizes.PathRows.Length) - { - error = $"The staged-scene path-row buffer reserves {bufferSizes.PathRows.Length} entries, but this scene needs at least {pathRowEstimate}."; - return false; - } - - if (pathTileFloor > bufferSizes.PathTiles.Length) - { - error = $"The staged-scene path-tile buffer reserves {bufferSizes.PathTiles.Length} entries, but this scene needs at least {pathTileFloor} sparse tiles."; - return false; - } - - if (initialPtclWords > bufferSizes.Ptcl.Length) - { - error = $"The staged-scene PTCL buffer reserves {bufferSizes.Ptcl.Length} words, but the tile-grid bootstrap alone requires {initialPtclWords}."; - return false; - } - - error = null; - return true; - } - /// /// Dispatches the early Vello-style scheduling stages for one staged scene. /// + /// The staged scene whose scheduling stages are being dispatched. + /// The reusable scheduling arena that owns the transient scratch buffers. + /// Receives the transient scheduling resources bound by the later fine and readback stages. + /// Receives the recording or dispatch failure reason. + /// when every scheduling stage was dispatched successfully; otherwise, . public static bool TryDispatchSchedulingStages( ref WebGPUStagedScene stagedScene, WebGPUSceneSchedulingArena arena, @@ -357,7 +641,7 @@ private static unsafe bool TryDispatchSharedSchedulingStages( nuint pathBboxBufferSize = bufferSizes.PathBboxes.ByteLength; nuint drawReducedBufferSize = bufferSizes.DrawReduced.ByteLength; nuint drawMonoidBufferSize = bufferSizes.DrawMonoids.ByteLength; - nuint infoBinDataBufferSize = checked(GetBindingByteLength(encodedScene.InfoBufferWordCount) + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength); + nuint infoBinDataBufferSize = checked(bufferSizes.Info.ByteLength + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength); nuint clipInputBufferSize = bufferSizes.ClipInputs.ByteLength; nuint clipElementBufferSize = bufferSizes.ClipElements.ByteLength; nuint clipBicBufferSize = bufferSizes.ClipBics.ByteLength; @@ -365,7 +649,7 @@ private static unsafe bool TryDispatchSharedSchedulingStages( nuint drawBboxBufferSize = bufferSizes.DrawBboxes.ByteLength; nuint lineBufferSize = bufferSizes.Lines.ByteLength; nuint binHeaderBufferSize = bufferSizes.BinHeaders.ByteLength; - if (encodedScene.FillCount == 0) + if (GetRenderableDrawCount(stagedScene) == 0) { error = null; return true; @@ -377,8 +661,8 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } - WgpuBuffer* binHeaderBuffer = arena.BinHeaderBuffer; - WgpuBuffer* bumpBuffer = arena.BumpBuffer; + WGPUBufferImpl* binHeaderBuffer = arena.BinHeaderBuffer; + WGPUBufferImpl* bumpBuffer = arena.BumpBuffer; WebGPUSceneResourceRegistry resourceRegistry = WebGPUSceneResourceRegistry.Create(stagedScene.Resources); resourceRegistry.RegisterSchedulingBuffers( @@ -394,9 +678,10 @@ private static unsafe bool TryDispatchSharedSchedulingStages( WebGPUSceneComputeRecording recording = new(resourceRegistry); + // Prepare must run before any allocating stage: it zeroes the bump counters and failure + // mask on the GPU so this attempt reports true scratch demand without a CPU buffer clear. if (!TryDispatchPrepare( recording, - stagedScene.Resources.HeaderBuffer, bumpBuffer, PrepareComputeShader.GetDispatchX(), out error)) @@ -416,6 +701,8 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } + // Tag streams too large for one level of workgroup partials need the three-pass chain + // (reduce2 -> scan1 -> large scan); small streams scan the first-level partials directly. if (workgroupCounts.UseLargePathScan) { if (!TryDispatchPathtagReduce2( @@ -471,6 +758,8 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } + // Flatten accumulates path bboxes with atomic min/max, so every box must be reset to an + // inverted empty state first. if (!TryDispatchBboxClear( recording, flushContext, @@ -525,7 +814,10 @@ private static unsafe bool TryDispatchSharedSchedulingStages( return false; } - if (encodedScene.ClipCount > 0) + // ClipLeafX is zero when the scene has no BeginClip/EndClip records. clip_leaf processes + // the final (possibly partial) 256-element partition itself, so clip_reduce is skipped + // when the clip stream fits inside a single partition (ClipReduceX == 0). + if (workgroupCounts.ClipLeafX > 0) { if (workgroupCounts.ClipReduceX > 0 && !TryDispatchClipReduce( @@ -559,6 +851,9 @@ private static unsafe bool TryDispatchSharedSchedulingStages( } } + // Binning is the last shared stage: it needs final draw monoids and clip bboxes, and its + // outputs (bin headers, element lists, clipped draw bboxes) are chunk-invariant, which is + // what lets the oversized-scene path reuse this whole recording across tile windows. if (!TryDispatchBinning( recording, flushContext, @@ -604,7 +899,8 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( WebGPUSceneWorkgroupCounts workgroupCounts = config.WorkgroupCounts; nuint sceneBufferSize = GetBindingByteLength(encodedScene.SceneWordCount); nuint drawMonoidBufferSize = bufferSizes.DrawMonoids.ByteLength; - nuint infoBinDataBufferSize = checked(GetBindingByteLength(encodedScene.InfoBufferWordCount) + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength); + nuint infoBinDataBufferSize = checked(bufferSizes.Info.ByteLength + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength); + nuint pathBboxBufferSize = bufferSizes.PathBboxes.ByteLength; nuint drawBboxBufferSize = bufferSizes.DrawBboxes.ByteLength; nuint pathBufferSize = bufferSizes.Paths.ByteLength; nuint lineBufferSize = bufferSizes.Lines.ByteLength; @@ -614,7 +910,7 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( nuint segCountBufferSize = bufferSizes.SegCounts.ByteLength; nuint segmentBufferSize = bufferSizes.Segments.ByteLength; nuint ptclBufferSize = bufferSizes.Ptcl.ByteLength; - if (encodedScene.FillCount == 0) + if (GetRenderableDrawCount(stagedScene) == 0) { error = null; return true; @@ -626,15 +922,15 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } - WgpuBuffer* binHeaderBuffer = arena.BinHeaderBuffer; - WgpuBuffer* indirectCountBuffer = arena.IndirectCountBuffer; - WgpuBuffer* pathRowBuffer = arena.PathRowBuffer; - WgpuBuffer* pathTileBuffer = arena.PathTileBuffer; - WgpuBuffer* segCountBuffer = arena.SegCountBuffer; - WgpuBuffer* segmentBuffer = arena.SegmentBuffer; - WgpuBuffer* blendBuffer = arena.BlendBuffer; - WgpuBuffer* ptclBuffer = arena.PtclBuffer; - WgpuBuffer* bumpBuffer = arena.BumpBuffer; + WGPUBufferImpl* binHeaderBuffer = arena.BinHeaderBuffer; + WGPUBufferImpl* indirectCountBuffer = arena.IndirectCountBuffer; + WGPUBufferImpl* pathRowBuffer = arena.PathRowBuffer; + WGPUBufferImpl* pathTileBuffer = arena.PathTileBuffer; + WGPUBufferImpl* segCountBuffer = arena.SegCountBuffer; + WGPUBufferImpl* segmentBuffer = arena.SegmentBuffer; + WGPUBufferImpl* blendBuffer = arena.BlendBuffer; + WGPUBufferImpl* ptclBuffer = arena.PtclBuffer; + WGPUBufferImpl* bumpBuffer = arena.BumpBuffer; WebGPUSceneResourceRegistry resourceRegistry = WebGPUSceneResourceRegistry.Create(stagedScene.Resources); resourceRegistry.RegisterSchedulingBuffers( @@ -650,6 +946,8 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( WebGPUSceneComputeRecording recording = new(resourceRegistry); + // chunk_reset clears only the chunk-local bump counters; the shared flatten/binning + // counters and failure bits must survive because those stages are not rerun per window. if (resetChunkLocalBumpAllocators && !TryDispatchChunkReset(recording, bumpBuffer, ChunkResetComputeShader.GetDispatchX(), out error)) { @@ -672,6 +970,10 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } + // path_count_setup reads only bump.lines, which the shared flatten pass has already + // produced, so it can run here even though path_count itself comes later. The same + // indirect argument buffer drives both per-line dispatches (path_row_span and path_count) + // because they use the same workgroup size. if (!TryDispatchPathCountSetup( recording, flushContext, @@ -698,6 +1000,8 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } + // tile_alloc must observe final row spans from path_row_span before it widens them and + // reserves the backing tile storage; path_count then relies on each row's tile base index. if (!TryDispatchTileAlloc( recording, flushContext, @@ -733,6 +1037,8 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } + // backdrop converts the per-tile winding deltas accumulated by path_count into absolute + // winding numbers, which coarse needs to classify solid versus empty tiles. if (!TryDispatchBackdrop( recording, flushContext, @@ -771,6 +1077,8 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( return false; } + // path_tiling_setup runs after coarse so it can perform the late seg_counts overflow check + // and, on failure, zero the indirect dispatch and write the PTCL abort marker for fine. if (!TryDispatchPathTilingSetup( recording, flushContext, @@ -834,6 +1142,11 @@ private static unsafe bool TryDispatchChunkLocalSchedulingStages( /// that reference. When this method replaces an undersized arena, it clears the reference /// before storing the new arena so the caller never returns a disposed arena to a cache. /// + /// The flush context that owns the device used to create replacement buffers. + /// The buffer plan the arena must be able to satisfy. + /// The required size of the map-readable status buffer. + /// The rented arena slot, replaced in place when undersized. + /// The arena stored in after the check. private static WebGPUSceneSchedulingArena EnsureSchedulingArena( WebGPUFlushContext flushContext, WebGPUSceneBufferSizes bufferSizes, @@ -852,175 +1165,565 @@ private static WebGPUSceneSchedulingArena EnsureSchedulingArena( return arena; } - private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( - WebGPUFlushContext flushContext, - WebGPUSceneBufferSizes bufferSizes, - nuint readbackByteLength) - { - WgpuBuffer* binHeaderBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.BinHeaders.ByteLength); - WgpuBuffer* indirectCountBuffer = CreateArenaIndirectStorageBuffer(flushContext, bufferSizes.IndirectCount.ByteLength); - WgpuBuffer* pathRowBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathRows.ByteLength); - WgpuBuffer* pathTileBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathTiles.ByteLength); - WgpuBuffer* segCountBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.SegCounts.ByteLength); - WgpuBuffer* segmentBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.Segments.ByteLength); - WgpuBuffer* blendBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.BlendSpill.ByteLength); - WgpuBuffer* ptclBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.Ptcl.ByteLength); - - GpuSceneBumpAllocators bumpAllocators = default; - WgpuBuffer* bumpBuffer = CreateAndUploadArenaStorageBuffer(flushContext, in bumpAllocators); - - BufferDescriptor readbackDescriptor = new() - { - Usage = BufferUsage.CopyDst | BufferUsage.MapRead, - Size = readbackByteLength, - MappedAtCreation = false - }; - - WgpuBuffer* readbackBuffer; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - readbackBuffer = flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in readbackDescriptor); - } - - return new WebGPUSceneSchedulingArena( - flushContext.Api, - flushContext.DeviceHandle, - bufferSizes, - readbackByteLength, - binHeaderBuffer, - indirectCountBuffer, - pathRowBuffer, - pathTileBuffer, - segCountBuffer, - segmentBuffer, - blendBuffer, - ptclBuffer, - bumpBuffer, - readbackBuffer); - } - /// - /// Executes the staged scene pipeline against the current flush target. + /// Submits one ordered range into an uncommitted composition texture and copies its allocator + /// status into a caller-owned barrier buffer without mapping or waiting for the GPU. /// - /// The staged scene to render. - /// The reusable scheduling arena slot for this render. - /// The advisory chunk height to try first when this scene requires chunking. - /// Receives whether the caller should retry with larger scratch capacities. - /// Receives the scratch capacities reported by the render attempt. - /// Receives the largest chunk height that rendered successfully. - /// Receives the failure reason when the staged scene cannot be rendered. - /// when the staged scene rendered successfully; otherwise, . - public static unsafe bool TryRenderStagedScene( + /// The staged range to submit. + /// The validated or transaction-local image sampled by the fine pass. + /// The composition texture receiving the complete range output. + /// The storage view for . + /// The reusable scheduling arena slot for this flush. + /// The advisory first chunk height for an oversized range. + /// The flush barrier buffer receiving allocator records. + /// The byte offset of this range's first allocator record. + /// Receives the capacity used by each emitted status record. + /// Receives each chunk height, or zero for a monolithic range. + /// Receives the number of allocator records emitted by this range. + /// Receives the range height in fine-pass tile rows. + /// Receives the largest chunk height submitted successfully. + /// Receives the exact index of the submission containing the final status copy. + /// Receives the submission failure reason. + /// when the range was submitted; otherwise, . + public static unsafe bool TrySubmitTransactionalStagedScene( ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, + WGPUTextureImpl* outputTexture, + WGPUTextureViewImpl* outputTextureView, ref WebGPUSceneSchedulingArena? schedulingArena, uint initialChunkTileHeightHint, - out bool requiresGrowth, - out WebGPUSceneBumpSizes grownBumpSizes, + WGPUBufferImpl* statusReadbackBuffer, + nuint statusReadbackOffset, + Span submittedChunkBumpSizes, + Span submittedChunkTileHeights, + out int statusCount, + out uint fullTileHeight, out uint successfulChunkTileHeight, + out ulong submissionIndex, out string? error) { - requiresGrowth = false; - grownBumpSizes = stagedScene.Config.BumpSizes; + statusCount = 0; + fullTileHeight = checked((uint)Math.Max((backdropTarget.Bounds.Height + 15) / 16, 1)); successfulChunkTileHeight = 0; + submissionIndex = default; error = null; - WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; - if (encodedScene.FillCount == 0) - { - return true; - } - - // Oversized scenes that exceed the device binding limit use tile-row chunking. - // The chunked path ensures its own arena per chunk with chunk-local sizes. if (IsChunkableBindingFailure(stagedScene.BindingLimitFailure.Buffer)) { - return TryRenderSegmentChunkedStagedScene( + return TrySubmitTransactionalChunkedStagedScene( ref stagedScene, + backdropTarget, + outputTexture, + outputTextureView, ref schedulingArena, initialChunkTileHeightHint, - out requiresGrowth, - out grownBumpSizes, + statusReadbackBuffer, + statusReadbackOffset, + submittedChunkBumpSizes, + submittedChunkTileHeights, + out statusCount, + out fullTileHeight, out successfulChunkTileHeight, + out submissionIndex, out error); } - // Normal path: ensure arena with full-scene sizes. WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena( stagedScene.FlushContext, stagedScene.Config.BufferSizes, (nuint)sizeof(GpuSceneBumpAllocators), ref schedulingArena); - if (!TryDispatchSchedulingStages(ref stagedScene, currentArena, out WebGPUSceneSchedulingResources scheduling, out error)) - { - return false; - } - WebGPUFlushContext flushContext = stagedScene.FlushContext; - int targetWidth = encodedScene.TargetSize.Width; - int targetHeight = encodedScene.TargetSize.Height; - - if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out Texture* outputTexture, out TextureView* outputTextureView, out error)) - { - return false; - } - - if (!TryDispatchFineArea( + if (!TryWriteSceneHeader(flushContext, stagedScene.Resources.HeaderBuffer, CreateHeader(stagedScene, 0U), out error) || + !TryDispatchSchedulingStages(ref stagedScene, currentArena, out WebGPUSceneSchedulingResources scheduling, out error) || + !TryDispatchFineArea( flushContext, stagedScene.Resources, - encodedScene, stagedScene.Config.BufferSizes, scheduling, outputTextureView, - (uint)encodedScene.TileCountX, - (uint)encodedScene.TileCountY, - out error)) + backdropTarget.TextureView, + checked((uint)((backdropTarget.Bounds.Width + 15) / 16)), + checked((uint)((backdropTarget.Bounds.Height + 15) / 16)), + out error) || + !TryEnqueueSchedulingStatusReadback(flushContext, scheduling.BumpBuffer, statusReadbackBuffer, statusReadbackOffset, out error) || + !WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out submissionIndex)) { return false; } - // Single submit: scheduling + fine + readback all in one command encoder. - // The readback map blocks until the GPU finishes everything. - if (!TryEnqueueSchedulingStatusReadback(flushContext, scheduling.BumpBuffer, currentArena.ReadbackBuffer, 0, out error) || - !WebGPUDrawingBackend.TrySubmit(flushContext) || - !TryReadSchedulingStatus(flushContext, currentArena.ReadbackBuffer, out GpuSceneBumpAllocators bumpAllocators, out error)) - { - return false; - } + submittedChunkBumpSizes[0] = stagedScene.Config.BumpSizes; + submittedChunkTileHeights[0] = 0; + statusCount = 1; + return true; + } - // Overflow: the fine output is discarded, but the scheduling readback still reports - // the scratch usage visible to this pass. Later-stage demand can stay hidden until - // earlier overflows are resolved, so the backend retries with larger buffers until - // the capacities converge or the bounded attempt budget is exhausted. - if (RequiresScratchReallocation(in bumpAllocators, stagedScene.Config.BumpSizes)) - { - requiresGrowth = true; - grownBumpSizes = GrowBumpSizes(stagedScene.Config.BumpSizes, in bumpAllocators); - error = "The staged WebGPU scene needs larger scratch buffers and will be retried."; - return false; - } + /// + /// Submits one oversized ordered range in tile-row chunks while retaining every allocator + /// record in the caller's shared transaction buffer. + /// + /// The oversized staged range to submit. + /// The transaction image sampled by every chunk's fine pass. + /// The composition texture receiving all chunk output. + /// The storage view for . + /// The reusable scheduling arena slot for this flush. + /// The advisory first chunk height. + /// The flush barrier buffer receiving chunk allocator records. + /// The byte offset of the first chunk record. + /// Receives the capacity used by each chunk. + /// Receives the real height of each chunk. + /// Receives the number of submitted chunks. + /// Receives the full range height in tile rows. + /// Receives the largest successful chunk height. + /// Receives the submission containing the final chunk status. + /// Receives the chunk submission failure reason. + /// when every chunk was submitted; otherwise, . + private static unsafe bool TrySubmitTransactionalChunkedStagedScene( + ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, + WGPUTextureImpl* outputTexture, + WGPUTextureViewImpl* outputTextureView, + ref WebGPUSceneSchedulingArena? schedulingArena, + uint initialChunkTileHeightHint, + WGPUBufferImpl* statusReadbackBuffer, + nuint statusReadbackOffset, + Span submittedChunkBumpSizes, + Span submittedChunkTileHeights, + out int statusCount, + out uint fullTileHeight, + out uint successfulChunkTileHeight, + out ulong submissionIndex, + out string? error) + { + statusCount = 0; + fullTileHeight = checked((uint)Math.Max((backdropTarget.Bounds.Height + 15) / 16, 1)); + successfulChunkTileHeight = 0; + submissionIndex = default; + error = null; + + WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; + WebGPUFlushContext flushContext = stagedScene.FlushContext; + nuint maxStorageBufferBindingSize = flushContext.ScratchBufferBindingSizeLimit; if (!flushContext.EnsureCommandEncoder()) { - error = "Failed to create a command encoder for the staged-scene final copy."; + error = "Failed to create a command encoder for the transactional chunk prefill."; + return false; + } + + // Chunked fine dispatch writes only its assigned tile rows. Seed the complete output from + // the transaction backdrop so untouched pixels preserve the preceding logical image. + CopyTargetToTexture(ref stagedScene, backdropTarget, outputTexture, backdropTarget.Bounds.Width, backdropTarget.Bounds.Height); + + uint tileYStart = 0; + uint nextChunkTileHeight = initialChunkTileHeightHint == 0 + ? GetInitialChunkTileHeight(fullTileHeight, stagedScene.BindingLimitFailure) + : Math.Min(initialChunkTileHeightHint, fullTileHeight); + + bool sharedSchedulingPrepared = false; + + while (tileYStart < fullTileHeight) + { + uint remainingTileHeight = fullTileHeight - tileYStart; + uint requestedTileHeight = Math.Min(nextChunkTileHeight, remainingTileHeight); + + while (true) + { + WebGPUSceneChunkWindow chunkWindow = CreateChunkWindow(tileYStart, requestedTileHeight, remainingTileHeight); + WebGPUSceneBumpSizes chunkBumpSize = stagedScene.Range.HasValue + ? ScaleChunkBumpSizes(stagedScene.Config.BumpSizes, stagedScene.Range.Value, chunkWindow.TileHeight) + : ScaleChunkBumpSizes(stagedScene.Config.BumpSizes, encodedScene, chunkWindow.TileHeight); + + WebGPUSceneConfig chunkConfig = stagedScene.Range.HasValue + ? WebGPUSceneConfig.Create(encodedScene, stagedScene.Range.Value, chunkBumpSize, chunkWindow) + : WebGPUSceneConfig.Create(encodedScene, chunkBumpSize, chunkWindow); + + if (!TryValidateBindingSizes(encodedScene, chunkConfig, maxStorageBufferBindingSize, out BindingLimitFailure bindingLimitFailure, out error)) + { + if (IsChunkableBindingFailure(bindingLimitFailure.Buffer)) + { + uint smallerTileHeight = ShrinkChunkTileHeight(requestedTileHeight, remainingTileHeight, bindingLimitFailure); + if (smallerTileHeight >= requestedTileHeight) + { + return false; + } + + requestedTileHeight = smallerTileHeight; + continue; + } + + return false; + } + + WebGPUStagedScene chunkScene = stagedScene.Range.HasValue + ? new WebGPUStagedScene(flushContext, encodedScene, stagedScene.Range.Value, chunkConfig, stagedScene.Resources, BindingLimitFailure.None) + : new WebGPUStagedScene(flushContext, encodedScene, chunkConfig, stagedScene.Resources, BindingLimitFailure.None, false, false); + + WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena( + flushContext, + chunkConfig.BufferSizes, + (nuint)sizeof(GpuSceneBumpAllocators), + ref schedulingArena); + + bool useSharedSchedulingState = sharedSchedulingPrepared || chunkWindow.TileHeight < fullTileHeight; + if (!sharedSchedulingPrepared && useSharedSchedulingState) + { + if (!TryDispatchSharedSchedulingStages(ref stagedScene, currentArena, out error) || + !WebGPUDrawingBackend.TrySubmitPendingCommands(flushContext)) + { + error ??= "Failed to submit the shared transactional chunk scheduling stages."; + return false; + } + + sharedSchedulingPrepared = true; + } + + nuint chunkStatusOffset = checked(statusReadbackOffset + ((nuint)statusCount * (nuint)sizeof(GpuSceneBumpAllocators))); + bool recordedChunk = useSharedSchedulingState + ? TryRecordChunkAttempt( + ref chunkScene, + backdropTarget, + currentArena, + outputTextureView, + statusReadbackBuffer, + chunkStatusOffset, + statusCount > 0, + out error) + : TryRenderChunkAttempt( + ref chunkScene, + backdropTarget, + currentArena, + outputTextureView, + statusReadbackBuffer, + chunkStatusOffset, + useSharedSchedulingState, + resetChunkLocalBumpAllocators: false, + out submissionIndex, + out error); + + if (!recordedChunk) + { + return false; + } + + submittedChunkBumpSizes[statusCount] = chunkConfig.BumpSizes; + submittedChunkTileHeights[statusCount] = chunkWindow.TileHeight; + statusCount++; + tileYStart += chunkWindow.TileHeight; + nextChunkTileHeight = requestedTileHeight; + successfulChunkTileHeight = Math.Max(successfulChunkTileHeight, chunkWindow.TileHeight); + break; + } + } + + if (sharedSchedulingPrepared && !WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out submissionIndex)) + { + error = "Failed to submit the transactional chunk passes."; + return false; + } + + return true; + } + + /// + /// Creates a new scheduling arena: the transient scratch storage buffers, the zero-initialized + /// bump-allocator buffer, and the map-readable status readback buffer. + /// + /// The flush context that owns the device used to create the buffers. + /// The buffer plan that sizes each scratch buffer. + /// The size of the map-readable status buffer; the chunked path reserves one allocator record per chunk. + /// The newly created arena, owned by the caller. + private static unsafe WebGPUSceneSchedulingArena CreateSchedulingArena( + WebGPUFlushContext flushContext, + WebGPUSceneBufferSizes bufferSizes, + nuint readbackByteLength) + { + WGPUBufferImpl* binHeaderBuffer = null; + WGPUBufferImpl* indirectCountBuffer = null; + WGPUBufferImpl* pathRowBuffer = null; + WGPUBufferImpl* pathTileBuffer = null; + WGPUBufferImpl* segCountBuffer = null; + WGPUBufferImpl* segmentBuffer = null; + WGPUBufferImpl* blendBuffer = null; + WGPUBufferImpl* ptclBuffer = null; + WGPUBufferImpl* bumpBuffer = null; + + try + { + binHeaderBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.BinHeaders.ByteLength); + indirectCountBuffer = CreateArenaIndirectStorageBuffer(flushContext, bufferSizes.IndirectCount.ByteLength); + pathRowBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathRows.ByteLength); + pathTileBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.PathTiles.ByteLength); + segCountBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.SegCounts.ByteLength); + segmentBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.Segments.ByteLength); + blendBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.BlendSpill.ByteLength); + ptclBuffer = CreateArenaStorageBuffer(flushContext, bufferSizes.Ptcl.ByteLength); + + GpuSceneBumpAllocators bumpAllocators = default; + bumpBuffer = CreateAndUploadArenaStorageBuffer(flushContext, in bumpAllocators); + + WGPUBufferDescriptor readbackDescriptor = new() + { + usage = (ulong)(BufferUsage.CopyDst | BufferUsage.MapRead), + size = readbackByteLength, + mappedAtCreation = 0U, + }; + + WGPUBufferImpl* readbackBuffer; + using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) + { + readbackBuffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in readbackDescriptor); + } + + return new WebGPUSceneSchedulingArena( + flushContext.Api, + flushContext.DeviceHandle, + bufferSizes, + readbackByteLength, + binHeaderBuffer, + indirectCountBuffer, + pathRowBuffer, + pathTileBuffer, + segCountBuffer, + segmentBuffer, + blendBuffer, + ptclBuffer, + bumpBuffer, + readbackBuffer); + } + catch + { + // Nothing owns these buffers until the arena is constructed; a throw mid-sequence + // (a closed device handle, checked-size overflow) must release the ones created. + WebGPU api = flushContext.Api; + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, binHeaderBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, indirectCountBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, pathRowBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, pathTileBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, segCountBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, segmentBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, blendBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, ptclBuffer); + WebGPUSceneSchedulingArena.ReleaseArenaBuffer(api, bumpBuffer); + throw; + } + } + + /// + /// Executes the staged scene pipeline against the current flush target. + /// + /// The staged scene to render. + /// The reusable scheduling arena slot for this render. + /// The advisory chunk height to try first when this scene requires chunking. + /// + /// Whether to defer the scratch-overflow readback instead of blocking on the GPU. + /// + /// Receives whether the caller should retry with larger scratch capacities. + /// Receives the scratch capacities reported by the render attempt. + /// Receives the largest chunk height that rendered successfully. + /// + /// Receives the deferred scheduling-status readback when the render deferred its overflow + /// check; the caller must resolve it before its next flush of the same scene. + /// + /// Receives the failure reason when the staged scene cannot be rendered. + /// when the staged scene rendered successfully; otherwise, . + public static unsafe bool TryRenderStagedScene( + ref WebGPUStagedScene stagedScene, + ref WebGPUSceneSchedulingArena? schedulingArena, + uint initialChunkTileHeightHint, + bool deferOverflowCheck, + out bool requiresGrowth, + out WebGPUSceneBumpSizes grownBumpSizes, + out uint successfulChunkTileHeight, + out WebGPUPendingSchedulingStatus? pendingStatus, + out string? error) + { + WebGPUFlushContext flushContext = stagedScene.FlushContext; + using WebGPUHandle.HandleReference targetTextureReference = flushContext.TargetTextureHandle.AcquireReference(); + using WebGPUHandle.HandleReference targetTextureViewReference = flushContext.TargetTextureViewHandle.AcquireReference(); + + WebGPUSceneTarget target = new( + (WGPUTextureImpl*)targetTextureReference.Handle, + (WGPUTextureViewImpl*)targetTextureViewReference.Handle, + flushContext.TargetBounds, + flushContext.TargetTextureOffset); + + if (!WebGPUDrawingBackend.TryCreateSampleableBackdrop(flushContext, target, out WebGPUSceneTarget backdropTarget, out error)) + { + requiresGrowth = false; + grownBumpSizes = stagedScene.Config.BumpSizes; + successfulChunkTileHeight = 0; + pendingStatus = null; + return false; + } + + return TryRenderStagedScene( + ref stagedScene, + backdropTarget, + target, + ref schedulingArena, + initialChunkTileHeightHint, + deferOverflowCheck, + out requiresGrowth, + out grownBumpSizes, + out successfulChunkTileHeight, + out pendingStatus, + out error); + } + + /// + /// Executes the staged scene pipeline against the supplied render-time target. + /// + /// The staged scene to render. + /// The sampleable texture holding the target contents before this render. + /// The render-time target receiving the staged scene output. + /// The reusable scheduling arena slot for this render. + /// The advisory chunk height to try first when this scene requires chunking. + /// + /// Whether to defer the scratch-overflow readback instead of blocking on the GPU. + /// + /// Receives whether the caller should retry with larger scratch capacities. + /// Receives the scratch capacities reported by the render attempt. + /// Receives the largest chunk height that rendered successfully. + /// + /// Receives the deferred scheduling-status readback when the render deferred its overflow + /// check; the caller must resolve it before its next flush of the same scene. + /// + /// Receives the failure reason when the staged scene cannot be rendered. + /// when the staged scene rendered successfully; otherwise, . + public static unsafe bool TryRenderStagedScene( + ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, + WebGPUSceneTarget target, + ref WebGPUSceneSchedulingArena? schedulingArena, + uint initialChunkTileHeightHint, + bool deferOverflowCheck, + out bool requiresGrowth, + out WebGPUSceneBumpSizes grownBumpSizes, + out uint successfulChunkTileHeight, + out WebGPUPendingSchedulingStatus? pendingStatus, + out string? error) + { + requiresGrowth = false; + grownBumpSizes = stagedScene.Config.BumpSizes; + successfulChunkTileHeight = 0; + pendingStatus = null; + error = null; + + WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; + if (GetRenderableDrawCount(stagedScene) == 0) + { + return true; + } + + // Oversized scenes that exceed the device binding limit use tile-row chunking. + // The chunked path ensures its own arena per chunk with chunk-local sizes. + if (IsChunkableBindingFailure(stagedScene.BindingLimitFailure.Buffer)) + { + return TryRenderSegmentChunkedStagedScene( + ref stagedScene, + backdropTarget, + target, + ref schedulingArena, + initialChunkTileHeightHint, + deferOverflowCheck, + out requiresGrowth, + out grownBumpSizes, + out successfulChunkTileHeight, + out pendingStatus, + out error); + } + + // Normal path: ensure arena with full-scene sizes. + WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena( + stagedScene.FlushContext, + stagedScene.Config.BufferSizes, + (nuint)sizeof(GpuSceneBumpAllocators), + ref schedulingArena); + + WebGPUFlushContext flushContext = stagedScene.FlushContext; + if (!TryWriteSceneHeader(flushContext, stagedScene.Resources.HeaderBuffer, CreateHeader(stagedScene, 0U), out error)) + { + return false; + } + + if (!TryDispatchSchedulingStages(ref stagedScene, currentArena, out WebGPUSceneSchedulingResources scheduling, out error)) + { return false; } - using (WebGPUHandle.HandleReference targetTextureReference = flushContext.TargetTextureHandle.AcquireReference()) + int targetWidth = target.Bounds.Width; + int targetHeight = target.Bounds.Height; + + if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out WGPUTextureImpl* outputTexture, out WGPUTextureViewImpl* outputTextureView, out error)) { - WebGPUDrawingBackend.CopyTextureRegion( + return false; + } + + if (!TryDispatchFineArea( flushContext, + stagedScene.Resources, + stagedScene.Config.BufferSizes, + scheduling, + outputTextureView, + backdropTarget.TextureView, + checked((uint)((targetWidth + 15) / 16)), + checked((uint)((targetHeight + 15) / 16)), + out error)) + { + return false; + } + + if (deferOverflowCheck) + { + // Deferred (presentation) path: the frame is submitted whole - scheduling, fine, + // status copy, and target copy in one submission - and the status map is started + // without waiting for the GPU. The caller resolves the pending status before its + // next flush; a rare overflowed frame presents once with incomplete coverage + // (the never-cancel protocol keeps under-provisioned stages well-defined) and the + // grown capacities apply from the following frame. + return TryDeferSchedulingStatus( + ref stagedScene, + target, + scheduling.BumpBuffer, outputTexture, - 0, - 0, - (Texture*)targetTextureReference.Handle, - flushContext.TargetBounds.X, - flushContext.TargetBounds.Y, targetWidth, - targetHeight); + targetHeight, + out pendingStatus, + out error); } - if (!WebGPUDrawingBackend.TrySubmit(flushContext)) + // Single submit: scheduling + fine + readback all in one command encoder. + // The readback map blocks until the GPU finishes everything. + if (!TryEnqueueSchedulingStatusReadback(flushContext, scheduling.BumpBuffer, currentArena.ReadbackBuffer, 0, out error) || + !WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out ulong schedulingSubmissionIndex) || + !TryReadSchedulingStatus(flushContext, currentArena.ReadbackBuffer, schedulingSubmissionIndex, out GpuSceneBumpAllocators bumpAllocators, out error)) + { + return false; + } + + // Overflow: the fine output is discarded, but the scheduling readback still reports + // the scratch usage visible to this pass. Later-stage demand can stay hidden until + // earlier overflows are resolved, so the backend retries with larger buffers until + // the capacities converge or the bounded attempt budget is exhausted. + if (RequiresScratchReallocation(in bumpAllocators, stagedScene.Config.BumpSizes)) + { + requiresGrowth = true; + grownBumpSizes = GrowBumpSizes(stagedScene.Config.BumpSizes, in bumpAllocators); + error = "The staged WebGPU scene needs larger scratch buffers and will be retried."; + return false; + } + + if (!flushContext.EnsureCommandEncoder()) + { + error = "Failed to create a command encoder for the staged-scene final copy."; + return false; + } + + CopyToTarget(ref stagedScene, target, outputTexture, targetWidth, targetHeight); + + if (!WebGPUDrawingBackend.TrySubmitPendingCommands(flushContext)) { error = "Failed to submit the staged-scene final copy."; return false; @@ -1040,6 +1743,62 @@ public static unsafe bool TryRenderStagedScene( return true; } + /// + /// Records a copy of the current target region into a composition texture. + /// + /// The staged scene that owns the flush context recording the copy. + /// The render-time target whose region is copied. + /// The composition texture receiving the target contents. + /// The width of the copied region in pixels. + /// The height of the copied region in pixels. + private static unsafe void CopyTargetToTexture( + ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget target, + WGPUTextureImpl* destinationTexture, + int width, + int height) + { + WebGPUFlushContext flushContext = stagedScene.FlushContext; + WebGPUDrawingBackend.CopyTextureRegion( + flushContext, + target.Texture, + target.Bounds.X + target.TextureOffset.X, + target.Bounds.Y + target.TextureOffset.Y, + destinationTexture, + 0, + 0, + width, + height); + } + + /// + /// Records a copy of a composition texture back into the target region. + /// + /// The staged scene that owns the flush context recording the copy. + /// The render-time target receiving the composition result. + /// The composition texture holding the rendered output. + /// The width of the copied region in pixels. + /// The height of the copied region in pixels. + private static unsafe void CopyToTarget( + ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget target, + WGPUTextureImpl* sourceTexture, + int width, + int height) + { + WebGPUFlushContext flushContext = stagedScene.FlushContext; + WebGPUDrawingBackend.CopyTextureRegion( + flushContext, + sourceTexture, + 0, + 0, + target.Texture, + target.Bounds.X + target.TextureOffset.X, + target.Bounds.Y + target.TextureOffset.Y, + width, + height); + } + /// /// Executes the oversized-scene path by rerunning the GPU scheduling and fine stages in tile-row chunks. /// @@ -1049,34 +1808,43 @@ public static unsafe bool TryRenderStagedScene( /// and rerun per chunk so normal scenes keep the existing single-pass fast path. /// /// The flush-scoped scene whose full-scene segments buffer exceeded the device binding limit. + /// The sampleable texture holding the target contents before this render. + /// The render-time target receiving the chunked output. /// The flush-local reusable scheduling scratch and readback arena. /// The advisory chunk height to try before estimating from the full-scene overflow. + /// Whether to defer the scratch-overflow readback instead of blocking on the GPU. /// Receives whether the chunked path needs the caller to retry with larger global scratch capacities. /// Receives the enlarged scratch capacities when is . /// Receives the largest chunk height that rendered successfully. + /// Receives the deferred chunk scheduling-status readback on success. /// Receives the chunked-render failure reason when the oversized scene cannot be completed. /// when every chunk rendered successfully; otherwise, . private static unsafe bool TryRenderSegmentChunkedStagedScene( ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, + WebGPUSceneTarget target, ref WebGPUSceneSchedulingArena? schedulingArena, uint initialChunkTileHeightHint, + bool deferOverflowCheck, out bool requiresGrowth, out WebGPUSceneBumpSizes grownBumpSizes, out uint successfulChunkTileHeight, + out WebGPUPendingSchedulingStatus? pendingStatus, out string? error) { requiresGrowth = false; grownBumpSizes = stagedScene.Config.BumpSizes; successfulChunkTileHeight = 0; + pendingStatus = null; error = null; WebGPUEncodedScene encodedScene = stagedScene.EncodedScene; WebGPUFlushContext flushContext = stagedScene.FlushContext; - int targetWidth = encodedScene.TargetSize.Width; - int targetHeight = encodedScene.TargetSize.Height; - nuint maxStorageBufferBindingSize = flushContext.DeviceState.MaxStorageBufferBindingSize; + int targetWidth = target.Bounds.Width; + int targetHeight = target.Bounds.Height; + nuint maxStorageBufferBindingSize = flushContext.ScratchBufferBindingSizeLimit; - if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out Texture* outputTexture, out TextureView* outputTextureView, out error)) + if (!WebGPUDrawingBackend.TryCreateCompositionTexture(flushContext, targetWidth, targetHeight, out WGPUTextureImpl* outputTexture, out WGPUTextureViewImpl* outputTextureView, out error)) { return false; } @@ -1087,204 +1855,263 @@ private static unsafe bool TryRenderSegmentChunkedStagedScene( return false; } - using (WebGPUHandle.HandleReference targetTextureReference = flushContext.TargetTextureHandle.AcquireReference()) - { - WebGPUDrawingBackend.CopyTextureRegion( - flushContext, - (Texture*)targetTextureReference.Handle, - flushContext.TargetBounds.X, - flushContext.TargetBounds.Y, - outputTexture, - 0, - 0, - targetWidth, - targetHeight); - } + // Seed the composition texture with the current target contents. The final copy back to + // the target covers the full rectangle, so any pixel a chunked fine pass does not write + // must already hold its original value. + CopyTargetToTexture(ref stagedScene, backdropTarget, outputTexture, targetWidth, targetHeight); - if (!WebGPUDrawingBackend.TrySubmit(flushContext)) + if (!WebGPUDrawingBackend.TrySubmitPendingCommands(flushContext)) { error = "Failed to submit the staged-scene chunk prefill copy."; return false; } uint tileYStart = 0U; - uint totalTileHeight = checked((uint)encodedScene.TileCountY); + uint totalTileHeight = checked((uint)((targetHeight + 15) / 16)); uint nextChunkTileHeight = initialChunkTileHeightHint == 0 - ? GetInitialChunkTileHeight(encodedScene, stagedScene.BindingLimitFailure) + ? GetInitialChunkTileHeight(totalTileHeight, stagedScene.BindingLimitFailure) : Math.Min(initialChunkTileHeightHint, totalTileHeight); // Readback buffer holds one BumpAllocators per chunk so we can batch-read after all chunks. nuint readbackByteLength = checked(Math.Max(totalTileHeight, 1U) * (uint)sizeof(GpuSceneBumpAllocators)); + WGPUBufferImpl* deferredReadbackBuffer = null; + bool deferredReadbackBufferTransferred = false; + if (deferOverflowCheck) + { + deferredReadbackBuffer = flushContext.DeviceState.RentStatusReadbackBuffer(readbackByteLength); + if (deferredReadbackBuffer is null) + { + error = "Failed to create the deferred chunk scheduling-status readback buffer."; + return false; + } + } - // Track each chunk's config sizes and tile heights for the batch readback at the end. - using IMemoryOwner chunkBumpSizeOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U))); - using IMemoryOwner chunkTileHeightOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U))); - Span chunkAttemptBumpSizes = chunkBumpSizeOwner.Memory.Span; - Span chunkAttemptTileHeights = chunkTileHeightOwner.Memory.Span; + try + { + // Track each chunk's config sizes and tile heights for the batch readback at the end. + using IMemoryOwner chunkBumpSizeOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U))); + using IMemoryOwner chunkTileHeightOwner = flushContext.MemoryAllocator.Allocate(checked((int)Math.Max(totalTileHeight, 1U))); + Span chunkAttemptBumpSizes = chunkBumpSizeOwner.Memory.Span; + Span chunkAttemptTileHeights = chunkTileHeightOwner.Memory.Span; - int chunkReadbackCount = 0; - bool sharedSchedulingPrepared = false; + int chunkReadbackCount = 0; + bool sharedSchedulingPrepared = false; - // Outer loop: advance through tile rows. Each iteration renders one chunk. - while (tileYStart < totalTileHeight) - { - uint remainingTileHeight = totalTileHeight - tileYStart; - uint requestedTileHeight = Math.Min(nextChunkTileHeight, remainingTileHeight); + // The direct single-chunk path assigns this when it submits the chunk attempt. The + // shared multi-chunk path assigns it when the accumulated chunk encoder is submitted + // below, so status mapping never observes the initial value. + ulong chunkSubmissionIndex = default; - // Inner loop: shrink the chunk if its buffers still exceed the device binding limit. - while (true) + // Outer loop: advance through tile rows. Each iteration renders one chunk. + while (tileYStart < totalTileHeight) { - WebGPUSceneChunkWindow chunkWindow = CreateChunkWindow(tileYStart, requestedTileHeight, remainingTileHeight); - WebGPUSceneBumpSizes chunkBumpSize = ScaleChunkBumpSizes(stagedScene.Config.BumpSizes, encodedScene, chunkWindow.TileHeight); - WebGPUSceneConfig chunkConfig = WebGPUSceneConfig.Create(encodedScene, chunkBumpSize, chunkWindow); + uint remainingTileHeight = totalTileHeight - tileYStart; + uint requestedTileHeight = Math.Min(nextChunkTileHeight, remainingTileHeight); - if (!TryValidateBindingSizes(encodedScene, chunkConfig, maxStorageBufferBindingSize, out BindingLimitFailure bindingLimitFailure, out error)) + // Inner loop: shrink the chunk if its buffers still exceed the device binding limit. + while (true) { - if (IsChunkableBindingFailure(bindingLimitFailure.Buffer)) + WebGPUSceneChunkWindow chunkWindow = CreateChunkWindow(tileYStart, requestedTileHeight, remainingTileHeight); + WebGPUSceneBumpSizes chunkBumpSize = stagedScene.Range.HasValue + ? ScaleChunkBumpSizes(stagedScene.Config.BumpSizes, stagedScene.Range.Value, chunkWindow.TileHeight) + : ScaleChunkBumpSizes(stagedScene.Config.BumpSizes, encodedScene, chunkWindow.TileHeight); + + WebGPUSceneConfig chunkConfig = stagedScene.Range.HasValue + ? WebGPUSceneConfig.Create(encodedScene, stagedScene.Range.Value, chunkBumpSize, chunkWindow) + : WebGPUSceneConfig.Create(encodedScene, chunkBumpSize, chunkWindow); + + if (!TryValidateBindingSizes(encodedScene, chunkConfig, maxStorageBufferBindingSize, out BindingLimitFailure bindingLimitFailure, out error)) { - uint smallerTileHeight = ShrinkChunkTileHeight(requestedTileHeight, remainingTileHeight, bindingLimitFailure); - if (smallerTileHeight >= requestedTileHeight) + if (IsChunkableBindingFailure(bindingLimitFailure.Buffer)) { - return false; + uint smallerTileHeight = ShrinkChunkTileHeight(requestedTileHeight, remainingTileHeight, bindingLimitFailure); + if (smallerTileHeight >= requestedTileHeight) + { + return false; + } + + requestedTileHeight = smallerTileHeight; + continue; } - requestedTileHeight = smallerTileHeight; - continue; + return false; } - return false; - } + // Chunk fits. Re-ensure the arena for this chunk's buffer sizes (may be + // different from the previous chunk if the scene has non-uniform density). + WebGPUStagedScene chunkScene = stagedScene.Range.HasValue + ? new WebGPUStagedScene(flushContext, encodedScene, stagedScene.Range.Value, chunkConfig, stagedScene.Resources, BindingLimitFailure.None) + : new WebGPUStagedScene(flushContext, encodedScene, chunkConfig, stagedScene.Resources, BindingLimitFailure.None, false, false); + WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena(flushContext, chunkConfig.BufferSizes, readbackByteLength, ref schedulingArena); + WGPUBufferImpl* statusReadbackBuffer = deferOverflowCheck ? deferredReadbackBuffer : currentArena.ReadbackBuffer; + + // A first chunk that already covers the full tile height renders through the + // monolithic scheduling path below; genuine multi-chunk renders run the shared + // full-scene stages exactly once and then replay only the chunk-local stages. + bool useSharedSchedulingState = sharedSchedulingPrepared || chunkWindow.TileHeight < totalTileHeight; + if (!sharedSchedulingPrepared && useSharedSchedulingState) + { + if (!TryDispatchSharedSchedulingStages(ref stagedScene, currentArena, out error)) + { + return false; + } - // Chunk fits. Re-ensure the arena for this chunk's buffer sizes (may be - // different from the previous chunk if the scene has non-uniform density). - WebGPUStagedScene chunkScene = new(flushContext, encodedScene, chunkConfig, stagedScene.Resources, BindingLimitFailure.None); - WebGPUSceneSchedulingArena currentArena = EnsureSchedulingArena(flushContext, chunkConfig.BufferSizes, readbackByteLength, ref schedulingArena); + if (!WebGPUDrawingBackend.TrySubmitPendingCommands(flushContext)) + { + error = "Failed to submit the staged-scene shared chunk scheduling passes."; + return false; + } - bool useSharedSchedulingState = sharedSchedulingPrepared || chunkWindow.TileHeight < totalTileHeight; - if (!sharedSchedulingPrepared && useSharedSchedulingState) - { - if (!TryDispatchSharedSchedulingStages(ref stagedScene, currentArena, out error)) - { - return false; + sharedSchedulingPrepared = true; } - if (!WebGPUDrawingBackend.TrySubmit(flushContext)) + // Render this chunk: scheduling + fine into the shared output texture. + // Readback is deferred - each chunk copies its bump status into a different + // offset in the readback buffer. All chunks are checked in one batch below. + bool recordedChunk = useSharedSchedulingState + ? TryRecordChunkAttempt( + ref chunkScene, + backdropTarget, + currentArena, + outputTextureView, + statusReadbackBuffer, + (nuint)chunkReadbackCount * (nuint)sizeof(GpuSceneBumpAllocators), + chunkReadbackCount > 0, + out error) + : TryRenderChunkAttempt( + ref chunkScene, + backdropTarget, + currentArena, + outputTextureView, + statusReadbackBuffer, + (nuint)chunkReadbackCount * (nuint)sizeof(GpuSceneBumpAllocators), + useSharedSchedulingState, + resetChunkLocalBumpAllocators: false, + out chunkSubmissionIndex, + out error); + + if (recordedChunk) { - error = "Failed to submit the staged-scene shared chunk scheduling passes."; - return false; + chunkAttemptBumpSizes[chunkReadbackCount++] = chunkConfig.BumpSizes; + chunkAttemptTileHeights[chunkReadbackCount - 1] = chunkWindow.TileHeight; + tileYStart += chunkWindow.TileHeight; + nextChunkTileHeight = requestedTileHeight; + successfulChunkTileHeight = Math.Max(successfulChunkTileHeight, chunkWindow.TileHeight); + break; } - sharedSchedulingPrepared = true; + // The chunk passed binding validation, so an attempt failure cannot be cured by + // shrinking the window; retrying would re-record the identical chunk forever. + // Fail the flush with the attempt's error instead. + return false; } + } - // Render this chunk: scheduling + fine into the shared output texture. - // Readback is deferred - each chunk copies its bump status into a different - // offset in the readback buffer. All chunks are checked in one batch below. - bool recordedChunk = useSharedSchedulingState - ? TryRecordChunkAttempt( - ref chunkScene, - currentArena, - outputTextureView, - (nuint)chunkReadbackCount * (nuint)sizeof(GpuSceneBumpAllocators), - chunkReadbackCount > 0, - out error) - : TryRenderChunkAttempt( - ref chunkScene, - currentArena, - outputTextureView, - (nuint)chunkReadbackCount * (nuint)sizeof(GpuSceneBumpAllocators), - useSharedSchedulingState, - resetChunkLocalBumpAllocators: false, - out error); + if (sharedSchedulingPrepared && !WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out chunkSubmissionIndex)) + { + error = "Failed to submit the staged-scene chunk passes."; + return false; + } - if (recordedChunk) + if (!deferOverflowCheck) + { + // All chunks submitted. Map the readback buffer once and check every chunk's + // bump allocators. If any overflowed, grow sizes and the outer retry loop re-runs. + if (!TryReadChunkSchedulingStatuses( + flushContext, + schedulingArena, + chunkReadbackCount, + chunkAttemptBumpSizes, + chunkAttemptTileHeights, + totalTileHeight, + chunkSubmissionIndex, + out requiresGrowth, + out grownBumpSizes, + out error)) { - chunkAttemptBumpSizes[chunkReadbackCount++] = chunkConfig.BumpSizes; - chunkAttemptTileHeights[chunkReadbackCount - 1] = chunkWindow.TileHeight; - tileYStart += chunkWindow.TileHeight; - nextChunkTileHeight = requestedTileHeight; - successfulChunkTileHeight = Math.Max(successfulChunkTileHeight, chunkWindow.TileHeight); - break; + return false; + } + + if (requiresGrowth) + { + error = "One or more staged WebGPU scene chunks need larger scratch buffers and will be retried."; + return false; } } - } - if (sharedSchedulingPrepared && !WebGPUDrawingBackend.TrySubmit(flushContext)) - { - error = "Failed to submit the staged-scene chunk passes."; - return false; - } + if (!flushContext.EnsureCommandEncoder()) + { + error = "Failed to create a command encoder for the staged-scene final copy."; + return false; + } - // All chunks submitted. Map the readback buffer once and check every chunk's - // bump allocators. If any overflowed, grow sizes and the outer retry loop re-runs. - if (!TryReadChunkSchedulingStatuses( - flushContext, - schedulingArena, - chunkReadbackCount, - chunkAttemptBumpSizes, - chunkAttemptTileHeights, - totalTileHeight, - out requiresGrowth, - out grownBumpSizes, - out error)) - { - return false; - } + CopyToTarget(ref stagedScene, target, outputTexture, targetWidth, targetHeight); - if (requiresGrowth) - { - error = "One or more staged WebGPU scene chunks need larger scratch buffers and will be retried."; - return false; - } + if (!WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out chunkSubmissionIndex)) + { + return false; + } - if (!flushContext.EnsureCommandEncoder()) - { - error = "Failed to create a command encoder for the staged-scene final copy."; - return false; - } + if (deferOverflowCheck) + { + pendingStatus = new WebGPUPendingSchedulingStatus( + flushContext.Api, + flushContext.DeviceHandle, + flushContext.DeviceState, + deferredReadbackBuffer, + readbackByteLength, + stagedScene.Config.BumpSizes, + chunkAttemptBumpSizes[..chunkReadbackCount], + chunkAttemptTileHeights[..chunkReadbackCount], + totalTileHeight, + chunkSubmissionIndex); + deferredReadbackBufferTransferred = true; + } - using (WebGPUHandle.HandleReference targetTextureReference = flushContext.TargetTextureHandle.AcquireReference()) + return true; + } + finally { - WebGPUDrawingBackend.CopyTextureRegion( - flushContext, - outputTexture, - 0, - 0, - (Texture*)targetTextureReference.Handle, - flushContext.TargetBounds.X, - flushContext.TargetBounds.Y, - targetWidth, - targetHeight); + if (!deferredReadbackBufferTransferred && deferredReadbackBuffer is not null) + { + flushContext.Api.BufferRelease(deferredReadbackBuffer); + } } - - return WebGPUDrawingBackend.TrySubmit(flushContext); } /// /// Executes one chunk attempt by uploading the chunk header, running scheduling, and fine-rendering into the shared output texture. /// /// The chunk-scoped staged scene describing the tile-row window being rendered. + /// The sampleable texture holding the target contents before this render. /// The flush-local reusable scheduling scratch and readback arena. /// The shared output texture view receiving fine-pass results for every chunk. + /// The map-readable buffer receiving the copied allocator state. /// The byte offset inside the shared readback buffer where this chunk should copy its bump allocators. /// Whether this chunk should reuse the flush-scoped shared scheduling results instead of rerunning the full scheduling pipeline. /// Whether the chunk-local bump counters should be cleared while preserving the shared full-scene scheduling state. + /// Receives the queue submission index for the chunk's submission. /// Receives the chunk failure reason when scheduling or fine dispatch cannot complete. /// when this chunk rendered successfully; otherwise, . private static unsafe bool TryRenderChunkAttempt( ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, WebGPUSceneSchedulingArena schedulingArena, - TextureView* outputTextureView, + WGPUTextureViewImpl* outputTextureView, + WGPUBufferImpl* readbackBuffer, nuint readbackOffset, bool reuseSharedSchedulingState, bool resetChunkLocalBumpAllocators, + out ulong submissionIndex, out string? error) { + submissionIndex = default; + error = null; WebGPUSceneSchedulingResources scheduling; // Upload the chunk-specific config header (tile window, buffer sizes). - if (!TryWriteSceneHeader(stagedScene.FlushContext, stagedScene.Resources.HeaderBuffer, WebGPUSceneResources.CreateHeader(stagedScene.EncodedScene, stagedScene.Config, 0U), out error)) + if (!TryWriteSceneHeader(stagedScene.FlushContext, stagedScene.Resources.HeaderBuffer, CreateHeader(stagedScene, 0U), out error)) { return false; } @@ -1306,19 +2133,19 @@ private static unsafe bool TryRenderChunkAttempt( if (!TryDispatchFineArea( stagedScene.FlushContext, stagedScene.Resources, - stagedScene.EncodedScene, stagedScene.Config.BufferSizes, scheduling, outputTextureView, - (uint)stagedScene.EncodedScene.TileCountX, + backdropTarget.TextureView, + checked((uint)((backdropTarget.Bounds.Width + 15) / 16)), stagedScene.Config.ChunkWindow.TileHeight, out error)) { return false; } - if (!TryEnqueueSchedulingStatusReadback(stagedScene.FlushContext, scheduling.BumpBuffer, schedulingArena.ReadbackBuffer, readbackOffset, out error) || - !WebGPUDrawingBackend.TrySubmit(stagedScene.FlushContext)) + if (!TryEnqueueSchedulingStatusReadback(stagedScene.FlushContext, scheduling.BumpBuffer, readbackBuffer, readbackOffset, out error) || + !WebGPUDrawingBackend.TrySubmitWithIndex(stagedScene.FlushContext, out submissionIndex)) { return false; } @@ -1330,30 +2157,34 @@ private static unsafe bool TryRenderChunkAttempt( /// Records one chunk attempt into the current command encoder by copying a dedicated chunk header, replaying the chunk-local scheduling stages, and appending the scheduling-status readback copy without submitting yet. /// /// The chunk-scoped staged scene describing the tile-row window being recorded. + /// The sampleable texture holding the target contents before this render. /// The flush-local reusable scheduling scratch and readback arena. /// The shared output texture view receiving fine-pass results for this chunk. + /// The map-readable buffer receiving the copied allocator state. /// The byte offset inside the shared readback buffer where this chunk should copy its bump allocators. /// Whether the chunk-local allocator counters should be cleared while preserving the shared full-scene scheduling state. /// Receives the chunk failure reason when scheduling, fine dispatch, or readback staging cannot complete. /// when this chunk was recorded successfully into the active command encoder; otherwise, . private static unsafe bool TryRecordChunkAttempt( ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget backdropTarget, WebGPUSceneSchedulingArena schedulingArena, - TextureView* outputTextureView, + WGPUTextureViewImpl* outputTextureView, + WGPUBufferImpl* readbackBuffer, nuint readbackOffset, bool resetChunkLocalBumpAllocators, out string? error) { error = null; - GpuSceneConfig header = WebGPUSceneResources.CreateHeader(stagedScene.EncodedScene, stagedScene.Config, 0U); + GpuSceneConfig header = CreateHeader(stagedScene, 0U); if (!stagedScene.FlushContext.EnsureCommandEncoder()) { error = "Failed to create a command encoder for the staged-scene chunk pass."; return false; } - WgpuBuffer* headerSourceBuffer = CreateAndUploadCopySourceBuffer(stagedScene.FlushContext, in header); + WGPUBufferImpl* headerSourceBuffer = CreateAndUploadCopySourceBuffer(stagedScene.FlushContext, in header); if (!TryEnqueueSceneHeaderCopy(stagedScene.FlushContext, headerSourceBuffer, stagedScene.Resources.HeaderBuffer, out error) || !TryDispatchChunkLocalSchedulingStages(ref stagedScene, schedulingArena, resetChunkLocalBumpAllocators, out WebGPUSceneSchedulingResources scheduling, out error)) @@ -1364,18 +2195,18 @@ private static unsafe bool TryRecordChunkAttempt( if (!TryDispatchFineArea( stagedScene.FlushContext, stagedScene.Resources, - stagedScene.EncodedScene, stagedScene.Config.BufferSizes, scheduling, outputTextureView, - (uint)stagedScene.EncodedScene.TileCountX, + backdropTarget.TextureView, + checked((uint)((backdropTarget.Bounds.Width + 15) / 16)), stagedScene.Config.ChunkWindow.TileHeight, out error)) { return false; } - return TryEnqueueSchedulingStatusReadback(stagedScene.FlushContext, scheduling.BumpBuffer, schedulingArena.ReadbackBuffer, readbackOffset, out error); + return TryEnqueueSchedulingStatusReadback(stagedScene.FlushContext, scheduling.BumpBuffer, readbackBuffer, readbackOffset, out error); } /// @@ -1388,17 +2219,36 @@ private static unsafe bool TryRecordChunkAttempt( /// when the header upload completed; otherwise, . private static unsafe bool TryWriteSceneHeader( WebGPUFlushContext flushContext, - WgpuBuffer* headerBuffer, + WGPUBufferImpl* headerBuffer, GpuSceneConfig header, out string? error) { nuint headerSize = (nuint)sizeof(GpuSceneConfig); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); - flushContext.Api.QueueWriteBuffer((Queue*)queueReference.Handle, headerBuffer, 0, &header, headerSize); + flushContext.Api.QueueWriteBuffer((WGPUQueueImpl*)queueReference.Handle, headerBuffer, 0, &header, headerSize); error = null; return true; } + /// + /// Creates the staged-scene config header for the current full-scene or range render. + /// + /// The staged scene whose header is being uploaded. + /// The packed base color used by the fine pass. + /// The config block matching the staged scene's full-scene or range layout. + private static GpuSceneConfig CreateHeader(WebGPUStagedScene stagedScene, uint baseColor) + => stagedScene.Range.HasValue + ? WebGPUSceneResources.CreateHeader(stagedScene.EncodedScene, stagedScene.Range.Value, stagedScene.Config, baseColor) + : WebGPUSceneResources.CreateHeader(stagedScene.EncodedScene, stagedScene.Config, baseColor); + + /// + /// Gets the draw-count that controls whether the staged scene has work to dispatch. + /// + /// The staged scene whose full-scene or range work is being checked. + /// The retained draw count for the staged scene. + private static int GetRenderableDrawCount(WebGPUStagedScene stagedScene) + => stagedScene.Range.HasValue ? stagedScene.Range.Value.FillCount : stagedScene.EncodedScene.FillCount; + /// /// Records one copy from a per-chunk header upload buffer into the shared staged-scene header buffer so subsequent chunk-local passes see the correct tile window. /// @@ -1409,8 +2259,8 @@ private static unsafe bool TryWriteSceneHeader( /// when the header copy was recorded successfully; otherwise, . private static unsafe bool TryEnqueueSceneHeaderCopy( WebGPUFlushContext flushContext, - WgpuBuffer* sourceBuffer, - WgpuBuffer* destinationBuffer, + WGPUBufferImpl* sourceBuffer, + WGPUBufferImpl* destinationBuffer, out string? error) { flushContext.EndComputePassIfOpen(); @@ -1431,36 +2281,21 @@ private static unsafe bool TryEnqueueSceneHeaderCopy( return true; } - /// - /// Resets the shared scheduling bump-allocator buffer so one chunk attempt does not inherit - /// the previous chunk's failure state through the prepare shader's previous-run path. - /// - private static unsafe bool TryResetSceneBumpAllocators( - WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - out string? error) - { - GpuSceneBumpAllocators reset = default; - using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); - flushContext.Api.QueueWriteBuffer((Queue*)queueReference.Handle, bumpBuffer, 0, &reset, (nuint)sizeof(GpuSceneBumpAllocators)); - error = null; - return true; - } - /// /// Chooses the first chunk height from the exact full-scene chunkable binding overflow. /// - /// The encoded scene whose full-scene chunkable binding exceeded the device limit. + /// The full tile-row height of the target range being chunked. /// The exact chunk-local binding overflow reported during full-scene planning. /// The initial tile-row chunk height to try for the oversized scene. - private static uint GetInitialChunkTileHeight(WebGPUEncodedScene scene, BindingLimitFailure bindingLimitFailure) + private static uint GetInitialChunkTileHeight(uint fullTileHeight, BindingLimitFailure bindingLimitFailure) { - uint fullTileHeight = checked((uint)scene.TileCountY); if (!IsChunkableBindingFailure(bindingLimitFailure.Buffer) || bindingLimitFailure.RequiredBytes == 0) { return fullTileHeight; } + // Keep a 1/16 headroom below the device limit so the linearly scaled estimate does not + // land exactly on the boundary and immediately fail chunk validation again. ulong usableBytes = Math.Max(bindingLimitFailure.LimitBytes - (bindingLimitFailure.LimitBytes / 16UL), 1UL); uint estimatedTileHeight = checked((uint)Math.Max(1UL, (usableBytes * fullTileHeight) / bindingLimitFailure.RequiredBytes)); return AlignChunkTileHeight(Math.Min(estimatedTileHeight, fullTileHeight), fullTileHeight); @@ -1480,6 +2315,7 @@ private static uint ShrinkChunkTileHeight(uint currentTileHeight, uint remaining return currentTileHeight; } + // Same 1/16 headroom as the initial estimate; see GetInitialChunkTileHeight. ulong usableBytes = Math.Max(bindingLimitFailure.LimitBytes - (bindingLimitFailure.LimitBytes / 16UL), 1UL); uint estimatedTileHeight = checked((uint)Math.Max(1UL, (usableBytes * currentTileHeight) / bindingLimitFailure.RequiredBytes)); uint alignedTileHeight = AlignChunkTileHeight(Math.Min(estimatedTileHeight, remainingTileHeight), remainingTileHeight); @@ -1488,6 +2324,9 @@ private static uint ShrinkChunkTileHeight(uint currentTileHeight, uint remaining return alignedTileHeight; } + // The proportional estimate failed to shrink, so force progress: step down to the previous + // 16-row bin boundary, then row by row. Returning an unchanged height makes the caller + // abort rather than loop forever. if (currentTileHeight > 16U) { return (currentTileHeight - 1U) & ~15U; @@ -1575,29 +2414,148 @@ private static WebGPUSceneBumpSizes ScaleChunkBumpSizes(WebGPUSceneBumpSizes sou ScaleCount(sourceBumpSizes.Ptcl, chunkTileHeight, fullTileHeight)); } + /// + /// Scales the chunk-local scheduling capacities from the last known-good range budget. + /// + /// The range scratch budget used as the source for chunk-local sizing. + /// The encoded range whose aggregate tile and line counts provide lower bounds. + /// The real chunk height, in tile rows. + /// The chunk-local scratch capacities to use for this tile-row window. + private static WebGPUSceneBumpSizes ScaleChunkBumpSizes(WebGPUSceneBumpSizes sourceBumpSizes, WebGPUSceneRange range, uint chunkTileHeight) + { + uint fullTileHeight = checked((uint)((range.TargetBounds.Height + 15) / 16)); + uint pathRowFloor = ScaleCount((uint)Math.Max(range.TotalPathRowCount, range.PathCount), chunkTileHeight, fullTileHeight); + uint pathTileFloor = pathRowFloor; + uint segmentFloor = AddSizingSlack(ScaleCount((uint)range.LineCount, chunkTileHeight, fullTileHeight)); + + return new WebGPUSceneBumpSizes( + sourceBumpSizes.Lines, + sourceBumpSizes.Binning, + Math.Max(ScaleCount(sourceBumpSizes.PathRows, chunkTileHeight, fullTileHeight), pathRowFloor), + Math.Max(ScaleCount(sourceBumpSizes.PathTiles, chunkTileHeight, fullTileHeight), pathTileFloor), + Math.Max(ScaleCount(sourceBumpSizes.SegCounts, chunkTileHeight, fullTileHeight), segmentFloor), + Math.Max(ScaleCount(sourceBumpSizes.Segments, chunkTileHeight, fullTileHeight), segmentFloor), + ScaleCount(sourceBumpSizes.BlendSpill, chunkTileHeight, fullTileHeight), + ScaleCount(sourceBumpSizes.Ptcl, chunkTileHeight, fullTileHeight)); + } + /// /// Raises the caller-provided scratch capacities to the scene-specific lower bounds already known on the CPU so the first GPU attempt does not waste a full-scene pass on allocations that cannot possibly fit. /// /// The persisted scratch capacities carried into this flush. - /// The encoded scene whose line count and path-tile estimate provide guaranteed minimum capacities. + /// The encoded scene whose CPU-side demand estimates provide minimum capacities. + /// The device-reported maximum size of one storage-buffer binding, used to cap conservative estimates. /// The retained capacities raised to the scene's known CPU-side lower bounds. - private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes currentSizes, WebGPUEncodedScene scene) + private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes currentSizes, WebGPUEncodedScene scene, ulong maxStorageBufferBindingSize) + => SeedSceneBumpSizes( + currentSizes, + scene.LineCount, + scene.TotalPathRowCount, + scene.PathCount, + scene.EstimatedTileCrossings, + scene.EstimatedBinFootprint, + maxStorageBufferBindingSize); + + /// + /// Raises the caller-provided scratch capacities to the range-specific lower bounds already known on the CPU. + /// + /// The persisted scratch capacities carried into this flush. + /// The encoded range whose CPU-side demand estimates provide minimum capacities. + /// The device-reported maximum size of one storage-buffer binding, used to cap conservative estimates. + /// The retained capacities raised to the range's known CPU-side lower bounds. + private static WebGPUSceneBumpSizes SeedSceneBumpSizes(WebGPUSceneBumpSizes currentSizes, WebGPUSceneRange range, ulong maxStorageBufferBindingSize) + => SeedSceneBumpSizes( + currentSizes, + range.LineCount, + range.TotalPathRowCount, + range.PathCount, + range.EstimatedTileCrossings, + range.EstimatedBinFootprint, + maxStorageBufferBindingSize); + + /// + /// Raises scratch capacities to CPU-side lower bounds derived from encode-time demand + /// estimates. Tile crossings bound the segment, segment-count, and path-tile records; the + /// bin footprint bounds binning records; the PTCL budget derives from crossings. Estimates + /// are conservative upper bounds, so each is capped at the device binding limit; the GPU + /// overflow protocol remains the safety net for any underestimate. + /// + /// The persisted scratch capacities carried into this flush. + /// The encoded (or GPU-expansion estimated) line count. + /// 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 per-(draw, bin) records. + /// 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( + WebGPUSceneBumpSizes currentSizes, + int lineCount, + int totalPathRowCount, + int pathCount, + long estimatedTileCrossings, + long estimatedBinFootprint, + ulong maxStorageBufferBindingSize) { - uint lineFloor = AddSizingSlack(checked((uint)Math.Max(scene.LineCount, 1))); - uint pathRowFloor = checked((uint)Math.Max(scene.TotalPathRowCount, scene.PathCount)); - uint pathTileFloor = pathRowFloor; + uint lineFloor = AddSizingSlack(checked((uint)Math.Max(lineCount, 1))); + uint pathRowFloor = checked((uint)Math.Max(totalPathRowCount, pathCount)); + + uint segCountFloor = Math.Max( + ClampEstimate(estimatedTileCrossings, maxStorageBufferBindingSize, (uint)Unsafe.SizeOf()), + lineFloor); + uint segmentFloor = Math.Max( + ClampEstimate(estimatedTileCrossings, maxStorageBufferBindingSize, (uint)Unsafe.SizeOf()), + lineFloor); + uint pathTileFloor = Math.Max( + ClampEstimate(estimatedTileCrossings, maxStorageBufferBindingSize, (uint)Unsafe.SizeOf()), + pathRowFloor); + uint binningFloor = ClampEstimate(estimatedBinFootprint, maxStorageBufferBindingSize, sizeof(uint)); + uint ptclFloor = ClampEstimate(estimatedTileCrossings * PtclWordsPerCrossing, maxStorageBufferBindingSize, sizeof(uint)); return new WebGPUSceneBumpSizes( Math.Max(currentSizes.Lines, lineFloor), - currentSizes.Binning, + Math.Max(currentSizes.Binning, binningFloor), Math.Max(currentSizes.PathRows, pathRowFloor), Math.Max(currentSizes.PathTiles, pathTileFloor), - Math.Max(currentSizes.SegCounts, lineFloor), - Math.Max(currentSizes.Segments, lineFloor), + Math.Max(currentSizes.SegCounts, segCountFloor), + Math.Max(currentSizes.Segments, segmentFloor), currentSizes.BlendSpill, - currentSizes.Ptcl); + Math.Max(currentSizes.Ptcl, ptclFloor)); + } + + /// + /// Converts one conservative CPU-side demand estimate into a seed capacity: sizing slack is + /// applied, then the count is capped so the resulting buffer cannot exceed the device's + /// storage-buffer binding limit (an over-cap estimate must not push an otherwise-fitting + /// scene onto the chunked path). + /// + /// The conservative element-count estimate. + /// The device-reported maximum size of one storage-buffer binding. + /// The byte size of one buffer element. + /// The clamped seed capacity. + private static uint ClampEstimate(long estimate, ulong maxStorageBufferBindingSize, uint elementByteSize) + { + if (estimate <= 0) + { + return 0; + } + + // The estimate is already a conservative upper bound, so it is used directly and only capped to + // the element count that keeps the buffer within one storage binding. + ulong maxElements = maxStorageBufferBindingSize / Math.Max(elementByteSize, 1U); + return (uint)Math.Min((ulong)estimate, Math.Min(maxElements, uint.MaxValue)); } + /// + /// Determines whether a binding-limit failure can be resolved by tile-row chunking. + /// + /// + /// The listed buffers scale with the tile-row window, so shrinking the window shrinks the + /// binding. Every other staged-scene binding is sized by the scene itself and cannot be + /// reduced by chunking, making its overflow fatal for the flush. + /// + /// The binding that exceeded the device limit. + /// when the failed binding shrinks with the chunk window; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsChunkableBindingFailure(BindingLimitBuffer buffer) => buffer is BindingLimitBuffer.PathRows or BindingLimitBuffer.PathTiles or BindingLimitBuffer.SegCounts or BindingLimitBuffer.Segments or BindingLimitBuffer.BlendSpill or BindingLimitBuffer.Ptcl; @@ -1639,7 +2597,7 @@ private static uint AlignUp(uint value, uint alignment) /// The capacities used for the current attempt. /// when the flush should be retried with larger scratch buffers; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool RequiresScratchReallocation(in GpuSceneBumpAllocators bumpAllocators, WebGPUSceneBumpSizes currentSizes) + public static bool RequiresScratchReallocation(in GpuSceneBumpAllocators bumpAllocators, WebGPUSceneBumpSizes currentSizes) => bumpAllocators.Failed != 0 || bumpAllocators.Binning > currentSizes.Binning || bumpAllocators.Ptcl > currentSizes.Ptcl || @@ -1656,7 +2614,7 @@ private static bool RequiresScratchReallocation(in GpuSceneBumpAllocators bumpAl /// The capacities used for the current attempt. /// The bump allocator counters read back from the GPU. /// The enlarged capacities to use for the next retry. - private static WebGPUSceneBumpSizes GrowBumpSizes(WebGPUSceneBumpSizes currentSizes, in GpuSceneBumpAllocators bumpAllocators) + public static WebGPUSceneBumpSizes GrowBumpSizes(WebGPUSceneBumpSizes currentSizes, in GpuSceneBumpAllocators bumpAllocators) { WebGPUSceneBumpSizes grownSizes = new( GrowBumpSize(currentSizes.Lines, bumpAllocators.Lines), @@ -1684,6 +2642,12 @@ private static WebGPUSceneBumpSizes GrowBumpSizes(WebGPUSceneBumpSizes currentSi ForceGrowBumpSize(currentSizes.Ptcl)); } + /// + /// Combines two scratch-capacity budgets by taking the per-allocator maximum. + /// + /// The first budget. + /// The second budget. + /// The per-allocator maximum of both budgets. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static WebGPUSceneBumpSizes MaxBumpSizes(WebGPUSceneBumpSizes left, WebGPUSceneBumpSizes right) => new( @@ -1696,6 +2660,12 @@ private static WebGPUSceneBumpSizes MaxBumpSizes(WebGPUSceneBumpSizes left, WebG Math.Max(left.BlendSpill, right.BlendSpill), Math.Max(left.Ptcl, right.Ptcl)); + /// + /// Compares two scratch-capacity budgets for per-allocator equality. + /// + /// The first budget. + /// The second budget. + /// when every allocator capacity matches; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool AreBumpSizesEqual(WebGPUSceneBumpSizes left, WebGPUSceneBumpSizes right) => left.Lines == right.Lines && @@ -1725,6 +2695,18 @@ private static uint GrowBumpSize(uint currentSize, uint requiredSize) return checked(requiredSize * 2U); } + /// + /// Unconditionally enlarges one scratch capacity when the GPU flagged a failure but no counter + /// exceeded its budget. + /// + /// + /// A failed stage can be cancelled before it counts all of its demand (for example, the setup + /// passes zero the indirect dispatch after an upstream failure), so the reported counters can + /// undercount. Growing every capacity by half, with a 4096-element floor, guarantees the retry + /// loop always makes forward progress. + /// + /// The capacity used for the current attempt. + /// The enlarged capacity. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint ForceGrowBumpSize(uint currentSize) => checked(currentSize + Math.Max(currentSize / 2U, 4096U)); @@ -1732,6 +2714,14 @@ private static uint ForceGrowBumpSize(uint currentSize) /// /// Records the first pathtag reduction pass that collapses raw path tags into workgroup monoids. /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, and reduction buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the first-level reduction buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathtagReduce( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1741,7 +2731,7 @@ private static unsafe bool TryDispatchPathtagReduce( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[3]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.PathReducedBuffer, pathReducedBufferSize); @@ -1752,6 +2742,14 @@ private static unsafe bool TryDispatchPathtagReduce( /// /// Records the second pathtag reduction pass used by the large-scan variant. /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the reduction buffers. + /// The byte length of the first-level reduction buffer binding. + /// The byte length of the second-level reduction buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathtagReduce2( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1761,7 +2759,7 @@ private static unsafe bool TryDispatchPathtagReduce2( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[2]; entries[0] = CreateBufferBinding(0, resources.PathReducedBuffer, pathReducedBufferSize); entries[1] = CreateBufferBinding(1, resources.PathReduced2Buffer, pathReduced2BufferSize); @@ -1769,8 +2767,17 @@ private static unsafe bool TryDispatchPathtagReduce2( } /// - /// Records the prefix-scan setup pass used by the large pathtag scan path. + /// Records the middle scan pass of the large pathtag scan path, producing per-workgroup exclusive prefixes. /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the reduction and scan buffers. + /// The byte length of the first-level reduction buffer binding. + /// The byte length of the second-level reduction buffer binding. + /// The byte length of the per-workgroup prefix buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathtagScan1( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1781,7 +2788,7 @@ private static unsafe bool TryDispatchPathtagScan1( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[3]; entries[0] = CreateBufferBinding(0, resources.PathReducedBuffer, pathReducedBufferSize); entries[1] = CreateBufferBinding(1, resources.PathReduced2Buffer, pathReduced2BufferSize); entries[2] = CreateBufferBinding(2, resources.PathReducedScanBuffer, pathReducedScanBufferSize); @@ -1792,6 +2799,16 @@ private static unsafe bool TryDispatchPathtagScan1( /// /// Records the final pathtag prefix-scan pass, selecting the small or large shader variant. /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, parent, and monoid buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the parent-prefix buffer binding (workgroup totals for the small variant, precomputed prefixes for the large variant). + /// The byte length of the per-tag-word monoid output buffer binding. + /// The X workgroup count for the stage. + /// Whether the single-level scan shader should be used; the small variant scans the workgroup totals in shared memory instead of reading precomputed prefixes. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathtagScan( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1803,9 +2820,9 @@ private static unsafe bool TryDispatchPathtagScan( bool useSmallVariant, out string? error) { - WgpuBuffer* parentBuffer = useSmallVariant ? resources.PathReducedBuffer : resources.PathReducedScanBuffer; + WGPUBufferImpl* parentBuffer = useSmallVariant ? resources.PathReducedBuffer : resources.PathReducedScanBuffer; - BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[4]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, parentBuffer, parentBufferSize); @@ -1814,6 +2831,17 @@ private static unsafe bool TryDispatchPathtagScan( return recording.TryRecord(useSmallVariant ? WebGPUSceneShaderId.PathtagScanSmall : WebGPUSceneShaderId.PathtagScan, entries, 4, dispatchX, 1, 1, out error); } + /// + /// Records the pass that resets every per-path bbox to an inverted empty box so flatten can + /// accumulate extents with atomic min/max. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header and path-bbox buffers. + /// The byte length of the per-path bbox buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchBboxClear( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1822,13 +2850,28 @@ private static unsafe bool TryDispatchBboxClear( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[2]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.PathBboxBuffer, pathBboxBufferSize); return recording.TryRecord(WebGPUSceneShaderId.BboxClear, entries, 2, dispatchX, 1, 1, out error); } + /// + /// Records the flatten stage that converts encoded path segments into the device-space line + /// soup and accumulates per-path bboxes, bump-allocating lines from the shared counters. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, monoid, bbox, and line buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the pathtag monoid buffer binding. + /// The byte length of the per-path bbox buffer binding. + /// The scheduling bump-allocator buffer that tracks line usage. + /// The byte length of the flattened line buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchFlatten( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1836,12 +2879,12 @@ private static unsafe bool TryDispatchFlatten( nuint sceneBufferSize, nuint pathMonoidBufferSize, nuint pathBboxBufferSize, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, nuint lineBufferSize, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[6]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.PathMonoidBuffer, pathMonoidBufferSize); @@ -1852,6 +2895,18 @@ private static unsafe bool TryDispatchFlatten( return recording.TryRecord(WebGPUSceneShaderId.Flatten, entries, 6, dispatchX, 1, 1, out error); } + /// + /// Records the first pass of the draw-tag prefix sum, reducing each workgroup's draw tags to + /// one aggregate draw monoid. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, and reduction buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the per-workgroup draw-monoid buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchDrawReduce( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1861,7 +2916,7 @@ private static unsafe bool TryDispatchDrawReduce( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[3]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[3]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.DrawReducedBuffer, drawReducedBufferSize); @@ -1869,6 +2924,22 @@ private static unsafe bool TryDispatchDrawReduce( return recording.TryRecord(WebGPUSceneShaderId.DrawReduce, entries, 3, dispatchX, 1, 1, out error); } + /// + /// Records the draw-tag scan that finalizes per-draw monoids, materializes brush info words, + /// and emits the clip inputs consumed by the clip stages. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, monoid, bbox, info, and clip-input buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the per-workgroup draw-monoid buffer binding. + /// The byte length of the per-path bbox buffer binding. + /// The byte length of the per-draw monoid output buffer binding. + /// The byte length of the combined info/bin-data buffer binding. + /// The byte length of the clip-input buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchDrawLeaf( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1882,7 +2953,7 @@ private static unsafe bool TryDispatchDrawLeaf( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.DrawReducedBuffer, drawReducedBufferSize); @@ -1894,6 +2965,20 @@ private static unsafe bool TryDispatchDrawLeaf( return recording.TryRecord(WebGPUSceneShaderId.DrawLeaf, entries, 7, dispatchX, 1, 1, out error); } + /// + /// Records the first pass of the clip stack-monoid scan, reducing each 256-record span of + /// BeginClip/EndClip inputs to one aggregate for clip_leaf. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the clip and bbox buffers. + /// The byte length of the clip-input buffer binding. + /// The byte length of the per-path bbox buffer binding. + /// The byte length of the bicyclic-semigroup aggregate buffer binding. + /// The byte length of the open-clip stack element buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchClipReduce( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1905,7 +2990,7 @@ private static unsafe bool TryDispatchClipReduce( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[4]; entries[0] = CreateBufferBinding(0, resources.ClipInputBuffer, clipInputBufferSize); entries[1] = CreateBufferBinding(1, resources.PathBboxBuffer, pathBboxBufferSize); entries[2] = CreateBufferBinding(2, resources.ClipBicBuffer, clipBicBufferSize); @@ -1914,6 +2999,22 @@ private static unsafe bool TryDispatchClipReduce( return recording.TryRecord(WebGPUSceneShaderId.ClipReduce, entries, 4, dispatchX, 1, 1, out error); } + /// + /// Records the clip-stack scan that resolves conservative clip bboxes and rewrites EndClip + /// draw monoids to reference their matching BeginClip records. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, clip, bbox, and monoid buffers. + /// The byte length of the clip-input buffer binding. + /// The byte length of the per-path bbox buffer binding. + /// The byte length of the bicyclic-semigroup aggregate buffer binding. + /// The byte length of the open-clip stack element buffer binding. + /// The byte length of the per-draw monoid buffer binding. + /// The byte length of the per-clip bbox output buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchClipLeaf( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1927,7 +3028,7 @@ private static unsafe bool TryDispatchClipLeaf( uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.ClipInputBuffer, clipInputBufferSize); entries[2] = CreateBufferBinding(2, resources.PathBboxBuffer, pathBboxBufferSize); @@ -1939,6 +3040,23 @@ private static unsafe bool TryDispatchClipLeaf( return recording.TryRecord(WebGPUSceneShaderId.ClipLeaf, entries, 7, dispatchX, 1, 1, out error); } + /// + /// Records the binning stage that assigns each draw object to every 16x16-tile bin its + /// clip-intersected bbox touches, producing the bin headers and element lists read by coarse. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, monoid, bbox, and bin-data buffers. + /// The byte length of the per-draw monoid buffer binding. + /// The byte length of the per-path bbox buffer binding. + /// The byte length of the per-clip bbox buffer binding. + /// The byte length of the intersected draw-bbox output buffer binding. + /// The byte length of the combined info/bin-data buffer binding. + /// The scheduling bump-allocator buffer that tracks bin-data usage. + /// The workgroup count along the draw-partition axis. + /// The workgroup count along the bin-chunk axis. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchBinning( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -1948,12 +3066,12 @@ private static unsafe bool TryDispatchBinning( nuint clipBboxBufferSize, nuint drawBboxBufferSize, nuint infoBinDataBufferSize, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, uint dispatchX, uint dispatchY, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.DrawMonoidBuffer, drawMonoidBufferSize); entries[2] = CreateBufferBinding(2, resources.PathBboxBuffer, pathBboxBufferSize); @@ -1979,10 +3097,11 @@ private static unsafe bool TryDispatchBinning( /// The flush-local readback buffer receiving the copied allocator state. /// The byte offset inside for this copy. /// Receives the copy-recording failure reason. + /// when the copy was recorded successfully; otherwise, . private static unsafe bool TryEnqueueSchedulingStatusReadback( WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* readbackBuffer, + WGPUBufferImpl* bumpBuffer, + WGPUBufferImpl* readbackBuffer, nuint destinationOffset, out string? error) { @@ -2004,32 +3123,194 @@ private static unsafe bool TryEnqueueSchedulingStatusReadback( return true; } + /// + /// Completes a presentation flush without waiting for the GPU: records the scheduling-status + /// copy into a dedicated map-readable buffer, records the final target copy into the same + /// command encoder, submits everything as one submission, and starts the asynchronous status + /// map. The returned pending status owns the buffer and is resolved by the caller before its + /// next flush of the same scene. + /// + /// The staged scene whose flush context records the copies. + /// The render-time target receiving the composition result. + /// The scheduling bump allocator buffer to copy from. + /// The composition texture holding the rendered output. + /// The width of the copied region in pixels. + /// The height of the copied region in pixels. + /// Receives the deferred scheduling-status readback on success. + /// Receives the failure reason when the deferral cannot be recorded or submitted. + /// when the frame was submitted and the map started; otherwise, . + private static unsafe bool TryDeferSchedulingStatus( + ref WebGPUStagedScene stagedScene, + WebGPUSceneTarget target, + WGPUBufferImpl* bumpBuffer, + WGPUTextureImpl* outputTexture, + int targetWidth, + int targetHeight, + out WebGPUPendingSchedulingStatus? pendingStatus, + out string? error) + { + pendingStatus = null; + WebGPUFlushContext flushContext = stagedScene.FlushContext; + + // The pending map outlives this flush, so it gets its own tiny buffer instead of the + // pooled arena's readback buffer: the arena returns to its pool when the flush ends and + // must never be handed out with a map still pending. The buffer itself comes from the + // device-scoped pool because one is needed per flush and creation is a driver call. + WGPUBufferImpl* readbackBuffer = flushContext.DeviceState.RentStatusReadbackBuffer((nuint)sizeof(GpuSceneBumpAllocators)); + if (readbackBuffer is null) + { + error = "Failed to create the deferred scheduling-status readback buffer."; + return false; + } + + if (!TryEnqueueSchedulingStatusReadback(flushContext, bumpBuffer, readbackBuffer, 0, out error)) + { + // The map was never started, so the buffer is clean and can be recycled. + flushContext.DeviceState.ReturnStatusReadbackBuffer(readbackBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + return false; + } + + // The target copy rides the same submission; WebGPU queue ordering guarantees the fine + // output is complete before the copy executes, no CPU-side wait required. + CopyToTarget(ref stagedScene, target, outputTexture, targetWidth, targetHeight); + + if (!WebGPUDrawingBackend.TrySubmitWithIndex(flushContext, out ulong submissionIndex)) + { + // The submit failed after the copy into the buffer was recorded but never executed; + // the map was not started, so recycling stays safe. + flushContext.DeviceState.ReturnStatusReadbackBuffer(readbackBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + error = "Failed to submit the deferred staged-scene frame."; + return false; + } + + try + { + pendingStatus = new WebGPUPendingSchedulingStatus( + flushContext.Api, + flushContext.DeviceHandle, + flushContext.DeviceState, + readbackBuffer, + stagedScene.Config.BumpSizes, + submissionIndex); + } + catch + { + // The ctor starts the map, so on a throw the buffer may already have a pending map + // and must be released outright rather than recycled through the pool. + flushContext.Api.BufferRelease(readbackBuffer); + throw; + } + + return true; + } + + /// + /// Resolves a deferred scheduling-status readback from an earlier presentation flush and + /// folds the observed GPU demand into scratch capacities for subsequent frames, mirroring + /// both the overflow-growth and the known-good-usage merges of the synchronous path. + /// + /// The deferred readback to resolve; always disposed by this call. + /// + /// Receives the capacities subsequent frames should render with: grown when the deferred + /// frame overflowed, otherwise the frame's sizes merged with its actual usage. Falls back to + /// the submitted sizes when the readback cannot be resolved. + /// + /// when the deferred frame overflowed and was rendered with incomplete coverage; otherwise, . + public static bool ResolveDeferredSchedulingStatus( + WebGPUPendingSchedulingStatus pendingStatus, + out WebGPUSceneBumpSizes grownBumpSizes) + { + WebGPUSceneBumpSizes submittedSizes = pendingStatus.SubmittedBumpSizes; + try + { + if (pendingStatus.IsChunked) + { + if (!pendingStatus.TryResolveChunked(out GpuSceneBumpAllocators[]? chunkStatuses) || + chunkStatuses is null || + pendingStatus.ChunkBumpSizes is null || + pendingStatus.ChunkTileHeights is null) + { + grownBumpSizes = submittedSizes; + return false; + } + + bool requiresGrowth = ResolveChunkSchedulingStatuses( + chunkStatuses, + pendingStatus.ChunkBumpSizes, + pendingStatus.ChunkTileHeights, + pendingStatus.FullTileHeight, + out grownBumpSizes); + return requiresGrowth; + } + + if (!pendingStatus.TryResolve(out GpuSceneBumpAllocators bumpAllocators)) + { + grownBumpSizes = submittedSizes; + return false; + } + + if (RequiresScratchReallocation(in bumpAllocators, submittedSizes)) + { + grownBumpSizes = GrowBumpSizes(submittedSizes, in bumpAllocators); + return true; + } + + // No overflow: keep the larger of the submitted capacity and the actual usage so + // later renders start from known-good sizes, matching the synchronous success path. + grownBumpSizes = new WebGPUSceneBumpSizes( + Math.Max(bumpAllocators.Lines, submittedSizes.Lines), + Math.Max(bumpAllocators.Binning, submittedSizes.Binning), + Math.Max(bumpAllocators.PathRows, 1U), + Math.Max(bumpAllocators.Tile, 1U), + Math.Max(bumpAllocators.SegCounts, submittedSizes.SegCounts), + Math.Max(bumpAllocators.Segments, submittedSizes.Segments), + Math.Max(bumpAllocators.BlendSpill, submittedSizes.BlendSpill), + Math.Max(bumpAllocators.Ptcl, submittedSizes.Ptcl)); + return false; + } + finally + { + pendingStatus.Dispose(); + } + } + /// /// Maps the already-copied scheduling status from the flush-local readback buffer. /// + /// + /// The map blocks until the GPU completes all previously submitted work, so this doubles as + /// the synchronization point between the scheduling submission and the CPU-side overflow check. + /// + /// The flush context that owns the device used to pump the map callback. + /// The map-readable buffer holding the copied allocator state. + /// The queue submission index for this readback. + /// Receives the bump-allocator counters reported by the GPU. + /// Receives the map failure reason. + /// when the status was read successfully; otherwise, . private static unsafe bool TryReadSchedulingStatus( WebGPUFlushContext flushContext, - WgpuBuffer* readbackBuffer, + WGPUBufferImpl* readbackBuffer, + ulong submissionIndex, out GpuSceneBumpAllocators bumpAllocators, out string? error) { bumpAllocators = default; - BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + WGPUMapAsyncStatus mapStatus = default; using ManualResetEventSlim mapReady = new(false); - void Callback(BufferMapAsyncStatus status, void* userData) + void Callback(WGPUMapAsyncStatus status, void* userData) { _ = userData; mapStatus = status; mapReady.Set(); } - using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback.From(Callback); flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, (nuint)sizeof(GpuSceneBumpAllocators), callback, null); using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - if (!WaitForMapSignal(flushContext.WgpuExtension, (Device*)deviceReference.Handle, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) || mapStatus != WGPUMapAsyncStatus.Success) { error = $"Failed to map staged-scene scheduling status with status '{mapStatus}'."; return false; @@ -2056,9 +3337,74 @@ void Callback(BufferMapAsyncStatus status, void* userData) } } + /// + /// Aggregates chunk-local scheduling statuses into a full-scene scratch budget. + /// + /// The chunk-local allocator counters copied from the GPU. + /// The chunk-local capacities used by each recorded chunk attempt. + /// The real tile-row height of each recorded chunk attempt. + /// The full tile-row height of the chunked target range. + /// Receives the merged full-scene scratch budget derived from all chunks. + /// when any chunk overflowed and the next render needs larger capacities. + public static bool ResolveChunkSchedulingStatuses( + ReadOnlySpan statuses, + ReadOnlySpan chunkBumpSizes, + ReadOnlySpan chunkTileHeights, + uint fullTileHeight, + out WebGPUSceneBumpSizes grownBumpSizes) + { + bool requiresGrowth = false; + grownBumpSizes = default; + + for (int i = 0; i < statuses.Length; i++) + { + GpuSceneBumpAllocators bumpAllocators = statuses[i]; + WebGPUSceneBumpSizes currentSizes = chunkBumpSizes[i]; + WebGPUSceneBumpSizes chunkActuals = new( + Math.Max(bumpAllocators.Lines, currentSizes.Lines), + Math.Max(bumpAllocators.Binning, currentSizes.Binning), + Math.Max(bumpAllocators.PathRows, 1U), + Math.Max(bumpAllocators.Tile, 1U), + Math.Max(bumpAllocators.SegCounts, currentSizes.SegCounts), + Math.Max(bumpAllocators.Segments, currentSizes.Segments), + Math.Max(bumpAllocators.BlendSpill, currentSizes.BlendSpill), + Math.Max(bumpAllocators.Ptcl, currentSizes.Ptcl)); + WebGPUSceneBumpSizes expandedActuals = ExpandChunkBumpSizesToSceneBudget(chunkActuals, fullTileHeight, chunkTileHeights[i]); + grownBumpSizes = MaxBumpSizes(grownBumpSizes, expandedActuals); + + if (!RequiresScratchReallocation(in bumpAllocators, currentSizes)) + { + continue; + } + + WebGPUSceneBumpSizes grownChunkBumpSizes = GrowBumpSizes(currentSizes, in bumpAllocators); + WebGPUSceneBumpSizes grownSourceBumpSizes = ExpandChunkBumpSizesToSceneBudget(grownChunkBumpSizes, fullTileHeight, chunkTileHeights[i]); + grownBumpSizes = MaxBumpSizes(grownBumpSizes, grownSourceBumpSizes); + requiresGrowth = true; + } + + return requiresGrowth; + } + /// /// Maps the chunked scheduling-status buffer once, then aggregates the retry budget required by all chunk submissions. /// + /// + /// Each chunk's counters are chunk-local, so both the observed actuals and any required growth + /// are scaled back to a full-scene budget before being merged; the merged budget stays valid + /// for whatever window heights a retry ends up using. + /// + /// The flush context that owns the device used to pump the map callback. + /// The scheduling arena whose readback buffer holds one allocator record per chunk. + /// The number of chunk records copied into the readback buffer. + /// The chunk-local capacities used by each recorded chunk attempt. + /// The real tile-row height of each recorded chunk attempt. + /// The full tile-row height of the chunked target range. + /// The queue submission index for this readback. + /// Receives whether any chunk overflowed and the flush must be retried. + /// Receives the merged full-scene scratch budget derived from all chunks. + /// Receives the map failure reason. + /// when every chunk status was read successfully; otherwise, . private static unsafe bool TryReadChunkSchedulingStatuses( WebGPUFlushContext flushContext, WebGPUSceneSchedulingArena? arena, @@ -2066,6 +3412,7 @@ private static unsafe bool TryReadChunkSchedulingStatuses( ReadOnlySpan chunkBumpSizes, ReadOnlySpan chunkTileHeights, uint fullTileHeight, + ulong submissionIndex, out bool requiresGrowth, out WebGPUSceneBumpSizes grownBumpSizes, out string? error) @@ -2079,7 +3426,7 @@ private static unsafe bool TryReadChunkSchedulingStatuses( return false; } - WgpuBuffer* readbackBuffer = arena.ReadbackBuffer; + WGPUBufferImpl* readbackBuffer = arena.ReadbackBuffer; if (chunkCount == 0) { @@ -2087,10 +3434,10 @@ private static unsafe bool TryReadChunkSchedulingStatuses( return true; } - BufferMapAsyncStatus mapStatus = BufferMapAsyncStatus.Unknown; + WGPUMapAsyncStatus mapStatus = default; using ManualResetEventSlim mapReady = new(false); - void Callback(BufferMapAsyncStatus status, void* userData) + void Callback(WGPUMapAsyncStatus status, void* userData) { _ = userData; mapStatus = status; @@ -2098,11 +3445,11 @@ void Callback(BufferMapAsyncStatus status, void* userData) } nuint mappedByteLength = checked((nuint)chunkCount * (nuint)sizeof(GpuSceneBumpAllocators)); - using PfnBufferMapCallback callback = PfnBufferMapCallback.From(Callback); + using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback.From(Callback); flushContext.Api.BufferMapAsync(readbackBuffer, MapMode.Read, 0, mappedByteLength, callback, null); using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - if (!WaitForMapSignal(flushContext.WgpuExtension, (Device*)deviceReference.Handle, mapReady) || mapStatus != BufferMapAsyncStatus.Success) + if (!WaitForMapSignal(flushContext.Api, (WGPUDeviceImpl*)deviceReference.Handle, mapReady, submissionIndex) || mapStatus != WGPUMapAsyncStatus.Success) { error = $"Failed to map staged-scene chunk scheduling status with status '{mapStatus}'."; return false; @@ -2119,34 +3466,13 @@ void Callback(BufferMapAsyncStatus status, void* userData) try { - GpuSceneBumpAllocators* statuses = (GpuSceneBumpAllocators*)mapped; - for (int i = 0; i < chunkCount; i++) - { - GpuSceneBumpAllocators bumpAllocators = statuses[i]; - WebGPUSceneBumpSizes currentSizes = chunkBumpSizes[i]; - WebGPUSceneBumpSizes chunkActuals = new( - Math.Max(bumpAllocators.Lines, currentSizes.Lines), - Math.Max(bumpAllocators.Binning, currentSizes.Binning), - Math.Max(bumpAllocators.PathRows, 1U), - Math.Max(bumpAllocators.Tile, 1U), - Math.Max(bumpAllocators.SegCounts, currentSizes.SegCounts), - Math.Max(bumpAllocators.Segments, currentSizes.Segments), - Math.Max(bumpAllocators.BlendSpill, currentSizes.BlendSpill), - Math.Max(bumpAllocators.Ptcl, currentSizes.Ptcl)); - WebGPUSceneBumpSizes expandedActuals = ExpandChunkBumpSizesToSceneBudget(chunkActuals, fullTileHeight, chunkTileHeights[i]); - grownBumpSizes = MaxBumpSizes(grownBumpSizes, expandedActuals); - - if (!RequiresScratchReallocation(in bumpAllocators, currentSizes)) - { - continue; - } - - WebGPUSceneBumpSizes grownChunkBumpSizes = GrowBumpSizes(currentSizes, in bumpAllocators); - WebGPUSceneBumpSizes grownSourceBumpSizes = ExpandChunkBumpSizesToSceneBudget(grownChunkBumpSizes, fullTileHeight, chunkTileHeights[i]); - grownBumpSizes = MaxBumpSizes(grownBumpSizes, grownSourceBumpSizes); - requiresGrowth = true; - } - + ReadOnlySpan statuses = new(mapped, chunkCount); + requiresGrowth = ResolveChunkSchedulingStatuses( + statuses, + chunkBumpSizes, + chunkTileHeights, + fullTileHeight, + out grownBumpSizes); error = null; return true; } @@ -2156,6 +3482,18 @@ void Callback(BufferMapAsyncStatus status, void* userData) } } + /// + /// Converts a chunk-local scratch budget into the equivalent full-scene budget by scaling the + /// tile-dependent capacities up by the height ratio. + /// + /// + /// Lines and binning are produced by the shared full-scene stages and are already scene-sized, + /// so they pass through unscaled. + /// + /// The chunk-local capacities or actuals to expand. + /// The full tile-row height of the chunked target range. + /// The real tile-row height of the chunk. + /// The full-scene equivalent budget. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static WebGPUSceneBumpSizes ExpandChunkBumpSizesToSceneBudget(WebGPUSceneBumpSizes chunkSizes, uint fullTileHeight, uint chunkTileHeight) => new( @@ -2168,6 +3506,14 @@ private static WebGPUSceneBumpSizes ExpandChunkBumpSizesToSceneBudget(WebGPUScen ScaleChunkRequirementToSceneBudget(chunkSizes.BlendSpill, fullTileHeight, chunkTileHeight), ScaleChunkRequirementToSceneBudget(chunkSizes.Ptcl, fullTileHeight, chunkTileHeight)); + /// + /// Scales one chunk-local requirement up to a full-scene budget, rounding up so the scaled + /// value can never understate the chunk that produced it. + /// + /// The chunk-local requirement to scale. + /// The full tile-row height used as the scale numerator. + /// The chunk tile-row height used as the scale denominator. + /// The non-zero full-scene equivalent of . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint ScaleChunkRequirementToSceneBudget(uint chunkRequired, uint fullTileHeight, uint chunkTileHeight) { @@ -2198,13 +3544,13 @@ private static unsafe bool TryDispatchPathRowAlloc( nuint sceneBufferSize, nuint drawBboxBufferSize, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[6]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[6]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.DrawBboxBuffer, drawBboxBufferSize); @@ -2239,15 +3585,15 @@ private static unsafe bool TryDispatchPathRowSpan( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, nuint lineBufferSize, - WgpuBuffer* indirectCountBuffer, + WGPUBufferImpl* indirectCountBuffer, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[5]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[5]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[2] = CreateBufferBinding(2, resources.LineBuffer, lineBufferSize); @@ -2263,20 +3609,36 @@ private static unsafe bool TryDispatchPathRowSpan( return true; } + /// + /// Records the tile allocation stage that finalizes each sparse row's horizontal extent and + /// bump-allocates the backing tile storage for it. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header and path buffers. + /// The scheduling bump-allocator buffer that tracks tile usage. + /// The byte length of the per-path buffer binding. + /// The sparse path-row buffer whose spans are finalized. + /// The byte length of the sparse path-row buffer binding. + /// The tile buffer receiving the zeroed row tile runs. + /// The byte length of the tile buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchTileAlloc( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* pathTileBuffer, + WGPUBufferImpl* pathTileBuffer, nuint pathTileBufferSize, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[5]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[5]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[2] = CreateBufferBinding(2, resources.PathBuffer, pathBufferSize); @@ -2292,20 +3654,36 @@ private static unsafe bool TryDispatchTileAlloc( return true; } + /// + /// Records the backdrop propagation stage that converts per-tile winding deltas into absolute + /// winding numbers along each sparse path row. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header and path buffers. + /// The scheduling bump-allocator buffer. + /// The byte length of the per-path buffer binding. + /// The sparse path-row buffer providing each row's extent and backdrop seed. + /// The byte length of the sparse path-row buffer binding. + /// The tile buffer whose backdrops are rewritten in place. + /// The byte length of the tile buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchBackdrop( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* pathTileBuffer, + WGPUBufferImpl* pathTileBuffer, nuint pathTileBufferSize, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[5]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[5]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[2] = CreateBufferBinding(2, resources.PathBuffer, pathBufferSize); @@ -2321,15 +3699,26 @@ private static unsafe bool TryDispatchBackdrop( return true; } + /// + /// Records the one-workgroup pass that converts the flattened line count into indirect + /// dispatch arguments for the per-line stages, or zeroes the dispatch after an upstream failure. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The scheduling bump-allocator buffer providing the line count and failure mask. + /// The indirect-dispatch argument buffer to populate. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathCountSetup( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, - WgpuBuffer* bumpBuffer, - WgpuBuffer* indirectCountBuffer, + WGPUBufferImpl* bumpBuffer, + WGPUBufferImpl* indirectCountBuffer, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[2]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[2]; entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[1] = CreateBufferBinding(1, indirectCountBuffer, (nuint)sizeof(GpuSceneIndirectCount)); @@ -2343,28 +3732,29 @@ private static unsafe bool TryDispatchPathCountSetup( } /// - /// Records the one-workgroup prepare stage that resets bump counters and can cancel the - /// remaining scheduling pipeline when the prior run already proved the current scratch - /// capacities are too small. + /// Records the one-workgroup prepare stage that zeroes every bump-allocator counter and the + /// failure mask on the GPU. /// + /// + /// The shader deliberately never cancels the run: letting all stages execute means the + /// counters report the true demand for every buffer in a single pass, so the CPU-side retry + /// can size all of them at once instead of discovering overflows one stage at a time. + /// /// The flush-scoped compute recording that receives the staged dispatch. - /// The scene config buffer shared by all staged-scene passes. /// The scratch bump allocator buffer that tracks dynamic scheduling usage. /// The X workgroup count for the prepare stage. /// Receives the recording failure reason when the dispatch cannot be staged. /// when the prepare dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPrepare( WebGPUSceneComputeRecording recording, - WgpuBuffer* headerBuffer, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[2]; - entries[0] = CreateBufferBinding(0, headerBuffer, (nuint)sizeof(GpuSceneConfig)); - entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[1]; + entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); - if (!recording.TryRecord(WebGPUSceneShaderId.Prepare, entries, 2, dispatchX, 1, 1, out error)) + if (!recording.TryRecord(WebGPUSceneShaderId.Prepare, entries, 1, dispatchX, 1, 1, out error)) { return false; } @@ -2383,11 +3773,11 @@ private static unsafe bool TryDispatchPrepare( /// when the reset dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchChunkReset( WebGPUSceneComputeRecording recording, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[1]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[1]; entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); if (!recording.TryRecord(WebGPUSceneShaderId.ChunkReset, entries, 1, dispatchX, 1, 1, out error)) @@ -2399,23 +3789,42 @@ private static unsafe bool TryDispatchChunkReset( return true; } + /// + /// Records the per-line stage that counts tile crossings, accumulates per-tile winding + /// deltas, and emits one segment-count record per crossing for path_tiling. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, line, and path buffers. + /// The scheduling bump-allocator buffer that tracks segment-count usage. + /// The byte length of the per-path buffer binding. + /// The sparse path-row buffer used to resolve tile indices. + /// The byte length of the sparse path-row buffer binding. + /// The tile buffer whose counts and backdrops are updated atomically. + /// The byte length of the tile buffer binding. + /// The segment-count buffer receiving one record per crossing. + /// The byte length of the segment-count buffer binding. + /// The byte length of the flattened line buffer binding. + /// The indirect-dispatch argument buffer seeded by path_count_setup. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathCount( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* pathTileBuffer, + WGPUBufferImpl* pathTileBuffer, nuint pathTileBufferSize, - WgpuBuffer* segCountBuffer, + WGPUBufferImpl* segCountBuffer, nuint segCountBufferSize, nuint lineBufferSize, - WgpuBuffer* indirectCountBuffer, + WGPUBufferImpl* indirectCountBuffer, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[2] = CreateBufferBinding(2, resources.LineBuffer, lineBufferSize); @@ -2433,6 +3842,28 @@ private static unsafe bool TryDispatchPathCount( return true; } + /// + /// Records the coarse rasterization stage that merges the binned draw objects back into draw + /// order and serializes each tile's PTCL command list for the fine pass. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the header, scene, monoid, info, and path buffers. + /// The byte length of the packed scene buffer binding. + /// The byte length of the per-draw monoid buffer binding. + /// The byte length of the combined info/bin-data buffer binding. + /// The byte length of the per-path buffer binding. + /// The sparse path-row buffer used to resolve tile indices. + /// The byte length of the sparse path-row buffer binding. + /// The tile buffer whose segment indices are rewritten to allocated slots. + /// The byte length of the tile buffer binding. + /// The PTCL buffer receiving each tile's command list. + /// The byte length of the PTCL buffer binding. + /// The scheduling bump-allocator buffer that tracks PTCL, segment, and blend-spill usage. + /// The workgroup count along the bin-column axis. + /// The workgroup count along the bin-row axis within the current chunk. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchCoarse( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, @@ -2441,18 +3872,18 @@ private static unsafe bool TryDispatchCoarse( nuint drawMonoidBufferSize, nuint infoBinDataBufferSize, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* pathTileBuffer, + WGPUBufferImpl* pathTileBuffer, nuint pathTileBufferSize, - WgpuBuffer* ptclBuffer, + WGPUBufferImpl* ptclBuffer, nuint ptclBufferSize, - WgpuBuffer* bumpBuffer, + WGPUBufferImpl* bumpBuffer, uint dispatchX, uint dispatchY, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[9]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[9]; entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, resources.SceneBuffer, sceneBufferSize); entries[2] = CreateBufferBinding(2, resources.DrawMonoidBuffer, drawMonoidBufferSize); @@ -2472,18 +3903,33 @@ private static unsafe bool TryDispatchCoarse( return true; } + /// + /// Records the one-workgroup pass that sizes the indirect path_tiling dispatch from the + /// segment-count total and performs the late seg-count overflow check, writing the PTCL abort + /// marker on failure. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The scene config buffer shared by all staged-scene passes. + /// The scheduling bump-allocator buffer providing the segment-count total and failure mask. + /// The indirect-dispatch argument buffer to populate. + /// The PTCL buffer receiving the abort marker when the run has failed. + /// The byte length of the PTCL buffer binding. + /// The X workgroup count for the stage. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathTilingSetup( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, - WgpuBuffer* headerBuffer, - WgpuBuffer* bumpBuffer, - WgpuBuffer* indirectCountBuffer, - WgpuBuffer* ptclBuffer, + WGPUBufferImpl* headerBuffer, + WGPUBufferImpl* bumpBuffer, + WGPUBufferImpl* indirectCountBuffer, + WGPUBufferImpl* ptclBuffer, nuint ptclBufferSize, uint dispatchX, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[4]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[4]; entries[0] = CreateBufferBinding(0, headerBuffer, (nuint)sizeof(GpuSceneConfig)); entries[1] = CreateBufferBinding(1, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[2] = CreateBufferBinding(2, indirectCountBuffer, (nuint)sizeof(GpuSceneIndirectCount)); @@ -2498,25 +3944,46 @@ private static unsafe bool TryDispatchPathTilingSetup( return true; } + /// + /// Records the per-crossing stage that clips each flattened line to its tile and writes the + /// final tile-relative segments consumed by the fine pass. + /// + /// The compute recording that receives the staged dispatch. + /// The flush context for the current render. + /// The staged-scene resource set that provides the line and path buffers. + /// The scheduling bump-allocator buffer providing the segment-count total. + /// The segment-count records emitted by path_count. + /// The byte length of the segment-count buffer binding. + /// The byte length of the flattened line buffer binding. + /// The byte length of the per-path buffer binding. + /// The sparse path-row buffer used to resolve tile indices. + /// The byte length of the sparse path-row buffer binding. + /// The tile buffer providing each tile's allocated segment base index. + /// The byte length of the tile buffer binding. + /// The segment buffer receiving the clipped tile-relative segments. + /// The byte length of the segment buffer binding. + /// The indirect-dispatch argument buffer seeded by path_tiling_setup. + /// Receives the recording failure reason when the dispatch cannot be staged. + /// when the dispatch was recorded successfully; otherwise, . private static unsafe bool TryDispatchPathTiling( WebGPUSceneComputeRecording recording, WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WgpuBuffer* bumpBuffer, - WgpuBuffer* segCountBuffer, + WGPUBufferImpl* bumpBuffer, + WGPUBufferImpl* segCountBuffer, nuint segCountBufferSize, nuint lineBufferSize, nuint pathBufferSize, - WgpuBuffer* pathRowBuffer, + WGPUBufferImpl* pathRowBuffer, nuint pathRowBufferSize, - WgpuBuffer* pathTileBuffer, + WGPUBufferImpl* pathTileBuffer, nuint pathTileBufferSize, - WgpuBuffer* segmentBuffer, + WGPUBufferImpl* segmentBuffer, nuint segmentBufferSize, - WgpuBuffer* indirectCountBuffer, + WGPUBufferImpl* indirectCountBuffer, out string? error) { - BindGroupEntry* entries = stackalloc BindGroupEntry[7]; + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[7]; entries[0] = CreateBufferBinding(0, bumpBuffer, (nuint)sizeof(GpuSceneBumpAllocators)); entries[1] = CreateBufferBinding(1, segCountBuffer, segCountBufferSize); entries[2] = CreateBufferBinding(2, resources.LineBuffer, lineBufferSize); @@ -2534,137 +4001,76 @@ private static unsafe bool TryDispatchPathTiling( return true; } + /// + /// Creates the format-specific fine pipeline and dispatches the analytic fine pass that + /// shades one 16x16 tile per workgroup by interpreting the PTCL command lists. + /// + /// + /// Unlike the recorded scheduling stages, fine is dispatched immediately because its pipeline + /// depends on the target texture format and it binds texture views alongside buffers. + /// + /// The flush context that owns the device, encoder, and texture format. + /// The staged-scene resource set that provides the header, info, and texture bindings. + /// The buffer plan providing the byte length of each scheduling binding. + /// The transient scheduling buffers produced by the earlier stages. + /// The storage texture view receiving the shaded output. + /// The texture view holding the existing target contents sampled as the backdrop. + /// The workgroup count along the tile-column axis. + /// The workgroup count along the tile-row axis for the current chunk window. + /// Receives the pipeline-creation or dispatch failure reason. + /// when the fine pass was dispatched successfully; otherwise, . private static unsafe bool TryDispatchFineArea( WebGPUFlushContext flushContext, WebGPUSceneResourceSet resources, - WebGPUEncodedScene encodedScene, WebGPUSceneBufferSizes bufferSizes, WebGPUSceneSchedulingResources scheduling, - TextureView* outputTextureView, + WGPUTextureViewImpl* outputTextureView, + WGPUTextureViewImpl* backdropTextureView, uint groupCountX, uint groupCountY, out string? error) { - bool useAliasedThreshold = encodedScene.FineRasterizationMode == RasterizationMode.Aliased; - byte[] shaderCode = useAliasedThreshold - ? FineAliasedThresholdComputeShader.GetCode(flushContext.TextureFormat) - : FineAreaComputeShader.GetCode(flushContext.TextureFormat); - - bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) - => useAliasedThreshold - ? FineAliasedThresholdComputeShader.TryCreateBindGroupLayout( - api, - device, - flushContext.TextureFormat, - out layout, - out layoutError) - : FineAreaComputeShader.TryCreateBindGroupLayout( - api, - device, - flushContext.TextureFormat, - out layout, - out layoutError); + // 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, + flushContext.TextureFormat, + out layout, + out layoutError); if (!flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( - $"{(useAliasedThreshold ? FineAliasedThresholdPipelineKey : FineAreaPipelineKey)}/{flushContext.TextureFormat}", + $"{FineAreaPipelineKey}/{flushContext.TextureFormat}/{alphaRepresentation}/{numericEncoding}", shaderCode, - useAliasedThreshold ? FineAliasedThresholdComputeShader.EntryPoint : FineAreaComputeShader.EntryPoint, + FineAreaComputeShader.EntryPoint, LayoutFactory, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUComputePipelineImpl* pipeline, out error)) { return false; } - BindGroupEntry* entries = stackalloc BindGroupEntry[9]; - entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); - entries[1] = CreateBufferBinding(1, scheduling.SegmentBuffer, bufferSizes.Segments.ByteLength); - entries[2] = CreateBufferBinding(2, scheduling.PtclBuffer, bufferSizes.Ptcl.ByteLength); - entries[3] = CreateBufferBinding(3, resources.InfoBinDataBuffer, checked(GetBindingByteLength(encodedScene.InfoBufferWordCount) + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength)); - entries[4] = CreateBufferBinding(4, scheduling.BlendBuffer, bufferSizes.BlendSpill.ByteLength); - entries[5] = new BindGroupEntry { Binding = 5, TextureView = outputTextureView }; - entries[6] = new BindGroupEntry { Binding = 6, TextureView = resources.GradientTextureView }; - entries[7] = new BindGroupEntry { Binding = 7, TextureView = resources.ImageAtlasTextureView }; - - using WebGPUHandle.HandleReference targetViewReference = flushContext.TargetTextureViewHandle.AcquireReference(); - entries[8] = new BindGroupEntry { Binding = 8, TextureView = (TextureView*)targetViewReference.Handle }; - - if (!TryDispatchComputePass(flushContext, bindGroupLayout, pipeline, entries, 9, groupCountX, groupCountY, 1, out error)) - { - return false; - } - - error = null; - return true; - } - - /// - /// Creates a bind group and dispatches one direct compute pass immediately. - /// - internal static unsafe bool TryDispatchComputePass( - WebGPUFlushContext flushContext, - BindGroupLayout* bindGroupLayout, - ComputePipeline* pipeline, - BindGroupEntry* entries, - uint entryCount, - uint groupCountX, - uint groupCountY, - uint groupCountZ, - out string? error) - { - if (groupCountX == 0 || groupCountY == 0 || groupCountZ == 0) - { - error = null; - return true; - } - - BindGroupDescriptor descriptor = new() - { - Layout = bindGroupLayout, - EntryCount = entryCount, - Entries = entries - }; - - BindGroup* bindGroup; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - bindGroup = flushContext.Api.DeviceCreateBindGroup((Device*)deviceReference.Handle, in descriptor); - } - - if (bindGroup is null) - { - error = "Failed to create a staged-scene compute bind group."; - return false; - } - - flushContext.TrackBindGroup(bindGroup); - bool ownsPassEncoder = false; - ComputePassEncoder* passEncoder = flushContext.ComputePassEncoder; - if (passEncoder is null) - { - if (!flushContext.BeginComputePass()) - { - error = "Failed to begin a staged-scene compute pass."; - return false; - } + WGPUBindGroupEntry* entries = stackalloc WGPUBindGroupEntry[9]; + entries[0] = CreateBufferBinding(0, resources.HeaderBuffer, (nuint)sizeof(GpuSceneConfig)); + entries[1] = CreateBufferBinding(1, scheduling.SegmentBuffer, bufferSizes.Segments.ByteLength); + entries[2] = CreateBufferBinding(2, scheduling.PtclBuffer, bufferSizes.Ptcl.ByteLength); + entries[3] = CreateBufferBinding(3, resources.InfoBinDataBuffer, checked(bufferSizes.Info.ByteLength + bufferSizes.BinData.ByteLength + bufferSizes.BinHeaders.ByteLength)); + entries[4] = CreateBufferBinding(4, scheduling.BlendBuffer, bufferSizes.BlendSpill.ByteLength); + entries[5] = new WGPUBindGroupEntry { binding = 5, textureView = outputTextureView }; + entries[6] = new WGPUBindGroupEntry { binding = 6, textureView = resources.GradientTextureView }; + entries[7] = new WGPUBindGroupEntry { binding = 7, textureView = resources.ImageAtlasTextureView }; - passEncoder = flushContext.ComputePassEncoder; - ownsPassEncoder = true; - } + entries[8] = new WGPUBindGroupEntry { binding = 8, textureView = backdropTextureView }; - try - { - flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); - flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, groupCountX, groupCountY, groupCountZ); - } - finally + if (!TryDispatchComputePass(flushContext, bindGroupLayout, pipeline, entries, 9, groupCountX, groupCountY, 1, out error)) { - if (ownsPassEncoder) - { - flushContext.EndComputePassIfOpen(); - } + return false; } error = null; @@ -2672,29 +4078,46 @@ internal static unsafe bool TryDispatchComputePass( } /// - /// Creates a bind group and dispatches one indirect compute pass immediately. + /// Creates a bind group and dispatches one direct compute pass immediately. /// - internal static unsafe bool TryDispatchComputePassIndirect( + /// The flush context that owns the device and compute pass encoder. + /// The bind-group layout matching the pipeline. + /// The compute pipeline to dispatch. + /// The bind-group entries for the dispatch. + /// The number of entries in . + /// The X workgroup count; a zero count on any axis records nothing. + /// The Y workgroup count. + /// The Z workgroup count. + /// Receives the bind-group or pass failure reason. + /// when the pass was dispatched (or skipped as empty); otherwise, . + private static unsafe bool TryDispatchComputePass( WebGPUFlushContext flushContext, - BindGroupLayout* bindGroupLayout, - ComputePipeline* pipeline, - BindGroupEntry* entries, + WGPUBindGroupLayoutImpl* bindGroupLayout, + WGPUComputePipelineImpl* pipeline, + WGPUBindGroupEntry* entries, uint entryCount, - WgpuBuffer* indirectBuffer, - ulong indirectOffset, + uint groupCountX, + uint groupCountY, + uint groupCountZ, out string? error) { - BindGroupDescriptor descriptor = new() + if (groupCountX == 0 || groupCountY == 0 || groupCountZ == 0) + { + error = null; + return true; + } + + WGPUBindGroupDescriptor descriptor = new() { - Layout = bindGroupLayout, - EntryCount = entryCount, - Entries = entries + layout = bindGroupLayout, + entryCount = entryCount, + entries = entries }; - BindGroup* bindGroup; + WGPUBindGroupImpl* bindGroup; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - bindGroup = flushContext.Api.DeviceCreateBindGroup((Device*)deviceReference.Handle, in descriptor); + bindGroup = flushContext.Api.DeviceCreateBindGroup((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); } if (bindGroup is null) @@ -2705,7 +4128,7 @@ internal static unsafe bool TryDispatchComputePassIndirect( flushContext.TrackBindGroup(bindGroup); bool ownsPassEncoder = false; - ComputePassEncoder* passEncoder = flushContext.ComputePassEncoder; + WGPUComputePassEncoderImpl* passEncoder = flushContext.ComputePassEncoder; if (passEncoder is null) { if (!flushContext.BeginComputePass()) @@ -2722,7 +4145,7 @@ internal static unsafe bool TryDispatchComputePassIndirect( { flushContext.Api.ComputePassEncoderSetPipeline(passEncoder, pipeline); flushContext.Api.ComputePassEncoderSetBindGroup(passEncoder, 0, bindGroup, 0, null); - flushContext.Api.ComputePassEncoderDispatchWorkgroupsIndirect(passEncoder, indirectBuffer, indirectOffset); + flushContext.Api.ComputePassEncoderDispatchWorkgroups(passEncoder, groupCountX, groupCountY, groupCountZ); } finally { @@ -2739,6 +4162,10 @@ internal static unsafe bool TryDispatchComputePassIndirect( /// /// Replays the recorded scheduling commands, resolving bind groups and pipelines just before submission. /// + /// The flush context that owns the device and command encoder. + /// The recording whose commands are replayed in submission order. + /// Receives the pipeline, bind-group, or pass failure reason, annotated with the failing stage name. + /// when every recorded command was replayed successfully; otherwise, . private static unsafe bool TryExecuteComputeRecording( WebGPUFlushContext flushContext, WebGPUSceneComputeRecording recording, @@ -2747,12 +4174,14 @@ private static unsafe bool TryExecuteComputeRecording( foreach (WebGPUSceneComputeCommand command in recording.Commands) { string shaderName = GetShaderDebugName(command.ShaderId); - if (!TryResolveComputeShader(flushContext, command.ShaderId, out BindGroupLayout* bindGroupLayout, out ComputePipeline* pipeline, out error)) + if (!TryResolveComputeShader(flushContext.DeviceState, command.ShaderId, out WGPUBindGroupLayoutImpl* bindGroupLayout, out WGPUComputePipelineImpl* pipeline, out error)) { error = error is null ? null : $"{error} Stage: {shaderName}."; return false; } + // A direct command with a zero workgroup count on any axis is a no-op; skip it + // entirely rather than paying bind-group and pass setup for nothing. if (!command.IsIndirect && (command.GroupCountX == 0 || command.GroupCountY == 0 || command.GroupCountZ == 0)) { @@ -2767,20 +4196,20 @@ private static unsafe bool TryExecuteComputeRecording( try { - BindGroupEntry[] entries = command.ResolveEntries(recording.ResourceRegistry); - fixed (BindGroupEntry* entriesPtr = entries) + WGPUBindGroupEntry[] entries = command.ResolveEntries(recording.ResourceRegistry); + fixed (WGPUBindGroupEntry* entriesPtr = entries) { - BindGroupDescriptor descriptor = new() + WGPUBindGroupDescriptor descriptor = new() { - Layout = bindGroupLayout, - EntryCount = (uint)entries.Length, - Entries = entriesPtr + layout = bindGroupLayout, + entryCount = (uint)entries.Length, + entries = entriesPtr }; - BindGroup* bindGroup; + WGPUBindGroupImpl* bindGroup; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - bindGroup = flushContext.Api.DeviceCreateBindGroup((Device*)deviceReference.Handle, in descriptor); + bindGroup = flushContext.Api.DeviceCreateBindGroup((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); } if (bindGroup is null) @@ -2858,17 +4287,23 @@ private static string GetShaderDebugName(WebGPUSceneShaderId shaderId) /// /// Resolves the cached bind-group layout and compute pipeline for one staged-scene shader identifier. /// + /// The shared device state that caches the compiled pipelines. + /// The staged-scene shader identifier to resolve. + /// Receives the cached bind-group layout for the shader. + /// Receives the cached compute pipeline for the shader. + /// Receives the pipeline-creation failure reason. + /// when the pipeline was resolved successfully; otherwise, . private static unsafe bool TryResolveComputeShader( - WebGPUFlushContext flushContext, + WebGPURuntime.DeviceSharedState deviceState, WebGPUSceneShaderId shaderId, - out BindGroupLayout* bindGroupLayout, - out ComputePipeline* pipeline, + out WGPUBindGroupLayoutImpl* bindGroupLayout, + out WGPUComputePipelineImpl* pipeline, out string? error) { bindGroupLayout = null; pipeline = null; - bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out string? layoutError) => + bool LayoutFactory(WebGPU api, WGPUDeviceImpl* device, out WGPUBindGroupLayoutImpl* layout, out string? layoutError) => shaderId switch { WebGPUSceneShaderId.Prepare => PrepareComputeShader.TryCreateBindGroupLayout(api, device, out layout, out layoutError), @@ -2979,7 +4414,7 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out _ => throw new UnreachableException() }; - return flushContext.DeviceState.TryGetOrCreateCompositeComputePipeline( + return deviceState.TryGetOrCreateCompositeComputePipeline( pipelineKey, shaderCode, entryPoint, @@ -2989,23 +4424,140 @@ bool LayoutFactory(WebGPU api, Device* device, out BindGroupLayout* layout, out out error); } + /// + /// 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. + /// + /// The shared device state whose pipeline caches are warmed. + public static void BeginPipelineWarmup(WebGPURuntime.DeviceSharedState deviceState) + { + bool queued = ThreadPool.UnsafeQueueUserWorkItem( + static state => + { + try + { + WarmPipelines(state); + } + finally + { + state.CompletePipelineWarmup(); + } + }, + deviceState, + preferLocal: false); + + // A failed queue operation has no worker that can signal completion. Mark it complete so + // device teardown does not wait forever for work that was never scheduled. + if (!queued) + { + deviceState.CompletePipelineWarmup(); + } + } + + /// + /// 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. + /// + /// 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.Flatten, + 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 _); + } + } + catch + { + // Best-effort warmup only; the render path surfaces real pipeline failures. + } + } + /// /// Creates one buffer binding entry covering the full bound range of the target buffer. /// + /// The shader binding slot. + /// The buffer to bind. + /// The number of bytes to bind starting at offset zero. + /// The populated bind-group entry. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffer* buffer, nuint size) + private static unsafe WGPUBindGroupEntry CreateBufferBinding(uint binding, WGPUBufferImpl* buffer, nuint size) => new() { - Binding = binding, - Buffer = buffer, - Offset = 0, - Size = size + binding = binding, + buffer = buffer, + offset = 0, + size = size }; /// /// Creates one reusable scheduling storage buffer that is owned outside the flush-context tracking lists. /// - private static unsafe WgpuBuffer* CreateArenaStorageBuffer( + /// The flush context that owns the device used to create the buffer. + /// The buffer size in bytes. + /// The created buffer, owned by the arena. + private static unsafe WGPUBufferImpl* CreateArenaStorageBuffer( WebGPUFlushContext flushContext, nuint size) => CreateArenaBuffer( @@ -3016,7 +4568,10 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// /// Creates one reusable scheduling buffer that can also serve as an indirect dispatch argument buffer. /// - private static unsafe WgpuBuffer* CreateArenaIndirectStorageBuffer( + /// The flush context that owns the device used to create the buffer. + /// The buffer size in bytes. + /// The created buffer, owned by the arena. + private static unsafe WGPUBufferImpl* CreateArenaIndirectStorageBuffer( WebGPUFlushContext flushContext, nuint size) => CreateArenaBuffer( @@ -3027,7 +4582,11 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// /// Creates one flush-scoped buffer, promoting zero-byte requests to a one-word allocation for WebGPU validation. /// - private static unsafe WgpuBuffer* CreateBuffer( + /// The flush context that owns the device and tracks the buffer for disposal. + /// The buffer size in bytes. + /// The buffer usage flags. + /// The created buffer, tracked by the flush context. + private static unsafe WGPUBufferImpl* CreateBuffer( WebGPUFlushContext flushContext, nuint size, BufferUsage usage) @@ -3037,15 +4596,15 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe size = sizeof(uint); } - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = usage, - Size = size + usage = (ulong)usage, + size = size }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - WgpuBuffer* buffer = flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); flushContext.TrackBuffer(buffer); return buffer; } @@ -3054,7 +4613,11 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// /// Creates one reusable scheduling buffer without attaching it to the current flush-context tracking lists. /// - private static unsafe WgpuBuffer* CreateArenaBuffer( + /// The flush context that owns the device used to create the buffer. + /// The buffer size in bytes; zero-byte requests are promoted to one word for WebGPU validation. + /// The buffer usage flags. + /// The created buffer, owned by the arena rather than the flush context. + private static unsafe WGPUBufferImpl* CreateArenaBuffer( WebGPUFlushContext flushContext, nuint size, BufferUsage usage) @@ -3064,15 +4627,15 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe size = sizeof(uint); } - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = usage, - Size = size + usage = (ulong)usage, + size = size }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - return flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); + return flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); } } @@ -3083,16 +4646,16 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// The unmanaged value to upload into the new copy-source buffer. /// The populated copy-source buffer. /// The unmanaged payload type uploaded into the new copy-source buffer. - private static unsafe WgpuBuffer* CreateAndUploadCopySourceBuffer( + private static unsafe WGPUBufferImpl* CreateAndUploadCopySourceBuffer( WebGPUFlushContext flushContext, in T value) where T : unmanaged { - WgpuBuffer* buffer = CreateBuffer(flushContext, (nuint)sizeof(T), BufferUsage.CopySrc | BufferUsage.CopyDst); + WGPUBufferImpl* buffer = CreateBuffer(flushContext, (nuint)sizeof(T), BufferUsage.CopySrc | BufferUsage.CopyDst); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); flushContext.Api.QueueWriteBuffer( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, buffer, 0, Unsafe.AsPointer(ref Unsafe.AsRef(in value)), @@ -3104,16 +4667,20 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// /// Creates one reusable scheduling storage buffer and uploads a single unmanaged value into it. /// - private static unsafe WgpuBuffer* CreateAndUploadArenaStorageBuffer( + /// The unmanaged payload type uploaded into the new buffer. + /// The flush context that owns the device and queue used to create and populate the buffer. + /// The unmanaged value to upload. + /// The populated buffer, owned by the arena. + private static unsafe WGPUBufferImpl* CreateAndUploadArenaStorageBuffer( WebGPUFlushContext flushContext, in T value) where T : unmanaged { - WgpuBuffer* buffer = CreateArenaStorageBuffer(flushContext, (nuint)sizeof(T)); + WGPUBufferImpl* buffer = CreateArenaStorageBuffer(flushContext, (nuint)sizeof(T)); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); flushContext.Api.QueueWriteBuffer( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, buffer, 0, Unsafe.AsPointer(ref Unsafe.AsRef(in value)), @@ -3125,6 +4692,9 @@ private static unsafe BindGroupEntry CreateBufferBinding(uint binding, WgpuBuffe /// /// Gets the byte length required to bind unmanaged elements, preserving WebGPU's non-zero binding rule. /// + /// The unmanaged element type of the binding. + /// The number of elements; zero is promoted to one element. + /// The non-zero binding byte length. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static nuint GetBindingByteLength(int count) where T : unmanaged @@ -3163,22 +4733,31 @@ private static bool TryValidateBufferSize( /// /// Pumps the WebGPU device while waiting for one asynchronous map callback to signal completion. /// - /// The optional native WGPU extension used to advance callback delivery. + /// The WebGPU API used to advance callback delivery. /// The device that owns the mapped readback buffer. /// The event that the map callback sets when the copy is ready to read. + /// The queue submission index to scope the wait to this readback. /// when the callback completed before the timeout; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static unsafe bool WaitForMapSignal(Wgpu? extension, Device* device, ManualResetEventSlim signal) + private static unsafe bool WaitForMapSignal( + WebGPU api, + WGPUDeviceImpl* device, + ManualResetEventSlim signal, + ulong submissionIndex) { - if (extension is null) - { - return signal.Wait(5000); - } - + // Preserve the readback's exact submission boundary while polling without a native wait. + // DevicePoll(wait: true) can itself exceed the five-second managed bound on a lost or + // stalled device, preventing the stopwatch from enforcing the documented timeout. Stopwatch stopwatch = Stopwatch.StartNew(); + while (!signal.IsSet && stopwatch.ElapsedMilliseconds < 5000) { - _ = extension.DevicePoll(device, true, (WrappedSubmissionIndex*)null); + _ = api.DevicePoll(device, false, &submissionIndex); + + if (!signal.IsSet) + { + Thread.Yield(); + } } return signal.IsSet; @@ -3194,9 +4773,16 @@ internal sealed unsafe class WebGPUSceneComputeRecording { private readonly List commands = []; + /// + /// Initializes a new instance of the class. + /// + /// The registry used to proxy resources at record time and resolve them at replay time. public WebGPUSceneComputeRecording(WebGPUSceneResourceRegistry resourceRegistry) => this.ResourceRegistry = resourceRegistry; + /// + /// Gets the registry that maps recorded resource proxies back to live buffers and texture views. + /// public WebGPUSceneResourceRegistry ResourceRegistry { get; } /// @@ -3207,9 +4793,17 @@ public WebGPUSceneComputeRecording(WebGPUSceneResourceRegistry resourceRegistry) /// /// Records one direct compute dispatch. /// + /// The staged-scene shader to dispatch. + /// The bind-group entries, converted to registry proxies for later resolution. + /// The number of entries in . + /// The X workgroup count. + /// The Y workgroup count. + /// The Z workgroup count. + /// Always receives ; recording cannot fail. + /// Always . public bool TryRecord( WebGPUSceneShaderId shaderId, - BindGroupEntry* entries, + WGPUBindGroupEntry* entries, uint entryCount, uint groupCountX, uint groupCountY, @@ -3232,11 +4826,18 @@ public bool TryRecord( /// /// Records one indirect compute dispatch. /// + /// The staged-scene shader to dispatch. + /// The bind-group entries, converted to registry proxies for later resolution. + /// The number of entries in . + /// The buffer holding the GPU-written workgroup counts. + /// The byte offset of the workgroup counts inside . + /// Always receives ; recording cannot fail. + /// Always . public bool TryRecordIndirect( WebGPUSceneShaderId shaderId, - BindGroupEntry* entries, + WGPUBindGroupEntry* entries, uint entryCount, - WgpuBuffer* indirectBuffer, + WGPUBufferImpl* indirectBuffer, ulong indirectOffset, out string? error) { @@ -3253,7 +4854,14 @@ public bool TryRecordIndirect( return true; } - private WebGPUSceneResourceProxy[] CopyResources(BindGroupEntry* entries, uint entryCount) + /// + /// Converts the caller's stack-allocated bind-group entries into heap-held registry proxies so + /// the recorded command remains valid after the entries go out of scope. + /// + /// The live bind-group entries to proxy. + /// The number of entries in . + /// The proxy array stored on the recorded command. + private WebGPUSceneResourceProxy[] CopyResources(WGPUBindGroupEntry* entries, uint entryCount) { WebGPUSceneResourceProxy[] resources = new WebGPUSceneResourceProxy[entryCount]; for (int i = 0; i < resources.Length; i++) @@ -3270,13 +4878,24 @@ private WebGPUSceneResourceProxy[] CopyResources(BindGroupEntry* entries, uint e /// internal readonly unsafe struct WebGPUSceneComputeCommand { + /// + /// Initializes a new instance of the struct. + /// + /// The staged-scene shader to dispatch. + /// The X workgroup count; zero for indirect commands. + /// The Y workgroup count; zero for indirect commands. + /// The Z workgroup count; zero for indirect commands. + /// The recorded binding proxies resolved at replay time. + /// The indirect argument buffer, or for direct commands. + /// The byte offset of the workgroup counts inside . + /// Whether the command dispatches with GPU-written workgroup counts. public WebGPUSceneComputeCommand( WebGPUSceneShaderId shaderId, uint groupCountX, uint groupCountY, uint groupCountZ, WebGPUSceneResourceProxy[] resources, - WgpuBuffer* indirectBuffer, + WGPUBufferImpl* indirectBuffer, ulong indirectOffset, bool isIndirect) { @@ -3290,25 +4909,54 @@ public WebGPUSceneComputeCommand( this.IsIndirect = isIndirect; } + /// + /// Gets the staged-scene shader dispatched by this command. + /// public WebGPUSceneShaderId ShaderId { get; } + /// + /// Gets the recorded binding proxies resolved to live entries at replay time. + /// public WebGPUSceneResourceProxy[] Resources { get; } + /// + /// Gets the X workgroup count; zero for indirect commands. + /// public uint GroupCountX { get; } + /// + /// Gets the Y workgroup count; zero for indirect commands. + /// public uint GroupCountY { get; } + /// + /// Gets the Z workgroup count; zero for indirect commands. + /// public uint GroupCountZ { get; } - public WgpuBuffer* IndirectBuffer { get; } + /// + /// Gets the indirect argument buffer, or for direct commands. + /// + public WGPUBufferImpl* IndirectBuffer { get; } + /// + /// Gets the byte offset of the workgroup counts inside . + /// public ulong IndirectOffset { get; } + /// + /// Gets a value indicating whether the command dispatches with GPU-written workgroup counts. + /// public bool IsIndirect { get; } - public BindGroupEntry[] ResolveEntries(WebGPUSceneResourceRegistry resourceRegistry) + /// + /// Resolves the recorded binding proxies back to live bind-group entries for execution. + /// + /// The registry that recorded the proxies. + /// The live bind-group entries in binding order. + public WGPUBindGroupEntry[] ResolveEntries(WebGPUSceneResourceRegistry resourceRegistry) { - BindGroupEntry[] entries = new BindGroupEntry[this.Resources.Length]; + WGPUBindGroupEntry[] entries = new WGPUBindGroupEntry[this.Resources.Length]; for (int i = 0; i < entries.Length; i++) { entries[i] = resourceRegistry.Resolve(this.Resources[i]); @@ -3323,36 +4971,135 @@ public BindGroupEntry[] ResolveEntries(WebGPUSceneResourceRegistry resourceRegis /// internal enum WebGPUSceneShaderId { + /// + /// prepare.wgsl: zeroes the bump counters and failure mask before a scheduling run. + /// Prepare = 0, + + /// + /// pathtag_reduce.wgsl: first-level reduction of path tags into workgroup monoids. + /// PathtagReduce = 1, + + /// + /// pathtag_reduce2.wgsl: second-level reduction used by the large scan variant. + /// PathtagReduce2 = 2, + + /// + /// pathtag_scan1.wgsl: middle scan pass of the large pathtag scan. + /// PathtagScan1 = 3, + + /// + /// pathtag_scan.wgsl (large variant): final per-tag-word exclusive prefix scan. + /// PathtagScan = 4, + + /// + /// pathtag_scan.wgsl (small variant): single-level scan for small tag streams. + /// PathtagScanSmall = 5, + + /// + /// bbox_clear.wgsl: resets per-path bboxes to inverted empty boxes. + /// BboxClear = 6, + + /// + /// flatten.wgsl: lowers encoded path segments into the device-space line soup. + /// Flatten = 7, + + /// + /// draw_reduce.wgsl: first pass of the draw-tag prefix sum. + /// DrawReduce = 8, + + /// + /// draw_leaf.wgsl: finalizes draw monoids, brush info, and clip inputs. + /// DrawLeaf = 9, + + /// + /// clip_reduce.wgsl: first pass of the clip stack-monoid scan. + /// ClipReduce = 10, + + /// + /// clip_leaf.wgsl: resolves the clip stack and conservative clip bboxes. + /// ClipLeaf = 11, + + /// + /// binning.wgsl: assigns draw objects to 16x16-tile bins. + /// Binning = 12, + + /// + /// path_row_alloc.wgsl: allocates sparse per-path tile-row records. + /// PathRowAlloc = 13, + + /// + /// path_row_span.wgsl: derives each sparse row's active column span from the lines. + /// PathRowSpan = 14, + + /// + /// tile_alloc.wgsl: finalizes row spans and allocates the backing tile storage. + /// TileAlloc = 15, + + /// + /// backdrop_dyn.wgsl: propagates winding backdrops along each sparse row. + /// Backdrop = 16, + + /// + /// path_count_setup.wgsl: sizes the indirect dispatch for the per-line stages. + /// PathCountSetup = 17, + + /// + /// path_count.wgsl: counts per-tile segment crossings and emits SegmentCount records. + /// PathCount = 18, + + /// + /// coarse.wgsl: serializes each tile's PTCL command list. + /// Coarse = 19, + + /// + /// path_tiling_setup.wgsl: sizes the indirect path_tiling dispatch and checks seg-count overflow. + /// PathTilingSetup = 20, + + /// + /// path_tiling.wgsl: writes the final clipped tile-relative segments. + /// PathTiling = 21, + + /// + /// chunk_reset.wgsl: clears the chunk-local bump counters between tile windows. + /// ChunkReset = 22 } /// -/// Serializable placeholder for one buffer or texture-view binding recorded before execution. +/// Recorded reference to one buffer or texture-view binding resolved before execution. /// internal readonly struct WebGPUSceneResourceProxy { + /// + /// Initializes a new instance of the struct. + /// + /// The shader binding slot. + /// The byte offset of the bound range; zero for texture views. + /// The byte length of the bound range; zero for texture views. + /// The registry-assigned identifier of the referenced resource. + /// Whether the proxy resolves to a buffer or a texture view. private WebGPUSceneResourceProxy( uint binding, nuint offset, @@ -3367,19 +5114,48 @@ private WebGPUSceneResourceProxy( this.Kind = kind; } + /// + /// Gets the shader binding slot. + /// public uint Binding { get; } + /// + /// Gets the byte offset of the bound range; zero for texture views. + /// public nuint Offset { get; } + /// + /// Gets the byte length of the bound range; zero for texture views. + /// public nuint Size { get; } + /// + /// Gets the registry-assigned identifier of the referenced resource. + /// public uint ResourceId { get; } + /// + /// Gets whether the proxy resolves to a buffer or a texture view. + /// public WebGPUSceneResourceProxyKind Kind { get; } + /// + /// Creates a proxy for one buffer binding. + /// + /// The shader binding slot. + /// The registry-assigned buffer identifier. + /// The byte offset of the bound range. + /// The byte length of the bound range. + /// The buffer proxy. public static WebGPUSceneResourceProxy CreateBuffer(uint binding, uint resourceId, nuint offset, nuint size) => new(binding, offset, size, resourceId, WebGPUSceneResourceProxyKind.Buffer); + /// + /// Creates a proxy for one texture-view binding. + /// + /// The shader binding slot. + /// The registry-assigned texture-view identifier. + /// The texture-view proxy. public static WebGPUSceneResourceProxy CreateTextureView(uint binding, uint resourceId) => new(binding, 0, 0, resourceId, WebGPUSceneResourceProxyKind.TextureView); } @@ -3389,7 +5165,14 @@ public static WebGPUSceneResourceProxy CreateTextureView(uint binding, uint reso /// internal enum WebGPUSceneResourceProxyKind { + /// + /// The proxy resolves to a storage or uniform buffer. + /// Buffer = 0, + + /// + /// The proxy resolves to a texture view. + /// TextureView = 1 } @@ -3411,6 +5194,8 @@ private WebGPUSceneResourceRegistry() /// /// Creates a registry preloaded with the persistent resources owned by the staged scene. /// + /// The staged-scene resource set whose buffers and texture views are preregistered. + /// The populated registry. public static WebGPUSceneResourceRegistry Create(WebGPUSceneResourceSet resources) { WebGPUSceneResourceRegistry registry = new(); @@ -3439,16 +5224,25 @@ public static WebGPUSceneResourceRegistry Create(WebGPUSceneResourceSet resource /// /// Registers the transient buffers produced by the scheduling passes. /// + /// The bin-header buffer. + /// The indirect dispatch-count buffer. + /// The sparse path-row buffer. + /// The path-tile buffer. + /// The segment-count buffer. + /// The segment buffer. + /// The blend-spill buffer. + /// The PTCL buffer. + /// The bump-allocator buffer. public void RegisterSchedulingBuffers( - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* indirectCountBuffer, - WgpuBuffer* pathRowBuffer, - WgpuBuffer* pathTileBuffer, - WgpuBuffer* segCountBuffer, - WgpuBuffer* segmentBuffer, - WgpuBuffer* blendBuffer, - WgpuBuffer* ptclBuffer, - WgpuBuffer* bumpBuffer) + WGPUBufferImpl* binHeaderBuffer, + WGPUBufferImpl* indirectCountBuffer, + WGPUBufferImpl* pathRowBuffer, + WGPUBufferImpl* pathTileBuffer, + WGPUBufferImpl* segCountBuffer, + WGPUBufferImpl* segmentBuffer, + WGPUBufferImpl* blendBuffer, + WGPUBufferImpl* ptclBuffer, + WGPUBufferImpl* bumpBuffer) { this.RegisterBuffer(binHeaderBuffer); this.RegisterBuffer(indirectCountBuffer); @@ -3464,26 +5258,34 @@ public void RegisterSchedulingBuffers( /// /// Converts one live bind-group entry into a stable proxy that can be resolved later. /// - public WebGPUSceneResourceProxy CreateProxy(BindGroupEntry entry) - => entry.TextureView is not null - ? WebGPUSceneResourceProxy.CreateTextureView(entry.Binding, this.GetTextureViewId(entry.TextureView)) - : WebGPUSceneResourceProxy.CreateBuffer(entry.Binding, this.GetBufferId(entry.Buffer), checked((nuint)entry.Offset), checked((nuint)entry.Size)); + /// The live bind-group entry; its buffer or texture view must already be registered. + /// The stable proxy for the entry. + public WebGPUSceneResourceProxy CreateProxy(WGPUBindGroupEntry entry) + => entry.textureView is not null + ? WebGPUSceneResourceProxy.CreateTextureView(entry.binding, this.GetTextureViewId(entry.textureView)) + : WebGPUSceneResourceProxy.CreateBuffer(entry.binding, this.GetBufferId(entry.buffer), checked((nuint)entry.offset), checked((nuint)entry.size)); /// /// Resolves one previously recorded proxy back to the live bind-group entry for execution. /// - public BindGroupEntry Resolve(WebGPUSceneResourceProxy proxy) + /// The recorded proxy to resolve. + /// The live bind-group entry. + public WGPUBindGroupEntry Resolve(WebGPUSceneResourceProxy proxy) => proxy.Kind == WebGPUSceneResourceProxyKind.TextureView - ? new BindGroupEntry { Binding = proxy.Binding, TextureView = (TextureView*)this.textureViews[proxy.ResourceId] } - : new BindGroupEntry + ? new WGPUBindGroupEntry { binding = proxy.Binding, textureView = (WGPUTextureViewImpl*)this.textureViews[proxy.ResourceId] } + : new WGPUBindGroupEntry { - Binding = proxy.Binding, - Buffer = (WgpuBuffer*)this.buffers[proxy.ResourceId], - Offset = proxy.Offset, - Size = proxy.Size + binding = proxy.Binding, + buffer = (WGPUBufferImpl*)this.buffers[proxy.ResourceId], + offset = proxy.Offset, + size = proxy.Size }; - private void RegisterBuffer(WgpuBuffer* buffer) + /// + /// Assigns a stable identifier to one buffer, ignoring buffers registered earlier. + /// + /// The buffer to register. + private void RegisterBuffer(WGPUBufferImpl* buffer) { nint handle = (nint)buffer; if (this.bufferIds.ContainsKey(handle)) @@ -3496,7 +5298,11 @@ private void RegisterBuffer(WgpuBuffer* buffer) this.buffers[id] = (nint)buffer; } - private void RegisterTextureView(TextureView* textureView) + /// + /// Assigns a stable identifier to one texture view, ignoring views registered earlier. + /// + /// The texture view to register. + private void RegisterTextureView(WGPUTextureViewImpl* textureView) { nint handle = (nint)textureView; if (this.textureViewIds.ContainsKey(handle)) @@ -3509,9 +5315,19 @@ private void RegisterTextureView(TextureView* textureView) this.textureViews[id] = (nint)textureView; } - private uint GetBufferId(WgpuBuffer* buffer) => this.bufferIds[(nint)buffer]; + /// + /// Looks up the identifier assigned to one registered buffer. + /// + /// The registered buffer. + /// The registry-assigned identifier. + private uint GetBufferId(WGPUBufferImpl* buffer) => this.bufferIds[(nint)buffer]; - private uint GetTextureViewId(TextureView* textureView) => this.textureViewIds[(nint)textureView]; + /// + /// Looks up the identifier assigned to one registered texture view. + /// + /// The registered texture view. + /// The registry-assigned identifier. + private uint GetTextureViewId(WGPUTextureViewImpl* textureView) => this.textureViewIds[(nint)textureView]; } /// @@ -3519,30 +5335,64 @@ private void RegisterTextureView(TextureView* textureView) /// internal readonly struct WebGPUStagedScene : IDisposable { + /// + /// Initializes a new instance of the struct for a full scene + /// with explicit encoded-scene and flush-context ownership. Chunk-scoped views over another + /// staged scene must own neither: disposing a chunk view would otherwise tear down the outer + /// flush mid-render and dispose the caller-owned retained payload. + /// + /// The flush context recording this staged scene. + /// The encoded scene payload. + /// The dispatch and buffer plan for the scene. + /// The flush-scoped GPU resources created for the scene. + /// The recorded binding-limit overflow, if any. + /// Whether disposing this staged scene also disposes . + /// Whether disposing this staged scene also disposes . public WebGPUStagedScene( WebGPUFlushContext flushContext, WebGPUEncodedScene encodedScene, WebGPUSceneConfig config, WebGPUSceneResourceSet resources, - WebGPUSceneDispatch.BindingLimitFailure bindingLimitFailure) - : this(flushContext, encodedScene, config, resources, bindingLimitFailure, ownsEncodedScene: true) + WebGPUSceneDispatch.BindingLimitFailure bindingLimitFailure, + bool ownsEncodedScene, + bool ownsFlushContext) { + this.FlushContext = flushContext; + this.EncodedScene = encodedScene; + this.Config = config; + this.Resources = resources; + this.BindingLimitFailure = bindingLimitFailure; + this.OwnsEncodedScene = ownsEncodedScene; + this.Range = null; + this.OwnsFlushContext = ownsFlushContext; } + /// + /// Initializes a new instance of the struct for one scene + /// range; neither the encoded scene nor the caller-supplied flush context is owned. + /// + /// The caller-owned flush context; never disposed by this instance. + /// The encoded scene that owns the range; never disposed by this instance. + /// The scene range rendered by this staged scene. + /// The dispatch and buffer plan for the range. + /// The flush-scoped GPU resources created for the range. + /// The recorded binding-limit overflow, if any. public WebGPUStagedScene( WebGPUFlushContext flushContext, WebGPUEncodedScene encodedScene, + WebGPUSceneRange range, WebGPUSceneConfig config, WebGPUSceneResourceSet resources, - WebGPUSceneDispatch.BindingLimitFailure bindingLimitFailure, - bool ownsEncodedScene) + WebGPUSceneDispatch.BindingLimitFailure bindingLimitFailure) { this.FlushContext = flushContext; this.EncodedScene = encodedScene; this.Config = config; this.Resources = resources; this.BindingLimitFailure = bindingLimitFailure; - this.OwnsEncodedScene = ownsEncodedScene; + this.OwnsEncodedScene = false; + this.Range = range; + this.OwnsFlushContext = false; } /// @@ -3575,6 +5425,16 @@ public WebGPUStagedScene( /// public bool OwnsEncodedScene { get; } + /// + /// Gets the encoded range rendered by this staged scene, when it is not rendering the full scene. + /// + public WebGPUSceneRange? Range { get; } + + /// + /// Gets a value indicating whether this staged scene owns its flush context. + /// + public bool OwnsFlushContext { get; } + /// /// Releases the encoded scene and the flush context that owns the tracked native resources. /// @@ -3585,7 +5445,10 @@ public void Dispose() this.EncodedScene.Dispose(); } - this.FlushContext.Dispose(); + if (this.OwnsFlushContext) + { + this.FlushContext.Dispose(); + } } } @@ -3594,16 +5457,28 @@ public void Dispose() /// internal readonly unsafe struct WebGPUSceneSchedulingResources { + /// + /// Initializes a new instance of the struct. + /// + /// The bin-header buffer produced by the scheduling passes. + /// The indirect dispatch-count buffer. + /// The sparse path-row buffer. + /// The path-tile buffer. + /// The segment-count buffer. + /// The segment buffer. + /// The blend-spill buffer. + /// The PTCL buffer. + /// The bump-allocator buffer. public WebGPUSceneSchedulingResources( - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* indirectCountBuffer, - WgpuBuffer* pathRowBuffer, - WgpuBuffer* pathTileBuffer, - WgpuBuffer* segCountBuffer, - WgpuBuffer* segmentBuffer, - WgpuBuffer* blendBuffer, - WgpuBuffer* ptclBuffer, - WgpuBuffer* bumpBuffer) + WGPUBufferImpl* binHeaderBuffer, + WGPUBufferImpl* indirectCountBuffer, + WGPUBufferImpl* pathRowBuffer, + WGPUBufferImpl* pathTileBuffer, + WGPUBufferImpl* segCountBuffer, + WGPUBufferImpl* segmentBuffer, + WGPUBufferImpl* blendBuffer, + WGPUBufferImpl* ptclBuffer, + WGPUBufferImpl* bumpBuffer) { this.BinHeaderBuffer = binHeaderBuffer; this.IndirectCountBuffer = indirectCountBuffer; @@ -3619,47 +5494,47 @@ public WebGPUSceneSchedulingResources( /// /// Gets the bin-header buffer produced by the scheduling passes. /// - public WgpuBuffer* BinHeaderBuffer { get; } + public WGPUBufferImpl* BinHeaderBuffer { get; } /// /// Gets the indirect dispatch-count buffer produced by the scheduling passes. /// - public WgpuBuffer* IndirectCountBuffer { get; } + public WGPUBufferImpl* IndirectCountBuffer { get; } /// /// Gets the sparse path-row buffer produced by the scheduling passes. /// - public WgpuBuffer* PathRowBuffer { get; } + public WGPUBufferImpl* PathRowBuffer { get; } /// /// Gets the path-tile buffer produced by the scheduling passes. /// - public WgpuBuffer* PathTileBuffer { get; } + public WGPUBufferImpl* PathTileBuffer { get; } /// /// Gets the segment-count buffer produced by the scheduling passes. /// - public WgpuBuffer* SegCountBuffer { get; } + public WGPUBufferImpl* SegCountBuffer { get; } /// /// Gets the segment buffer produced by the scheduling passes. /// - public WgpuBuffer* SegmentBuffer { get; } + public WGPUBufferImpl* SegmentBuffer { get; } /// /// Gets the blend-spill buffer produced by the scheduling passes. /// - public WgpuBuffer* BlendBuffer { get; } + public WGPUBufferImpl* BlendBuffer { get; } /// /// Gets the PTCL buffer produced by the scheduling passes. /// - public WgpuBuffer* PtclBuffer { get; } + public WGPUBufferImpl* PtclBuffer { get; } /// /// Gets the bump allocator buffer used to coordinate scratch allocation across scheduling passes. /// - public WgpuBuffer* BumpBuffer { get; } + public WGPUBufferImpl* BumpBuffer { get; } } /// @@ -3671,21 +5546,40 @@ public WebGPUSceneSchedulingResources( /// internal sealed unsafe class WebGPUSceneSchedulingArena { + private int disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The API facade used to release the arena buffers. + /// The device that created the arena buffers. + /// The buffer plan the arena buffers were sized from. + /// The size of the map-readable status buffer in bytes. + /// The bin-header scratch buffer. + /// The indirect dispatch-count buffer. + /// The sparse path-row scratch buffer. + /// The path-tile scratch buffer. + /// The segment-count scratch buffer. + /// The segment scratch buffer. + /// The blend-spill scratch buffer. + /// The PTCL scratch buffer. + /// The bump-allocator buffer. + /// The map-readable status readback buffer. public WebGPUSceneSchedulingArena( WebGPU api, WebGPUDeviceHandle device, WebGPUSceneBufferSizes capacitySizes, nuint readbackByteLength, - WgpuBuffer* binHeaderBuffer, - WgpuBuffer* indirectCountBuffer, - WgpuBuffer* pathRowBuffer, - WgpuBuffer* pathTileBuffer, - WgpuBuffer* segCountBuffer, - WgpuBuffer* segmentBuffer, - WgpuBuffer* blendBuffer, - WgpuBuffer* ptclBuffer, - WgpuBuffer* bumpBuffer, - WgpuBuffer* readbackBuffer) + WGPUBufferImpl* binHeaderBuffer, + WGPUBufferImpl* indirectCountBuffer, + WGPUBufferImpl* pathRowBuffer, + WGPUBufferImpl* pathTileBuffer, + WGPUBufferImpl* segCountBuffer, + WGPUBufferImpl* segmentBuffer, + WGPUBufferImpl* blendBuffer, + WGPUBufferImpl* ptclBuffer, + WGPUBufferImpl* bumpBuffer, + WGPUBufferImpl* readbackBuffer) { this.Api = api; this.Device = device; @@ -3713,33 +5607,74 @@ public WebGPUSceneSchedulingArena( /// public WebGPUDeviceHandle Device { get; } + /// + /// Gets the buffer plan the arena buffers were sized from; a new scene can reuse the arena + /// only when its own plan fits within these capacities. + /// public WebGPUSceneBufferSizes CapacitySizes { get; } + /// + /// Gets the size of the map-readable status buffer in bytes. + /// public nuint ReadbackByteLength { get; } - public WgpuBuffer* BinHeaderBuffer { get; } + /// + /// Gets the bin-header scratch buffer. + /// + public WGPUBufferImpl* BinHeaderBuffer { get; private set; } - public WgpuBuffer* IndirectCountBuffer { get; } + /// + /// Gets the indirect dispatch-count buffer. + /// + public WGPUBufferImpl* IndirectCountBuffer { get; private set; } - public WgpuBuffer* PathRowBuffer { get; } + /// + /// Gets the sparse path-row scratch buffer. + /// + public WGPUBufferImpl* PathRowBuffer { get; private set; } - public WgpuBuffer* PathTileBuffer { get; } + /// + /// Gets the path-tile scratch buffer. + /// + public WGPUBufferImpl* PathTileBuffer { get; private set; } - public WgpuBuffer* SegCountBuffer { get; } + /// + /// Gets the segment-count scratch buffer. + /// + public WGPUBufferImpl* SegCountBuffer { get; private set; } - public WgpuBuffer* SegmentBuffer { get; } + /// + /// Gets the segment scratch buffer. + /// + public WGPUBufferImpl* SegmentBuffer { get; private set; } - public WgpuBuffer* BlendBuffer { get; } + /// + /// Gets the blend-spill scratch buffer. + /// + public WGPUBufferImpl* BlendBuffer { get; private set; } - public WgpuBuffer* PtclBuffer { get; } + /// + /// Gets the PTCL scratch buffer. + /// + public WGPUBufferImpl* PtclBuffer { get; private set; } - public WgpuBuffer* BumpBuffer { get; } + /// + /// Gets the bump-allocator buffer. + /// + public WGPUBufferImpl* BumpBuffer { get; private set; } - public WgpuBuffer* ReadbackBuffer { get; } + /// + /// Gets the map-readable status readback buffer. + /// + public WGPUBufferImpl* ReadbackBuffer { get; private set; } /// /// Returns true if every buffer fits the required sizes for this scene. /// + /// The flush context whose device must match the arena's creating device. + /// The buffer plan the arena must be able to satisfy. + /// The required size of the map-readable status buffer. + /// when the arena can be reused as-is; otherwise, . public bool CanReuse(WebGPUFlushContext flushContext, WebGPUSceneBufferSizes bufferSizes, nuint readbackByteLength) => ReferenceEquals(this.Device, flushContext.DeviceHandle) && this.BinHeaderBuffer is not null && @@ -3763,30 +5698,49 @@ this.ReadbackBuffer is not null && readbackByteLength <= this.ReadbackByteLength; /// - /// Releases all GPU buffers owned by this arena. + /// Releases all GPU buffers owned by this arena. Arena caches hand instances between + /// owners with atomic exchanges, so disposal can race; the exchanged flag guarantees the + /// native buffers are released exactly once, and nulling each handle turns any stale + /// reference into a failed instead of a native use-after-free. /// + /// The arena to dispose; is ignored. public static void Dispose(WebGPUSceneSchedulingArena? arena) { - if (arena is null || arena.BinHeaderBuffer is null) + if (arena is null || Interlocked.Exchange(ref arena.disposed, 1) == 1) { return; } WebGPU api = arena.Api; ReleaseArenaBuffer(api, arena.ReadbackBuffer); + arena.ReadbackBuffer = null; ReleaseArenaBuffer(api, arena.BumpBuffer); + arena.BumpBuffer = null; ReleaseArenaBuffer(api, arena.PtclBuffer); + arena.PtclBuffer = null; ReleaseArenaBuffer(api, arena.BlendBuffer); + arena.BlendBuffer = null; ReleaseArenaBuffer(api, arena.SegmentBuffer); + arena.SegmentBuffer = null; ReleaseArenaBuffer(api, arena.SegCountBuffer); + arena.SegCountBuffer = null; ReleaseArenaBuffer(api, arena.PathRowBuffer); + arena.PathRowBuffer = null; ReleaseArenaBuffer(api, arena.PathTileBuffer); + arena.PathTileBuffer = null; ReleaseArenaBuffer(api, arena.IndirectCountBuffer); + arena.IndirectCountBuffer = null; ReleaseArenaBuffer(api, arena.BinHeaderBuffer); + arena.BinHeaderBuffer = null; } + /// + /// Releases one arena buffer, ignoring null handles. + /// + /// The API facade used to release the buffer. + /// The buffer to release, or . [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ReleaseArenaBuffer(WebGPU api, WgpuBuffer* buffer) + public static void ReleaseArenaBuffer(WebGPU api, WGPUBufferImpl* buffer) { if (buffer is not null) { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs index a441347c4..1071ccf96 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneEncoder.cs @@ -9,7 +9,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -18,18 +18,105 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Builds the flush-scoped scene payload consumed by the staged WebGPU rasterizer. /// +/// +/// Commands are lowered into Vello-style scene streams (path tags, path data, draw tags, +/// draw data, transforms, styles) plus auxiliary gradient and image payloads. Large batches +/// are planned and encoded in parallel over contiguous command-range partitions whose +/// boundaries are snapped forward so no layer scope is cut and whose open clip scopes are +/// replayed per partition; the partitions are then concatenated in timeline order into one +/// packed scene buffer. +/// internal static class WebGPUSceneEncoder { - private const int GradientWidth = 512; - private const int PathGradientHeaderWordCount = 4; - private const int PathGradientEdgeWordCount = 6; - private const int StyleWordCount = 5; + /// + /// The pixel width of every sampled gradient ramp row uploaded to the gradient texture. + /// Shared with so the packed buffer slice + /// always matches the encoder's ramp stride. + /// + public const int GradientWidth = 512; + + // Gradient ramps use RGBA16Float: each texel occupies two uint words. + internal const int GradientRowWordCount = GradientWidth * 2; + + /// + /// The word count of the path-gradient payload header: center x/y, max distance, and four center-color components. + /// + private const int PathGradientHeaderWordCount = 7; + + /// + /// The word count of one path-gradient edge record: start x/y, end x/y, and four components for each endpoint color. + /// + private const int PathGradientEdgeWordCount = 12; + + /// + /// The word count of one RecolorBrush auxiliary record: two f32 colors and the squared-distance threshold. + /// + private const int RecolorDataWordCount = 9; + + /// + /// The word count of one encoded style record. Must match STYLE_SIZE_IN_WORDS in pathtag.wgsl. + /// + private const int StyleWordCount = 10; + + /// + /// The word count of one encoded transform: 2x2 linear part, translation, and the m14/m24/m44 projective terms. + /// private const int TransformWordCount = 9; + + /// + /// The tile width in pixels used by binning, coarse, and fine. Must match TILE_WIDTH in config.wgsl. + /// private const int TileWidth = 16; - private const int TileHeight = 16; + + /// + /// The tile height in pixels used by binning, coarse, and fine. Must match TILE_HEIGHT in config.wgsl. + /// + public const int TileHeight = 16; + + /// + /// 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 + /// the substitute just above the micro-segment collapse threshold. + /// private const float PointStrokeSegmentHalfLength = 1F / 128F; + + /// + /// The stroke micro-segment collapse threshold in pixels. Mirrors the CPU stroker's 1/64 px + /// preprocessing in DefaultRasterizer.StrokeLinearizer so GPU strokes consume the same + /// segment list the CPU stroker strokes. + /// private const float StrokeMicroSegmentEpsilon = 1F / 64F; - private static readonly GraphicsOptions DefaultClipGraphicsOptions = new(); + + /// + /// Vello's clip blend word, (MIX_CLIP << 8) | COMPOSE_SRC_OVER. Values must match blend.wgsl. + /// + private const uint ClipBlendMode = (128U << 8) | 3U; + + /// + /// High bit of the clip blend word marking a Difference clip, an ImageSharp extension over + /// Vello's clip records. Must match CLIP_DIFFERENCE_MASK_BIT in clip.wgsl and ptcl.wgsl. + /// + private const uint ClipDifferenceMaskBit = 0x80000000U; + + /// + /// Bit of the clip blend word marking a hard-edge (aliased) clip mask. + /// Must match CLIP_HARD_MASK_BIT in drawtag.wgsl. + /// + private const uint ClipHardMaskBit = 0x40000000U; + + /// + /// Bit of the clip blend word marking an isolated group (a layer). Isolated groups seed + /// transparent on the GPU and composite back as a unit; canvas clips leave this clear and + /// render as coverage masks over the live tile content, matching the CPU backend's + /// per-draw clip masking. Must match CLIP_ISOLATED_MASK_BIT in drawtag.wgsl. + /// + private const uint ClipIsolatedMaskBit = 0x20000000U; + + /// + /// Default options used when encoding a layer's bounding-rectangle clip mask. The layer's real + /// graphics options apply when the layer composites back, not when its mask is bounded. + /// + private static readonly GraphicsOptions LayerMaskGraphicsOptions = new(); /// /// Tags packed into the byte path-tag stream consumed by the WebGPU shaders. @@ -77,6 +164,11 @@ private enum StyleFlags : uint Stroke = 1U << 31 } + /// + /// Converts one path tag to its byte-stream wire representation. + /// + /// The path tag to pack. + /// The packed tag byte. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static byte PackPathTag(PathTag tag) => (byte)tag; @@ -109,9 +201,22 @@ public static bool TryEncode( int lastTargetRowBandIndex = (targetBounds.Bottom - 1) / TileHeight; int targetRowCount = (lastTargetRowBandIndex - firstTargetRowBandIndex) + 1; int commandCount = scene.CommandCount; + + // Clip scopes do not force a single range: each partition replays the clip scopes open at + // its boundary and closes its own, so contiguous partitions concatenate into one valid + // push/pop clip sequence. Layer scopes composite their contents as one group with the + // layer's graphics options when they close, so a partition boundary may not cut through + // an open layer; the prescan snaps boundaries forward to the next point where no layer + // scope is open instead of serializing the whole scene. int partitionCount = GetPartitionCount(maxDegreeOfParallelism, commandCount, targetRowCount); + int[]? partitionBoundaries = null; + CompositionCommand[][]? partitionSeedClips = null; + if (partitionCount > 1 && (scene.HasClipControls || scene.HasLayers)) + { + CreatePartitionCommandRanges(scene, partitionCount, out partitionBoundaries, out partitionSeedClips); + } - SceneEncodingPlan plan = SceneEncodingPlan.Create(scene, maxDegreeOfParallelism, partitionCount); + SceneEncodingPlan plan = SceneEncodingPlan.Create(scene, maxDegreeOfParallelism, partitionCount, partitionBoundaries); if (partitionCount > 1) { Rectangle targetRectangle = targetBounds; @@ -129,14 +234,17 @@ public static bool TryEncode( { // Match the CPU retained scene builder: partitions are contiguous command ranges, // and the final merge appends them by partition index to preserve timeline order. - int commandStart = (partitionIndex * commandCount) / partitionCount; - int commandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; + // Boundary-snapped ranges can be empty; empty ranges skip clip seeding so they + // do not emit balanced-but-useless clip records. + int commandStart = partitionBoundaries is null ? (partitionIndex * commandCount) / partitionCount : partitionBoundaries[partitionIndex]; + int commandEnd = partitionBoundaries is null ? ((partitionIndex + 1) * commandCount) / partitionCount : partitionBoundaries[partitionIndex + 1]; SceneEncodingPlan partitionPlan = plan.GetPartitionPlan(partitionIndex); SupportedSubsetSceneEncoding partitionEncoding = new(allocator, targetRectangle, partitionPlan); try { - if (!partitionEncoding.TryBuild(scene, partitionPlan, commandStart, commandEnd, out string? partitionError)) + CompositionCommand[]? seedClips = commandStart == commandEnd ? null : partitionSeedClips?[partitionIndex]; + if (!partitionEncoding.TryBuild(scene, partitionPlan, commandStart, commandEnd, seedClips, out string? partitionError)) { failed[partitionIndex] = true; errors[partitionIndex] = partitionError; @@ -170,37 +278,17 @@ public static bool TryEncode( } int fillCount = 0; - bool hasFineRasterizationMode = false; - RasterizationMode fineRasterizationMode = RasterizationMode.Antialiased; - float fineCoverageThreshold = 0F; - for (int i = 0; i < partitions.Length; i++) { - SceneEncodingPartition partition = partitions[i]!; - fillCount += partition.FillCount; - if (partition.VisibleFillCount == 0) - { - continue; - } - - if (!hasFineRasterizationMode) - { - fineRasterizationMode = partition.FineRasterizationMode; - fineCoverageThreshold = partition.FineCoverageThreshold; - hasFineRasterizationMode = true; - continue; - } - - if (partition.FineRasterizationMode != fineRasterizationMode || - partition.FineCoverageThreshold != fineCoverageThreshold) - { - DisposePartitions(partitions); - encodedScene = WebGPUEncodedScene.Empty; - error = "The staged WebGPU scene pipeline does not support mixed fine rasterization modes or thresholds within one flush."; - return false; - } + fillCount += partitions[i]!.FillCount; } + // Rasterization mode and the aliased coverage threshold are carried per fill, so partitions + // impose no flush-wide rasterization constraint. These scene-wide values are retained only + // as encoded-scene metadata and are no longer consumed by the fine pass. + RasterizationMode fineRasterizationMode = RasterizationMode.Antialiased; + float fineCoverageThreshold = 0F; + if (fillCount == 0) { DisposePartitions(partitions); @@ -237,7 +325,7 @@ public static bool TryEncode( return true; } - encodedScene = SupportedSubsetSceneResolver.Resolve(ref encoding, targetBounds, allocator); + encodedScene = SupportedSubsetSceneResolver.Resolve(ref encoding, targetBounds, allocator, []); error = null; return true; } @@ -248,153 +336,818 @@ public static bool TryEncode( } /// - /// Disposes any partition encodings that were produced before the parallel operation completed. + /// Encodes a prepared command batch containing Apply into ordered WebGPU scene operations. /// - private static void DisposePartitions(SceneEncodingPartition?[] partitions) + /// The prepared command batch. + /// The target bounds used for target-local coordinate conversion. + /// The allocator used for temporary and packed scene storage. + /// The maximum degree of parallelism used by leaf scene encoding. + /// Receives the encoded ordered scene on success. + /// Receives the failure reason when encoding fails. + /// when the scene encoded successfully; otherwise, . + public static bool TryEncodeOrdered( + DrawingCommandBatch scene, + in Rectangle targetBounds, + MemoryAllocator allocator, + int maxDegreeOfParallelism, + out WebGPUEncodedScene encodedScene, + out string? error) { - for (int i = 0; i < partitions.Length; i++) + // Ordered scenes interleave draw ranges with Apply/layer barriers whose results feed later + // draws, so encoding stays sequential; the parallelism argument is accepted only for + // signature parity with TryEncode. + _ = maxDegreeOfParallelism; + SceneEncodingPlan plan = SceneEncodingPlan.CreateDefault(scene.CommandCount); + SupportedSubsetSceneEncoding encoding = new(allocator, targetBounds, plan); + List operations = []; + int nextLayerTargetId = 1; + + try { - partitions[i]?.Dispose(); + SceneEncodingCheckpoint rangeStart = encoding.BeginPackedIndependentRange(targetBounds, ClipBlendMode, replayActiveClips: true); + if (!TryEncodeOrderedOperations(scene, 0, scene.CommandCount, targetBounds, 0, ref encoding, operations, ref rangeStart, ref nextLayerTargetId, out error)) + { + encodedScene = WebGPUEncodedScene.Empty; + return false; + } + + WebGPUSceneRange finalRange = encoding.EndPackedIndependentRange(targetBounds, rangeStart, closeActiveClips: true); + if (finalRange.FillCount != 0) + { + operations.Add(new WebGPUSceneOperation(finalRange)); + } + + if (encoding.IsEmpty) + { + encodedScene = WebGPUEncodedScene.Empty; + error = null; + return true; + } + + FinalizeOrderedOperations(operations); + encodedScene = SupportedSubsetSceneResolver.Resolve(ref encoding, targetBounds, allocator, [.. operations]); + error = null; + return true; + } + finally + { + encoding.Dispose(); } } /// - /// Encoding plan produced before the mutable scene streams are allocated. - /// - private readonly struct SceneEncodingPlan + /// Encodes one command range into ordered scene operations, recursing into scoped layers and + /// splitting the packed draw stream at every Apply barrier. + /// + /// The prepared command batch. + /// The inclusive command start index. + /// The exclusive command end index. + /// The bounds of the target the range renders into. + /// The stable identity of the target the range renders into. + /// The shared mutable scene encoding. + /// Receives the ordered scene operations. + /// The checkpoint of the currently open packed range; updated whenever the range is split. + /// The next stable target identity available for a scoped layer. + /// Receives the failure reason when encoding fails. + /// when the range encoded successfully. + private static bool TryEncodeOrderedOperations( + DrawingCommandBatch scene, + int commandStart, + int commandEnd, + Rectangle targetBounds, + int targetId, + ref SupportedSubsetSceneEncoding encoding, + List operations, + ref SceneEncodingCheckpoint rangeStart, + ref int nextLayerTargetId, + out string? error) { - private readonly LinearGeometry?[]? geometries; + IReadOnlyList commands = scene.Commands; - public SceneEncodingPlan( - int pathTagCapacity, - int pathDataWordCapacity, - int drawTagCapacity, - int drawDataWordCapacity, - int transformWordCapacity, - int styleWordCapacity, - int gradientPixelCapacity, - int pathGradientDataWordCapacity, - LinearGeometry?[]? geometries, - SceneEncodingPlan[]? partitionPlans) + for (int i = commandStart; i < commandEnd; i++) { - this.PathTagCapacity = pathTagCapacity; - this.PathDataWordCapacity = pathDataWordCapacity; - this.DrawTagCapacity = drawTagCapacity; - this.DrawDataWordCapacity = drawDataWordCapacity; - this.TransformWordCapacity = transformWordCapacity; - this.StyleWordCapacity = styleWordCapacity; - this.GradientPixelCapacity = gradientPixelCapacity; - this.PathGradientDataWordCapacity = pathGradientDataWordCapacity; - this.geometries = geometries; - this.PartitionPlans = partitionPlans; - } - - /// - /// Gets the initial byte capacity for path tags. - /// - public int PathTagCapacity { get; } - - /// - /// Gets the initial word capacity for path data. - /// - public int PathDataWordCapacity { get; } - - /// - /// Gets the initial draw-tag capacity. - /// - public int DrawTagCapacity { get; } + CompositionSceneCommand sceneCommand = commands[i]; + LinearGeometry? geometry = null; - /// - /// Gets the initial word capacity for draw data. - /// - public int DrawDataWordCapacity { get; } + if (sceneCommand is PathCompositionSceneCommand pathCommand) + { + CompositionCommand command = pathCommand.Command; + if (command.Kind == CompositionCommandKind.BeginLayer && command.RequiresScopedApply) + { + WebGPUSceneRange pendingRange = encoding.EndPackedIndependentRange(targetBounds, rangeStart, closeActiveClips: true); + if (pendingRange.FillCount != 0) + { + operations.Add(new WebGPUSceneOperation(pendingRange)); + } - /// - /// Gets the initial word capacity for transforms. - /// - public int TransformWordCapacity { get; } + int layerEnd = FindMatchingLayerEnd(scene, i, commandEnd); + if (layerEnd < 0) + { + error = "The WebGPU Apply scene encoder could not find the matching EndLayer command."; + return false; + } - /// - /// Gets the initial word capacity for styles. - /// - public int StyleWordCapacity { get; } + Rectangle layerBounds = command.LayerBounds; + int layerTargetId = nextLayerTargetId++; + operations.Add(new WebGPUSceneOperation(layerBounds, layerTargetId, targetId)); - /// - /// Gets the initial gradient-pixel capacity. - /// - public int GradientPixelCapacity { get; } + SceneEncodingCheckpoint childStart = encoding.BeginPackedIndependentRange(layerBounds, ClipBlendMode, replayActiveClips: true); + if (!TryEncodeOrderedOperations(scene, i + 1, layerEnd, layerBounds, layerTargetId, ref encoding, operations, ref childStart, ref nextLayerTargetId, out error)) + { + return false; + } - /// - /// Gets the initial path-gradient data capacity. - /// - public int PathGradientDataWordCapacity { get; } + WebGPUSceneRange childRange = encoding.EndPackedIndependentRange(layerBounds, childStart, closeActiveClips: true); + if (childRange.FillCount != 0) + { + operations.Add(new WebGPUSceneOperation(childRange)); + } - /// - /// Gets per-partition capacity plans produced by the parallel planning pass. - /// - public SceneEncodingPlan[]? PartitionPlans { get; } + // Children are rendered into the temporary layer with the active clip stack + // already replayed. Replaying the same clips again while compositing the + // layer would clip antialias/filter coverage twice and diverge from the CPU + // retained renderer's scoped-layer execution. + SceneEncodingCheckpoint compositeStart = encoding.BeginPackedIndependentRange(targetBounds, ClipBlendMode, replayActiveClips: false); + if (!TryAppendLayerCompositeCommand(command, targetBounds, ref encoding, out error)) + { + return false; + } - /// - /// Creates an encoding plan for the supplied command batch. - /// - public static SceneEncodingPlan Create( - DrawingCommandBatch scene, - int maxDegreeOfParallelism, - int partitionCount) - { - int commandCount = scene.CommandCount; - if (partitionCount <= 1) - { - return CreateDefault(commandCount); - } + WebGPUSceneRange compositeRange = encoding.EndPackedIndependentRange(targetBounds, compositeStart, closeActiveClips: false); + operations.Add(new WebGPUSceneOperation(compositeRange, layerTargetId, targetId)); - LinearGeometry?[] geometries = new LinearGeometry?[commandCount]; - SceneCapacityEstimate[] estimates = new SceneCapacityEstimate[partitionCount]; + rangeStart = encoding.BeginPackedIndependentRange(targetBounds, ClipBlendMode, replayActiveClips: true); + i = layerEnd; + continue; + } - _ = Parallel.For( - 0, - partitionCount, - CreateParallelOptions(maxDegreeOfParallelism, partitionCount), - partitionIndex => + if (command.Kind == CompositionCommandKind.Apply) { - int commandStart = (partitionIndex * commandCount) / partitionCount; - int commandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; - SceneCapacityEstimate estimate = default; + uint applyClipBlendMode = PackBlendMode(command.DrawingOptions.GraphicsOptions); + AddRenderRange(targetBounds, ref encoding, operations, ref rangeStart, applyClipBlendMode); - for (int i = commandStart; i < commandEnd; i++) + if (!TryAppendApplyCommand(command, targetBounds, ref encoding, rangeStart, out WebGPUSceneOperation? applyOperation, out error)) { - estimate.Add(EstimateCommand(scene.Commands[i], i, geometries)); + return false; } - estimates[partitionIndex] = estimate; - }); + if (applyOperation is not null) + { + operations.Add(applyOperation); + rangeStart = encoding.BeginPackedIndependentRange(targetBounds, ClipBlendMode, replayActiveClips: true); + } - SceneCapacityEstimate total = SceneCapacityEstimate.CreateInitial(); - SceneEncodingPlan[] partitionPlans = new SceneEncodingPlan[partitionCount]; - for (int i = 0; i < estimates.Length; i++) + continue; + } + } + + if (sceneCommand is PathCompositionSceneCommand fillCommand) { - total.Add(estimates[i]); + CompositionCommand command = fillCommand.Command; + if (command.Kind == CompositionCommandKind.FillLayer) + { + geometry = command.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(command.Transform)); + } - int commandStart = (i * commandCount) / partitionCount; - int commandEnd = ((i + 1) * commandCount) / partitionCount; - partitionPlans[i] = estimates[i].ToPlan(commandEnd - commandStart, geometries, null); + if (!encoding.TryAppend(command, geometry, out error)) + { + return false; + } + } + else if (sceneCommand is StrokePathCompositionSceneCommand strokePathCommand) + { + geometry = strokePathCommand.Command.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(strokePathCommand.Command.Transform)); + if (!encoding.TryAppend(strokePathCommand.Command, geometry, out error)) + { + return false; + } + } + else if (sceneCommand is LineSegmentCompositionSceneCommand lineSegmentCommand) + { + if (!encoding.TryAppend(lineSegmentCommand.Command, out error)) + { + return false; + } + } + else if (sceneCommand is PolylineCompositionSceneCommand polylineCommand) + { + geometry = LinearGeometry.CreateOpenPolyline(polylineCommand.Command.SourcePoints, MatrixUtilities.GetScale(polylineCommand.Command.Transform)); + if (!encoding.TryAppend(polylineCommand.Command, geometry, out error)) + { + return false; + } } - - return total.ToPlan(commandCount, geometries, partitionPlans); } - /// - /// Gets prepared geometry for a command when the parallel plan computed it. - /// - public LinearGeometry? GetGeometry(int commandIndex) - => this.geometries?[commandIndex]; + error = null; + return true; + } + + /// + /// Closes the currently open packed range, emitting it as an operation when it produced fills, + /// and opens the next range with the supplied clip blend mode. + /// + /// The bounds of the target the ranges render into. + /// The shared mutable scene encoding. + /// Receives the closed range when it produced fills. + /// The checkpoint of the open packed range; replaced with the new range's checkpoint. + /// The blend mode used when the next range closes replayed clips. + private static void AddRenderRange( + Rectangle targetBounds, + ref SupportedSubsetSceneEncoding encoding, + List operations, + ref SceneEncodingCheckpoint rangeStart, + uint nextClipBlendMode) + { + WebGPUSceneRange range = encoding.EndPackedIndependentRange(targetBounds, rangeStart, closeActiveClips: true); + + if (range.FillCount != 0) + { + operations.Add(new WebGPUSceneOperation(range)); + } + + rangeStart = encoding.BeginPackedIndependentRange(targetBounds, nextClipBlendMode, replayActiveClips: true); + } + + /// + /// Encodes one Apply command as an external-texture fill and wraps the pending draw range in an + /// Apply scene item that carries the canvas source rectangle to sample at render time. + /// + /// The Apply composition command. + /// The bounds of the target the Apply composites into. + /// The shared mutable scene encoding. + /// The checkpoint of the packed range that owns the Apply fill. + /// Receives the Apply operation, or when the source rectangle is empty. + /// Receives the failure reason when encoding fails. + /// when the command encoded or was skipped as empty. + private static bool TryAppendApplyCommand( + in CompositionCommand source, + Rectangle targetBounds, + ref SupportedSubsetSceneEncoding encoding, + in SceneEncodingCheckpoint drawStart, + out WebGPUSceneOperation? operation, + out string? error) + { + RectangleF rawOutputBounds = RectangleF.Transform(source.ApplyOutputBounds, source.DrawingOptions.Transform); + Rectangle outputRect = Rectangle.Intersect(source.ApplyCanvasBounds, ToConservativeBounds(rawOutputBounds)); + + if (outputRect.Width <= 0 || outputRect.Height <= 0) + { + operation = null; + error = null; + return true; + } + + Point brushOffset = new( + outputRect.X - (int)MathF.Floor(rawOutputBounds.Left), + outputRect.Y - (int)MathF.Floor(rawOutputBounds.Top)); + + IWebGPUShaderEffectSource? shaderEffect = source.ApplyEffect as IWebGPUShaderEffectSource; + bool hasNativeEffect = shaderEffect is not null; + GpuImageSource imageSource = hasNativeEffect + ? new GpuImageSource(new Size(outputRect.Width, outputRect.Height), brushOffset, WrapMode.Repeat, WrapMode.Repeat, WebGPUShaderEffectWorkingTexture.Descriptor) + : new GpuImageSource(new Size(outputRect.Width, outputRect.Height), brushOffset, WrapMode.Repeat, WrapMode.Repeat); + + RasterizerOptions rasterizerOptions = source.RasterizerOptions; + LinearGeometry geometry = source.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(source.Transform)); + if (!encoding.TryAppendExternalTextureFill( + source.SourcePath, + imageSource, + source.DrawingOptions, + in rasterizerOptions, + source.DestinationOffset, + targetBounds, + geometry, + out error)) + { + operation = null; + return false; + } + + WebGPUSceneRange drawRange = encoding.EndPackedIndependentRange(targetBounds, drawStart, closeActiveClips: true); + operation = hasNativeEffect + ? new WebGPUSceneOperation(new WebGPUShaderEffectSceneItem(shaderEffect!, outputRect, outputRect, source.ApplyWriteBackOffset, drawRange)) + : new WebGPUSceneOperation(new WebGPUApplySceneItem(source.ApplyOperation, source.ApplyEffect, outputRect, outputRect, source.ApplyWriteBackOffset, drawRange)); + error = null; + return true; + } + + /// + /// Encodes the fill that composites a scoped layer's temporary texture back into the target, + /// applying the layer's graphics options at composite time. + /// + /// The begin-layer command that opened the scoped layer. + /// The bounds of the target the layer composites into. + /// The shared mutable scene encoding. + /// Receives the failure reason when encoding fails. + /// when the composite fill encoded successfully. + private static bool TryAppendLayerCompositeCommand( + in CompositionCommand beginCommand, + Rectangle targetBounds, + ref SupportedSubsetSceneEncoding encoding, + out string? error) + { + Rectangle destinationBounds = beginCommand.LayerBounds; + DrawingOptions options = new() + { + GraphicsOptions = beginCommand.LayerOptions + }; + + RasterizerOptions rasterizerOptions = CreateRasterizerOptions(destinationBounds, options); + RectanglePolygon path = new(0, 0, destinationBounds.Width, destinationBounds.Height); + LinearGeometry geometry = path.ToLinearGeometry(MatrixUtilities.GetScale(options.Transform)); + return encoding.TryAppendExternalTextureFill( + path, + new GpuImageSource(new Size(destinationBounds.Width, destinationBounds.Height), Point.Empty, WrapMode.Repeat, WrapMode.Repeat), + options, + in rasterizerOptions, + destinationBounds.Location, + targetBounds, + geometry, + out error); + } + + /// + /// Finds the EndLayer command that closes the BeginLayer at by depth counting. + /// + /// The prepared command batch. + /// The command index of the BeginLayer command. + /// The exclusive command index bounding the search. + /// The matching EndLayer command index, or -1 when the scope is unbalanced within the range. + private static int FindMatchingLayerEnd(DrawingCommandBatch scene, int layerBegin, int commandEnd) + { + int depth = 0; + for (int i = layerBegin; i < commandEnd; i++) + { + if (scene.Commands[i] is not PathCompositionSceneCommand pathCommand) + { + continue; + } + + switch (pathCommand.Command.Kind) + { + case CompositionCommandKind.BeginLayer: + depth++; + break; + case CompositionCommandKind.EndLayer: + depth--; + if (depth == 0) + { + return i; + } + + break; + } + } + + return -1; + } + + /// + /// Completes immutable Apply-group and barrier-capacity metadata after the ordered stream has + /// been flattened into explicit layer entry and exit operations. + /// + /// The ordered operations to finalize in place. + private static void FinalizeOrderedOperations(List operations) + { + int pendingStatusCapacity = 0; + int denseApplyIndex = 0; + + for (int i = 0; i < operations.Count; i++) + { + WebGPUSceneOperation operation = operations[i]; + switch (operation.Kind) + { + case WebGPUSceneOperationKind.RenderRange: + case WebGPUSceneOperationKind.EndLayer: + pendingStatusCapacity = checked(pendingStatusCapacity + operation.Range.MaximumStatusRecordCount); + break; + + case WebGPUSceneOperationKind.Apply: + int groupCount = 1; + + // Every candidate read must be disjoint from every earlier conservative write. + // Pairwise testing represents the true union of rectangles; replacing it with + // one bounding rectangle would incorrectly split groups across empty gaps. + for (int candidateIndex = i + 1; candidateIndex < operations.Count; candidateIndex++) + { + WebGPUSceneOperation candidateOperation = operations[candidateIndex]; + if (candidateOperation.Kind != WebGPUSceneOperationKind.Apply) + { + break; + } + + WebGPUApplySceneItem candidate = candidateOperation.Apply!; + Rectangle candidateRead = candidate.InputRect; + candidateRead.Offset(-candidate.ReadOffset.X, -candidate.ReadOffset.Y); + bool intersectsEarlierWrite = false; + + for (int previousIndex = i; previousIndex < candidateIndex; previousIndex++) + { + Rectangle previousWrite = operations[previousIndex].Apply!.OutputRect; + if (candidateRead.Left < previousWrite.Right && + candidateRead.Right > previousWrite.Left && + candidateRead.Top < previousWrite.Bottom && + candidateRead.Bottom > previousWrite.Top) + { + intersectsEarlierWrite = true; + break; + } + } + + if (intersectsEarlierWrite) + { + break; + } + + groupCount++; + } + + operation.SetApplyGroup(groupCount, pendingStatusCapacity); + + // The Apply draw ranges execute after this barrier and become the complete + // pending transaction seen by the next group or end-of-flush validation. + pendingStatusCapacity = 0; + for (int operationIndex = i; operationIndex < i + groupCount; operationIndex++) + { + operations[operationIndex].SetApplyIndex(denseApplyIndex++); + pendingStatusCapacity = checked(pendingStatusCapacity + operations[operationIndex].Apply!.DrawRange.MaximumStatusRecordCount); + } + + i += groupCount - 1; + break; + + case WebGPUSceneOperationKind.ShaderEffect: + // A native effect consumes the committed source produced by every preceding + // range. Its write-back becomes the first range in the next transaction. + pendingStatusCapacity = operation.ShaderEffect!.DrawRange.MaximumStatusRecordCount; + break; + } + } + } + + /// + /// Creates rasterizer options for an encoder-synthesized fill from the drawing options it composites with. + /// + /// The absolute raster interest bounds of the synthesized fill. + /// The drawing options supplying antialiasing and intersection state. + /// The created rasterizer options. + private static RasterizerOptions CreateRasterizerOptions(Rectangle interest, DrawingOptions options) + { + GraphicsOptions graphicsOptions = options.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + return new RasterizerOptions( + interest, + options.IntersectionRule, + rasterizationMode, + graphicsOptions.AntialiasThreshold); + } + + /// + /// Rounds floating-point bounds outward to the smallest enclosing integer rectangle. + /// + /// The bounds to round. + /// The enclosing integer rectangle. + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + /// + /// Expands path bounds by the stroke joins and caps that can extend past the path geometry. + /// + /// The path geometry bounds before stroke inflation. + /// The pen that defines stroke width, joins, caps, and miter limit. + /// The transform-derived scale applied to the stroke width. + /// The inflated stroke bounds. + private static RectangleF GetStrokeBounds(RectangleF bounds, Pen pen, float widthScale) + { + float halfWidth = pen.StrokeWidth * widthScale * 0.5F; + float joinInflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; + + float capInflate = pen.StrokeOptions.LineCap == LineCap.Square + ? halfWidth * MathF.Sqrt(2F) + : halfWidth; + + float inflate = MathF.Max(joinInflate, capInflate); + + bounds.Inflate(new SizeF(inflate, inflate)); + return bounds; + } + + /// + /// Immutable snapshot of every scene stream length and record count, captured at a stream position + /// so two checkpoints delimit one renderable range. + /// + private readonly struct SceneEncodingCheckpoint + { + /// + /// Initializes a new instance of the struct. + /// + /// The path-tag stream length in bytes. + /// The path-data stream length in words. + /// The draw-tag stream length. + /// The draw-data stream length in words. + /// The transform stream length in words. + /// The style stream length in words. + /// The info-word count implied by the emitted draw tags. + /// The number of paths emitted so far. + /// The number of clip records emitted so far. + /// The number of fill records emitted so far. + /// The number of non-horizontal line segments emitted so far. + /// The accumulated sparse path-row estimate. + /// The accumulated tile-crossing upper bound. + /// The accumulated per-(draw, bin) record upper bound. + public SceneEncodingCheckpoint( + int pathTagByteCount, + int pathDataWordCount, + int drawTagCount, + int drawDataWordCount, + int transformWordCount, + int styleWordCount, + int infoWordCount, + int pathCount, + int clipCount, + int fillCount, + int lineCount, + long estimatedPathRowCount, + long estimatedTileCrossings, + long estimatedBinFootprint) + { + this.PathTagByteCount = pathTagByteCount; + this.PathDataWordCount = pathDataWordCount; + this.DrawTagCount = drawTagCount; + this.DrawDataWordCount = drawDataWordCount; + this.TransformWordCount = transformWordCount; + this.StyleWordCount = styleWordCount; + this.InfoWordCount = infoWordCount; + this.PathCount = pathCount; + this.ClipCount = clipCount; + this.FillCount = fillCount; + this.LineCount = lineCount; + this.EstimatedPathRowCount = estimatedPathRowCount; + this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedBinFootprint = estimatedBinFootprint; + } + + /// + /// Gets the path-tag stream length in bytes. + /// + public int PathTagByteCount { get; } + + /// + /// Gets the path-data stream length in words. + /// + public int PathDataWordCount { get; } + + /// + /// Gets the draw-tag stream length. + /// + public int DrawTagCount { get; } + + /// + /// Gets the draw-data stream length in words. + /// + public int DrawDataWordCount { get; } + + /// + /// Gets the transform stream length in words. + /// + public int TransformWordCount { get; } + + /// + /// Gets the style stream length in words. + /// + public int StyleWordCount { get; } + + /// + /// Gets the info-word count implied by the draw tags emitted so far. + /// + public int InfoWordCount { get; } + + /// + /// Gets the number of paths emitted so far. + /// + public int PathCount { get; } + + /// + /// Gets the number of clip records emitted so far. + /// + public int ClipCount { get; } + + /// + /// Gets the number of fill records emitted so far. + /// + public int FillCount { get; } + + /// + /// Gets the number of non-horizontal line segments emitted so far. + /// + public int LineCount { get; } + + /// + /// Gets the accumulated sparse path-row estimate. + /// + public long EstimatedPathRowCount { get; } + + /// + /// Gets the accumulated tile-crossing upper bound. + /// + public long EstimatedTileCrossings { get; } + + /// + /// Gets the accumulated per-(draw, bin) record upper bound. + /// + public long EstimatedBinFootprint { get; } + } + + /// + /// Disposes any partition encodings that were produced before the parallel operation completed. + /// + /// The partition slots to dispose; unproduced slots are . + private static void DisposePartitions(SceneEncodingPartition?[] partitions) + { + for (int i = 0; i < partitions.Length; i++) + { + partitions[i]?.Dispose(); + } + } + + /// + /// Encoding plan produced before the mutable scene streams are allocated. + /// + private readonly struct SceneEncodingPlan + { + private readonly LinearGeometry?[]? geometries; + + /// + /// Initializes a new instance of the struct. + /// + /// The initial byte capacity for path tags. + /// The initial word capacity for path data. + /// The initial draw-tag capacity. + /// The initial word capacity for draw data. + /// The initial word capacity for transforms. + /// The initial word capacity for styles. + /// The initial gradient-pixel capacity. + /// The initial path-gradient data capacity. + /// The prepared geometry per command, or when planning was skipped. + /// The per-partition plans, or for single-partition encodes. + public SceneEncodingPlan( + int pathTagCapacity, + int pathDataWordCapacity, + int drawTagCapacity, + int drawDataWordCapacity, + int transformWordCapacity, + int styleWordCapacity, + int gradientPixelCapacity, + int pathGradientDataWordCapacity, + LinearGeometry?[]? geometries, + SceneEncodingPlan[]? partitionPlans) + { + this.PathTagCapacity = pathTagCapacity; + this.PathDataWordCapacity = pathDataWordCapacity; + this.DrawTagCapacity = drawTagCapacity; + this.DrawDataWordCapacity = drawDataWordCapacity; + this.TransformWordCapacity = transformWordCapacity; + this.StyleWordCapacity = styleWordCapacity; + this.GradientPixelCapacity = gradientPixelCapacity; + this.PathGradientDataWordCapacity = pathGradientDataWordCapacity; + this.geometries = geometries; + this.PartitionPlans = partitionPlans; + } + + /// + /// Gets the initial byte capacity for path tags. + /// + public int PathTagCapacity { get; } + + /// + /// Gets the initial word capacity for path data. + /// + public int PathDataWordCapacity { get; } + + /// + /// Gets the initial draw-tag capacity. + /// + public int DrawTagCapacity { get; } + + /// + /// Gets the initial word capacity for draw data. + /// + public int DrawDataWordCapacity { get; } + + /// + /// Gets the initial word capacity for transforms. + /// + public int TransformWordCapacity { get; } + + /// + /// Gets the initial word capacity for styles. + /// + public int StyleWordCapacity { get; } + + /// + /// Gets the initial gradient-pixel capacity. + /// + public int GradientPixelCapacity { get; } + + /// + /// Gets the initial path-gradient data capacity. + /// + public int PathGradientDataWordCapacity { get; } + + /// + /// Gets per-partition capacity plans produced by the parallel planning pass. + /// + public SceneEncodingPlan[]? PartitionPlans { get; } + + /// + /// Creates an encoding plan for the supplied command batch. + /// + /// The command batch to plan. + /// The maximum parallel worker count. + /// The number of command-range partitions. + /// The snapped range boundaries, or for uniform ranges. + /// The created encoding plan. + public static SceneEncodingPlan Create( + DrawingCommandBatch scene, + int maxDegreeOfParallelism, + int partitionCount, + int[]? partitionBoundaries) + { + int commandCount = scene.CommandCount; + if (partitionCount <= 1) + { + return CreateDefault(commandCount); + } + + LinearGeometry?[] geometries = new LinearGeometry?[commandCount]; + SceneCapacityEstimate[] estimates = new SceneCapacityEstimate[partitionCount]; + + _ = Parallel.For( + 0, + partitionCount, + CreateParallelOptions(maxDegreeOfParallelism, partitionCount), + partitionIndex => + { + int commandStart = partitionBoundaries is null ? (partitionIndex * commandCount) / partitionCount : partitionBoundaries[partitionIndex]; + int commandEnd = partitionBoundaries is null ? ((partitionIndex + 1) * commandCount) / partitionCount : partitionBoundaries[partitionIndex + 1]; + SceneCapacityEstimate estimate = default; + + for (int i = commandStart; i < commandEnd; i++) + { + estimate.Add(EstimateCommand(scene.Commands[i], i, geometries)); + } + + estimates[partitionIndex] = estimate; + }); + + SceneCapacityEstimate total = SceneCapacityEstimate.CreateInitial(); + SceneEncodingPlan[] partitionPlans = new SceneEncodingPlan[partitionCount]; + for (int i = 0; i < estimates.Length; i++) + { + total.Add(estimates[i]); + + int commandStart = partitionBoundaries is null ? (i * commandCount) / partitionCount : partitionBoundaries[i]; + int commandEnd = partitionBoundaries is null ? ((i + 1) * commandCount) / partitionCount : partitionBoundaries[i + 1]; + partitionPlans[i] = estimates[i].ToPlan(commandEnd - commandStart, geometries, null); + } + + return total.ToPlan(commandCount, geometries, partitionPlans); + } + + /// + /// Gets prepared geometry for a command when the parallel plan computed it. + /// + /// The command index. + /// The prepared geometry, or when no geometry was prepared. + public LinearGeometry? GetGeometry(int commandIndex) + => this.geometries?[commandIndex]; /// /// Gets the stream capacity plan for one contiguous command-range partition. /// + /// The partition index. + /// The partition plan. public readonly SceneEncodingPlan GetPartitionPlan(int partitionIndex) => this.PartitionPlans is null ? this : this.PartitionPlans[partitionIndex]; + /// + /// Creates the fallback plan used when no per-command planning pass ran, sizing streams + /// by fixed per-command multipliers. + /// + /// The number of commands covered by the plan. + /// The heuristic default plan. public static SceneEncodingPlan CreateDefault(int commandCount) => new( Math.Max(commandCount * 8, 256), @@ -414,18 +1167,50 @@ public static SceneEncodingPlan CreateDefault(int commandCount) /// private struct SceneCapacityEstimate { + /// + /// The estimated path-tag byte count. + /// public long PathTagCount; + + /// + /// The estimated path-data word count. + /// public long PathDataWordCount; + + /// + /// The estimated draw-tag count. + /// public long DrawTagCount; + + /// + /// The estimated draw-data word count. + /// public long DrawDataWordCount; + + /// + /// The estimated transform word count. + /// public long TransformWordCount; + + /// + /// The estimated style word count. + /// public long StyleWordCount; + + /// + /// The estimated gradient-ramp pixel count. + /// public long GradientPixelCount; + + /// + /// The estimated path-gradient payload word count. + /// public long PathGradientDataWordCount; /// /// Creates the initial estimate for the transform emitted by every scene. /// + /// The initial estimate. public static SceneCapacityEstimate CreateInitial() => new() { @@ -436,6 +1221,7 @@ public static SceneCapacityEstimate CreateInitial() /// /// Adds another estimate into this accumulator. /// + /// The estimate to add. public void Add(in SceneCapacityEstimate other) { this.PathTagCount += other.PathTagCount; @@ -449,19 +1235,78 @@ public void Add(in SceneCapacityEstimate other) } /// - /// Adds capacity for one encoded fill path. + /// Adds capacity for one encoded fill path. + /// + /// The fill geometry information. + /// The fill brush. + public void AddFill(LinearGeometryInfo geometryInfo, Brush brush) + { + uint drawTag = GetDrawTag(brush); + this.PathTagCount += geometryInfo.SegmentCount + 3L; + this.PathDataWordCount += (geometryInfo.ContourCount * 2L) + (geometryInfo.SegmentCount * 2L); + this.AddDrawPayload(drawTag, brush); + } + + /// + /// Adds capacity for one explicit begin-clip command. + /// + /// The clip descriptor opened by the command. + public void AddBeginClip(DrawingClipDescriptor descriptor) + { + int rectangleCount = GetRectangleClipCount(in descriptor); + + // The encoder resets clip commands to identity before emitting descriptor + // geometry so a preceding draw transform cannot leak into the clip stack. + this.PathTagCount++; + this.TransformWordCount += WebGPUSceneEncoder.TransformWordCount; + + if (rectangleCount > 0) + { + this.PathTagCount += (rectangleCount * 4L) + 2L; + this.PathDataWordCount += rectangleCount * 10L; + } + else if (descriptor.Kind == DrawingClipKind.Path) + { + LinearGeometry clipGeometry = descriptor.ToLinearGeometry(out Matrix4x4 descriptorTransform); + this.PathTagCount += clipGeometry.Info.SegmentCount + 2L; + this.PathDataWordCount += clipGeometry.Info.SegmentCount == 0 + ? 4L + : (clipGeometry.Info.ContourCount * 2L) + (clipGeometry.Info.SegmentCount * 2L); + + if (descriptorTransform != Matrix4x4.Identity) + { + // Clip descriptors share the path stream with ordinary draws. Count the + // transform transition explicitly so a path clip cannot leave a residual + // transform active for later draws. + this.PathTagCount++; + this.TransformWordCount += WebGPUSceneEncoder.TransformWordCount; + } + } + else + { + this.PathTagCount += 2L; + this.PathDataWordCount += 4L; + } + + this.DrawTagCount++; + this.DrawDataWordCount += 2; + this.StyleWordCount += WebGPUSceneEncoder.StyleWordCount; + } + + /// + /// Adds capacity for one explicit end-clip command. /// - public void AddFill(LinearGeometryInfo geometryInfo, Brush brush) + public void AddEndClip() { - uint drawTag = GetDrawTag(brush); - this.PathTagCount += geometryInfo.SegmentCount + 3L; - this.PathDataWordCount += (geometryInfo.ContourCount * 2L) + (geometryInfo.SegmentCount * 2L); - this.AddDrawPayload(drawTag, brush); + this.PathTagCount++; + this.DrawTagCount++; } /// /// Adds capacity for one encoded stroked path. /// + /// The stroke geometry. + /// The stroke brush. public void AddStroke(LinearGeometry geometry, Brush brush) { uint drawTag = GetDrawTag(brush); @@ -480,6 +1325,7 @@ public void AddStroke(LinearGeometry geometry, Brush brush) /// /// Adds capacity for one encoded explicit two-point stroke. /// + /// The stroke brush. public void AddOpenSegmentStroke(Brush brush) { uint drawTag = GetDrawTag(brush); @@ -512,6 +1358,10 @@ public void AddEndLayer() /// /// Converts the accumulated counts into an encoder plan. /// + /// The number of commands covered by the plan. + /// The prepared geometry array. + /// The per-partition plans. + /// The created encoder plan. public readonly SceneEncodingPlan ToPlan( int commandCount, LinearGeometry?[] geometries, @@ -532,6 +1382,11 @@ public readonly SceneEncodingPlan ToPlan( partitionPlans); } + /// + /// Adds capacity for the draw tag, draw data, transform, style, and brush payload shared by every draw record. + /// + /// The draw tag selected for the brush. + /// The brush that determines auxiliary payload sizes. private void AddDrawPayload(uint drawTag, Brush brush) { this.DrawTagCount++; @@ -541,7 +1396,7 @@ private void AddDrawPayload(uint drawTag, Brush brush) if (DrawTagUsesGradientRamp(drawTag)) { - this.GradientPixelCount += GradientWidth; + this.GradientPixelCount += GradientRowWordCount; } this.PathGradientDataWordCount += GetPathGradientDataWordCount(brush); @@ -551,6 +1406,10 @@ private void AddDrawPayload(uint drawTag, Brush brush) /// /// Estimates one command and stores prepared geometry for the sequential append phase. /// + /// The command to estimate. + /// The command's index in the batch. + /// Receives the prepared geometry at when the command flattens a path. + /// The capacity estimate for the command; zero when the command is unsupported. private static SceneCapacityEstimate EstimateCommand( CompositionSceneCommand command, int commandIndex, @@ -569,7 +1428,7 @@ private static SceneCapacityEstimate EstimateCommand( return estimate; } - LinearGeometry fillGeometry = composition.SourcePath.ToLinearGeometry(ExtractScale(composition.Transform)); + LinearGeometry fillGeometry = composition.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(composition.Transform)); geometries[commandIndex] = fillGeometry; estimate.AddFill(fillGeometry.Info, composition.Brush); return estimate; @@ -581,6 +1440,14 @@ private static SceneCapacityEstimate EstimateCommand( case CompositionCommandKind.EndLayer: estimate.AddEndLayer(); return estimate; + + case CompositionCommandKind.BeginClip: + estimate.AddBeginClip(composition.ClipDescriptor); + return estimate; + + case CompositionCommandKind.EndClip: + estimate.AddEndClip(); + return estimate; } } @@ -592,7 +1459,7 @@ private static SceneCapacityEstimate EstimateCommand( return estimate; } - LinearGeometry strokeGeometry = composition.SourcePath.ToLinearGeometry(ExtractScale(composition.Transform)); + LinearGeometry strokeGeometry = composition.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(composition.Transform)); geometries[commandIndex] = strokeGeometry; estimate.AddStroke(strokeGeometry, composition.Brush); return estimate; @@ -609,13 +1476,18 @@ private static SceneCapacityEstimate EstimateCommand( return estimate; } - StrokePolylineCommand polyline = ((PolylineCompositionSceneCommand)command).Command; + if (command is not PolylineCompositionSceneCommand polylineCommand) + { + return estimate; + } + + StrokePolylineCommand polyline = polylineCommand.Command; if (!IsSupportedBrush(polyline.Brush)) { return estimate; } - LinearGeometry polylineGeometry = LinearGeometry.CreateOpenPolyline(polyline.SourcePoints, ExtractScale(polyline.Transform)); + LinearGeometry polylineGeometry = LinearGeometry.CreateOpenPolyline(polyline.SourcePoints, MatrixUtilities.GetScale(polyline.Transform)); geometries[commandIndex] = polylineGeometry; estimate.AddStroke(polylineGeometry, polyline.Brush); return estimate; @@ -624,6 +1496,10 @@ private static SceneCapacityEstimate EstimateCommand( /// /// Computes the number of useful partitions using the same limits as the retained CPU scene builder. /// + /// The configured maximum parallelism; -1 selects the processor count. + /// The number of work items available to split. + /// An additional cap, here the number of target tile rows. + /// The partition count. private static int GetPartitionCount( int maxDegreeOfParallelism, int workItemCount, @@ -635,9 +1511,85 @@ private static int GetPartitionCount( /// /// Creates the parallel options for an already-sized partitioned encoder operation. /// + /// The configured maximum parallelism. + /// The number of partitions to process. + /// The parallel options. private static ParallelOptions CreateParallelOptions(int maxDegreeOfParallelism, int partitionCount) => new() { MaxDegreeOfParallelism = Math.Min(maxDegreeOfParallelism, partitionCount) }; + /// + /// Computes the partition command-range boundaries and, for each partition's command start, + /// the stack of clip scopes opened by earlier partitions, outermost first. + /// + /// + /// This single sequential prescan only inspects command kinds, so it stays cheap relative to + /// the parallel encode it enables. Each partition replays its seed clips before its range and + /// closes every open clip after it, keeping the concatenated clip stream balanced. Layer + /// scopes cannot be replayed the same way because a layer composites its contents as one + /// group when it closes, so each boundary snaps forward from its ideal position to the next + /// command index where no layer scope is open. Snapping can produce empty trailing ranges + /// when one layer spans most of the scene; those partitions simply encode nothing. + /// + /// The command batch being partitioned. + /// The partition count used to compute the ideal boundaries. + /// Receives the + 1 range boundaries. + /// Receives the per-partition seed clip stacks. + private static void CreatePartitionCommandRanges( + DrawingCommandBatch scene, + int partitionCount, + out int[] boundaries, + out CompositionCommand[][] seedClips) + { + int commandCount = scene.CommandCount; + boundaries = new int[partitionCount + 1]; + boundaries[partitionCount] = commandCount; + seedClips = new CompositionCommand[partitionCount][]; + seedClips[0] = []; + + List openClips = []; + int layerDepth = 0; + int cursor = 0; + + for (int partitionIndex = 1; partitionIndex < partitionCount; partitionIndex++) + { + int ideal = (partitionIndex * commandCount) / partitionCount; + while (cursor < commandCount && (cursor < ideal || layerDepth > 0)) + { + if (scene.Commands[cursor] is PathCompositionSceneCommand pathCommand) + { + switch (pathCommand.Command.Kind) + { + case CompositionCommandKind.BeginClip: + openClips.Add(pathCommand.Command); + break; + case CompositionCommandKind.EndClip: + if (openClips.Count > 0) + { + openClips.RemoveAt(openClips.Count - 1); + } + + break; + case CompositionCommandKind.BeginLayer: + layerDepth++; + break; + case CompositionCommandKind.EndLayer: + if (layerDepth > 0) + { + layerDepth--; + } + + break; + } + } + + cursor++; + } + + boundaries[partitionIndex] = cursor; + seedClips[partitionIndex] = openClips.Count == 0 ? [] : [.. openClips]; + } + } + /// /// Mutable flush-scoped encoder state used while appending supported commands into contiguous scene streams. /// @@ -648,13 +1600,29 @@ private ref struct SupportedSubsetSceneEncoding private bool gradientPixelsDetached; private bool pathGradientDataDetached; private long estimatedPathRowCount; + + // 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. + private long estimatedTileCrossings; + private long estimatedBinFootprint; + + // Cache of the last emitted 10-word style record. Consecutive draws with identical + // styles share one record, so the encoder only appends a style when a word changes. private uint lastStyle0; private uint lastStyle1; private uint lastStyle2; private uint lastStyle3; private uint lastStyle4; + private uint lastStyle5; + private uint lastStyle6; + private uint lastStyle7; + private uint lastStyle8; + private uint lastStyle9; private GpuSceneTransform lastTransform; - private readonly Rectangle rootTargetBounds; + private Rectangle rootTargetBounds; + private List? activeClips; private List? openLayerBounds; /// @@ -663,7 +1631,7 @@ private ref struct SupportedSubsetSceneEncoding /// The allocator used for all temporary scene streams. /// The root target bounds used for target-local coordinate conversion. /// The large-batch encoding plan used for initial stream sizing and prepared geometry. - internal SupportedSubsetSceneEncoding( + public SupportedSubsetSceneEncoding( MemoryAllocator allocator, in Rectangle rootTargetBounds, in SceneEncodingPlan plan) @@ -679,6 +1647,7 @@ internal SupportedSubsetSceneEncoding( this.GradientPixels = new OwnedStream(allocator, plan.GradientPixelCapacity); this.PathGradientData = new OwnedStream(allocator, plan.PathGradientDataWordCapacity); this.Images = []; + this.Recolors = []; this.FillCount = 0; this.PathCount = 0; this.LineCount = 0; @@ -694,10 +1663,18 @@ internal SupportedSubsetSceneEncoding( this.lastStyle2 = 0; this.lastStyle3 = 0; this.lastStyle4 = 0; + this.lastStyle5 = 0; + this.lastStyle6 = 0; + this.lastStyle7 = 0; + this.lastStyle8 = 0; + this.lastStyle9 = 0; this.rootTargetBounds = rootTargetBounds; this.lastTransform = GpuSceneTransform.Identity; + this.activeClips = null; this.openLayerBounds = null; this.estimatedPathRowCount = 0; + this.estimatedTileCrossings = 0; + this.estimatedBinFootprint = 0; this.VisibleFillCount = 0; this.FineRasterizationMode = RasterizationMode.Antialiased; this.FineCoverageThreshold = 0F; @@ -751,6 +1728,11 @@ internal SupportedSubsetSceneEncoding( /// public List Images; + /// + /// Gets or sets the deferred RecolorBrush payload descriptors specialized for the render target pixel type. + /// + public List Recolors; + /// /// Gets the number of emitted fill records. /// @@ -791,6 +1773,17 @@ internal SupportedSubsetSceneEncoding( /// public readonly int EstimatedPathRowCount => (int)Math.Min(this.estimatedPathRowCount, int.MaxValue); + /// + /// Gets the CPU-side upper-bound estimate of tile-boundary crossings produced by the + /// scene's flattened lines. Bounds the GPU segment, segment-count, and path-tile demand. + /// + public readonly long EstimatedTileCrossings => this.estimatedTileCrossings; + + /// + /// Gets the CPU-side upper-bound estimate of per-(draw, bin) binning records. + /// + public readonly long EstimatedBinFootprint => this.estimatedBinFootprint; + /// /// Gets the flush-wide fine rasterization mode selected while encoding visible fills. /// @@ -806,6 +1799,189 @@ internal SupportedSubsetSceneEncoding( /// public readonly bool IsEmpty => this.FillCount == 0; + /// + /// Captures the current stream offsets for a renderable range inside this scene. + /// + /// The captured checkpoint. + public readonly SceneEncodingCheckpoint CaptureCheckpoint() + => new( + this.PathTags.Count, + this.PathData.Count, + this.DrawTags.Count, + this.DrawData.Count, + this.Transforms.Count, + this.Styles.Count, + this.InfoWordCount, + this.PathCount, + this.ClipCount, + this.FillCount, + this.LineCount, + this.estimatedPathRowCount, + this.estimatedTileCrossings, + this.estimatedBinFootprint); + + /// + /// Starts a range that can be decoded independently by the staged pipeline. + /// + /// The target bounds for the independent range. + private void BeginIndependentRange(Rectangle targetBounds) + { + this.PadPathTagsToSegmentBoundary(); + this.rootTargetBounds = targetBounds; + this.openLayerBounds?.Clear(); + this.hasLastStyle = false; + this.lastTransform = GpuSceneTransform.Identity; + } + + /// + /// Starts a packed independent range and replays the logical clip stack into it. + /// + /// The target bounds for the independent range. + /// The blend mode used when closing replayed clips. + /// Whether the current active clip stack is replayed into the range. + /// The checkpoint captured before replaying active clips. + public SceneEncodingCheckpoint BeginPackedIndependentRange( + Rectangle targetBounds, + uint clipBlendMode, + bool replayActiveClips) + { + this.BeginIndependentRange(targetBounds); + SceneEncodingCheckpoint checkpoint = this.CaptureCheckpoint(); + + // Each packed range is scanned from its own path-tag base, but flatten.wgsl + // still rebases transform indices by the implicit identity transform. Emit + // that identity inside the range so range-local identity paths resolve to + // transform index zero instead of underflowing into unrelated transform data. + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.PathTags.Add(PackPathTag(PathTag.Transform)); + AppendIdentityTransform(ref this.Transforms); + this.lastTransform = GpuSceneTransform.Identity; + + if (replayActiveClips) + { + this.AppendActiveClipBegins(clipBlendMode); + } + + return checkpoint; + } + + /// + /// Ends a packed independent range after balancing the logical clip stack. + /// + /// The target bounds for the independent range. + /// The range checkpoint captured by . + /// Whether the current active clip stack is closed before the range ends. + /// The balanced range. + public WebGPUSceneRange EndPackedIndependentRange( + Rectangle targetBounds, + in SceneEncodingCheckpoint start, + bool closeActiveClips) + { + if (closeActiveClips) + { + this.AppendActiveClipEnds(); + } + + this.EndIndependentRange(); + return CreateRange(start, this.CaptureCheckpoint(), targetBounds); + } + + /// + /// Records one logical begin-clip command in the active clip stack. + /// + /// The begin-clip command to keep active for later packed ranges. + private void PushActiveClip(in CompositionCommand command) + => (this.activeClips ??= []).Add(command); + + /// + /// Removes the most recently opened logical clip from the active clip stack. + /// + private readonly void PopActiveClip() + { + List? activeClips = this.activeClips; + activeClips?.RemoveAt(activeClips.Count - 1); + } + + /// + /// Replays the logical active clip stack at the start of a packed independent range. + /// + /// The blend mode used when the replayed clips close. + private void AppendActiveClipBegins(uint clipBlendMode) + { + List? activeClips = this.activeClips; + if (activeClips is null) + { + return; + } + + for (int i = 0; i < activeClips.Count; i++) + { + this.AppendBeginClip(activeClips[i], clipBlendMode); + } + } + + /// + /// Appends physical end-clip records to balance the current packed independent range. + /// + private void AppendActiveClipEnds() + { + List? activeClips = this.activeClips; + if (activeClips is null) + { + return; + } + + for (int i = activeClips.Count - 1; i >= 0; i--) + { + this.AppendEndClip(); + } + } + + /// + /// Ends a range that can be decoded independently by the staged pipeline. + /// + private void EndIndependentRange() => this.PadPathTagsToSegmentBoundary(); + + /// + /// Creates one render range from two stream checkpoints. + /// + /// + /// Ranges begin on the padded scan-segment boundaries produced by + /// , so the starting path-tag byte offset always + /// divides exactly into the word offset consumed by the tag-scan shaders. + /// + /// The checkpoint captured at the start of the range. + /// The checkpoint captured at the end of the range. + /// The target bounds for the range. + /// The created scene range. + public static WebGPUSceneRange CreateRange( + in SceneEncodingCheckpoint start, + in SceneEncodingCheckpoint end, + Rectangle targetBounds) + => new( + targetBounds, + start.PathTagByteCount / 4, + end.PathTagByteCount - start.PathTagByteCount, + start.PathDataWordCount, + end.PathDataWordCount - start.PathDataWordCount, + start.DrawTagCount, + end.DrawTagCount - start.DrawTagCount, + start.DrawDataWordCount, + end.DrawDataWordCount - start.DrawDataWordCount, + start.TransformWordCount, + end.TransformWordCount - start.TransformWordCount, + start.StyleWordCount, + end.StyleWordCount - start.StyleWordCount, + end.InfoWordCount - start.InfoWordCount, + end.PathCount - start.PathCount, + end.ClipCount - start.ClipCount, + end.FillCount - start.FillCount, + end.LineCount - start.LineCount, + checked((int)(end.EstimatedPathRowCount - start.EstimatedPathRowCount)), + end.EstimatedTileCrossings - start.EstimatedTileCrossings, + end.EstimatedBinFootprint - start.EstimatedBinFootprint); + /// /// Disposes all owned stream storage that has not already been detached. /// @@ -850,7 +2026,11 @@ public void Dispose() /// /// Appends all supported scene operations into the mutable scene streams. /// - internal bool TryBuild( + /// The command batch to append. + /// The encoding plan for the command batch. + /// The error message when appending fails. + /// when all supported commands were appended. + public bool TryBuild( DrawingCommandBatch scene, in SceneEncodingPlan plan, out string? error) @@ -859,7 +2039,81 @@ internal bool TryBuild( /// /// Appends a contiguous command range into this partition's mutable scene streams. /// - internal bool TryBuild( + /// The command batch to append. + /// The encoding plan for the command batch. + /// The inclusive command start index. + /// The exclusive command end index. + /// The error message when appending fails. + /// when all supported commands were appended. + public bool TryBuild( + DrawingCommandBatch scene, + in SceneEncodingPlan plan, + int commandStart, + int commandEnd, + out string? error) + => this.TryBuild(scene, plan, commandStart, commandEnd, seedClips: null, out error); + + /// + /// Appends a contiguous command range into this partition's mutable scene streams, + /// replaying clip scopes opened by earlier partitions and closing every clip scope + /// still open at the range end. + /// + /// + /// Seeding and closing keep each partition's clip stream balanced independently, so + /// contiguous partitions concatenate into one valid Vello-style push/pop clip sequence + /// even when a partition boundary falls inside an open clip scope. This is what allows + /// clipped scenes to encode in parallel. + /// + /// The command batch to append. + /// The encoding plan for the command batch. + /// The inclusive command start index. + /// The exclusive command end index. + /// The begin-clip commands opened by earlier partitions, outermost first. + /// The error message when appending fails. + /// when all supported commands were appended. + public bool TryBuild( + DrawingCommandBatch scene, + in SceneEncodingPlan plan, + int commandStart, + int commandEnd, + CompositionCommand[]? seedClips, + out string? error) + { + if (seedClips is not null) + { + for (int i = 0; i < seedClips.Length; i++) + { + CompositionCommand seed = seedClips[i]; + this.AppendBeginClip(seed, ClipBlendMode); + this.PushActiveClip(seed); + } + } + + if (!this.TryBuildCore(scene, plan, commandStart, commandEnd, out error)) + { + return false; + } + + while (this.activeClips is { Count: > 0 }) + { + this.AppendEndClip(); + this.PopActiveClip(); + } + + return true; + } + + /// + /// Dispatches each command in the range to its typed append overload, reusing geometry + /// prepared by the planning pass when available. + /// + /// The command batch to append. + /// The encoding plan carrying prepared geometry. + /// The inclusive command start index. + /// The exclusive command end index. + /// The error message when appending fails. + /// when all supported commands were appended. + private bool TryBuildCore( DrawingCommandBatch scene, in SceneEncodingPlan plan, int commandStart, @@ -891,9 +2145,9 @@ internal bool TryBuild( return false; } } - else + else if (command is PolylineCompositionSceneCommand polylineCommand) { - if (!this.TryAppend(((PolylineCompositionSceneCommand)command).Command, geometry, out error)) + if (!this.TryAppend(polylineCommand.Command, geometry, out error)) { return false; } @@ -907,7 +2161,11 @@ internal bool TryBuild( /// /// Appends one prepared fill-path or layer-based command to the scene streams when the command kind is supported. /// - private bool TryAppend( + /// The command to append. + /// The prepared geometry for the command. + /// The error message when appending fails. + /// when the command was appended or ignored as non-renderable. + public bool TryAppend( in CompositionCommand command, LinearGeometry? geometry, out string? error) @@ -915,21 +2173,28 @@ private bool TryAppend( switch (command.Kind) { case CompositionCommandKind.FillLayer: - if (!TryResolveCommand(command, out ResolvedPathCommand resolved)) + geometry ??= command.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(command.Transform)); + if (geometry.Info.SegmentCount == 0) { error = null; return true; } - if (!this.TryRegisterVisibleFill(resolved.Brush, resolved.RasterizerOptions, out error)) + if (!TryResolveCommand(command, geometry, out ResolvedPathCommand resolved)) + { + error = null; + return true; + } + + if (!this.TryRegisterVisibleFill(resolved.RasterizerOptions, out error)) { return false; } - Matrix4x4 fillResidual = ComputeResidual(ExtractScale(command.Transform), command.Transform); - this.AppendTransformIfChanged(GetSceneTransform(fillResidual, resolved.RasterizerOptions.SamplingOrigin)); + Matrix4x4 fillResidual = MatrixUtilities.GetResidual(MatrixUtilities.GetScale(command.Transform), command.Transform); + Point pathDataOffset = this.AppendTargetTransform(fillResidual, command.DestinationOffset); - this.AppendPlainFill(resolved, geometry); + this.AppendPlainFill(resolved, geometry, pathDataOffset); error = null; return true; @@ -942,8 +2207,82 @@ private bool TryAppend( this.AppendEndLayer(); error = null; return true; + + case CompositionCommandKind.BeginClip: + this.AppendBeginClip(command, ClipBlendMode); + this.PushActiveClip(command); + + error = null; + return true; + + case CompositionCommandKind.EndClip: + this.AppendEndClip(); + this.PopActiveClip(); + + error = null; + return true; + } + + error = null; + return true; + } + + /// + /// Appends one image fill whose texture view is supplied by the WebGPU render path. + /// + /// The path to fill. + /// The external texture source metadata to encode. + /// Drawing options used for the fill. + /// Local rasterizer options used to generate coverage. + /// Absolute destination offset where coverage is composited. + /// Absolute target bounds that own the composited pixels. + /// Optional pre-flattened geometry for . + /// The error message when the command cannot be encoded. + /// when the command was encoded. + public bool TryAppendExternalTextureFill( + IPath path, + in GpuImageSource imageSource, + DrawingOptions options, + in RasterizerOptions rasterizerOptions, + Point destinationOffset, + Rectangle targetBounds, + LinearGeometry? geometry, + out string? error) + { + Vector2 scale = MatrixUtilities.GetScale(options.Transform); + geometry ??= path.ToLinearGeometry(scale); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, options.Transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + + if (!TryResolveRasterization( + rasterizerOptions, + geometryBounds, + destinationOffset, + targetBounds, + out RasterizerOptions resolvedOptions, + out Rectangle brushBounds)) + { + error = null; + return true; + } + + if (!this.TryRegisterVisibleFill(resolvedOptions, out error)) + { + return false; } + ResolvedPathCommand resolved = new( + path, + imageSource, + options.GraphicsOptions, + resolvedOptions, + destinationOffset, + brushBounds, + options.Transform); + + Point pathDataOffset = this.AppendTargetTransform(residual, destinationOffset); + + this.AppendPlainFill(resolved, geometry, pathDataOffset); error = null; return true; } @@ -951,25 +2290,37 @@ private bool TryAppend( /// /// Appends one prepared stroked path command to the scene streams. /// - private bool TryAppend( + /// The command to append. + /// The prepared geometry for the command. + /// The error message when appending fails. + /// when the command was appended or ignored as non-renderable. + public bool TryAppend( in StrokePathCommand command, LinearGeometry? geometry, out string? error) { - if (!TryResolveCommand(command, out ResolvedPathCommand resolved)) + geometry ??= command.SourcePath.ToLinearGeometry(MatrixUtilities.GetScale(command.Transform)); + if (geometry.Info.SegmentCount == 0) + { + error = null; + return true; + } + + if (!TryResolveCommand(command, geometry, out ResolvedPathCommand resolved)) { error = null; return true; } - if (!this.TryRegisterVisibleFill(resolved.Brush, resolved.RasterizerOptions, out error)) + if (!this.TryRegisterVisibleFill(resolved.RasterizerOptions, out error)) { return false; } - Matrix4x4 strokeResidual = ComputeResidual(ExtractScale(command.Transform), command.Transform); - this.AppendTransformIfChanged(GetSceneTransform(strokeResidual, resolved.RasterizerOptions.SamplingOrigin)); - this.AppendPlainStroke(resolved, command.Pen, geometry); + Matrix4x4 strokeResidual = MatrixUtilities.GetResidual(MatrixUtilities.GetScale(command.Transform), command.Transform); + Point pathDataOffset = this.AppendTargetTransform(strokeResidual, command.DestinationOffset); + + this.AppendPlainStroke(resolved, command.Brush, command.Pen, geometry, pathDataOffset); error = null; return true; } @@ -977,7 +2328,10 @@ private bool TryAppend( /// /// Appends one prepared explicit line-segment stroke to the scene streams. /// - private bool TryAppend(in StrokeLineSegmentCommand command, out string? error) + /// The command to append. + /// The error message when appending fails. + /// when the command was appended or ignored as non-renderable. + public bool TryAppend(in StrokeLineSegmentCommand command, out string? error) { if (!TryResolveCommand(command, out ResolvedLineSegmentCommand resolved)) { @@ -985,27 +2339,27 @@ private bool TryAppend(in StrokeLineSegmentCommand command, out string? error) return true; } - if (!this.TryRegisterVisibleFill(resolved.Brush, resolved.RasterizerOptions, out error)) + if (!this.TryRegisterVisibleFill(resolved.RasterizerOptions, out error)) { return false; } - Vector2 segmentScale = ExtractScale(command.Transform); - Matrix4x4 segmentResidual = ComputeResidual(segmentScale, command.Transform); - this.AppendTransformIfChanged(GetSceneTransform(segmentResidual, resolved.RasterizerOptions.SamplingOrigin)); + Vector2 segmentScale = MatrixUtilities.GetScale(command.Transform); + Matrix4x4 segmentResidual = MatrixUtilities.GetResidual(segmentScale, command.Transform); PointF start = new(resolved.Start.X * segmentScale.X, resolved.Start.Y * segmentScale.Y); PointF end = new(resolved.End.X * segmentScale.X, resolved.End.Y * segmentScale.Y); float widthScale = GetTransformWidthScale(command.Transform); + + Point pathDataOffset = this.AppendTargetTransform(segmentResidual, resolved.DestinationOffset); + this.AppendExplicitStroke( resolved.Brush, resolved.GraphicsOptions, resolved.RasterizerOptions, - resolved.DestinationOffset, + pathDataOffset, resolved.BrushBounds, resolved.Pen, widthScale, - command.Transform, - segmentScale, start, end); error = null; @@ -1015,66 +2369,62 @@ private bool TryAppend(in StrokeLineSegmentCommand command, out string? error) /// /// Appends one prepared explicit polyline stroke to the scene streams. /// - private bool TryAppend( + /// The command to append. + /// The prepared geometry for the command. + /// The error message when appending fails. + /// when the command was appended or ignored as non-renderable. + public bool TryAppend( in StrokePolylineCommand command, LinearGeometry? geometry, out string? error) { - if (!TryResolveCommand(command, out ResolvedPolylineCommand resolved)) + Vector2 polylineScale = MatrixUtilities.GetScale(command.Transform); + geometry ??= LinearGeometry.CreateOpenPolyline(command.SourcePoints, polylineScale); + if (!TryResolveCommand(command, geometry, out ResolvedPolylineCommand resolved)) { error = null; return true; } - if (!this.TryRegisterVisibleFill(resolved.Brush, resolved.RasterizerOptions, out error)) + if (!this.TryRegisterVisibleFill(resolved.RasterizerOptions, out error)) { return false; } - Vector2 polylineScale = ExtractScale(command.Transform); - Matrix4x4 polylineResidual = ComputeResidual(polylineScale, command.Transform); - this.AppendTransformIfChanged(GetSceneTransform(polylineResidual, resolved.RasterizerOptions.SamplingOrigin)); - geometry ??= LinearGeometry.CreateOpenPolyline(resolved.Points, polylineScale); float widthScale = GetTransformWidthScale(command.Transform); + + Matrix4x4 polylineResidual = MatrixUtilities.GetResidual(polylineScale, command.Transform); + Point pathDataOffset = this.AppendTargetTransform(polylineResidual, resolved.DestinationOffset); + this.AppendExplicitStroke( resolved.Brush, resolved.GraphicsOptions, resolved.RasterizerOptions, - resolved.DestinationOffset, + pathDataOffset, resolved.BrushBounds, resolved.Pen, widthScale, - command.Transform, - polylineScale, geometry); error = null; return true; } - private bool TryRegisterVisibleFill(Brush brush, in RasterizerOptions options, out string? error) + /// + /// Counts one fill that passed visibility resolution. Kept as the single seam where + /// staged-scene validation could reject a fill before any stream bytes are written. + /// + /// The resolved rasterizer options for the fill. + /// Always ; present for the validation contract. + /// Always ; per-fill rasterization state imposes no flush-wide constraint. + private bool TryRegisterVisibleFill(in RasterizerOptions options, out string? error) { - if (!IsSupportedBrush(brush)) - { - error = $"The staged WebGPU scene pipeline does not support brush type '{brush.GetType().Name}'."; - return false; - } - this.VisibleFillCount++; - if (this.VisibleFillCount == 1) - { - this.FineRasterizationMode = options.RasterizationMode; - this.FineCoverageThreshold = options.AntialiasThreshold; - error = null; - return true; - } - - if (options.RasterizationMode != this.FineRasterizationMode || - options.AntialiasThreshold != this.FineCoverageThreshold) - { - error = "The staged WebGPU scene pipeline does not support mixed fine rasterization modes or thresholds within one flush."; - return false; - } + // Both the rasterization mode (draw-flags aliased bit) and the aliased coverage threshold + // (the per-fill coverage word in CMD_FILL) travel to the fine pass per fill, matching the + // CPU rasterizer exactly. A flush may freely mix antialiased and aliased fills with any + // thresholds, so no flush-wide constraint remains. + _ = options; error = null; return true; } @@ -1082,6 +2432,7 @@ private bool TryRegisterVisibleFill(Brush brush, in RasterizerOptions options, o /// /// Emits a transform tag + data if the transform differs from the last one emitted. /// + /// The transform to emit. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AppendTransformIfChanged(Matrix4x4 matrix) { @@ -1096,36 +2447,83 @@ private void AppendTransformIfChanged(Matrix4x4 matrix) this.lastTransform = transform; } - // Path geometry is baked at device-space scale via ToLinearGeometry(scale); the scene-side - // transform carries the rotation/shear/translation/perspective residual composed with the - // sampling-origin half-pixel shift, so the flatten shader lands each point in device space - // at the correct sampling origin in one transform_apply per vertex. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Matrix4x4 GetSceneTransform(Matrix4x4 residual, RasterizerSamplingOrigin samplingOrigin) - => samplingOrigin == RasterizerSamplingOrigin.PixelCenter - ? residual * Matrix4x4.CreateTranslation(0.5f, 0.5f, 0f) - : residual; + /// + /// Emits the path transform with the canvas destination offset applied after local geometry transforms. + /// + /// The residual transform for the draw's geometry. + /// The absolute destination offset of the draw. + /// The destination offset that path data should still apply directly; the root + /// target origin when the offset was folded into the emitted transform. + private Point AppendTargetTransform(Matrix4x4 transform, Point destinationOffset) + { + float translateX = destinationOffset.X - this.rootTargetBounds.X; + float translateY = destinationOffset.Y - this.rootTargetBounds.Y; + if (translateX != 0F || translateY != 0F) + { + // Canvas offsets are post-transform translations. Encoding the offset in the + // transform keeps rotated/skewed region-local clips aligned with the CPU path. + transform *= Matrix4x4.CreateTranslation(translateX, translateY, 0F); + destinationOffset = new Point(this.rootTargetBounds.X, this.rootTargetBounds.Y); + } + + this.AppendTransformIfChanged(transform); + return destinationOffset; + } + + /// + /// Pads the byte path-tag stream so a later render range can start at a WGSL scan-segment boundary. + /// + private void PadPathTagsToSegmentBoundary() + { + // One pathtag_reduce workgroup (WG_SIZE = 256) reduces 256 tag words of 4 tag bytes + // each, so a range decodes independently only when it starts on a 1024-byte boundary. + // Zero padding bytes are PathTag.None and contribute nothing to the tag monoid. + const int segmentByteCount = 256 * 4; + + int padding = this.PathTags.Count & (segmentByteCount - 1); + if (padding == 0) + { + return; + } + + Span paddedTags = this.PathTags.GetAppendSpan(segmentByteCount - padding); + paddedTags.Clear(); + this.PathTags.Advance(paddedTags.Length); + } /// /// Encodes one visible fill command into the path, draw, style, and auxiliary payload streams. /// + /// The resolved fill command. + /// The prepared geometry, or to flatten the command path here. + /// The destination offset that path data applies directly. private void AppendPlainFill( in ResolvedPathCommand command, - LinearGeometry? geometry) + LinearGeometry? geometry, + Point pathDataOffset) { - uint drawTag = GetDrawTag(command.Brush); + uint drawTag = GetDrawTag(command); GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); - (uint style0, uint style1, uint style2, uint style3, uint style4) = GetFillStyle(command.GraphicsOptions, command.RasterizerOptions.IntersectionRule); + Rectangle interestBounds = ToTargetLocal(command.RasterizerOptions.Interest, this.rootTargetBounds); + (uint style0, uint style1, uint style2, uint style3, uint style4, uint style5, uint style6, uint style7, uint style8, uint style9) = + GetFillStyle(command.GraphicsOptions, command.RasterizerOptions.IntersectionRule, interestBounds, command.RasterizerOptions.CoverageBoost); int pathTagCheckpoint = this.PathTags.Count; int pathDataCheckpoint = this.PathData.Count; + int transformCheckpoint = this.Transforms.Count; int styleCheckpoint = this.Styles.Count; + GpuSceneTransform lastTransformCheckpoint = this.lastTransform; bool appendStyle = !this.hasLastStyle || style0 != this.lastStyle0 || style1 != this.lastStyle1 || style2 != this.lastStyle2 || style3 != this.lastStyle3 || - style4 != this.lastStyle4; - Vector2 scale = ExtractScale(command.Transform); + style4 != this.lastStyle4 || + style5 != this.lastStyle5 || + style6 != this.lastStyle6 || + style7 != this.lastStyle7 || + style8 != this.lastStyle8 || + style9 != this.lastStyle9; + Vector2 scale = MatrixUtilities.GetScale(command.Transform); geometry ??= command.Path.ToLinearGeometry(scale); // Reserve the exact words/tags this item can append before encoding so the @@ -1134,7 +2532,7 @@ private void AppendPlainFill( geometry.Info, drawTag, appendStyle, - GetPathGradientDataWordCount(command.Brush), + GetPathGradientDataWordCount(command), ref this.PathTags, ref this.PathData, ref this.DrawTags, @@ -1151,21 +2549,29 @@ private void AppendPlainFill( this.Styles.Add(style2); this.Styles.Add(style3); this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); } int encodedPathCount = EncodePath( - command, + pathDataOffset, geometry, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { this.PathTags.SetCount(pathTagCheckpoint); this.PathData.SetCount(pathDataCheckpoint); + this.Transforms.SetCount(transformCheckpoint); this.Styles.SetCount(styleCheckpoint); + this.lastTransform = lastTransformCheckpoint; return; } @@ -1175,44 +2581,69 @@ private void AppendPlainFill( this.lastStyle2 = style2; this.lastStyle3 = style3; this.lastStyle4 = style4; - this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest); + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; this.InfoWordCount += (int)drawTagMonoid.InfoOffset; this.DrawTags.Add(drawTag); int gradientRowCount = this.GradientRowCount; + Brush? brush = command.Brush; + if (brush is null) + { + AppendImageData( + command.ImageSource, + ToTargetLocal(command.BrushBounds, this.rootTargetBounds), + ref this.DrawData, + this.Images); + } + else + { + AppendDrawData( + brush, + command.BrushBounds, + command.GraphicsOptions, + drawTag, + this.rootTargetBounds, + ref this.DrawData, + ref this.GradientPixels, + ref this.PathGradientData, + this.Images, + this.Recolors, + ref gradientRowCount); + } - AppendDrawData( - command.Brush, - command.BrushBounds, - command.GraphicsOptions, - drawTag, - scale, - command.Transform, - this.rootTargetBounds, - ref this.DrawData, - ref this.GradientPixels, - ref this.PathGradientData, - this.Images, - ref gradientRowCount); this.GradientRowCount = gradientRowCount; } /// /// Encodes one visible stroke command into the path, draw, style, and auxiliary payload streams. /// + /// The resolved stroke command. + /// The stroke brush. + /// The pen that defines stroke width, joins, and caps. + /// The prepared centerline geometry, or to flatten the command path here. + /// The destination offset that path data applies directly. private void AppendPlainStroke( in ResolvedPathCommand command, + Brush brush, Pen pen, - LinearGeometry? geometry) + LinearGeometry? geometry, + Point pathDataOffset) { - Vector2 scale = ExtractScale(command.Transform); + Vector2 scale = MatrixUtilities.GetScale(command.Transform); geometry ??= command.Path.ToLinearGeometry(scale); float widthScale = GetTransformWidthScale(command.Transform); - uint drawTag = GetDrawTag(command.Brush); + uint drawTag = GetDrawTag(brush); GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); - (uint style0, uint style1, uint style2, uint style3, uint style4) = GetStrokeStyle(command.GraphicsOptions, pen, widthScale); + Rectangle interestBounds = ToTargetLocal(command.RasterizerOptions.Interest, this.rootTargetBounds); + (uint style0, uint style1, uint style2, uint style3, uint style4, uint style5, uint style6, uint style7, uint style8, uint style9) = + GetStrokeStyle(command.GraphicsOptions, pen, widthScale, interestBounds); int pathTagCheckpoint = this.PathTags.Count; int styleCheckpoint = this.Styles.Count; bool appendStyle = !this.hasLastStyle || @@ -1220,13 +2651,18 @@ private void AppendPlainStroke( style1 != this.lastStyle1 || style2 != this.lastStyle2 || style3 != this.lastStyle3 || - style4 != this.lastStyle4; + style4 != this.lastStyle4 || + style5 != this.lastStyle5 || + style6 != this.lastStyle6 || + style7 != this.lastStyle7 || + style8 != this.lastStyle8 || + style9 != this.lastStyle9; ReservePlainStrokeCapacity( geometry, drawTag, appendStyle, - GetPathGradientDataWordCount(command.Brush), + GetPathGradientDataWordCount(brush), ref this.PathTags, ref this.PathData, ref this.DrawTags, @@ -1243,17 +2679,23 @@ private void AppendPlainStroke( this.Styles.Add(style2); this.Styles.Add(style3); this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); } int encodedPathCount = EncodeStrokePath( geometry, - command.DestinationOffset, + pathDataOffset, pen, widthScale, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -1268,7 +2710,12 @@ private void AppendPlainStroke( this.lastStyle2 = style2; this.lastStyle3 = style3; this.lastStyle4 = style4; - this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest); + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimate(command.RasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -1277,17 +2724,16 @@ private void AppendPlainStroke( int gradientRowCount = this.GradientRowCount; AppendDrawData( - command.Brush, + brush, command.BrushBounds, command.GraphicsOptions, drawTag, - scale, - command.Transform, this.rootTargetBounds, ref this.DrawData, ref this.GradientPixels, ref this.PathGradientData, this.Images, + this.Recolors, ref gradientRowCount); this.GradientRowCount = gradientRowCount; } @@ -1295,21 +2741,29 @@ private void AppendPlainStroke( /// /// Encodes one visible explicit stroke primitive into the path, draw, style, and auxiliary payload streams. /// + /// The stroke brush. + /// The graphics options used for blending. + /// The resolved rasterizer options for the stroke. + /// The destination offset that path data applies directly. + /// The absolute brush sampling bounds. + /// The pen that defines stroke width, joins, and caps. + /// The transform-derived scale applied to the stroke width. + /// The prepared open centerline geometry. private void AppendExplicitStroke( Brush brush, GraphicsOptions graphicsOptions, RasterizerOptions rasterizerOptions, - Point destinationOffset, + Point pathDataOffset, Rectangle brushBounds, Pen pen, float widthScale, - Matrix4x4 transform, - Vector2 scale, LinearGeometry geometry) { uint drawTag = GetDrawTag(brush); GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); - (uint style0, uint style1, uint style2, uint style3, uint style4) = GetStrokeStyle(graphicsOptions, pen, widthScale); + Rectangle interestBounds = ToTargetLocal(rasterizerOptions.Interest, this.rootTargetBounds); + (uint style0, uint style1, uint style2, uint style3, uint style4, uint style5, uint style6, uint style7, uint style8, uint style9) = + GetStrokeStyle(graphicsOptions, pen, widthScale, interestBounds); int pathTagCheckpoint = this.PathTags.Count; int styleCheckpoint = this.Styles.Count; bool appendStyle = !this.hasLastStyle || @@ -1317,7 +2771,12 @@ private void AppendExplicitStroke( style1 != this.lastStyle1 || style2 != this.lastStyle2 || style3 != this.lastStyle3 || - style4 != this.lastStyle4; + style4 != this.lastStyle4 || + style5 != this.lastStyle5 || + style6 != this.lastStyle6 || + style7 != this.lastStyle7 || + style8 != this.lastStyle8 || + style9 != this.lastStyle9; ReservePlainStrokeCapacity( geometry, @@ -1340,17 +2799,23 @@ private void AppendExplicitStroke( this.Styles.Add(style2); this.Styles.Add(style3); this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); } int encodedPathCount = EncodeStrokePath( geometry, - destinationOffset, + pathDataOffset, pen, widthScale, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -1365,7 +2830,12 @@ private void AppendExplicitStroke( this.lastStyle2 = style2; this.lastStyle3 = style3; this.lastStyle4 = style4; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest); + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -1378,13 +2848,12 @@ private void AppendExplicitStroke( brushBounds, graphicsOptions, drawTag, - scale, - transform, this.rootTargetBounds, ref this.DrawData, ref this.GradientPixels, ref this.PathGradientData, this.Images, + this.Recolors, ref gradientRowCount); this.GradientRowCount = gradientRowCount; } @@ -1392,22 +2861,31 @@ private void AppendExplicitStroke( /// /// Encodes one visible explicit line-segment stroke primitive into the path, draw, style, and auxiliary payload streams. /// + /// The stroke brush. + /// The graphics options used for blending. + /// The resolved rasterizer options for the stroke. + /// The destination offset that path data applies directly. + /// The absolute brush sampling bounds. + /// The pen that defines stroke width, joins, and caps. + /// The transform-derived scale applied to the stroke width. + /// The segment start point with the transform scale applied. + /// The segment end point with the transform scale applied. private void AppendExplicitStroke( Brush brush, GraphicsOptions graphicsOptions, RasterizerOptions rasterizerOptions, - Point destinationOffset, + Point pathDataOffset, Rectangle brushBounds, Pen pen, float widthScale, - Matrix4x4 transform, - Vector2 scale, PointF start, PointF end) { uint drawTag = GetDrawTag(brush); GpuSceneDrawMonoid drawTagMonoid = GpuSceneDrawTag.Map(drawTag); - (uint style0, uint style1, uint style2, uint style3, uint style4) = GetStrokeStyle(graphicsOptions, pen, widthScale); + Rectangle interestBounds = ToTargetLocal(rasterizerOptions.Interest, this.rootTargetBounds); + (uint style0, uint style1, uint style2, uint style3, uint style4, uint style5, uint style6, uint style7, uint style8, uint style9) = + GetStrokeStyle(graphicsOptions, pen, widthScale, interestBounds); int pathTagCheckpoint = this.PathTags.Count; int styleCheckpoint = this.Styles.Count; bool appendStyle = !this.hasLastStyle || @@ -1415,7 +2893,12 @@ private void AppendExplicitStroke( style1 != this.lastStyle1 || style2 != this.lastStyle2 || style3 != this.lastStyle3 || - style4 != this.lastStyle4; + style4 != this.lastStyle4 || + style5 != this.lastStyle5 || + style6 != this.lastStyle6 || + style7 != this.lastStyle7 || + style8 != this.lastStyle8 || + style9 != this.lastStyle9; ReservePlainStrokeCapacityForOpenSegment( drawTag, @@ -1437,18 +2920,24 @@ private void AppendExplicitStroke( this.Styles.Add(style2); this.Styles.Add(style3); this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); } int encodedPathCount = EncodeOpenSegmentStrokePath( start, end, - destinationOffset, + pathDataOffset, pen, widthScale, this.rootTargetBounds, ref this.PathTags, ref this.PathData, - out int geometryLineCount); + out int geometryLineCount, + out long geometryTileCrossings); if (encodedPathCount == 0) { @@ -1463,7 +2952,12 @@ private void AppendExplicitStroke( this.lastStyle2 = style2; this.lastStyle3 = style3; this.lastStyle4 = style4; - this.AccumulateDrawRowEstimate(rasterizerOptions.Interest); + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimate(rasterizerOptions.Interest, geometryLineCount, geometryTileCrossings); this.FillCount++; this.PathCount += encodedPathCount; this.LineCount += geometryLineCount; @@ -1476,32 +2970,351 @@ private void AppendExplicitStroke( brushBounds, graphicsOptions, drawTag, - scale, - transform, this.rootTargetBounds, ref this.DrawData, ref this.GradientPixels, ref this.PathGradientData, this.Images, + this.Recolors, ref gradientRowCount); this.GradientRowCount = gradientRowCount; } + /// + /// Emits the clip descriptor carried by one explicit begin-clip command. + /// + /// The begin-clip command to append. + /// The blend mode used when closing this clip. + private void AppendBeginClip(in CompositionCommand command, uint clipBlendMode) + { + DrawingClipDescriptor descriptor = command.ClipDescriptor; + + // Clip descriptors share the path stream with ordinary draws. Reset the path + // transform before emitting the descriptor so a previous draw transform cannot + // leak into clip geometry. + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + _ = this.AppendClipDescriptor(in descriptor, command.DestinationOffset, clipBlendMode); + } + + /// + /// Emits one begin-clip record from a normalized clip descriptor. + /// + /// The clip descriptor to emit. + /// The destination offset used to place canvas-local clip geometry. + /// The blend mode used when closing this clip. + /// when a non-empty clip record was emitted. + private bool AppendClipDescriptor(in DrawingClipDescriptor descriptor, Point destinationOffset, uint clipBlendMode) + { + Rectangle clipBounds = ToTargetLocal(descriptor.GetConservativeBounds(destinationOffset), this.rootTargetBounds); + + uint style0 = descriptor.IntersectionRule == IntersectionRule.EvenOdd ? (uint)StyleFlags.Fill : 0U; + const uint style1 = 0U; + const uint style2 = 0U; + const uint style3 = 0U; + const uint style4 = 0U; + const uint style5 = 0U; + + // Clip coverage must not be restricted by a raster interest rectangle; a zero interest + // makes binning intersect the clip path away. Use the full target region. + uint style6 = BitcastSingle(0F); + uint style7 = BitcastSingle(0F); + uint style8 = BitcastSingle(this.rootTargetBounds.Width); + uint style9 = BitcastSingle(this.rootTargetBounds.Height); + + int pathTagCheckpoint = this.PathTags.Count; + int pathDataCheckpoint = this.PathData.Count; + int transformCheckpoint = this.Transforms.Count; + int styleCheckpoint = this.Styles.Count; + GpuSceneTransform lastTransformCheckpoint = this.lastTransform; + bool appendStyle = !this.hasLastStyle || + this.lastStyle0 != style0 || + this.lastStyle1 != style1 || + this.lastStyle2 != style2 || + this.lastStyle3 != style3 || + this.lastStyle4 != style4 || + this.lastStyle5 != style5 || + this.lastStyle6 != style6 || + this.lastStyle7 != style7 || + this.lastStyle8 != style8 || + this.lastStyle9 != style9; + + int encodedPathCount; + int clipLineCount; + int rectangleCount = GetRectangleClipCount(in descriptor); + if (clipBounds.Width <= 0 || clipBounds.Height <= 0) + { + ReservePlainFillCapacity( + 0, + 0, + GpuSceneDrawTag.BeginClip, + appendStyle, + 0, + ref this.PathTags, + ref this.PathData, + ref this.DrawTags, + ref this.DrawData, + ref this.Styles, + ref this.GradientPixels, + ref this.PathGradientData); + + if (appendStyle) + { + this.PathTags.Add(PackPathTag(PathTag.Style)); + this.Styles.Add(style0); + this.Styles.Add(style1); + this.Styles.Add(style2); + this.Styles.Add(style3); + this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); + } + + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + + EncodeEmptyBeginClipPath(ref this.PathTags, ref this.PathData); + encodedPathCount = 1; + clipLineCount = 1; + } + else if (rectangleCount == 0 && descriptor.Kind == DrawingClipKind.Path) + { + LinearGeometry geometry = descriptor.ToLinearGeometry(out Matrix4x4 clipTransform); + if (geometry.Info.SegmentCount == 0) + { + ReservePlainFillCapacity( + 0, + 0, + GpuSceneDrawTag.BeginClip, + appendStyle, + 0, + ref this.PathTags, + ref this.PathData, + ref this.DrawTags, + ref this.DrawData, + ref this.Styles, + ref this.GradientPixels, + ref this.PathGradientData); + + if (appendStyle) + { + this.PathTags.Add(PackPathTag(PathTag.Style)); + this.Styles.Add(style0); + this.Styles.Add(style1); + this.Styles.Add(style2); + this.Styles.Add(style3); + this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); + } + + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + + EncodeEmptyBeginClipPath(ref this.PathTags, ref this.PathData); + encodedPathCount = 1; + clipLineCount = 1; + } + else + { + // General path clips are real clip nodes. They are not boolean-applied to the + // subject geometry; the GPU clip stack combines their coverage with later draws. + ReservePlainFillCapacity( + geometry.Info, + GpuSceneDrawTag.BeginClip, + appendStyle, + 0, + ref this.PathTags, + ref this.PathData, + ref this.DrawTags, + ref this.DrawData, + ref this.Styles, + ref this.GradientPixels, + ref this.PathGradientData); + + if (appendStyle) + { + this.PathTags.Add(PackPathTag(PathTag.Style)); + this.Styles.Add(style0); + this.Styles.Add(style1); + this.Styles.Add(style2); + this.Styles.Add(style3); + this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); + } + + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + Point pathDataOffset = this.AppendTargetTransform(clipTransform, destinationOffset); + + encodedPathCount = EncodePath( + pathDataOffset, + geometry, + this.rootTargetBounds, + ref this.PathTags, + ref this.PathData, + out clipLineCount, + out _); + } + } + else if (rectangleCount > 0) + { + // Rectangular clip descriptors are already a path-equivalent rect-set. Encode the + // contours directly so dirty regions never pass through Region.ToPath/LinearGeometry. + ReservePlainFillCapacity( + rectangleCount, + rectangleCount * 4, + GpuSceneDrawTag.BeginClip, + appendStyle, + 0, + ref this.PathTags, + ref this.PathData, + ref this.DrawTags, + ref this.DrawData, + ref this.Styles, + ref this.GradientPixels, + ref this.PathGradientData); + + if (appendStyle) + { + this.PathTags.Add(PackPathTag(PathTag.Style)); + this.Styles.Add(style0); + this.Styles.Add(style1); + this.Styles.Add(style2); + this.Styles.Add(style3); + this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); + } + + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + + encodedPathCount = EncodeRectangleClipPath( + in descriptor, + destinationOffset, + this.rootTargetBounds, + ref this.PathTags, + ref this.PathData, + out clipLineCount); + } + else + { + encodedPathCount = 0; + clipLineCount = 0; + } + + if (encodedPathCount == 0) + { + this.PathTags.SetCount(pathTagCheckpoint); + this.PathData.SetCount(pathDataCheckpoint); + this.Transforms.SetCount(transformCheckpoint); + this.Styles.SetCount(styleCheckpoint); + this.lastTransform = lastTransformCheckpoint; + + ReservePlainFillCapacity( + 0, + 0, + GpuSceneDrawTag.BeginClip, + appendStyle, + 0, + ref this.PathTags, + ref this.PathData, + ref this.DrawTags, + ref this.DrawData, + ref this.Styles, + ref this.GradientPixels, + ref this.PathGradientData); + + if (appendStyle) + { + this.PathTags.Add(PackPathTag(PathTag.Style)); + this.Styles.Add(style0); + this.Styles.Add(style1); + this.Styles.Add(style2); + this.Styles.Add(style3); + this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); + } + + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + + // Packed ranges replay active clips independently. Even when the clipped + // geometry is empty for this range, the physical BeginClip must remain in + // the stream so the later EndClip closes a real GPU clip-stack entry. + EncodeEmptyBeginClipPath(ref this.PathTags, ref this.PathData); + encodedPathCount = 1; + clipLineCount = 1; + } + + this.hasLastStyle = true; + this.lastStyle0 = style0; + this.lastStyle1 = style1; + this.lastStyle2 = style2; + this.lastStyle3 = style3; + this.lastStyle4 = style4; + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimateLocal(clipBounds, clipLineCount); + this.PathCount += encodedPathCount; + this.LineCount += clipLineCount; + this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; + this.DrawTags.Add(GpuSceneDrawTag.BeginClip); + AppendClipBeginData(descriptor.Operation, descriptor.EdgeMode, descriptor.AntialiasThreshold, clipBlendMode, ref this.DrawData); + this.ClipCount++; + return true; + } + /// /// Encodes one begin-layer clip command as a rectangular clip path and draw record. /// + /// The begin-layer command to append. private void AppendBeginLayer(in CompositionCommand command) { Rectangle layerBounds = ToTargetLocal(command.LayerBounds, this.rootTargetBounds); - (uint style0, uint style1, uint style2, uint style3, uint style4) = GetFillStyle(DefaultClipGraphicsOptions, IntersectionRule.NonZero); - int pathTagCheckpoint = this.PathTags.Count; - int styleCheckpoint = this.Styles.Count; + + // The layer rectangle opens an isolated target; layer graphics options are applied + // when that target is composited back, not when the temporary target is bounded. + (uint style0, uint style1, uint style2, uint style3, uint style4, uint style5, uint style6, uint style7, uint style8, uint style9) = + GetFillStyle(LayerMaskGraphicsOptions, IntersectionRule.NonZero, layerBounds); + bool appendStyle = !this.hasLastStyle || style0 != this.lastStyle0 || style1 != this.lastStyle1 || style2 != this.lastStyle2 || style3 != this.lastStyle3 || - style4 != this.lastStyle4; + style4 != this.lastStyle4 || + style5 != this.lastStyle5 || + style6 != this.lastStyle6 || + style7 != this.lastStyle7 || + style8 != this.lastStyle8 || + style9 != this.lastStyle9; // Begin-layer clip emission is fixed-size: one optional style record, // one rectangular clip path, and one BeginClip draw record. @@ -1521,13 +3334,27 @@ private void AppendBeginLayer(in CompositionCommand command) this.Styles.Add(style2); this.Styles.Add(style3); this.Styles.Add(style4); + this.Styles.Add(style5); + this.Styles.Add(style6); + this.Styles.Add(style7); + this.Styles.Add(style8); + this.Styles.Add(style9); } + this.PathTags.EnsureAdditionalCapacity(1); + this.Transforms.EnsureAdditionalCapacity(TransformWordCount); + this.AppendTransformIfChanged(Matrix4x4.Identity); + + int encodedPathCount; if (EncodeRectanglePath(layerBounds, ref this.PathTags, ref this.PathData, out int clipLineCount) == 0) { - this.PathTags.SetCount(pathTagCheckpoint); - this.Styles.SetCount(styleCheckpoint); - return; + EncodeEmptyBeginClipPath(ref this.PathTags, ref this.PathData); + encodedPathCount = 1; + clipLineCount = 1; + } + else + { + encodedPathCount = 1; } this.hasLastStyle = true; @@ -1536,29 +3363,46 @@ private void AppendBeginLayer(in CompositionCommand command) this.lastStyle2 = style2; this.lastStyle3 = style3; this.lastStyle4 = style4; - this.AccumulateDrawRowEstimateLocal(layerBounds); - this.PathCount++; + this.lastStyle5 = style5; + this.lastStyle6 = style6; + this.lastStyle7 = style7; + this.lastStyle8 = style8; + this.lastStyle9 = style9; + this.AccumulateDrawRowEstimateLocal(layerBounds, clipLineCount); + this.PathCount += encodedPathCount; this.LineCount += clipLineCount; this.InfoWordCount += (int)GpuSceneDrawTag.Map(GpuSceneDrawTag.BeginClip).InfoOffset; this.DrawTags.Add(GpuSceneDrawTag.BeginClip); - AppendBeginClipData(command.GraphicsOptions, ref this.DrawData); + AppendBeginClipData(command.LayerOptions, ref this.DrawData); this.ClipCount++; this.openLayerBounds ??= new List(4); this.openLayerBounds.Add(layerBounds); } /// - /// Adds one draw object's target-space interest rectangle to the sparse path-row estimate after converting it to root-target-local coordinates. + /// Adds one draw object's target-space interest rectangle to the sparse scratch estimates after converting it to root-target-local coordinates. /// /// The draw object's absolute raster interest bounds. - private void AccumulateDrawRowEstimate(Rectangle absoluteInterest) - => this.AccumulateDrawRowEstimateLocal(ToTargetLocal(absoluteInterest, this.rootTargetBounds)); + /// The draw object's flattened (or GPU-expansion estimated) line count. + /// + /// The exact summed per-line tile-crossing bound when the caller flattened the geometry on the CPU + /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). + /// + private void AccumulateDrawRowEstimate(Rectangle absoluteInterest, int lineCount, long tileCrossings = -1) + => this.AccumulateDrawRowEstimateLocal(ToTargetLocal(absoluteInterest, this.rootTargetBounds), lineCount, tileCrossings); /// - /// Adds one draw object's clipped root-target-local tile-row span to the sparse path-row estimate. + /// Adds one draw object's clipped root-target-local footprint to the sparse scratch estimates: + /// tile-row span for path rows, per-line tile-crossing bound for segments and path tiles, and + /// bin footprint for binning records. /// /// The draw object's root-target-local raster interest bounds. - private void AccumulateDrawRowEstimateLocal(Rectangle localBounds) + /// The draw object's flattened (or GPU-expansion estimated) line count. + /// + /// The exact summed per-line tile-crossing bound when the caller flattened the geometry on the CPU + /// (fills), or a negative value to fall back to the bounding-box diagonal bound (strokes, clips). + /// + private void AccumulateDrawRowEstimateLocal(Rectangle localBounds, int lineCount, long tileCrossings = -1) { Rectangle clippedBounds = Rectangle.Intersect(localBounds, new Rectangle(0, 0, this.rootTargetBounds.Width, this.rootTargetBounds.Height)); @@ -1579,6 +3423,21 @@ private void AccumulateDrawRowEstimateLocal(Rectangle localBounds) int tileY1 = DivideRoundUp(clippedBounds.Bottom, TileHeight); long rowCount = Math.Max(tileY1 - tileY0, 0); this.estimatedPathRowCount = Math.Min(this.estimatedPathRowCount + rowCount, int.MaxValue); + + // A line inside this interest crosses at most (tilesWide + tilesHigh + slack) tile + // boundaries, so lineCount x that diagonal bound caps the draw's segment and + // path-tile records. Deliberately conservative; over-seeding wastes memory while + // under-seeding costs a full-scene GPU retry. + long tilesWide = (clippedBounds.Width / TileWidth) + 2; + long tilesHigh = rowCount + 1; + long boundingBoxCrossings = Math.Max(lineCount, 1) * (tilesWide + tilesHigh + 2); + long crossings = tileCrossings >= 0 ? Math.Min(tileCrossings, boundingBoxCrossings) : boundingBoxCrossings; + this.estimatedTileCrossings += crossings; + + // 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; } /// @@ -1591,10 +3450,19 @@ private void AppendEndLayer() this.openLayerBounds.RemoveAt(this.openLayerBounds.Count - 1); } - // End-layer emission is fixed-size: one EndClip draw tag and one PathTagPath - // terminator for the zero-data end marker. In parallel encoding, a partition can - // see the closing layer command without the matching opener, so the command itself - // is the ordering contract rather than the local estimate stack. + // A packed range can contain an EndLayer whose matching BeginLayer was emitted + // in an earlier range. The command stream is still balanced, so the close marker + // must be emitted even when the local row-estimate stack has no opener to pop. + this.AppendEndClip(); + } + + /// + /// Encodes one end-clip command. + /// + private void AppendEndClip() + { + // EndClip is fixed-size: one draw tag plus a zero-data path terminator. The canvas + // records balanced command ranges, so this method does not inspect local stack state. ReserveEndLayerCapacity(ref this.PathTags, ref this.DrawTags); this.DrawTags.Add(GpuSceneDrawTag.EndClip); this.PathTags.Add(PackPathTag(PathTag.Path)); @@ -1616,10 +3484,40 @@ private sealed class SceneEncodingPartition : IDisposable private readonly IMemoryOwner stylesOwner; private readonly IMemoryOwner? gradientPixelsOwner; private readonly IMemoryOwner? pathGradientDataOwner; + private bool disposed; /// /// Initializes a new instance of the class. /// + /// The detached path-tag storage. + /// The detached path-data storage. + /// The detached draw-tag storage. + /// The detached draw-data storage. + /// The detached transform storage. + /// The detached style storage. + /// The detached gradient-pixel storage, or when no gradients were emitted. + /// The detached path-gradient storage, or when no path gradients were emitted. + /// The deferred image descriptors recorded by the partition. + /// The deferred recolor descriptors recorded by the partition. + /// The number of emitted fill records. + /// The number of visible fills accepted by staged-scene validation. + /// The number of emitted paths. + /// The number of emitted non-horizontal lines. + /// The total info-word count implied by the emitted draw tags. + /// The number of emitted clip records. + /// 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 per-(draw, bin) records. + /// The unpadded path-tag byte count. + /// The path-data word count. + /// The draw-tag count. + /// The draw-data word count. + /// The transform word count. + /// The style word count. + /// The path-gradient payload word count. + /// The fine-pass rasterization mode selected by the partition. + /// The aliased coverage threshold selected by the partition. private SceneEncodingPartition( IMemoryOwner pathTagsOwner, IMemoryOwner pathDataOwner, @@ -1630,6 +3528,7 @@ private SceneEncodingPartition( IMemoryOwner? gradientPixelsOwner, IMemoryOwner? pathGradientDataOwner, List images, + List recolors, int fillCount, int visibleFillCount, int pathCount, @@ -1638,6 +3537,8 @@ private SceneEncodingPartition( int clipCount, int gradientRowCount, int estimatedPathRowCount, + long estimatedTileCrossings, + long estimatedBinFootprint, int pathTagByteCount, int pathDataWordCount, int drawTagCount, @@ -1657,6 +3558,7 @@ private SceneEncodingPartition( this.gradientPixelsOwner = gradientPixelsOwner; this.pathGradientDataOwner = pathGradientDataOwner; this.Images = images; + this.Recolors = recolors; this.FillCount = fillCount; this.VisibleFillCount = visibleFillCount; this.PathCount = pathCount; @@ -1665,6 +3567,8 @@ private SceneEncodingPartition( this.ClipCount = clipCount; this.GradientRowCount = gradientRowCount; this.EstimatedPathRowCount = estimatedPathRowCount; + this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedBinFootprint = estimatedBinFootprint; this.PathTagByteCount = pathTagByteCount; this.PathDataWordCount = pathDataWordCount; this.DrawTagCount = drawTagCount; @@ -1712,7 +3616,7 @@ private SceneEncodingPartition( public ReadOnlySpan GradientPixels => this.gradientPixelsOwner is null ? ReadOnlySpan.Empty - : this.gradientPixelsOwner.Memory.Span[..(this.GradientRowCount * GradientWidth)]; + : this.gradientPixelsOwner.Memory.Span[..(this.GradientRowCount * GradientRowWordCount)]; /// /// Gets the encoded path-gradient payload words. @@ -1727,6 +3631,11 @@ public ReadOnlySpan PathGradientData /// public List Images { get; } + /// + /// Gets the deferred recolor descriptors recorded by this partition. + /// + public List Recolors { get; } + /// /// Gets the number of emitted fill records. /// @@ -1767,6 +3676,16 @@ public ReadOnlySpan PathGradientData /// public int EstimatedPathRowCount { get; } + /// + /// Gets the CPU-side upper bound for tile-boundary crossings. + /// + public long EstimatedTileCrossings { get; } + + /// + /// Gets the CPU-side upper bound for per-(draw, bin) binning records. + /// + public long EstimatedBinFootprint { get; } + /// /// Gets the unpadded path-tag byte count. /// @@ -1815,6 +3734,8 @@ public ReadOnlySpan PathGradientData /// /// Detaches all retained streams from a mutable partition encoder. /// + /// The mutable partition encoding. + /// The detached partition encoding. public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding encoding) { int pathTagByteCount = encoding.PathTags.Count; @@ -1858,6 +3779,7 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc gradientPixelsOwner, pathGradientDataOwner, encoding.Images, + encoding.Recolors, encoding.FillCount, encoding.VisibleFillCount, encoding.PathCount, @@ -1866,6 +3788,8 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc encoding.ClipCount, gradientRowCount, encoding.EstimatedPathRowCount, + encoding.EstimatedTileCrossings, + encoding.EstimatedBinFootprint, pathTagByteCount, pathDataWordCount, drawTagCount, @@ -1882,6 +3806,12 @@ public static SceneEncodingPartition Detach(ref SupportedSubsetSceneEncoding enc /// public void Dispose() { + if (this.disposed) + { + return; + } + + this.disposed = true; this.pathTagsOwner.Dispose(); this.pathDataOwner.Dispose(); this.drawTagsOwner.Dispose(); @@ -1901,12 +3831,21 @@ private static class SupportedSubsetSceneResolver /// /// Resolves the mutable encoding into the final packed scene buffers. /// + /// The mutable scene encoding. + /// The target bounds. + /// The allocator used for scene buffers. + /// The scene operations associated with the encoded payload. + /// The resolved encoded scene. public static WebGPUEncodedScene Resolve( ref SupportedSubsetSceneEncoding encoding, in Rectangle targetBounds, - MemoryAllocator allocator) + MemoryAllocator allocator, + WebGPUSceneOperation[] operations) { int pathTagByteCount = encoding.PathTags.Count; + + // Path tags are reduced in 256-word scan segments (pathtag_reduce WG_SIZE), so the + // word count is padded up to a whole segment before the path-data stream begins. int pathTagWordCount = AlignUp(DivideRoundUp(pathTagByteCount, 4), 256); int pathDataWordCount = encoding.PathData.Count; int drawTagCount = encoding.DrawTags.Count; @@ -1956,6 +3895,7 @@ public static WebGPUEncodedScene Resolve( DetachPathGradientData(ref encoding), pathGradientDataWordCount, encoding.Images, + encoding.Recolors, encoding.GradientRowCount, layout, encoding.FillCount, @@ -1974,7 +3914,10 @@ public static WebGPUEncodedScene Resolve( DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), encoding.FineRasterizationMode, - encoding.FineCoverageThreshold); + encoding.FineCoverageThreshold, + encoding.EstimatedTileCrossings, + encoding.EstimatedBinFootprint, + operations); } catch { @@ -1986,6 +3929,12 @@ public static WebGPUEncodedScene Resolve( /// /// Resolves ordered partition encodings into the final packed scene buffers. /// + /// The ordered partition encodings. + /// The target bounds. + /// The allocator used for scene buffers. + /// The fine-pass rasterization mode. + /// The scene-wide aliased coverage threshold. + /// The resolved encoded scene. public static WebGPUEncodedScene Resolve( SceneEncodingPartition?[] partitions, in Rectangle targetBounds, @@ -2001,6 +3950,7 @@ public static WebGPUEncodedScene Resolve( int styleWordCount = 0; int pathGradientDataWordCount = 0; int imageCount = 0; + int recolorCount = 0; int gradientRowCount = 0; int fillCount = 0; int pathCount = 0; @@ -2008,6 +3958,8 @@ public static WebGPUEncodedScene Resolve( int infoWordCount = 0; int clipCount = 0; long estimatedPathRowCount = 0; + long estimatedTileCrossings = 0; + long estimatedBinFootprint = 0; for (int i = 0; i < partitions.Length; i++) { @@ -2020,6 +3972,7 @@ public static WebGPUEncodedScene Resolve( styleWordCount += partition.StyleWordCount; pathGradientDataWordCount += partition.PathGradientDataWordCount; imageCount += partition.Images.Count; + recolorCount += partition.Recolors.Count; gradientRowCount += partition.GradientRowCount; fillCount += partition.FillCount; pathCount += partition.PathCount; @@ -2027,8 +3980,12 @@ public static WebGPUEncodedScene Resolve( infoWordCount += partition.InfoWordCount; clipCount += partition.ClipCount; estimatedPathRowCount = Math.Min(estimatedPathRowCount + partition.EstimatedPathRowCount, int.MaxValue); + estimatedTileCrossings += partition.EstimatedTileCrossings; + estimatedBinFootprint += partition.EstimatedBinFootprint; } + // Path tags are reduced in 256-word scan segments (pathtag_reduce WG_SIZE), so the + // word count is padded up to a whole segment before the path-data stream begins. int pathTagWordCount = AlignUp(DivideRoundUp(pathTagByteCount, 4), 256); int drawTagBase = pathTagWordCount + pathDataWordCount; int drawDataBase = drawTagBase + drawTagCount; @@ -2050,13 +4007,14 @@ public static WebGPUEncodedScene Resolve( (uint)styleBase); IMemoryOwner? sceneDataOwner = allocator.Allocate(sceneWordCount); - IMemoryOwner? gradientPixelsOwner = gradientRowCount == 0 ? null : allocator.Allocate(gradientRowCount * GradientWidth); + IMemoryOwner? gradientPixelsOwner = gradientRowCount == 0 ? null : allocator.Allocate(gradientRowCount * GradientRowWordCount); IMemoryOwner? pathGradientDataOwner = pathGradientDataWordCount == 0 ? null : allocator.Allocate(pathGradientDataWordCount); try { Span sceneWords = sceneDataOwner.Memory.Span[..sceneWordCount]; Span sceneBytes = MemoryMarshal.Cast(sceneWords); List images = new(imageCount); + List recolors = new(recolorCount); int pathTagOffset = 0; int pathDataOffset = 0; int drawTagOffset = 0; @@ -2088,7 +4046,18 @@ public static WebGPUEncodedScene Resolve( for (int imageIndex = 0; imageIndex < partition.Images.Count; imageIndex++) { GpuImageDescriptor image = partition.Images[imageIndex]; - images.Add(new GpuImageDescriptor(image.Brush, drawDataOffset + image.DrawDataWordOffset)); + images.Add(new GpuImageDescriptor(image.Source, drawDataOffset + image.DrawDataWordOffset)); + } + + for (int recolorIndex = 0; recolorIndex < partition.Recolors.Count; recolorIndex++) + { + GpuRecolorDescriptor recolor = partition.Recolors[recolorIndex]; + + recolors.Add(new GpuRecolorDescriptor( + recolor.SourceColor, + recolor.TargetColor, + recolor.Threshold, + pathGradientDataOffset + recolor.BrushDataWordOffset)); } drawDataOffset += partition.DrawDataWordCount; @@ -2101,8 +4070,8 @@ public static WebGPUEncodedScene Resolve( if (partition.GradientRowCount > 0) { - partition.GradientPixels.CopyTo(gradientPixelsOwner!.Memory.Span.Slice(gradientPixelOffset, partition.GradientRowCount * GradientWidth)); - gradientPixelOffset += partition.GradientRowCount * GradientWidth; + partition.GradientPixels.CopyTo(gradientPixelsOwner!.Memory.Span.Slice(gradientPixelOffset, partition.GradientRowCount * GradientRowWordCount)); + gradientPixelOffset += partition.GradientRowCount * GradientRowWordCount; } if (partition.PathGradientDataWordCount > 0) @@ -2124,6 +4093,7 @@ public static WebGPUEncodedScene Resolve( pathGradientDataOwner, pathGradientDataWordCount, images, + recolors, gradientRowCount, layout, fillCount, @@ -2142,7 +4112,10 @@ public static WebGPUEncodedScene Resolve( DivideRoundUp(targetBounds.Width, TileWidth), DivideRoundUp(targetBounds.Height, TileHeight), fineRasterizationMode, - fineCoverageThreshold); + fineCoverageThreshold, + estimatedTileCrossings, + estimatedBinFootprint, + []); sceneDataOwner = null; gradientPixelsOwner = null; @@ -2160,6 +4133,10 @@ public static WebGPUEncodedScene Resolve( /// /// Copies draw-data while rebasing partition-local auxiliary payload offsets into final scene offsets. /// + /// The partition supplying draw tags and draw data. + /// The destination slice in the packed scene buffer. + /// The number of gradient rows emitted by earlier partitions. + /// The number of path-gradient words emitted by earlier partitions. private static void CopyDrawDataWithOffsets( SceneEncodingPartition partition, Span destination, @@ -2179,9 +4156,11 @@ private static void CopyDrawDataWithOffsets( if (DrawTagUsesGradientRamp(drawTag)) { + // The gradient index_mode word packs (ramp row << 2) | extend mode, so the + // row rebase is shifted past the two extend-mode bits. destination[destinationOffset] += (uint)(gradientRowOffset << 2); } - else if (drawTag == GpuSceneDrawTag.FillPathGradient) + else if (drawTag is GpuSceneDrawTag.FillRecolor or GpuSceneDrawTag.FillPathGradient) { destination[destinationOffset] += (uint)pathGradientDataOffset; } @@ -2194,6 +4173,8 @@ private static void CopyDrawDataWithOffsets( /// /// Detaches the gradient pixel payload when gradients were emitted for the flush. /// + /// The mutable scene encoding. + /// The detached storage, or when no gradients were emitted. private static IMemoryOwner? DetachGradientPixels(ref SupportedSubsetSceneEncoding encoding) { if (encoding.GradientRowCount == 0) @@ -2205,6 +4186,11 @@ private static void CopyDrawDataWithOffsets( return encoding.GradientPixels.DetachOwner(); } + /// + /// Detaches the path-gradient payload when path gradients were emitted for the flush. + /// + /// The mutable scene encoding. + /// The detached storage, or when no path gradients were emitted. private static IMemoryOwner? DetachPathGradientData(ref SupportedSubsetSceneEncoding encoding) { if (encoding.PathGradientData.Count == 0) @@ -2220,6 +4206,8 @@ private static void CopyDrawDataWithOffsets( /// /// Returns whether the staged scene encoder knows how to lower the supplied brush type. /// + /// The brush to test. + /// when the brush type has a draw-tag lowering. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSupportedBrush(Brush brush) => brush is SolidBrush @@ -2233,9 +4221,17 @@ or PatternBrush or ImageBrush; /// - /// Maps one prepared fill command to the draw-tag consumed by the staged scene pipeline. + /// Resolves one prepared fill command's raster bounds against its target and captures the + /// state the encoder needs to emit it. /// - private static bool TryResolveCommand(in CompositionCommand command, out ResolvedPathCommand resolved) + /// The fill command to resolve. + /// The prepared geometry for the command. + /// Receives the resolved command state. + /// when the command is a fill that intersects its target. + private static bool TryResolveCommand( + in CompositionCommand command, + LinearGeometry geometry, + out ResolvedPathCommand resolved) { if (command.Kind is not CompositionCommandKind.FillLayer) { @@ -2243,59 +4239,230 @@ private static bool TryResolveCommand(in CompositionCommand command, out Resolve return false; } + Matrix4x4 transform = command.Transform; + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + + if (!TryResolveRasterization( + command.RasterizerOptions, + geometryBounds, + command.DestinationOffset, + command.TargetBounds, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + resolved = default; + return false; + } + resolved = new ResolvedPathCommand( command.SourcePath, command.Brush, - command.GraphicsOptions, - command.RasterizerOptions, + command.DrawingOptions.GraphicsOptions, + rasterizerOptions, command.DestinationOffset, - command.Brush is ImageBrush ? command.RasterizerOptions.Interest : default, + command.Brush is ImageBrush ? brushBounds : default, command.Transform); return true; } - private static bool TryResolveCommand(in StrokePathCommand command, out ResolvedPathCommand resolved) + /// + /// Resolves one prepared stroked-path command's stroke-inflated raster bounds against its + /// target and captures the state the encoder needs to emit it. + /// + /// The stroke command to resolve. + /// The prepared centerline geometry for the command. + /// Receives the resolved command state. + /// when the stroke intersects its target. + private static bool TryResolveCommand( + in StrokePathCommand command, + LinearGeometry geometry, + out ResolvedPathCommand resolved) { + Matrix4x4 transform = command.Transform; + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); + float widthScale = GetTransformWidthScale(transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); + + if (!TryResolveRasterization( + command.RasterizerOptions, + strokeBounds, + command.DestinationOffset, + command.TargetBounds, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + resolved = default; + return false; + } + resolved = new ResolvedPathCommand( command.SourcePath, command.Brush, command.GraphicsOptions, - command.RasterizerOptions, + rasterizerOptions, command.DestinationOffset, - command.Brush is ImageBrush ? command.RasterizerOptions.Interest : default, + command.Brush is ImageBrush ? brushBounds : default, command.Transform); return true; } + /// + /// Resolves one explicit line-segment stroke's stroke-inflated raster bounds against its + /// target and captures the state the encoder needs to emit it. + /// + /// The line-segment stroke command to resolve. + /// Receives the resolved command state. + /// when the stroke intersects its target. private static bool TryResolveCommand(in StrokeLineSegmentCommand command, out ResolvedLineSegmentCommand resolved) { + Matrix4x4 transform = command.Transform; + PointF start = transform.IsIdentity ? command.SourceStart : PointF.Transform(command.SourceStart, transform); + PointF end = transform.IsIdentity ? command.SourceEnd : PointF.Transform(command.SourceEnd, transform); + float widthScale = GetTransformWidthScale(transform); + RectangleF segmentBounds = RectangleF.FromLTRB( + MathF.Min(start.X, end.X), + MathF.Min(start.Y, end.Y), + MathF.Max(start.X, end.X), + MathF.Max(start.Y, end.Y)); + RectangleF strokeBounds = GetStrokeBounds(segmentBounds, command.Pen, widthScale); + + if (!TryResolveRasterization( + command.RasterizerOptions, + strokeBounds, + command.DestinationOffset, + command.TargetBounds, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + resolved = default; + return false; + } + resolved = new ResolvedLineSegmentCommand( command.SourceStart, command.SourceEnd, command.Pen, command.Brush, command.GraphicsOptions, - command.RasterizerOptions, + rasterizerOptions, command.DestinationOffset, - command.Brush is ImageBrush ? command.RasterizerOptions.Interest : default); + command.Brush is ImageBrush ? brushBounds : default); return true; } - private static bool TryResolveCommand(in StrokePolylineCommand command, out ResolvedPolylineCommand resolved) + /// + /// Resolves one explicit polyline stroke's stroke-inflated raster bounds against its target + /// and captures the state the encoder needs to emit it. + /// + /// The polyline stroke command to resolve. + /// The prepared open polyline geometry for the command. + /// Receives the resolved command state. + /// when the stroke intersects its target. + private static bool TryResolveCommand( + in StrokePolylineCommand command, + LinearGeometry geometry, + out ResolvedPolylineCommand resolved) { + Matrix4x4 transform = command.Transform; + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); + float widthScale = GetTransformWidthScale(transform); + RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); + RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); + + if (!TryResolveRasterization( + command.RasterizerOptions, + strokeBounds, + command.DestinationOffset, + command.TargetBounds, + out RasterizerOptions rasterizerOptions, + out Rectangle brushBounds)) + { + resolved = default; + return false; + } + resolved = new ResolvedPolylineCommand( command.SourcePoints, command.Pen, command.Brush, command.GraphicsOptions, - command.RasterizerOptions, + rasterizerOptions, command.DestinationOffset, - command.Brush is ImageBrush ? command.RasterizerOptions.Interest : default); + command.Brush is ImageBrush ? brushBounds : default); + return true; + } + + /// + /// Resolves local command raster bounds into absolute target bounds for scene encoding. + /// + /// The local rasterizer options stored on the command. + /// The prepared local geometry bounds used for rasterization. + /// The absolute destination offset for the command. + /// The absolute target bounds that own the command pixels. + /// The rasterizer options clipped to the owning target bounds. + /// The unclipped absolute brush sampling bounds. + /// when the command intersects the target bounds. + private static bool TryResolveRasterization( + in RasterizerOptions options, + RectangleF bounds, + Point destinationOffset, + in Rectangle targetBounds, + out RasterizerOptions resolvedOptions, + out Rectangle brushBounds) + { + Rectangle localInterest = Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right) + 1, + (int)MathF.Ceiling(bounds.Bottom) + 1); + + Rectangle absoluteInterest = new( + localInterest.X + destinationOffset.X, + localInterest.Y + destinationOffset.Y, + localInterest.Width, + localInterest.Height); + + Rectangle clippedDestination = Rectangle.Intersect(targetBounds, absoluteInterest); + + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) + { + resolvedOptions = default; + brushBounds = default; + return false; + } + + resolvedOptions = new RasterizerOptions( + clippedDestination, + options.IntersectionRule, + options.RasterizationMode, + options.AntialiasThreshold, + options.CoverageBoost); + + brushBounds = absoluteInterest; return true; } + /// + /// Target-resolved state for one fill or stroked-path command, painted by either a brush + /// or an external WebGPU texture. + /// private readonly struct ResolvedPathCommand { + /// + /// Initializes a new instance of the struct for a brush-painted command. + /// + /// The source path. + /// The paint brush. + /// The graphics options used for blending. + /// The rasterizer options clipped to the owning target. + /// The absolute destination offset for the command. + /// The absolute brush sampling bounds; default unless the brush is an image brush. + /// The full command transform. public ResolvedPathCommand( IPath path, Brush brush, @@ -2307,6 +4474,37 @@ public ResolvedPathCommand( { this.Path = path; this.Brush = brush; + this.ImageSource = new GpuImageSource(brush); + this.GraphicsOptions = graphicsOptions; + this.RasterizerOptions = rasterizerOptions; + this.DestinationOffset = destinationOffset; + this.BrushBounds = brushBounds; + this.Transform = transform; + } + + /// + /// Initializes a new instance of the struct for a fill + /// sampled from an external render-time texture instead of a brush. + /// + /// The source path. + /// The external texture source metadata. + /// The graphics options used for blending. + /// The rasterizer options clipped to the owning target. + /// The absolute destination offset for the command. + /// The absolute texture sampling bounds. + /// The full command transform. + public ResolvedPathCommand( + IPath path, + in GpuImageSource imageSource, + GraphicsOptions graphicsOptions, + RasterizerOptions rasterizerOptions, + Point destinationOffset, + Rectangle brushBounds, + Matrix4x4 transform) + { + this.Path = path; + this.Brush = null; + this.ImageSource = imageSource; this.GraphicsOptions = graphicsOptions; this.RasterizerOptions = rasterizerOptions; this.DestinationOffset = destinationOffset; @@ -2314,23 +4512,63 @@ public ResolvedPathCommand( this.Transform = transform; } + /// + /// Gets the source path. + /// public IPath Path { get; } - public Brush Brush { get; } + /// + /// Gets the paint brush, or when the fill samples an external texture. + /// + public Brush? Brush { get; } + + /// + /// Gets the image source metadata for image-painted fills. + /// + public GpuImageSource ImageSource { get; } + /// + /// Gets the graphics options used for blending. + /// public GraphicsOptions GraphicsOptions { get; } + /// + /// Gets the rasterizer options clipped to the owning target. + /// public RasterizerOptions RasterizerOptions { get; } + /// + /// Gets the absolute destination offset for the command. + /// public Point DestinationOffset { get; } + /// + /// Gets the absolute brush or texture sampling bounds. + /// public Rectangle BrushBounds { get; } + /// + /// Gets the full command transform. + /// public Matrix4x4 Transform { get; } } + /// + /// Target-resolved state for one explicit two-point line-segment stroke. + /// private readonly struct ResolvedLineSegmentCommand { + /// + /// Initializes a new instance of the struct. + /// + /// The untransformed segment start point. + /// The untransformed segment end point. + /// The pen that defines stroke width, joins, and caps. + /// The stroke brush. + /// The graphics options used for blending. + /// The rasterizer options clipped to the owning target. + /// The absolute destination offset for the command. + /// The absolute brush sampling bounds; default unless the brush is an image brush. public ResolvedLineSegmentCommand( PointF start, PointF end, @@ -2351,25 +4589,62 @@ public ResolvedLineSegmentCommand( this.BrushBounds = brushBounds; } + /// + /// Gets the untransformed segment start point. + /// public PointF Start { get; } + /// + /// Gets the untransformed segment end point. + /// public PointF End { get; } + /// + /// Gets the pen that defines stroke width, joins, and caps. + /// public Pen Pen { get; } + /// + /// Gets the stroke brush. + /// public Brush Brush { get; } + /// + /// Gets the graphics options used for blending. + /// public GraphicsOptions GraphicsOptions { get; } + /// + /// Gets the rasterizer options clipped to the owning target. + /// public RasterizerOptions RasterizerOptions { get; } + /// + /// Gets the absolute destination offset for the command. + /// public Point DestinationOffset { get; } + /// + /// Gets the absolute brush sampling bounds. + /// public Rectangle BrushBounds { get; } } + /// + /// Target-resolved state for one explicit open polyline stroke. + /// private readonly struct ResolvedPolylineCommand { + /// + /// Initializes a new instance of the struct. + /// + /// The untransformed polyline points. + /// The pen that defines stroke width, joins, and caps. + /// The stroke brush. + /// The graphics options used for blending. + /// The rasterizer options clipped to the owning target. + /// The absolute destination offset for the command. + /// The absolute brush sampling bounds; default unless the brush is an image brush. public ResolvedPolylineCommand( PointF[] points, Pen pen, @@ -2388,24 +4663,47 @@ public ResolvedPolylineCommand( this.BrushBounds = brushBounds; } + /// + /// Gets the untransformed polyline points. + /// public PointF[] Points { get; } + /// + /// Gets the pen that defines stroke width, joins, and caps. + /// public Pen Pen { get; } + /// + /// Gets the stroke brush. + /// public Brush Brush { get; } + /// + /// Gets the graphics options used for blending. + /// public GraphicsOptions GraphicsOptions { get; } + /// + /// Gets the rasterizer options clipped to the owning target. + /// public RasterizerOptions RasterizerOptions { get; } + /// + /// Gets the absolute destination offset for the command. + /// public Point DestinationOffset { get; } + /// + /// Gets the absolute brush sampling bounds. + /// public Rectangle BrushBounds { get; } } /// /// Maps one prepared brush to the draw-tag consumed by the staged scene pipeline. /// + /// The brush to map. + /// The draw tag. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint GetDrawTag(Brush brush) => brush switch @@ -2422,22 +4720,41 @@ private static uint GetDrawTag(Brush brush) _ => throw new UnreachableException($"Unsupported brush type '{brush.GetType().Name}' should have been rejected before scene encoding.") }; + /// + /// Maps one resolved fill source to the draw-tag consumed by the staged scene pipeline. + /// + /// The resolved command; brushless commands sample an external texture. + /// The draw tag. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint GetDrawTag(in ResolvedPathCommand command) + => command.Brush is not null ? GetDrawTag(command.Brush) : GpuSceneDrawTag.FillImage; + /// /// Encodes a lowered path into path-tag and path-data streams in target-local space. /// + /// The absolute destination offset applied to every point. + /// The flattened geometry to encode. + /// The root target bounds used for target-local conversion. + /// The path-tag stream. + /// The path-data stream. + /// Receives the number of non-horizontal line segments encoded. + /// + /// Receives the summed conservative tile-crossing bound over every encoded segment, used to seed the + /// segment and path-tile scratch buffers far more tightly than the draw's bounding-box diagonal. + /// + /// The number of encoded path objects: 1, or 0 when the geometry has no segments. private static int EncodePath( - in ResolvedPathCommand command, + Point destinationOffset, LinearGeometry geometry, in Rectangle rootTargetBounds, ref OwnedStream pathTags, ref OwnedStream pathData, - out int lineCount) + out int lineCount, + out long tileCrossings) { - float pointTranslateX = command.DestinationOffset.X - rootTargetBounds.X; - float pointTranslateY = command.DestinationOffset.Y - rootTargetBounds.Y; - lineCount = command.RasterizerOptions.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter - ? geometry.Info.NonHorizontalSegmentCountPixelCenter - : geometry.Info.NonHorizontalSegmentCountPixelBoundary; + Vector2 translate = new(destinationOffset.X - rootTargetBounds.X, destinationOffset.Y - rootTargetBounds.Y); + lineCount = 0; + tileCrossings = 0; for (int i = 0; i < geometry.Contours.Count; i++) { @@ -2453,21 +4770,24 @@ private static int EncodePath( // The tag index is just the segment index because there is exactly one emitted tag // for each derived segment in the contour. int dataIndex = 0; - PointF firstPoint = geometry.Points[contour.PointStart]; - float firstX = firstPoint.X + pointTranslateX; - float firstY = firstPoint.Y + pointTranslateY; - contourData[dataIndex++] = BitcastSingle(firstX); - contourData[dataIndex++] = BitcastSingle(firstY); + Vector2 current = (Vector2)geometry.Points[contour.PointStart] + translate; + contourData[dataIndex++] = BitcastSingle(current.X); + contourData[dataIndex++] = BitcastSingle(current.Y); for (int j = 0; j < contour.SegmentCount; j++) { int endPointIndex = contour.PointStart + ((j + 1) == contour.PointCount ? 0 : j + 1); - PointF endPoint = geometry.Points[endPointIndex]; - float translatedEndX = endPoint.X + pointTranslateX; - float translatedEndY = endPoint.Y + pointTranslateY; - contourData[dataIndex++] = BitcastSingle(translatedEndX); - contourData[dataIndex++] = BitcastSingle(translatedEndY); + Vector2 end = (Vector2)geometry.Points[endPointIndex] + translate; + contourData[dataIndex++] = BitcastSingle(end.X); + contourData[dataIndex++] = BitcastSingle(end.Y); contourTags[j] = PackPathTag(PathTag.LineToF32); + if (end.Y != current.Y) + { + lineCount++; + } + + tileCrossings += CountLineTileCrossings(current, end); + current = end; } if (contour.SegmentCount > 0) @@ -2491,8 +4811,76 @@ private static int EncodePath( } /// - /// Encodes a stroke centerline into Vello-style path tags and path data in target-local space. + /// Returns the number of 16x16 tiles a single line segment passes through: one starting tile plus the + /// tile column and row boundaries crossed between the two endpoints. + /// + /// The segment start point in pixels. + /// The segment end point in pixels. + /// The number of tiles the segment covers. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long CountLineTileCrossings(Vector2 start, Vector2 end) + { + Vector2 tileSize = new(TileWidth, TileHeight); + Vector2 spanned = Vector2.Abs(Floor(end / tileSize) - Floor(start / tileSize)); + return 1 + (long)Sum(spanned); + } + + /// + /// Returns the component-wise floor of a vector. + /// + /// The vector to floor. + /// The floored vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector2 Floor(Vector2 value) + => Vector128.Floor(value.AsVector128()).AsVector2(); + + /// + /// Returns the sum of a vector's two components. + /// + /// The vector to sum. + /// The sum of the X and Y components. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static float Sum(Vector2 value) + => Vector128.Sum(value.AsVector128()); + + /// + /// Scales a stroke centerline's tile-crossing count up to a conservative bound on the tiles the GPU + /// stroker's emitted geometry covers. The stroker expands each centerline into two offset sides plus + /// joins and caps, and a stroke of the given width spreads each side across roughly one extra tile per + /// 16 pixels, so the centerline count alone under-seeds the segment and path-tile scratch buffers. + /// + /// The summed centerline tile-crossing count. + /// The estimated emitted stroke line count, including joins, caps and arcs. + /// The transform-scaled stroke width in pixels. + /// The conservative emitted-stroke tile-crossing bound. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long ScaleStrokeTileCrossings(long centerlineCrossings, int lineCount, float strokeWidth) + { + long widthTiles = 1 + (long)MathF.Ceiling(MathF.Max(strokeWidth, 0F) / TileWidth); + return (centerlineCrossings + Math.Max(lineCount, 1)) * widthTiles; + } + + /// + /// Encodes a stroke centerline into Vello-style path tags and path data in target-local space, + /// applying the CPU stroker's contour preprocessing so the GPU stroker consumes the same + /// segment list the CPU stroker strokes. /// + /// + /// This ports DefaultRasterizer.StrokeLinearizer.BuildContourSegments (exact-duplicate + /// and 1/64 px micro-segment collapse) and IsContourClosedForEmission (closed-contour + /// normalization) to encode time. The GPU shader then expands clean segments with the ported + /// CPU join and cap geometry; it never sees a degenerate-tangent segment. + /// + /// The flattened centerline geometry to encode. + /// 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 root target bounds used for target-local conversion. + /// The path-tag stream. + /// The path-data stream. + /// Receives the estimated flattened line workload for the stroke. + /// Receives the conservative emitted-stroke tile-crossing bound used to seed the scratch buffers. + /// The number of encoded path objects: 1, or 0 when no contour survived preprocessing. private static int EncodeStrokePath( LinearGeometry geometry, Point destinationOffset, @@ -2501,66 +4889,162 @@ private static int EncodeStrokePath( 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; + Vector2 translate = new(pointTranslateX, pointTranslateY); lineCount = EstimateStrokeLineCount(geometry, pen, widthScale); + tileCrossings = 0; + float strokeWidth = pen.StrokeWidth * widthScale; int encodedContourCount = 0; + IReadOnlyList geometryPoints = geometry.Points; for (int i = 0; i < geometry.Contours.Count; i++) { LinearContour contour = geometry.Contours[i]; - if (TryGetPointStrokeContour(geometry, contour, out PointF point)) + if (contour.PointCount == 0) { - EncodePointStrokeContour( - point, - pointTranslateX, - pointTranslateY, - ref pathTags, - ref pathData); - encodedContourCount++; continue; } - int markerWordCount = contour.IsClosed ? 2 : 4; - Span contourData = pathData.GetAppendSpan(2 + (contour.SegmentCount * 2) + markerWordCount); - Span contourTags = pathTags.GetAppendSpan(contour.SegmentCount + 1); + PointF[] kept = ArrayPool.Shared.Rent(contour.PointCount); + try + { + // CPU stroker preprocessing: collapse exact duplicates and micro-segments below + // 1/64 px forward onto the last kept point. + int keptCount = 0; + PointF firstPoint = geometryPoints[contour.PointStart]; + kept[keptCount++] = firstPoint; + PointF pointLike = firstPoint; + for (int j = 1; j < contour.PointCount; j++) + { + PointF point = geometryPoints[contour.PointStart + j]; + PointF previous = kept[keptCount - 1]; + if (point == previous) + { + continue; + } - int dataIndex = 0; - int tagIndex = 0; - PointF firstPoint = geometry.Points[contour.PointStart]; - PointF firstTangentEndPoint = GetStrokeMarkerTangentPoint(geometry, contour); - contourData[dataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); + if (Vector2.DistanceSquared(previous, point) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) + { + kept[keptCount++] = point; + } - for (int segmentIndex = 0; segmentIndex < contour.SegmentCount; segmentIndex++) - { - int endPointIndex = contour.PointStart + ((segmentIndex + 1) == contour.PointCount ? 0 : segmentIndex + 1); - PointF endPoint = geometry.Points[endPointIndex]; - contourData[dataIndex++] = BitcastSingle(endPoint.X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(endPoint.Y + pointTranslateY); - contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); - } + pointLike = point; + } - if (!contour.IsClosed) - { - contourData[dataIndex++] = BitcastSingle(firstPoint.X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(firstPoint.Y + pointTranslateY); - contourData[dataIndex++] = BitcastSingle(firstTangentEndPoint.X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(firstTangentEndPoint.Y + pointTranslateY); - contourTags[tagIndex] = PackPathTag(PathTag.QuadToF32 | PathTag.SubpathEnd); + int segmentCount = keptCount - 1; + + // CPU stroker closed-contour normalization. + bool isClosed = false; + if (contour.PointCount >= 3) + { + PointF last = geometryPoints[contour.PointStart + contour.PointCount - 1]; + float closeThreshold = MathF.Max(strokeWidth, 1E-3F); + isClosed = contour.IsClosed || + firstPoint == last || + Vector2.DistanceSquared(firstPoint, last) <= closeThreshold * closeThreshold; + } + + bool duplicateClosingPoint = keptCount > 1 && kept[keptCount - 1] == kept[0]; + int distinctPointCount = keptCount - (isClosed && duplicateClosingPoint ? 1 : 0); + + if (segmentCount == 0) + { + EncodePointStrokeContour(pointLike, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + Vector2 pointCenter = (Vector2)pointLike + translate; + Vector2 pointHalf = new(PointStrokeSegmentHalfLength, 0F); + tileCrossings += CountLineTileCrossings(pointCenter - pointHalf, pointCenter + pointHalf); + + encodedContourCount++; + continue; + } + + if (segmentCount == 1 || distinctPointCount == 2) + { + // The CPU stroker emits these as one capped open segment even when declared closed. + EncodeOpenStrokeContour(kept.AsSpan(0, 2), pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + tileCrossings += CountLineTileCrossings((Vector2)kept[0] + translate, (Vector2)kept[1] + translate); + + encodedContourCount++; + continue; + } + + if (isClosed) + { + int emitCount = duplicateClosingPoint ? keptCount - 1 : keptCount; + + // A closing segment exists when the contour ends exactly on its first point or + // the gap back to it is a real (non micro) segment; a sub-1/64 gap is bridged + // by the wrap join, matching the CPU stroker. + bool closingSegment = duplicateClosingPoint || + (segmentCount > 1 && + Vector2.DistanceSquared(kept[keptCount - 1], kept[0]) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon); + + int linetoCount = (emitCount - 1) + (closingSegment ? 1 : 0); + Span contourData = pathData.GetAppendSpan(2 + (linetoCount * 2) + 2); + Span contourTags = pathTags.GetAppendSpan(linetoCount + 1); + + int dataIndex = 0; + int tagIndex = 0; + contourData[dataIndex++] = BitcastSingle(kept[0].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(kept[0].Y + pointTranslateY); + for (int j = 1; j < emitCount; j++) + { + contourData[dataIndex++] = BitcastSingle(kept[j].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(kept[j].Y + pointTranslateY); + contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); + } + + PointF lastEndpoint = kept[emitCount - 1]; + if (closingSegment) + { + contourData[dataIndex++] = BitcastSingle(kept[0].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(kept[0].Y + pointTranslateY); + contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); + lastEndpoint = kept[0]; + } + + // Closed-stroke marker: carries the first segment's tangent and length so the + // wrap join at the closing corner matches the CPU stroker's join arguments. + contourData[dataIndex++] = BitcastSingle(lastEndpoint.X + (kept[1].X - kept[0].X) + pointTranslateX); + contourData[dataIndex] = BitcastSingle(lastEndpoint.Y + (kept[1].Y - kept[0].Y) + pointTranslateY); + contourTags[tagIndex] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); + + pathData.Advance(contourData.Length); + pathTags.Advance(contourTags.Length); + + Vector2 current = (Vector2)kept[0] + translate; + for (int j = 1; j < emitCount; j++) + { + Vector2 next = (Vector2)kept[j] + translate; + tileCrossings += CountLineTileCrossings(current, next); + current = next; + } + + if (closingSegment) + { + tileCrossings += CountLineTileCrossings(current, (Vector2)kept[0] + translate); + } + + encodedContourCount++; + continue; + } + + EncodeOpenStrokeContour(kept.AsSpan(0, keptCount), pointTranslateX, pointTranslateY, ref pathTags, ref pathData); + for (int j = 0; j < keptCount - 1; j++) + { + tileCrossings += CountLineTileCrossings((Vector2)kept[j] + translate, (Vector2)kept[j + 1] + translate); + } + + encodedContourCount++; } - else + finally { - contourData[dataIndex++] = BitcastSingle(firstTangentEndPoint.X + pointTranslateX); - contourData[dataIndex++] = BitcastSingle(firstTangentEndPoint.Y + pointTranslateY); - contourTags[tagIndex] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); + ArrayPool.Shared.Return(kept); } - - pathData.Advance(contourData.Length); - pathTags.Advance(contourTags.Length); - encodedContourCount++; } if (encodedContourCount == 0) @@ -2568,13 +5052,63 @@ private static int EncodeStrokePath( return 0; } + // The loop counted centerline tiles; the GPU stroker emits offset sides, joins and caps, so + // scale the count up to the emitted geometry to avoid under-seeding the segment scratch. + tileCrossings = ScaleStrokeTileCrossings(tileCrossings, lineCount, strokeWidth); pathTags.Add(PackPathTag(PathTag.Path)); return 1; } + /// + /// Encodes one open (capped) stroke centerline contour from preprocessed points. + /// + /// The preprocessed contour points; at least two. + /// The x translation to target-local space. + /// The y translation to target-local space. + /// The path-tag stream. + /// The path-data stream. + private static void EncodeOpenStrokeContour( + scoped ReadOnlySpan points, + float pointTranslateX, + float pointTranslateY, + ref OwnedStream pathTags, + ref OwnedStream pathData) + { + int linetoCount = points.Length - 1; + Span contourData = pathData.GetAppendSpan(2 + (linetoCount * 2) + 4); + Span contourTags = pathTags.GetAppendSpan(linetoCount + 1); + + int dataIndex = 0; + int tagIndex = 0; + contourData[dataIndex++] = BitcastSingle(points[0].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(points[0].Y + pointTranslateY); + for (int j = 1; j < points.Length; j++) + { + contourData[dataIndex++] = BitcastSingle(points[j].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(points[j].Y + pointTranslateY); + contourTags[tagIndex++] = PackPathTag(PathTag.LineToF32); + } + + // Open-stroke cap marker: stores the subpath start point and its outgoing tangent point. + PointF tangentPoint = GetStrokeMarkerTangentPoint(points[0], points[1]); + contourData[dataIndex++] = BitcastSingle(points[0].X + pointTranslateX); + contourData[dataIndex++] = BitcastSingle(points[0].Y + pointTranslateY); + contourData[dataIndex++] = BitcastSingle(tangentPoint.X + pointTranslateX); + contourData[dataIndex] = BitcastSingle(tangentPoint.Y + pointTranslateY); + contourTags[tagIndex] = PackPathTag(PathTag.QuadToF32 | PathTag.SubpathEnd); + + pathData.Advance(contourData.Length); + pathTags.Advance(contourTags.Length); + } + /// /// Encodes one point-like stroke contour as a tiny centered open segment. /// + /// The point the contour collapsed to. + /// The x translation to target-local space. + /// The y translation to target-local space. + /// The path-tag stream. + /// The path-data stream. private static void EncodePointStrokeContour( PointF point, float pointTranslateX, @@ -2605,6 +5139,17 @@ private static void EncodePointStrokeContour( /// /// Encodes one explicit open two-point stroke centerline into Vello-style path tags and path data in target-local space. /// + /// The segment start point with the transform scale applied. + /// The segment end point with the transform scale applied. + /// 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 root target bounds used for target-local conversion. + /// The path-tag stream. + /// The path-data stream. + /// Receives the estimated flattened line workload for the stroke. + /// Receives the conservative emitted-stroke tile-crossing bound used to seed the scratch buffers. + /// The number of encoded path objects; always 1. private static int EncodeOpenSegmentStrokePath( PointF start, PointF end, @@ -2614,93 +5159,290 @@ private static int EncodeOpenSegmentStrokePath( 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; NormalizePointStrokeSegment(ref start, ref end); - PointF firstTangentEndPoint = GetStrokeMarkerTangentPoint(start, end); - - Span contourData = pathData.GetAppendSpan(8); - Span contourTags = pathTags.GetAppendSpan(2); - contourData[0] = BitcastSingle(start.X + pointTranslateX); - contourData[1] = BitcastSingle(start.Y + pointTranslateY); - contourData[2] = BitcastSingle(end.X + pointTranslateX); - contourData[3] = BitcastSingle(end.Y + pointTranslateY); - contourTags[0] = PackPathTag(PathTag.LineToF32); - contourData[4] = BitcastSingle(start.X + pointTranslateX); - contourData[5] = BitcastSingle(start.Y + pointTranslateY); - contourData[6] = BitcastSingle(firstTangentEndPoint.X + pointTranslateX); - contourData[7] = BitcastSingle(firstTangentEndPoint.Y + pointTranslateY); - contourTags[1] = PackPathTag(PathTag.QuadToF32 | PathTag.SubpathEnd); - pathData.Advance(contourData.Length); - pathTags.Advance(contourTags.Length); + Span segmentPoints = [start, end]; + EncodeOpenStrokeContour(segmentPoints, pointTranslateX, pointTranslateY, ref pathTags, ref pathData); pathTags.Add(PackPathTag(PathTag.Path)); lineCount = EstimateStrokeLineCountForOpenSegment(pen, widthScale); + Vector2 translate = new(pointTranslateX, pointTranslateY); + tileCrossings = ScaleStrokeTileCrossings( + CountLineTileCrossings((Vector2)start + translate, (Vector2)end + translate), + lineCount, + pen.StrokeWidth * widthScale); + return 1; } /// - /// Gets whether a contour collapses to the point-stroke path used by the CPU rasterizer. + /// Replaces a point-like explicit stroke segment with the tiny tangent segment used for point strokes. /// - private static bool TryGetPointStrokeContour(LinearGeometry geometry, LinearContour contour, out PointF point) + /// The segment start point; replaced when the segment is point-like. + /// The segment end point; replaced when the segment is point-like. + private static void NormalizePointStrokeSegment(ref PointF start, ref PointF end) { - point = default; - if (contour.PointCount == 0) + if (Vector2.DistanceSquared(start, end) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) { - return false; + return; + } + + PointF center = new((start.X + end.X) * 0.5F, (start.Y + end.Y) * 0.5F); + start = new PointF(center.X - PointStrokeSegmentHalfLength, center.Y); + end = new PointF(center.X + PointStrokeSegmentHalfLength, center.Y); + } + + /// + /// Encodes a rectangle as a fixed-size closed path. + /// + /// The rectangle to encode. + /// The path-tag stream. + /// The path-data stream. + /// Receives the number of non-horizontal line segments encoded. + /// The number of encoded path objects: 1, or 0 when the rectangle is degenerate. + private static int EncodeRectanglePath( + in Rectangle rectangle, + ref OwnedStream pathTags, + ref OwnedStream pathData, + out int lineCount) + => EncodeRectanglePath(new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height), ref pathTags, ref pathData, out lineCount); + + /// + /// Encodes the Vello empty path shape required by begin-clip records with no drawable geometry. + /// + /// The path-tag stream. + /// The path-data stream. + private static void EncodeEmptyBeginClipPath( + ref OwnedStream pathTags, + ref OwnedStream pathData) + { + // Vello reserves a bare Path tag for EndClip dummy paths. Empty BeginClip paths still + // carry one degenerate segment so the path scan has a real path object to associate + // with the BeginClip draw record. + Span data = pathData.GetAppendSpan(4); + Span tags = pathTags.GetAppendSpan(2); + data.Clear(); + tags[0] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); + tags[1] = PackPathTag(PathTag.Path); + + pathData.Advance(data.Length); + pathTags.Advance(tags.Length); + } + + /// + /// Encodes a floating-point rectangle as a fixed-size closed path. + /// + /// The rectangle to encode. + /// The path-tag stream. + /// The path-data stream. + /// Receives the number of non-horizontal line segments encoded. + /// The number of encoded path objects: 1, or 0 when the rectangle is degenerate. + private static int EncodeRectanglePath( + in RectangleF rectangle, + ref OwnedStream pathTags, + ref OwnedStream pathData, + out int lineCount) + { + lineCount = 0; + if (rectangle.Width <= 0 || rectangle.Height <= 0) + { + return 0; + } + + float left = rectangle.Left; + float top = rectangle.Top; + float right = rectangle.Right; + float bottom = rectangle.Bottom; + + Span data = pathData.GetAppendSpan(10); + Span tags = pathTags.GetAppendSpan(5); + data[0] = BitcastSingle(left); + data[1] = BitcastSingle(top); + + data[2] = BitcastSingle(right); + data[3] = BitcastSingle(top); + tags[0] = PackPathTag(PathTag.LineToF32); + + data[4] = BitcastSingle(right); + data[5] = BitcastSingle(bottom); + tags[1] = PackPathTag(PathTag.LineToF32); + + data[6] = BitcastSingle(left); + data[7] = BitcastSingle(bottom); + tags[2] = PackPathTag(PathTag.LineToF32); + + data[8] = BitcastSingle(left); + data[9] = BitcastSingle(top); + tags[3] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); + + tags[4] = PackPathTag(PathTag.Path); + + pathData.Advance(data.Length); + pathTags.Advance(tags.Length); + lineCount = 2; + return 1; + } + + /// + /// Gets the number of rectangle contours represented by a normalized clip descriptor. + /// + /// The clip descriptor to inspect. + /// The non-degenerate rectangle count; 0 for non-rectangular descriptors. + private static int GetRectangleClipCount(in DrawingClipDescriptor descriptor) + => descriptor.Kind switch + { + DrawingClipKind.Rectangle => descriptor.Rectangle.Width > 0 && descriptor.Rectangle.Height > 0 ? 1 : 0, + DrawingClipKind.IntegerRegion => GetPositiveIntegerRectangleCount(descriptor.IntegerRectangles), + DrawingClipKind.Region => GetPositiveRectangleCount(descriptor.Rectangles), + _ => 0 + }; + + /// + /// Encodes a rectangle or rectangle-set clip descriptor as one path with multiple contours. + /// + /// The rectangular clip descriptor to encode. + /// The absolute destination offset applied to every rectangle. + /// The root target bounds used for target-local conversion. + /// The path-tag stream. + /// The path-data stream. + /// Receives the number of non-horizontal line segments encoded. + /// The number of encoded path objects: 1, or 0 when every rectangle is degenerate. + private static int EncodeRectangleClipPath( + in DrawingClipDescriptor descriptor, + Point destinationOffset, + in Rectangle rootTargetBounds, + ref OwnedStream pathTags, + ref OwnedStream pathData, + out int lineCount) + { + float translateX = destinationOffset.X - rootTargetBounds.X; + float translateY = destinationOffset.Y - rootTargetBounds.Y; + lineCount = 0; + + switch (descriptor.Kind) + { + case DrawingClipKind.Rectangle: + { + if (descriptor.Rectangle.Width > 0 && descriptor.Rectangle.Height > 0) + { + AppendRectangleContour(descriptor.Rectangle, translateX, translateY, ref pathTags, ref pathData); + lineCount = 2; + } + + break; + } + + case DrawingClipKind.IntegerRegion: + { + IReadOnlyList rectangles = descriptor.IntegerRectangles; + for (int i = 0; i < rectangles.Count; i++) + { + Rectangle rectangle = rectangles[i]; + if (rectangle.Width <= 0 || rectangle.Height <= 0) + { + continue; + } + + AppendRectangleContour(new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height), translateX, translateY, ref pathTags, ref pathData); + lineCount += 2; + } + + break; + } + + case DrawingClipKind.Region: + { + IReadOnlyList rectangles = descriptor.Rectangles; + for (int i = 0; i < rectangles.Count; i++) + { + RectangleF rectangle = rectangles[i]; + if (rectangle.Width <= 0 || rectangle.Height <= 0) + { + continue; + } + + AppendRectangleContour(rectangle, translateX, translateY, ref pathTags, ref pathData); + lineCount += 2; + } + + break; + } + } + + if (lineCount == 0) + { + return 0; } - point = geometry.Points[contour.PointStart]; - for (int i = 1; i < contour.PointCount; i++) + pathTags.Add(PackPathTag(PathTag.Path)); + return 1; + } + + /// + /// Counts region rectangles that can produce non-degenerate GPU path contours. + /// + /// The region rectangles to count. + /// The number of rectangles with positive width and height. + private static int GetPositiveIntegerRectangleCount(IReadOnlyList rectangles) + { + int count = 0; + for (int i = 0; i < rectangles.Count; i++) { - PointF next = geometry.Points[contour.PointStart + i]; - if (Vector2.DistanceSquared(next, point) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) + Rectangle rectangle = rectangles[i]; + if (rectangle.Width > 0 && rectangle.Height > 0) { - return false; + count++; } } - return true; + return count; } /// - /// Replaces a point-like explicit stroke segment with the tiny tangent segment used for point strokes. + /// Counts region rectangles that can produce non-degenerate GPU path contours. /// - private static void NormalizePointStrokeSegment(ref PointF start, ref PointF end) + /// The region rectangles to count. + /// The number of rectangles with positive width and height. + private static int GetPositiveRectangleCount(IReadOnlyList rectangles) { - if (Vector2.DistanceSquared(start, end) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) + int count = 0; + for (int i = 0; i < rectangles.Count; i++) { - return; + RectangleF rectangle = rectangles[i]; + if (rectangle.Width > 0 && rectangle.Height > 0) + { + count++; + } } - PointF center = new((start.X + end.X) * 0.5F, (start.Y + end.Y) * 0.5F); - start = new PointF(center.X - PointStrokeSegmentHalfLength, center.Y); - end = new PointF(center.X + PointStrokeSegmentHalfLength, center.Y); + return count; } /// - /// Encodes a rectangle as a fixed-size closed path. + /// Appends one rectangle contour to a pending path without terminating the path. /// - private static int EncodeRectanglePath( - in Rectangle rectangle, + /// The rectangle to append. + /// The x translation to target-local space. + /// The y translation to target-local space. + /// The path-tag stream. + /// The path-data stream. + private static void AppendRectangleContour( + RectangleF rectangle, + float translateX, + float translateY, ref OwnedStream pathTags, - ref OwnedStream pathData, - out int lineCount) + ref OwnedStream pathData) { - lineCount = 0; - if (rectangle.Width <= 0 || rectangle.Height <= 0) - { - return 0; - } - - float left = rectangle.Left; - float top = rectangle.Top; - float right = rectangle.Right; - float bottom = rectangle.Bottom; + float left = rectangle.Left + translateX; + float top = rectangle.Top + translateY; + float right = rectangle.Right + translateX; + float bottom = rectangle.Bottom + translateY; Span data = pathData.GetAppendSpan(10); - Span tags = pathTags.GetAppendSpan(5); + Span tags = pathTags.GetAppendSpan(4); data[0] = BitcastSingle(left); data[1] = BitcastSingle(top); @@ -2720,17 +5462,25 @@ private static int EncodeRectanglePath( data[9] = BitcastSingle(top); tags[3] = PackPathTag(PathTag.LineToF32 | PathTag.SubpathEnd); - tags[4] = PackPathTag(PathTag.Path); - pathData.Advance(data.Length); pathTags.Advance(tags.Length); - lineCount = 2; - return 1; } /// /// Reserves the exact stream growth needed for one plain fill item. /// + /// The number of geometry contours. + /// The number of geometry segments. + /// The draw tag selected for the item. + /// Whether a new style record will be emitted. + /// The path-gradient payload word count for the item's brush. + /// The path-tag stream. + /// The path-data stream. + /// The draw-tag stream. + /// The draw-data stream. + /// The style stream. + /// The gradient-pixel stream. + /// The path-gradient payload stream. private static void ReservePlainFillCapacity( int contourCount, int segmentCount, @@ -2760,7 +5510,7 @@ private static void ReservePlainFillCapacity( if (DrawTagUsesGradientRamp(drawTag)) { - gradientPixels.EnsureAdditionalCapacity(GradientWidth); + gradientPixels.EnsureAdditionalCapacity(GradientRowWordCount); } if (pathGradientDataWordCount > 0) @@ -2772,6 +5522,17 @@ private static void ReservePlainFillCapacity( /// /// Reserves the exact stream growth needed for one plain fill item. /// + /// The geometry information supplying contour and segment counts. + /// The draw tag selected for the item. + /// Whether a new style record will be emitted. + /// The path-gradient payload word count for the item's brush. + /// The path-tag stream. + /// The path-data stream. + /// The draw-tag stream. + /// The draw-data stream. + /// The style stream. + /// The gradient-pixel stream. + /// The path-gradient payload stream. private static void ReservePlainFillCapacity( LinearGeometryInfo geometryInfo, uint drawTag, @@ -2801,6 +5562,17 @@ private static void ReservePlainFillCapacity( /// /// Reserves the exact stream growth needed for one stroke item. /// + /// The centerline geometry supplying contour and segment counts. + /// The draw tag selected for the item. + /// Whether a new style record will be emitted. + /// The path-gradient payload word count for the item's brush. + /// The path-tag stream. + /// The path-data stream. + /// The draw-tag stream. + /// The draw-data stream. + /// The style stream. + /// The gradient-pixel stream. + /// The path-gradient payload stream. private static void ReservePlainStrokeCapacity( LinearGeometry geometry, uint drawTag, @@ -2836,7 +5608,7 @@ private static void ReservePlainStrokeCapacity( if (DrawTagUsesGradientRamp(drawTag)) { - gradientPixels.EnsureAdditionalCapacity(GradientWidth); + gradientPixels.EnsureAdditionalCapacity(GradientRowWordCount); } if (pathGradientDataWordCount > 0) @@ -2848,6 +5620,16 @@ private static void ReservePlainStrokeCapacity( /// /// Reserves the exact stream growth needed for one explicit open line segment stroke. /// + /// The draw tag selected for the item. + /// Whether a new style record will be emitted. + /// The path-gradient payload word count for the item's brush. + /// The path-tag stream. + /// The path-data stream. + /// The draw-tag stream. + /// The draw-data stream. + /// The style stream. + /// The gradient-pixel stream. + /// The path-gradient payload stream. private static void ReservePlainStrokeCapacityForOpenSegment( uint drawTag, bool appendStyle, @@ -2872,7 +5654,7 @@ private static void ReservePlainStrokeCapacityForOpenSegment( if (DrawTagUsesGradientRamp(drawTag)) { - gradientPixels.EnsureAdditionalCapacity(GradientWidth); + gradientPixels.EnsureAdditionalCapacity(GradientRowWordCount); } if (pathGradientDataWordCount > 0) @@ -2884,6 +5666,10 @@ private static void ReservePlainStrokeCapacityForOpenSegment( /// /// Estimates the flattened line workload for one stroke. /// + /// The centerline geometry to estimate. + /// The pen that defines stroke width, joins, and caps. + /// The transform-derived scale applied to the stroke width. + /// The conservative line count; at least 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int EstimateStrokeLineCount(LinearGeometry geometry, Pen pen, float widthScale) { @@ -2913,6 +5699,9 @@ private static int EstimateStrokeLineCount(LinearGeometry geometry, Pen pen, flo /// /// Estimates the flattened line workload for one explicit open two-point stroke segment. /// + /// The pen that defines stroke width, joins, and caps. + /// The transform-derived scale applied to the stroke width. + /// The conservative line count; at least 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int EstimateStrokeLineCountForOpenSegment(Pen pen, float widthScale) => Math.Max(2 + (GetStrokeCapLineCost(pen, widthScale) * 2), 1); @@ -2920,6 +5709,9 @@ private static int EstimateStrokeLineCountForOpenSegment(Pen pen, float widthSca /// /// Returns the conservative flattened line cost of one stroke join. /// + /// The pen that defines the join style. + /// The transform-derived scale applied to the stroke width. + /// The line cost of one join. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetStrokeJoinLineCost(Pen pen, float widthScale) { @@ -2938,6 +5730,9 @@ private static int GetStrokeJoinLineCost(Pen pen, float widthScale) /// /// Returns the conservative flattened line cost of one stroke cap. /// + /// The pen that defines the cap style. + /// The transform-derived scale applied to the stroke width. + /// The line cost of one cap. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetStrokeCapLineCost(Pen pen, float widthScale) { @@ -2953,6 +5748,10 @@ private static int GetStrokeCapLineCost(Pen pen, float widthScale) /// /// Returns the conservative flattened line cost of one round arc in the stroke shaders. /// + /// The arc radius in pixels. + /// The swept angle in radians. + /// The pen's arc detail scale. + /// The line cost of the arc; at least 1. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetStrokeArcLineCost(float radius, float angle, double arcDetailScale) { @@ -2963,23 +5762,12 @@ private static int GetStrokeArcLineCost(float radius, float angle, double arcDet return Math.Max(1, (int)MathF.Ceiling(angle / theta)); } - /// - /// Returns the encoded tangent point used by the staged stroker's cap marker segment. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointF GetStrokeMarkerTangentPoint(LinearGeometry geometry, LinearContour contour) - { - PointF firstPoint = geometry.Points[contour.PointStart]; - int nextPointIndex = contour.PointStart + (contour.PointCount > 1 ? 1 : 0); - PointF nextPoint = geometry.Points[nextPointIndex]; - return new PointF( - firstPoint.X + ((nextPoint.X - firstPoint.X) / 3F), - firstPoint.Y + ((nextPoint.Y - firstPoint.Y) / 3F)); - } - /// /// Returns the encoded tangent point used by the staged stroker's cap marker segment for one explicit open line segment. /// + /// The subpath start point. + /// The first segment end point. + /// The tangent point, one third of the way from to . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static PointF GetStrokeMarkerTangentPoint(PointF start, PointF end) => new( @@ -2989,6 +5777,12 @@ private static PointF GetStrokeMarkerTangentPoint(PointF start, PointF end) /// /// Reserves the exact stream growth needed for one begin-layer clip item. /// + /// Whether a new style record will be emitted. + /// The path-tag stream. + /// The path-data stream. + /// The draw-tag stream. + /// The draw-data stream. + /// The style stream. private static void ReserveBeginLayerCapacity( bool appendStyle, ref OwnedStream pathTags, @@ -3004,7 +5798,7 @@ private static void ReserveBeginLayerCapacity( pathTags.EnsureAdditionalCapacity((appendStyle ? 1 : 0) + 5); // Rectangles write five points total: - // bottom-left, bottom-right, top-right, top-left, then bottom-left again. + // top-left, top-right, bottom-right, bottom-left, then top-left again. // Each point is two uint words, so 5 * 2 = 10 words. pathData.EnsureAdditionalCapacity(10); drawTags.EnsureAdditionalCapacity(1); @@ -3021,6 +5815,8 @@ private static void ReserveBeginLayerCapacity( /// /// Reserves the exact stream growth needed for one end-layer item. /// + /// The path-tag stream. + /// The draw-tag stream. private static void ReserveEndLayerCapacity( ref OwnedStream pathTags, ref OwnedStream drawTags) @@ -3035,13 +5831,15 @@ private static void ReserveEndLayerCapacity( /// /// Gets the exact draw-data word count emitted for one draw tag. /// + /// The draw tag to size. + /// The draw-data word count. private static int GetDrawDataWordCount(uint drawTag) => drawTag switch { // Each case matches the exact number of uint words written by AppendDrawData. GpuSceneDrawTag.BeginClip => 2, - GpuSceneDrawTag.FillColor => 1, - GpuSceneDrawTag.FillRecolor => 3, + GpuSceneDrawTag.FillColor => 2, + GpuSceneDrawTag.FillRecolor => 1, GpuSceneDrawTag.FillLinGradient => 5, GpuSceneDrawTag.FillRadGradient => 7, GpuSceneDrawTag.FillEllipticGradient => 7, @@ -3053,16 +5851,31 @@ private static int GetDrawDataWordCount(uint drawTag) }; /// - /// Gets the exact path-gradient payload word count emitted for one brush. + /// Gets the exact auxiliary brush-data word count emitted for one brush. /// + /// The brush to size. + /// The payload word count; 0 for brushes without auxiliary data. private static int GetPathGradientDataWordCount(Brush brush) - => brush is PathGradientBrush pathGradientBrush - ? PathGradientHeaderWordCount + (pathGradientBrush.Points.Length * PathGradientEdgeWordCount) - : 0; + => brush switch + { + RecolorBrush => RecolorDataWordCount, + PathGradientBrush pathGradientBrush => PathGradientHeaderWordCount + (pathGradientBrush.Points.Length * PathGradientEdgeWordCount), + _ => 0 + }; + + /// + /// Gets the exact path-gradient payload word count emitted for one resolved fill source. + /// + /// The resolved command to size. + /// The payload word count; 0 for brushless or non-path-gradient sources. + private static int GetPathGradientDataWordCount(in ResolvedPathCommand command) + => command.Brush is not null ? GetPathGradientDataWordCount(command.Brush) : 0; /// /// Returns a value indicating whether the draw tag emits one gradient ramp row. /// + /// The draw tag to test. + /// when the tag samples a gradient ramp. private static bool DrawTagUsesGradientRamp(uint drawTag) => drawTag is GpuSceneDrawTag.FillLinGradient or GpuSceneDrawTag.FillRadGradient @@ -3072,6 +5885,8 @@ or GpuSceneDrawTag.FillEllipticGradient /// /// Appends one 2x3 affine transform to the packed transform stream. /// + /// The transform to append. + /// The transform stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AppendTransform(GpuSceneTransform transform, ref OwnedStream transforms) { @@ -3089,6 +5904,7 @@ private static void AppendTransform(GpuSceneTransform transform, ref OwnedStream /// /// Appends the identity transform to the packed transform stream. /// + /// The transform stream. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void AppendIdentityTransform(ref OwnedStream transforms) => AppendTransform(GpuSceneTransform.Identity, ref transforms); @@ -3096,8 +5912,35 @@ private static void AppendIdentityTransform(ref OwnedStream transforms) /// /// Packs the style words that describe stroke behavior for one draw record. /// + /// + /// Word layout consumed by flatten.wgsl: 0 = style flags, 1 = device-space stroke width, + /// 2 = packed draw flags, 3 = miter limit, 4 = arc detail scale, 5 = coverage parameter, + /// 6-9 = target-local interest rectangle as left, top, right, bottom. + /// The coverage parameter carries the quantization threshold for aliased strokes and zero + /// for antialiased strokes: fine interprets the antialiased slot as the perceptual coverage + /// boost, which never applies to strokes. + /// + /// The graphics options used for blending. + /// The pen that defines stroke width, joins, caps, and miter limit. + /// The transform-derived scale applied to the stroke width. + /// The target-local raster interest bounds. + /// The ten packed style words. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (uint Style0, uint Style1, uint Style2, uint Style3, uint Style4) GetStrokeStyle(GraphicsOptions options, Pen pen, float widthScale) + private static ( + uint Style0, + uint Style1, + uint Style2, + uint Style3, + uint Style4, + uint Style5, + uint Style6, + uint Style7, + uint Style8, + uint Style9) GetStrokeStyle( + GraphicsOptions options, + Pen pen, + float widthScale, + Rectangle interestBounds) { StyleFlags styleFlags = StyleFlags.Stroke | EncodeStrokeJoinFlags(pen.StrokeOptions.LineJoin) @@ -3108,12 +5951,19 @@ private static (uint Style0, uint Style1, uint Style2, uint Style3, uint Style4) BitcastSingle(pen.StrokeWidth * widthScale), PackStyleDrawFlags(options), BitcastSingle((float)pen.StrokeOptions.MiterLimit), - BitcastSingle((float)pen.StrokeOptions.ArcDetailScale)); + BitcastSingle((float)pen.StrokeOptions.ArcDetailScale), + BitcastSingle(options.Antialias ? 0F : options.AntialiasThreshold), + BitcastSingle(interestBounds.Left), + BitcastSingle(interestBounds.Top), + BitcastSingle(interestBounds.Right), + BitcastSingle(interestBounds.Bottom)); } /// /// Returns the isotropic scale factor embedded in a drawing transform so stroke widths match device-space pixels. /// + /// The transform to inspect. + /// The scale factor to apply to stroke widths. /// /// Uses the square root of the absolute 2D determinant, the SVG-style fallback for non-uniform /// scale. Reduces to the uniform scale for pure scale/rotate/translate matrices. @@ -3130,19 +5980,11 @@ private static float GetTransformWidthScale(Matrix4x4 transform) return MathF.Sqrt(MathF.Abs(det)); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector2 ExtractScale(Matrix4x4 matrix) - => new( - MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)), - MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22))); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Matrix4x4 ComputeResidual(Vector2 scale, Matrix4x4 matrix) - => Matrix4x4.CreateScale(1F / scale.X, 1F / scale.Y, 1F) * matrix; - /// /// Packs the stroke join flags consumed by the staged flatten shader. /// + /// The pen join style. + /// The join style flags. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static StyleFlags EncodeStrokeJoinFlags(LineJoin lineJoin) => lineJoin switch @@ -3157,6 +5999,8 @@ private static StyleFlags EncodeStrokeJoinFlags(LineJoin lineJoin) /// /// Packs the start and end cap flags consumed by the staged flatten shader. /// + /// The pen cap style, applied to both segment ends. + /// The cap style flags. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static StyleFlags EncodeStrokeCapFlags(LineCap lineCap) => lineCap switch @@ -3169,23 +6013,67 @@ private static StyleFlags EncodeStrokeCapFlags(LineCap lineCap) /// /// Packs the style words that describe fill behavior for one draw record. /// + /// + /// Shares the stroke style word layout with the stroke-only words zeroed: 0 = style flags + /// (the fill bit selects even-odd), 2 = packed draw flags, 5 = coverage parameter, + /// 6-9 = target-local interest rectangle as left, top, right, bottom. + /// The coverage parameter is mode-dependent because the two uses are mutually exclusive: + /// aliased fills carry the quantization threshold, antialiased fills carry the perceptual + /// coverage boost applied by fine (zero for plain fills, non-zero only for text). + /// + /// The graphics options used for blending. + /// The fill rule for self-intersecting geometry. + /// The target-local raster interest bounds. + /// The perceptual coverage boost applied to antialiased coverage; zero disables it. + /// The ten packed style words. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static (uint Style0, uint Style1, uint Style2, uint Style3, uint Style4) GetFillStyle(GraphicsOptions options, IntersectionRule intersectionRule) + private static ( + uint Style0, + uint Style1, + uint Style2, + uint Style3, + uint Style4, + uint Style5, + uint Style6, + uint Style7, + uint Style8, + uint Style9) GetFillStyle( + GraphicsOptions options, + IntersectionRule intersectionRule, + Rectangle interestBounds, + float coverageBoost = 0F) { StyleFlags styleFlags = intersectionRule == IntersectionRule.EvenOdd ? StyleFlags.Fill : StyleFlags.None; - return ((uint)styleFlags, 0U, PackStyleDrawFlags(options), 0U, 0U); + float coverageParam = options.Antialias ? coverageBoost : options.AntialiasThreshold; + return ( + (uint)styleFlags, + 0U, + PackStyleDrawFlags(options), + 0U, + 0U, + BitcastSingle(coverageParam), + BitcastSingle(interestBounds.Left), + BitcastSingle(interestBounds.Top), + BitcastSingle(interestBounds.Right), + BitcastSingle(interestBounds.Bottom)); } /// /// Packs the draw-flags word carried through flatten into coarse and fine. /// + /// The graphics options supplying blend mode, blend percentage, and antialiasing. + /// The packed draw-flags word: blend mode in bits 1-13, alpha in bits 14-29, and the aliased bit. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint PackStyleDrawFlags(GraphicsOptions options) - => (PackBlendMode(options) << 1) | (PackBlendAlpha(options.BlendPercentage) << 14); + => (PackBlendMode(options) << 1) + | (PackBlendAlpha(options.BlendPercentage) << 14) + | (options.Antialias ? 0U : GpuSceneDrawTag.FillInfoFlagsAliasedBit); /// /// Packs the blend percentage into the shader draw-flags alpha field. /// + /// The blend percentage in the range [0, 1]. + /// The alpha value quantized to 16 bits. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint PackBlendAlpha(float blendPercentage) => (uint)Math.Clamp((int)MathF.Round(Math.Clamp(blendPercentage, 0F, 1F) * 65535F), 0, 65535); @@ -3193,6 +6081,15 @@ private static uint PackBlendAlpha(float blendPercentage) /// /// Packs the individual scene streams into one final scene-word buffer using the resolved layout. /// + /// The resolved scene layout supplying each stream's base offset. + /// The padded path-tag word count. + /// The path-tag bytes. + /// The path-data words. + /// The draw tags. + /// The draw-data words. + /// The transform words. + /// The style words. + /// The destination scene-word buffer. private static void PackSceneData( GpuSceneLayout layout, int pathTagWordCount, @@ -3221,6 +6118,8 @@ private static void PackSceneData( /// /// Reinterprets one single-precision float as its raw IEEE 754 bit pattern. /// + /// The value to reinterpret. + /// The raw bit pattern. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint BitcastSingle(float value) => unchecked((uint)BitConverter.SingleToInt32Bits(value)); @@ -3228,6 +6127,9 @@ private static uint BitcastSingle(float value) /// /// Rounds up integer division for tile and buffer planning. /// + /// The dividend. + /// The divisor. + /// The quotient rounded toward positive infinity. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int DivideRoundUp(int value, int divisor) => (value + divisor - 1) / divisor; @@ -3235,6 +6137,9 @@ private static int DivideRoundUp(int value, int divisor) /// /// Rounds up to the next multiple of . /// + /// The value to align. + /// The alignment granularity. + /// The aligned value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int AlignUp(int value, int alignment) => value + ((alignment - (value % alignment)) % alignment); @@ -3242,6 +6147,9 @@ private static int AlignUp(int value, int alignment) /// /// Converts an absolute scene rectangle into root-target-local coordinates. /// + /// The absolute rectangle. + /// The root target bounds supplying the local origin. + /// The target-local rectangle. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Rectangle ToTargetLocal(in Rectangle absoluteBounds, in Rectangle rootTargetBounds) => new( @@ -3251,29 +6159,44 @@ private static Rectangle ToTargetLocal(in Rectangle absoluteBounds, in Rectangle absoluteBounds.Height); /// - /// Packs one solid brush color into the staged scene's premultiplied RGBA8 payload format. + /// Appends one solid brush color to the staged scene in associated binary16 form. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint PackSolidColor(SolidBrush solidBrush) - => PackPremultipliedColor(solidBrush.Color); + /// The solid brush to encode. + /// The draw-data stream. + private static void AppendSolidColor(SolidBrush solidBrush, ref OwnedStream drawData) + => AppendColor(solidBrush.Color.ToScaledVector4(PixelAlphaRepresentation.Associated), ref drawData); /// /// Appends the draw-data payload for one encoded draw tag. /// + /// The brush selected by the draw tag. + /// The absolute brush sampling bounds. + /// The graphics options used for blending. + /// The draw tag that selects the payload layout. + /// The root target bounds used for target-local conversion. + /// The draw-data stream. + /// The gradient-pixel stream. + /// The path-gradient payload stream. + /// Receives deferred image patch sites. + /// Receives deferred recolor payload patch sites. + /// The gradient row counter, advanced when a ramp is emitted. private static void AppendDrawData( Brush brush, Rectangle brushBounds, GraphicsOptions graphicsOptions, uint drawTag, - Vector2 scale, - Matrix4x4 transform, Rectangle rootTargetBounds, ref OwnedStream drawData, ref OwnedStream gradientPixels, ref OwnedStream pathGradientData, List images, + List recolors, ref int gradientRowCount) { + Rectangle localBrushBounds = brush is ImageBrush + ? ToTargetLocal(brushBounds, rootTargetBounds) + : brushBounds; + // The draw tag selects the payload layout, so this switch is the single place // where the encoded draw-data stream shape is kept in sync with the sizing logic. switch (drawTag) @@ -3282,28 +6205,28 @@ private static void AppendDrawData( AppendBeginClipData(graphicsOptions, ref drawData); break; case GpuSceneDrawTag.FillColor: - drawData.Add(PackSolidColor((SolidBrush)brush)); + AppendSolidColor((SolidBrush)brush, ref drawData); break; case GpuSceneDrawTag.FillRecolor: - AppendRecolorData((RecolorBrush)brush, ref drawData); + AppendRecolorData((RecolorBrush)brush, ref drawData, ref pathGradientData, recolors); break; case GpuSceneDrawTag.FillLinGradient: - AppendLinearGradientData((LinearGradientBrush)brush, scale, ref drawData, ref gradientPixels, ref gradientRowCount); + AppendLinearGradientData((LinearGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); break; case GpuSceneDrawTag.FillRadGradient: - AppendRadialGradientData((RadialGradientBrush)brush, scale, ref drawData, ref gradientPixels, ref gradientRowCount); + AppendRadialGradientData((RadialGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); break; case GpuSceneDrawTag.FillEllipticGradient: - AppendEllipticGradientData((EllipticGradientBrush)brush, scale, ref drawData, ref gradientPixels, ref gradientRowCount); + AppendEllipticGradientData((EllipticGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); break; case GpuSceneDrawTag.FillSweepGradient: - AppendSweepGradientData((SweepGradientBrush)brush, scale, ref drawData, ref gradientPixels, ref gradientRowCount); + AppendSweepGradientData((SweepGradientBrush)brush, ref drawData, ref gradientPixels, ref gradientRowCount); break; case GpuSceneDrawTag.FillPathGradient: - AppendPathGradientData((PathGradientBrush)brush, transform, rootTargetBounds, ref drawData, ref pathGradientData); + AppendPathGradientData((PathGradientBrush)brush, rootTargetBounds, ref drawData, ref pathGradientData); break; case GpuSceneDrawTag.FillImage: - AppendImageData(brush, brushBounds, ref drawData, images); + AppendImageData(new GpuImageSource(brush), localBrushBounds, ref drawData, images); break; default: throw new UnreachableException($"Unsupported draw tag '{drawTag}' reached scene draw-data encoding."); @@ -3313,15 +6236,52 @@ private static void AppendDrawData( /// /// Appends the draw-data payload for a begin-clip record. /// + /// The graphics options applied when the clip group composites. + /// The draw-data stream. private static void AppendBeginClipData(GraphicsOptions options, ref OwnedStream drawData) { - drawData.Add(PackBlendMode(options)); + // Layers are ISOLATED groups: they seed transparent on the GPU and composite back as a + // unit, matching the CPU backend's clean layer targets. Canvas clips never set this bit; + // they seed with the current content and pop with a coverage lerp (see drawtag.wgsl). + drawData.Add(PackBlendMode(options) | ClipIsolatedMaskBit); drawData.Add(BitcastSingle(Math.Clamp(options.BlendPercentage, 0F, 1F))); } + /// + /// Appends the draw-data payload for an ordinary clip record. + /// + /// The clip set operation; Difference sets the mask-inversion bit. + /// The clip edge mode; hard edges set the hard-mask bit. + /// The coverage threshold used for hard-edge clips. + /// The base clip blend word. + /// The draw-data stream. + private static void AppendClipBeginData( + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold, + uint clipBlendMode, + ref OwnedStream drawData) + { + // Difference is ImageSharp-specific clip state, not a Vello blend mode; encode it + // in a spare bit while ordinary clips keep Vello's exact MIX_CLIP|SRC_OVER marker. + uint blendMode = operation == ClipOperation.Difference + ? clipBlendMode | ClipDifferenceMaskBit + : clipBlendMode; + + if (edgeMode == DrawingClipEdgeMode.Hard) + { + blendMode |= ClipHardMaskBit; + } + + drawData.Add(blendMode); + drawData.Add(BitcastSingle(edgeMode == DrawingClipEdgeMode.Hard ? antialiasThreshold : 1F)); + } + /// /// Packs the color and alpha composition modes into the draw-data layout consumed by the shaders. /// + /// The graphics options supplying the blend and composition modes. + /// The packed blend word: color mix mode in bits 8-15, compose mode in bits 0-7. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint PackBlendMode(GraphicsOptions options) => (MapColorBlendMode(options.ColorBlendingMode) << 8) | MapAlphaCompositionMode(options.AlphaCompositionMode); @@ -3329,6 +6289,8 @@ private static uint PackBlendMode(GraphicsOptions options) /// /// Maps ImageSharp's color blend mode enum to the staged-scene shader contract. /// + /// The color blend mode to map. + /// The MIX_* constant declared in blend.wgsl. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MapColorBlendMode(PixelColorBlendingMode mode) => mode switch @@ -3348,6 +6310,8 @@ private static uint MapColorBlendMode(PixelColorBlendingMode mode) /// /// Maps ImageSharp's alpha composition mode enum to the staged-scene shader contract. /// + /// The alpha composition mode to map. + /// The COMPOSE_* constant declared in blend.wgsl. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MapAlphaCompositionMode(PixelAlphaCompositionMode mode) => mode switch @@ -3370,18 +6334,35 @@ private static uint MapAlphaCompositionMode(PixelAlphaCompositionMode mode) /// /// Appends the recolor brush payload. /// - private static void AppendRecolorData(RecolorBrush brush, ref OwnedStream drawData) + /// The recolor brush. + /// The draw-data stream. + /// The auxiliary brush-data stream. + /// Receives the target-specialized payload patch site. + private static void AppendRecolorData( + RecolorBrush brush, + ref OwnedStream drawData, + ref OwnedStream brushData, + List recolors) { - drawData.Add(PackPremultipliedColor(brush.SourceColor)); - drawData.Add(PackPremultipliedColor(brush.TargetColor)); - drawData.Add(BitcastSingle(brush.Threshold * 4F)); + int dataOffset = brushData.Count; + + // The target pixel type is known only when the retained scene is rendered. Reserve its + // fixed record now and patch the exact CPU Color conversions at the generic resource boundary. + brushData.GetAppendSpan(RecolorDataWordCount).Clear(); + brushData.Advance(RecolorDataWordCount); + drawData.Add(checked((uint)dataOffset)); + recolors.Add(new GpuRecolorDescriptor(brush.SourceColor, brush.TargetColor, brush.Threshold * 4F, dataOffset)); } /// - /// Appends the deferred image payload placeholder and records the patch site. + /// Appends the deferred image payload and records the patch site. /// + /// The image source metadata. + /// The target-local texture sampling bounds. + /// The draw-data stream. + /// Receives the patch site descriptor. private static void AppendImageData( - Brush brush, + in GpuImageSource source, Rectangle brushBounds, ref OwnedStream drawData, List images) @@ -3392,26 +6373,34 @@ private static void AppendImageData( drawData.Add(0); drawData.Add(0); drawData.Add(0); - if (brush is ImageBrush imageBrush) + if (source.Brush is ImageBrush imageBrush) { drawData.Add(BitcastSingle(brushBounds.Left + imageBrush.Offset.X)); drawData.Add(BitcastSingle(brushBounds.Top + imageBrush.Offset.Y)); } + else if (source.IsExternalTexture) + { + drawData.Add(BitcastSingle(brushBounds.Left + source.Offset.X)); + drawData.Add(BitcastSingle(brushBounds.Top + source.Offset.Y)); + } else { drawData.Add(0); drawData.Add(0); } - images.Add(new GpuImageDescriptor(brush, payloadWordOffset)); + images.Add(new GpuImageDescriptor(source, payloadWordOffset)); } /// /// Appends the linear gradient payload and its packed ramp row. /// + /// The linear gradient brush. + /// The draw-data stream. + /// The gradient-pixel stream. + /// The gradient row counter, advanced by one. private static void AppendLinearGradientData( LinearGradientBrush brush, - Vector2 scale, ref OwnedStream drawData, ref OwnedStream gradientPixels, ref int gradientRowCount) @@ -3420,22 +6409,22 @@ private static void AppendLinearGradientData( AppendGradientRamp(brush.ColorStops, ref gradientPixels); gradientRowCount++; - PointF start = new(brush.StartPoint.X * scale.X, brush.StartPoint.Y * scale.Y); - PointF end = new(brush.EndPoint.X * scale.X, brush.EndPoint.Y * scale.Y); - drawData.Add(indexMode); - drawData.Add(BitcastSingle(start.X)); - drawData.Add(BitcastSingle(start.Y)); - drawData.Add(BitcastSingle(end.X)); - drawData.Add(BitcastSingle(end.Y)); + drawData.Add(BitcastSingle(brush.StartPoint.X)); + drawData.Add(BitcastSingle(brush.StartPoint.Y)); + drawData.Add(BitcastSingle(brush.EndPoint.X)); + drawData.Add(BitcastSingle(brush.EndPoint.Y)); } /// /// Appends the radial gradient payload and its packed ramp row. /// + /// The radial gradient brush. + /// The draw-data stream. + /// The gradient-pixel stream. + /// The gradient row counter, advanced by one. private static void AppendRadialGradientData( RadialGradientBrush brush, - Vector2 scale, ref OwnedStream drawData, ref OwnedStream gradientPixels, ref int gradientRowCount) @@ -3464,12 +6453,6 @@ private static void AppendRadialGradientData( radius1 = brush.Radius0; } - float radiusScale = 0.5F * (scale.X + scale.Y); - center0 = new(center0.X * scale.X, center0.Y * scale.Y); - center1 = new(center1.X * scale.X, center1.Y * scale.Y); - radius0 *= radiusScale; - radius1 *= radiusScale; - drawData.Add(indexMode); drawData.Add(BitcastSingle(center0.X)); drawData.Add(BitcastSingle(center0.Y)); @@ -3482,9 +6465,12 @@ private static void AppendRadialGradientData( /// /// Appends the elliptic gradient payload and its packed ramp row. /// + /// The elliptic gradient brush. + /// The draw-data stream. + /// The gradient-pixel stream. + /// The gradient row counter, advanced by one. private static void AppendEllipticGradientData( EllipticGradientBrush brush, - Vector2 scale, ref OwnedStream drawData, ref OwnedStream gradientPixels, ref int gradientRowCount) @@ -3493,36 +6479,33 @@ private static void AppendEllipticGradientData( AppendGradientRamp(brush.ColorStops, ref gradientPixels); gradientRowCount++; - // The perpendicular axis of the ellipse must be constructed in local space where it is - // geometrically orthogonal to the main axis; componentwise scale-bake does not commute with - // rotation, so reconstructing it from the scale-baked main axis in the shader goes wrong - // under non-uniform scale. Bake all three points instead and let the shader derive the - // device-space axis ratio from the transformed distances. + // Brushes reach the encoder after batcher normalization, so gradient payload points are + // already in render coordinates. The shader still expects the ellipse as three points, + // so reconstruct the secondary-axis endpoint from the stored center, axis, and ratio. PointF localCenter = brush.Center; PointF localAxisEnd = brush.ReferenceAxisEnd; Vector2 localAxis = new(localAxisEnd.X - localCenter.X, localAxisEnd.Y - localCenter.Y); Vector2 localPerpendicular = new Vector2(-localAxis.Y, localAxis.X) * brush.AxisRatio; PointF localSecondEnd = new(localCenter.X + localPerpendicular.X, localCenter.Y + localPerpendicular.Y); - PointF center = new(localCenter.X * scale.X, localCenter.Y * scale.Y); - PointF axisEnd = new(localAxisEnd.X * scale.X, localAxisEnd.Y * scale.Y); - PointF secondEnd = new(localSecondEnd.X * scale.X, localSecondEnd.Y * scale.Y); - drawData.Add(indexMode); - drawData.Add(BitcastSingle(center.X)); - drawData.Add(BitcastSingle(center.Y)); - drawData.Add(BitcastSingle(axisEnd.X)); - drawData.Add(BitcastSingle(axisEnd.Y)); - drawData.Add(BitcastSingle(secondEnd.X)); - drawData.Add(BitcastSingle(secondEnd.Y)); + drawData.Add(BitcastSingle(localCenter.X)); + drawData.Add(BitcastSingle(localCenter.Y)); + drawData.Add(BitcastSingle(localAxisEnd.X)); + drawData.Add(BitcastSingle(localAxisEnd.Y)); + drawData.Add(BitcastSingle(localSecondEnd.X)); + drawData.Add(BitcastSingle(localSecondEnd.Y)); } /// /// Appends the sweep gradient payload and its packed ramp row. /// + /// The sweep gradient brush. + /// The draw-data stream. + /// The gradient-pixel stream. + /// The gradient row counter, advanced by one. private static void AppendSweepGradientData( SweepGradientBrush brush, - Vector2 scale, ref OwnedStream drawData, ref OwnedStream gradientPixels, ref int gradientRowCount) @@ -3531,18 +6514,21 @@ private static void AppendSweepGradientData( AppendGradientRamp(brush.ColorStops, ref gradientPixels); gradientRowCount++; - float t0 = brush.StartAngleDegrees / 360F; - float t1 = brush.EndAngleDegrees / 360F; - if (MathF.Abs(t1 - t0) < 1e-6F) + float sweepDegrees = brush.EndAngleDegrees - brush.StartAngleDegrees; + if (MathF.Abs(sweepDegrees) < 1e-6F) { - t1 = t0 + 1F; + // Match the CPU brush contract: only endpoints equal within the degree-space + // epsilon denote a full turn. Applying this epsilon after converting to turns + // would incorrectly expand small, non-zero sweeps. + sweepDegrees = 360F; } - PointF center = new(brush.Center.X * scale.X, brush.Center.Y * scale.Y); + float t0 = brush.StartAngleDegrees / 360F; + float t1 = t0 + (sweepDegrees / 360F); drawData.Add(indexMode); - drawData.Add(BitcastSingle(center.X)); - drawData.Add(BitcastSingle(center.Y)); + drawData.Add(BitcastSingle(brush.Center.X)); + drawData.Add(BitcastSingle(brush.Center.Y)); drawData.Add(BitcastSingle(t0)); drawData.Add(BitcastSingle(t1)); } @@ -3550,9 +6536,12 @@ private static void AppendSweepGradientData( /// /// Appends the path-gradient payload in target-local coordinates. /// + /// The path gradient brush. + /// The root target bounds used for target-local conversion. + /// The draw-data stream. + /// The path-gradient payload stream. private static void AppendPathGradientData( PathGradientBrush brush, - Matrix4x4 transform, Rectangle rootTargetBounds, ref OwnedStream drawData, ref OwnedStream pathGradientData) @@ -3564,7 +6553,8 @@ private static void AppendPathGradientData( for (int i = 0; i < edgeCount; i++) { - PointF point = TransformPathGradientPoint(points[i], transform, rootTargetBounds); + PointF sourcePoint = points[i]; + PointF point = new(sourcePoint.X - rootTargetBounds.X, sourcePoint.Y - rootTargetBounds.Y); center = new PointF(center.X + point.X, center.Y + point.Y); } @@ -3573,7 +6563,8 @@ private static void AppendPathGradientData( float maxDistance = 0F; for (int i = 0; i < edgeCount; i++) { - PointF point = TransformPathGradientPoint(points[i], transform, rootTargetBounds); + PointF sourcePoint = points[i]; + PointF point = new(sourcePoint.X - rootTargetBounds.X, sourcePoint.Y - rootTargetBounds.Y); maxDistance = MathF.Max(maxDistance, Vector2.Distance(point, center)); } @@ -3582,20 +6573,22 @@ private static void AppendPathGradientData( payload[0] = BitcastSingle(center.X); payload[1] = BitcastSingle(center.Y); payload[2] = BitcastSingle(maxDistance); - payload[3] = PackUnpremultipliedColor(brush.CenterColor); + WriteAssociatedColor(payload, 3, brush.CenterColor); for (int i = 0; i < edgeCount; i++) { int payloadOffset = PathGradientHeaderWordCount + (i * PathGradientEdgeWordCount); - PointF start = TransformPathGradientPoint(points[i], transform, rootTargetBounds); - PointF end = TransformPathGradientPoint(points[(i + 1) % edgeCount], transform, rootTargetBounds); + PointF sourceStart = points[i]; + PointF sourceEnd = points[(i + 1) % edgeCount]; + PointF start = new(sourceStart.X - rootTargetBounds.X, sourceStart.Y - rootTargetBounds.Y); + PointF end = new(sourceEnd.X - rootTargetBounds.X, sourceEnd.Y - rootTargetBounds.Y); payload[payloadOffset] = BitcastSingle(start.X); payload[payloadOffset + 1] = BitcastSingle(start.Y); payload[payloadOffset + 2] = BitcastSingle(end.X); payload[payloadOffset + 3] = BitcastSingle(end.Y); - payload[payloadOffset + 4] = PackUnpremultipliedColor(colors[i % colors.Length]); - payload[payloadOffset + 5] = PackUnpremultipliedColor(colors[(i + 1) % colors.Length]); + WriteAssociatedColor(payload, payloadOffset + 4, colors[i % colors.Length]); + WriteAssociatedColor(payload, payloadOffset + 8, colors[(i + 1) % colors.Length]); } pathGradientData.Advance(payload.Length); @@ -3605,37 +6598,27 @@ private static void AppendPathGradientData( drawData.Add(0U); } - /// - /// Transforms one path-gradient point into the target-local coordinates consumed by the fine pass. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointF TransformPathGradientPoint( - PointF point, - Matrix4x4 transform, - Rectangle rootTargetBounds) - { - PointF transformed = transform.IsIdentity ? point : PointF.Transform(point, transform); - return new PointF( - transformed.X - rootTargetBounds.X, - transformed.Y - rootTargetBounds.Y); - } - /// /// Appends one packed gradient ramp row sampled across the fixed gradient width. /// + /// The gradient color stops, ordered by ratio. + /// The gradient-pixel stream. private static void AppendGradientRamp(ReadOnlySpan colorStops, ref OwnedStream gradientPixels) { for (int x = 0; x < GradientWidth; x++) { float t = x / (float)(GradientWidth - 1); - gradientPixels.Add(EvaluateGradientColor(colorStops, t)); + AppendColor(EvaluateGradientColor(colorStops, t), ref gradientPixels); } } /// - /// Evaluates the packed premultiplied gradient color at the normalized parameter. + /// Evaluates the premultiplied gradient color at the normalized parameter. /// - private static uint EvaluateGradientColor(ReadOnlySpan colorStops, float t) + /// The gradient color stops, ordered by ratio. + /// The normalized sample position in the range [0, 1]. + /// The associated floating-point sample. + private static Vector4 EvaluateGradientColor(ReadOnlySpan colorStops, float t) { // Walk the stop list once to find the enclosing interval, then interpolate within it. ColorStop from = colorStops[0]; @@ -3653,93 +6636,69 @@ private static uint EvaluateGradientColor(ReadOnlySpan colorStops, fl if (from.Color.Equals(to.Color) || to.Ratio == from.Ratio) { - return PackPremultipliedColor(from.Color); + return from.Color.ToScaledVector4(PixelAlphaRepresentation.Associated); } float localT = (t - from.Ratio) / (to.Ratio - from.Ratio); - Vector4 color = Vector4.Lerp(from.Color.ToScaledVector4(), to.Color.ToScaledVector4(), localT); - return PackPremultipliedColor(Color.FromScaledVector(color)); - } - /// - /// Packs one premultiplied color into the staged scene's RGBA8 payload format. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint PackPremultipliedColor(in Color color) - { - // The staged scene keeps colors as one packed RGBA8 word. That gives up precision versus - // carrying float channels through the scene buffer, but it keeps the encoded payload small - // so the CPU writes less per draw and the shader reads less per sample. - Vector4 vector = color.ToScaledVector4(); - Premultiply(ref vector); - return Rgba32.FromScaledVector4(vector).Rgba; + // CSS Color 4 requires alpha premultiplication before interpolation. The ramp stores + // associated RGBA16Float directly, matching the fine shader's internal composition space. + return Vector4.Lerp( + from.Color.ToScaledVector4(PixelAlphaRepresentation.Associated), + to.Color.ToScaledVector4(PixelAlphaRepresentation.Associated), + localT); } /// - /// Packs one straight-alpha color into RGBA8 payload format. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static uint PackUnpremultipliedColor(in Color color) - => color.ToPixel().Rgba; - - /// - /// Premultiplies the RGB channels of a normalized RGBA vector by its alpha channel. + /// Appends one RGBA vector as two binary16 pairs. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void Premultiply(ref Vector4 source) + /// The vector to append. + /// The stream receiving two words. + private static void AppendColor(Vector4 color, ref OwnedStream destination) { - // Load into a local variable to prevent accessing the source from memory multiple times. - Vector4 src = source; - Vector4 alpha = PermuteW(src); - source = WithW(src * alpha, alpha); + destination.Add(PackHalf2(color.X, color.Y)); + destination.Add(PackHalf2(color.Z, color.W)); } /// - /// Broadcasts the W component of into every lane. + /// Packs two floating-point components into one binary16 pair matching WGSL's unpack2x16float. /// + /// The low component. + /// The high component. + /// The packed pair. [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector4 PermuteW(Vector4 value) - { - if (Sse.IsSupported) - { - return Sse.Shuffle(value.AsVector128(), value.AsVector128(), 0b_11_11_11_11).AsVector4(); - } - - return new Vector4(value.W); - } + private static uint PackHalf2(float x, float y) + => BitConverter.HalfToUInt16Bits((Half)x) | ((uint)BitConverter.HalfToUInt16Bits((Half)y) << 16); /// - /// Replaces the W component of with the supplied broadcast vector. + /// Writes one associated color to the path-gradient payload without reducing it to RGBA8 first. /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector4 WithW(Vector4 value, Vector4 w) + /// The payload receiving four consecutive floating-point words. + /// The destination word offset. + /// The color to convert to associated components. + private static void WriteAssociatedColor(Span destination, int offset, in Color color) { - if (Sse41.IsSupported) - { - return Sse41.Insert(value.AsVector128(), w.AsVector128(), 0b11_11_0000).AsVector4(); - } - - if (Sse.IsSupported) - { - // Create tmp as - // Then return (which is ) - Vector128 tmp = Sse.Shuffle(w.AsVector128(), value.AsVector128(), 0b00_10_00_11); - return Sse.Shuffle(value.AsVector128(), tmp, 0b00_10_01_00).AsVector4(); - } - - value.W = w.W; - return value; + // Path gradients interpolate endpoint colors directly in the shader. Keeping the source + // precision here matches the CPU renderer and defers quantization until target encoding. + Vector4 vector = color.ToScaledVector4(PixelAlphaRepresentation.Associated); + destination[offset] = BitcastSingle(vector.X); + destination[offset + 1] = BitcastSingle(vector.Y); + destination[offset + 2] = BitcastSingle(vector.Z); + destination[offset + 3] = BitcastSingle(vector.W); } /// /// Maps ImageSharp's gradient repetition mode to the staged-scene shader contract. /// + /// The gradient repetition mode to map. + /// The extend-mode value stored in the low two bits of the gradient index word. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint MapExtendMode(GradientRepetitionMode repetitionMode) => repetitionMode switch { GradientRepetitionMode.Repeat => 1U, GradientRepetitionMode.Reflect => 2U, + GradientRepetitionMode.DontFill => 3U, _ => 0U }; } @@ -3761,6 +6720,7 @@ internal sealed class WebGPUEncodedScene : IDisposable null, 0, [], + [], 0, default, 0, @@ -3779,17 +6739,53 @@ internal sealed class WebGPUEncodedScene : IDisposable 0, 0, RasterizationMode.Antialiased, - 0F); + 0F, + 0L, + 0L, + []); private readonly IMemoryOwner? sceneDataOwner; private readonly IMemoryOwner? gradientPixelsOwner; private readonly IMemoryOwner? pathGradientDataOwner; private readonly List images; + private readonly List recolors; + private readonly WebGPUSceneOperation[] operations; private bool disposed; /// /// Initializes a new instance of the class. /// + /// The encoded scene target size. + /// The number of info words. + /// The owner for packed scene-data words. + /// The number of scene-data words. + /// The owner for packed gradient pixels. + /// The owner for path-gradient data words. + /// The number of path-gradient data words. + /// The image descriptors referenced by draw data. + /// The recolor descriptors referenced by auxiliary brush data. + /// The number of gradient texture rows. + /// The packed GPU scene layout. + /// The number of fill draw records. + /// The number of encoded paths. + /// The number of encoded line segments. + /// The number of path-tag bytes. + /// The number of path-tag words. + /// The number of path-data words. + /// The number of draw tags. + /// The number of draw-data words. + /// The number of transform words. + /// The number of style words. + /// The number of clip records. + /// The number of unique draw definitions. + /// The total scheduled path-row count. + /// The horizontal tile count. + /// The vertical tile count. + /// The fine-pass rasterization mode. + /// The scene-wide aliased coverage threshold. + /// The CPU-side upper bound for tile-boundary crossings. + /// The CPU-side upper bound for per-(draw, bin) binning records. + /// The scene operations associated with the encoded payload. public WebGPUEncodedScene( Size targetSize, int infoWordCount, @@ -3799,6 +6795,7 @@ public WebGPUEncodedScene( IMemoryOwner? pathGradientDataOwner, int pathGradientDataWordCount, List images, + List recolors, int gradientRowCount, GpuSceneLayout layout, int fillCount, @@ -3817,7 +6814,10 @@ public WebGPUEncodedScene( int tileCountX, int tileCountY, RasterizationMode fineRasterizationMode, - float fineCoverageThreshold) + float fineCoverageThreshold, + long estimatedTileCrossings, + long estimatedBinFootprint, + WebGPUSceneOperation[] operations) { this.TargetSize = targetSize; this.InfoWordCount = infoWordCount; @@ -3826,6 +6826,8 @@ public WebGPUEncodedScene( this.pathGradientDataOwner = pathGradientDataOwner; this.PathGradientDataWordCount = pathGradientDataWordCount; this.images = images; + this.recolors = recolors; + this.operations = operations; this.SceneWordCount = sceneWordCount; this.GradientRowCount = gradientRowCount; this.Layout = layout; @@ -3846,8 +6848,98 @@ public WebGPUEncodedScene( this.TileCountY = tileCountY; this.FineRasterizationMode = fineRasterizationMode; this.FineCoverageThreshold = fineCoverageThreshold; + this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedBinFootprint = estimatedBinFootprint; + + int targetCount = operations.Length == 0 ? 0 : 1; + int applyCount = 0; + int shaderEffectCount = 0; + int maximumShaderEffectWidth = 0; + int maximumShaderEffectHeight = 0; + int maximumShaderUniformByteLength = 0; + int maxApplyGroupCount = 0; + int pendingRangeCount = 0; + int maxPendingRangeCount = 0; + int finalStatusCapacity = 0; + int maxStatusCapacity = 0; + + for (int i = 0; i < operations.Length; i++) + { + WebGPUSceneOperation operation = operations[i]; + switch (operation.Kind) + { + case WebGPUSceneOperationKind.RenderRange: + case WebGPUSceneOperationKind.EndLayer: + pendingRangeCount++; + finalStatusCapacity = checked(finalStatusCapacity + operation.Range.MaximumStatusRecordCount); + break; + + case WebGPUSceneOperationKind.BeginLayer: + targetCount = Math.Max(targetCount, operation.LayerTargetId + 1); + break; + + case WebGPUSceneOperationKind.Apply when operation.ApplyGroupCount != 0: + applyCount += operation.ApplyGroupCount; + maxApplyGroupCount = Math.Max(maxApplyGroupCount, operation.ApplyGroupCount); + maxPendingRangeCount = Math.Max(maxPendingRangeCount, pendingRangeCount); + maxStatusCapacity = Math.Max(maxStatusCapacity, operation.PendingStatusCapacity); + + pendingRangeCount = operation.ApplyGroupCount; + finalStatusCapacity = 0; + for (int applyIndex = i; applyIndex < i + operation.ApplyGroupCount; applyIndex++) + { + finalStatusCapacity = checked( + finalStatusCapacity + operations[applyIndex].Apply!.DrawRange.MaximumStatusRecordCount); + } + + i += operation.ApplyGroupCount - 1; + break; + + case WebGPUSceneOperationKind.ShaderEffect: + shaderEffectCount++; + WebGPUShaderEffectSceneItem shaderEffect = operation.ShaderEffect!; + maximumShaderEffectWidth = Math.Max(maximumShaderEffectWidth, shaderEffect.InputRect.Width); + maximumShaderEffectHeight = Math.Max(maximumShaderEffectHeight, shaderEffect.InputRect.Height); + + ReadOnlySpan shaderPasses = shaderEffect.Effect.GetShaderPasses(); + for (int passIndex = 0; passIndex < shaderPasses.Length; passIndex++) + { + maximumShaderUniformByteLength = Math.Max( + maximumShaderUniformByteLength, + shaderPasses[passIndex].Program.UniformLayout.ByteLength); + } + + maxPendingRangeCount = Math.Max(maxPendingRangeCount, pendingRangeCount); + maxStatusCapacity = Math.Max(maxStatusCapacity, finalStatusCapacity); + pendingRangeCount = 1; + finalStatusCapacity = operation.ShaderEffect!.DrawRange.MaximumStatusRecordCount; + break; + } + } + + this.OrderedTargetCount = targetCount; + this.OrderedApplyCount = applyCount; + this.OrderedShaderEffectCount = shaderEffectCount; + this.MaximumShaderEffectWidth = maximumShaderEffectWidth; + this.MaximumShaderEffectHeight = maximumShaderEffectHeight; + this.MaximumShaderUniformByteLength = maximumShaderUniformByteLength; + this.MaxApplyGroupCount = maxApplyGroupCount; + this.MaxPendingRangeCount = Math.Max(maxPendingRangeCount, pendingRangeCount); + this.FinalStatusCapacity = finalStatusCapacity; + this.MaxStatusCapacity = Math.Max(maxStatusCapacity, finalStatusCapacity); } + /// + /// Gets the CPU-side upper-bound estimate of tile-boundary crossings produced by the scene's + /// flattened lines. Bounds the GPU segment, segment-count, and path-tile demand. + /// + public long EstimatedTileCrossings { get; } + + /// + /// Gets the CPU-side upper-bound estimate of per-(draw, bin) binning records. + /// + public long EstimatedBinFootprint { get; } + /// /// Gets the target size for the encoded scene. /// @@ -3860,10 +6952,11 @@ public ReadOnlyMemory SceneData => this.sceneDataOwner is null ? ReadOnlyMemory.Empty : this.sceneDataOwner.Memory[..this.SceneWordCount]; /// - /// Gets the packed gradient-ramp pixels. + /// Gets the packed gradient-ramp pixels. Rows are + /// pixels wide, matching the encoder's fixed gradient ramp width. /// public ReadOnlyMemory GradientPixels - => this.gradientPixelsOwner is null ? ReadOnlyMemory.Empty : this.gradientPixelsOwner.Memory[..(this.GradientRowCount * 512)]; + => this.gradientPixelsOwner is null ? ReadOnlyMemory.Empty : this.gradientPixelsOwner.Memory[..(this.GradientRowCount * WebGPUSceneEncoder.GradientRowWordCount)]; /// /// Gets the encoded path-gradient payload stream. @@ -3886,6 +6979,71 @@ public ReadOnlyMemory PathGradientData /// public IReadOnlyList Images => this.images; + /// + /// Gets the deferred recolor descriptors that must be specialized for the target pixel type. + /// + public IReadOnlyList Recolors => this.recolors; + + /// + /// Gets the ordered retained operations for a scene containing Apply. + /// + public ReadOnlySpan OrderedOperations => this.operations; + + /// + /// Gets a value indicating whether this scene renders through ordered operations. + /// + public bool HasOperations => this.operations.Length != 0; + + /// + /// Gets the number of stable root and scoped-layer target identities in the ordered plan. + /// + public int OrderedTargetCount { get; } + + /// + /// Gets the number of Apply operations retained by the ordered plan. + /// + public int OrderedApplyCount { get; } + + /// + /// Gets the number of native shader-effect operations retained by the ordered plan. + /// + public int OrderedShaderEffectCount { get; } + + /// + /// Gets the largest native effect input width in this scene. + /// + public int MaximumShaderEffectWidth { get; } + + /// + /// Gets the largest native effect input height in this scene. + /// + public int MaximumShaderEffectHeight { get; } + + /// + /// Gets the largest user-uniform binding required by a native effect pass in this scene. + /// + public int MaximumShaderUniformByteLength { get; } + + /// + /// Gets the largest dependency-independent Apply group retained by the ordered plan. + /// + public int MaxApplyGroupCount { get; } + + /// + /// Gets the largest number of range submissions retained by one unvalidated transaction. + /// + public int MaxPendingRangeCount { get; } + + /// + /// Gets the maximum scheduling-status record count required by any ordered barrier. + /// + public int MaxStatusCapacity { get; } + + /// + /// Gets the maximum scheduling-status record count awaiting validation at end of flush. + /// + public int FinalStatusCapacity { get; } + /// /// Gets the number of fill records in the encoded scene. /// @@ -3996,6 +7154,22 @@ public ReadOnlyMemory PathGradientData /// public GpuSceneLayout Layout { get; } + /// + /// Gets the version of the packed scene stream. increments it for + /// every effective mutation so resident GPU copies of the stream can detect staleness. + /// + public int SceneDataVersion { get; private set; } + + /// + /// Gets the inclusive first scene-word index mutated after encoding, or when none. + /// + public int MutatedSceneWordMin { get; private set; } = int.MaxValue; + + /// + /// Gets the exclusive end of the scene-word span mutated after encoding, or zero when none. + /// + public int MutatedSceneWordEnd { get; private set; } + /// /// Overwrites one scene word in place. /// @@ -4008,7 +7182,18 @@ public void SetSceneWord(int index, uint value) throw new InvalidOperationException("The scene buffer is not available."); } - this.sceneDataOwner.Memory.Span[index] = value; + // Identical rewrites (for example overflow replay re-staging the same range) must not + // advance the version, or resident GPU streams would be patched needlessly every attempt. + Span sceneWords = this.sceneDataOwner.Memory.Span; + if (sceneWords[index] == value) + { + return; + } + + sceneWords[index] = value; + this.SceneDataVersion++; + this.MutatedSceneWordMin = Math.Min(this.MutatedSceneWordMin, index); + this.MutatedSceneWordEnd = Math.Max(this.MutatedSceneWordEnd, index + 1); } /// @@ -4028,6 +7213,107 @@ public void Dispose() } } +/// +/// Describes the source metadata for one encoded image fill. +/// +internal readonly struct GpuImageSource +{ + /// + /// Initializes a new instance of the struct from a CPU-backed image or pattern brush. + /// + /// The image or pattern brush. + public GpuImageSource(Brush brush) + { + this.Brush = brush; + this.Size = default; + this.Offset = default; + this.WrapX = default; + this.WrapY = default; + this.Descriptor = default; + this.HasExplicitDescriptor = false; + this.IsExternalTexture = false; + } + + /// + /// Initializes a new instance of the struct from a render-time WebGPU texture. + /// + /// The render-time texture size. + /// The image offset encoded into the fill payload. + /// The horizontal wrap mode. + /// The vertical wrap mode. + public GpuImageSource(Size size, Point offset, WrapMode wrapX, WrapMode wrapY) + { + this.Brush = null; + this.Size = size; + this.Offset = offset; + this.WrapX = wrapX; + this.WrapY = wrapY; + this.Descriptor = default; + this.HasExplicitDescriptor = false; + this.IsExternalTexture = true; + } + + /// + /// Initializes a new instance of the struct from a render-time WebGPU texture whose representation differs from the target. + /// + /// The render-time texture size. + /// The image offset encoded into the fill payload. + /// The horizontal wrap mode. + /// The vertical wrap mode. + /// The physical and logical representation of the supplied texture. + public GpuImageSource(Size size, Point offset, WrapMode wrapX, WrapMode wrapY, WebGPUTargetDescriptor descriptor) + { + this.Brush = null; + this.Size = size; + this.Offset = offset; + this.WrapX = wrapX; + this.WrapY = wrapY; + this.Descriptor = descriptor; + this.HasExplicitDescriptor = true; + this.IsExternalTexture = true; + } + + /// + /// Gets the CPU-backed image or pattern brush when this source is not an external texture. + /// + public Brush? Brush { get; } + + /// + /// Gets the render-time texture size. + /// + public Size Size { get; } + + /// + /// Gets the image offset encoded into the fill payload. + /// + public Point Offset { get; } + + /// + /// Gets the horizontal wrap mode. + /// + public WrapMode WrapX { get; } + + /// + /// Gets the vertical wrap mode. + /// + public WrapMode WrapY { get; } + + /// + /// Gets the physical and logical representation of an external texture that differs from the render target. + /// + public WebGPUTargetDescriptor Descriptor { get; } + + /// + /// Gets a value indicating whether replaces the render target descriptor for this source. + /// + public bool HasExplicitDescriptor { get; } + + /// + /// Gets a value indicating whether the source texture is supplied by the WebGPU render path. + /// + public bool IsExternalTexture { get; } +} + /// /// Describes one deferred image payload patch site in the encoded draw-data stream. /// @@ -4036,16 +7322,18 @@ internal readonly struct GpuImageDescriptor /// /// Initializes a new instance of the struct. /// - public GpuImageDescriptor(Brush brush, int drawDataWordOffset) + /// The image source metadata. + /// The draw-data word offset to patch once the source texture is known. + public GpuImageDescriptor(in GpuImageSource source, int drawDataWordOffset) { - this.Brush = brush; + this.Source = source; this.DrawDataWordOffset = drawDataWordOffset; } /// - /// Gets the image-producing brush instance. + /// Gets the image source metadata. /// - public Brush Brush { get; } + public GpuImageSource Source { get; } /// /// Gets the draw-data word offset to patch once the image atlas is built. @@ -4053,6 +7341,47 @@ public GpuImageDescriptor(Brush brush, int drawDataWordOffset) public int DrawDataWordOffset { get; } } +/// +/// Describes one RecolorBrush payload that must be converted for the render target pixel type. +/// +internal readonly struct GpuRecolorDescriptor +{ + /// + /// Initializes a new instance of the struct. + /// + /// The full-precision match key. + /// The replacement color. + /// The squared-distance threshold. + /// The auxiliary brush-data word offset to patch. + public GpuRecolorDescriptor(Color sourceColor, Color targetColor, float threshold, int brushDataWordOffset) + { + this.SourceColor = sourceColor; + this.TargetColor = targetColor; + this.Threshold = threshold; + this.BrushDataWordOffset = brushDataWordOffset; + } + + /// + /// Gets the full-precision match key. + /// + public Color SourceColor { get; } + + /// + /// Gets the replacement color. + /// + public Color TargetColor { get; } + + /// + /// Gets the squared-distance threshold. + /// + public float Threshold { get; } + + /// + /// Gets the auxiliary brush-data word offset to patch for the render target. + /// + public int BrushDataWordOffset { get; } +} + /// /// Represents one 2D affine transform encoded in the GPU scene format. /// @@ -4066,6 +7395,15 @@ public GpuImageDescriptor(Brush brush, int drawDataWordOffset) /// /// Initializes a new instance of the struct. /// + /// The row 1, column 1 matrix value. + /// The row 1, column 2 matrix value. + /// The row 2, column 1 matrix value. + /// The row 2, column 2 matrix value. + /// The x translation value. + /// The y translation value. + /// The row 1, column 4 perspective value. + /// The row 2, column 4 perspective value. + /// The row 4, column 4 homogeneous scale value. public GpuSceneTransform(float m11, float m12, float m21, float m22, float tx, float ty, float m14, float m24, float m44) { this.M11 = m11; @@ -4083,19 +7421,39 @@ public GpuSceneTransform(float m11, float m12, float m21, float m22, float tx, f /// Creates a from a . /// Extracts the 9 elements needed for projective 2D transformation with z=0. /// + /// The matrix to convert. + /// The GPU scene transform. public static GpuSceneTransform FromMatrix4x4(Matrix4x4 m) => new(m.M11, m.M12, m.M21, m.M22, m.M41, m.M42, m.M14, m.M24, m.M44); + /// + /// Gets the row 1, column 1 linear element. + /// public float M11 { get; } + /// + /// Gets the row 1, column 2 linear element. + /// public float M12 { get; } + /// + /// Gets the row 2, column 1 linear element. + /// public float M21 { get; } + /// + /// Gets the row 2, column 2 linear element. + /// public float M22 { get; } + /// + /// Gets the x translation element. + /// public float Tx { get; } + /// + /// Gets the y translation element. + /// public float Ty { get; } /// @@ -4113,6 +7471,12 @@ public static GpuSceneTransform FromMatrix4x4(Matrix4x4 m) /// public float M44 { get; } + /// + /// Compares components with exact float equality; the encoder only uses this to skip + /// re-emitting a transform identical to the last one written. + /// + /// The transform to compare against. + /// when every component matches exactly. public bool Equals(GpuSceneTransform other) => this.M11 == other.M11 && this.M12 == other.M12 @@ -4147,11 +7511,12 @@ public override int GetHashCode() /// /// Owns one growable contiguous stream backed by allocator storage. /// +/// The unmanaged element type of the stream. internal ref struct OwnedStream where T : unmanaged { private readonly MemoryAllocator allocator; - private IMemoryOwner owner; + private IMemoryOwner? owner; private Span span; /// @@ -4232,23 +7597,28 @@ public void EnsureAdditionalCapacity(int additionalCount) public void Advance(int count) => this.Count += count; /// - /// Disposes the current owner and clears the stream state. + /// Disposes the current owner and clears the stream state. The owner slot is nulled so a + /// dispose after cannot return the transferred owner to the + /// allocator a second time. /// public void Dispose() { - this.owner.Dispose(); + this.owner?.Dispose(); + this.owner = null; this.span = default; this.Count = 0; } /// - /// Detaches the current owner from the stream without copying. + /// Detaches the current owner from the stream without copying. The owner slot is nulled so + /// a later dispose cannot release the transferred owner. /// /// The detached owner. [MethodImpl(MethodImplOptions.AggressiveInlining)] public IMemoryOwner DetachOwner() { - IMemoryOwner detached = this.owner; + IMemoryOwner detached = this.owner!; + this.owner = null; this.span = default; this.Count = 0; return detached; @@ -4269,7 +7639,7 @@ private void EnsureCapacity(int requiredCapacity) // required size when a caller has already pre-reserved a larger single jump. IMemoryOwner next = this.allocator.Allocate(Math.Max(requiredCapacity, this.span.Length * 2)); this.span[..this.Count].CopyTo(next.Memory.Span); - this.owner.Dispose(); + this.owner!.Dispose(); this.owner = next; this.span = next.Memory.Span; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs new file mode 100644 index 000000000..bf03ef155 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneOperations.cs @@ -0,0 +1,481 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +#pragma warning disable SA1201 // Ordered WebGPU Apply metadata is kept together for readability. +#pragma warning disable SA1649 // This file groups the operation, range, and payload types used by one scene feature. + +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies one ordered operation inside a single retained WebGPU scene. +/// +internal enum WebGPUSceneOperationKind +{ + /// + /// Renders one draw range into the current target. + /// + RenderRange, + + /// + /// Applies an ImageSharp processor to the current target and draws it back through WebGPU scene data. + /// + Apply, + + /// + /// Executes a native layer-effect plan and draws its working texture back through retained scene data. + /// + ShaderEffect, + + /// + /// Begins rendering into a scoped-layer target. + /// + BeginLayer, + + /// + /// Composites a completed scoped-layer target into its parent target. + /// + EndLayer +} + +/// +/// One ordered operation retained by a single WebGPU scene. +/// +internal sealed class WebGPUSceneOperation +{ + /// + /// Initializes a new instance of the class. + /// + /// The encoded draw range to render. + public WebGPUSceneOperation(WebGPUSceneRange range) + { + this.Kind = WebGPUSceneOperationKind.RenderRange; + this.Range = range; + } + + /// + /// Initializes a new instance of the class. + /// + /// The retained Apply operation data. + public WebGPUSceneOperation(WebGPUApplySceneItem apply) + { + this.Kind = WebGPUSceneOperationKind.Apply; + this.Apply = apply; + } + + /// + /// Initializes a new instance of the class. + /// + /// The retained native shader-effect operation data. + public WebGPUSceneOperation(WebGPUShaderEffectSceneItem shaderEffect) + { + this.Kind = WebGPUSceneOperationKind.ShaderEffect; + this.ShaderEffect = shaderEffect; + } + + /// + /// Initializes a new instance of the class for a scoped-layer entry. + /// + /// The absolute bounds of the scoped layer. + /// The stable target identity assigned to the scoped layer. + /// The stable target identity of the containing target. + public WebGPUSceneOperation(Rectangle layerBounds, int layerTargetId, int parentTargetId) + { + this.Kind = WebGPUSceneOperationKind.BeginLayer; + this.LayerBounds = layerBounds; + this.LayerTargetId = layerTargetId; + this.ParentTargetId = parentTargetId; + } + + /// + /// Initializes a new instance of the class for a scoped-layer exit. + /// + /// The encoded range that composites the layer into its parent. + /// The stable target identity assigned to the scoped layer. + /// The stable target identity of the containing target. + public WebGPUSceneOperation(WebGPUSceneRange compositeRange, int layerTargetId, int parentTargetId) + { + this.Kind = WebGPUSceneOperationKind.EndLayer; + this.Range = compositeRange; + this.LayerTargetId = layerTargetId; + this.ParentTargetId = parentTargetId; + } + + /// + /// Gets the operation kind. + /// + public WebGPUSceneOperationKind Kind { get; } + + /// + /// Gets the encoded draw range for a render operation. + /// Only meaningful when is ; default otherwise. + /// + public WebGPUSceneRange Range { get; } + + /// + /// Gets the retained Apply data for an Apply operation. + /// Non-null only when is . + /// + public WebGPUApplySceneItem? Apply { get; } + + /// + /// Gets the retained native effect data for a shader-effect operation. + /// Non-null only when is . + /// + public WebGPUShaderEffectSceneItem? ShaderEffect { get; } + + /// + /// Gets the absolute bounds of a scoped layer. + /// Only meaningful when is . + /// + public Rectangle LayerBounds { get; } + + /// + /// Gets the stable target identity for a scoped-layer entry or exit. + /// + public int LayerTargetId { get; } + + /// + /// Gets the stable parent target identity for a scoped-layer entry or exit. + /// + public int ParentTargetId { get; } + + /// + /// Gets the number of consecutive dependency-independent Apply operations in the group that + /// begins at this operation. The value is non-zero only for an Apply group head. + /// + public int ApplyGroupCount { get; private set; } + + /// + /// Gets the maximum number of scheduling-status records awaiting validation immediately + /// before this Apply group reads its source pixels. + /// + public int PendingStatusCapacity { get; private set; } + + /// + /// Gets the dense zero-based Apply index used by the flush readback layout. + /// Only meaningful when is . + /// + public int ApplyIndex { get; private set; } + + /// + /// Completes the retained barrier metadata for an Apply group head. + /// + /// The number of Apply operations sharing the source snapshot. + /// The maximum number of status records produced before the shared readback. + public void SetApplyGroup(int applyGroupCount, int pendingStatusCapacity) + { + this.ApplyGroupCount = applyGroupCount; + this.PendingStatusCapacity = pendingStatusCapacity; + } + + /// + /// Assigns the dense Apply index used by the generic render-time layout. + /// + /// The zero-based Apply index. + public void SetApplyIndex(int applyIndex) => this.ApplyIndex = applyIndex; +} + +/// +/// Describes a draw range inside the packed WebGPU scene streams. +/// +internal readonly struct WebGPUSceneRange +{ + /// + /// Initializes a new instance of the struct. + /// + /// The target bounds used when this range was lowered. + /// The word offset of the range's path tags in the packed scene buffer. + /// The byte count of the range's path tags. + /// The word offset of the range's path data in the packed scene buffer. + /// The word count of the range's path data. + /// The draw-tag offset of the range. + /// The draw-tag count of the range. + /// The draw-data word offset of the range. + /// The draw-data word count of the range. + /// The transform word offset of the range. + /// The transform word count of the range. + /// The style word offset of the range. + /// The style word count of the range. + /// The range-local info word count. + /// The path count in the range. + /// The clip count in the range. + /// The visible fill count in the range. + /// 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 per-(draw, bin) binning records. + public WebGPUSceneRange( + Rectangle targetBounds, + int pathTagWordStart, + int pathTagByteCount, + int pathDataWordStart, + int pathDataWordCount, + int drawTagStart, + int drawTagCount, + int drawDataWordStart, + int drawDataWordCount, + int transformWordStart, + int transformWordCount, + int styleWordStart, + int styleWordCount, + int infoWordCount, + int pathCount, + int clipCount, + int fillCount, + int lineCount, + int totalPathRowCount, + long estimatedTileCrossings, + long estimatedBinFootprint) + { + this.TargetBounds = targetBounds; + this.PathTagWordStart = pathTagWordStart; + this.PathTagByteCount = pathTagByteCount; + this.PathDataWordStart = pathDataWordStart; + this.PathDataWordCount = pathDataWordCount; + this.DrawTagStart = drawTagStart; + this.DrawTagCount = drawTagCount; + this.DrawDataWordStart = drawDataWordStart; + this.DrawDataWordCount = drawDataWordCount; + this.TransformWordStart = transformWordStart; + this.TransformWordCount = transformWordCount; + this.StyleWordStart = styleWordStart; + this.StyleWordCount = styleWordCount; + this.InfoWordCount = infoWordCount; + this.PathCount = pathCount; + this.ClipCount = clipCount; + this.FillCount = fillCount; + this.LineCount = lineCount; + this.TotalPathRowCount = totalPathRowCount; + this.EstimatedTileCrossings = estimatedTileCrossings; + this.EstimatedBinFootprint = estimatedBinFootprint; + } + + /// + /// Gets the target bounds used when this range was lowered. + /// + public Rectangle TargetBounds { get; } + + /// + /// Gets the word offset of the range's path tags in the packed scene buffer. + /// + public int PathTagWordStart { get; } + + /// + /// Gets the byte count of the range's path tags. + /// + public int PathTagByteCount { get; } + + /// + /// Gets the word offset of the range's path data in the packed scene buffer. + /// + public int PathDataWordStart { get; } + + /// + /// Gets the word count of the range's path data. + /// + public int PathDataWordCount { get; } + + /// + /// Gets the draw-tag offset of the range. + /// + public int DrawTagStart { get; } + + /// + /// Gets the draw-tag count of the range. + /// + public int DrawTagCount { get; } + + /// + /// Gets the draw-data word offset of the range. + /// + public int DrawDataWordStart { get; } + + /// + /// Gets the draw-data word count of the range. + /// + public int DrawDataWordCount { get; } + + /// + /// Gets the transform word offset of the range. + /// + public int TransformWordStart { get; } + + /// + /// Gets the transform word count of the range. + /// + public int TransformWordCount { get; } + + /// + /// Gets the style word offset of the range. + /// + public int StyleWordStart { get; } + + /// + /// Gets the style word count of the range. + /// + public int StyleWordCount { get; } + + /// + /// Gets the range-local info word count. + /// + public int InfoWordCount { get; } + + /// + /// Gets the path count in the range. + /// + public int PathCount { get; } + + /// + /// Gets the clip count in the range. + /// + public int ClipCount { get; } + + /// + /// Gets the visible fill count in the range. + /// + public int FillCount { get; } + + /// + /// Gets the line count in the range. + /// + public int LineCount { get; } + + /// + /// Gets the estimated sparse row count for the range. + /// + public int TotalPathRowCount { get; } + + /// + /// Gets the CPU-side upper bound for the range's tile-boundary crossings. + /// + public long EstimatedTileCrossings { get; } + + /// + /// Gets the CPU-side upper bound for the range's per-(draw, bin) binning records. + /// + public long EstimatedBinFootprint { get; } + + /// + /// Gets the maximum number of allocator-status records produced by this range. The staged + /// pipeline emits one record per chunk, and one tile row is the smallest legal chunk. + /// + public int MaximumStatusRecordCount + => this.TargetBounds.Height > 0 + ? ((this.TargetBounds.Height - 1) / WebGPUSceneEncoder.TileHeight) + 1 + : 1; +} + +/// +/// Retained WebGPU data for one Apply operation. +/// +internal sealed class WebGPUApplySceneItem +{ + /// + /// Initializes a new instance of the class. + /// + /// The ImageSharp processor operation. + /// The layer effect represented by the operation, or for a direct Apply operation. + /// The rectangle containing the source pixels supplied to the operation. + /// The bounds within which the processed pixels are written. + /// The offset subtracted from when reading the source pixels. + /// The encoded image-fill draw range inside the retained scene. + public WebGPUApplySceneItem( + Action operation, + LayerEffect? effect, + Rectangle inputRect, + Rectangle outputRect, + Point readOffset, + WebGPUSceneRange drawRange) + { + this.Operation = operation; + this.Effect = effect; + this.InputRect = inputRect; + this.OutputRect = outputRect; + this.ReadOffset = readOffset; + this.DrawRange = drawRange; + } + + /// + /// Gets the ImageSharp processor operation. + /// + public Action Operation { get; } + + /// + /// Gets the layer effect represented by the operation, or for a direct Apply operation. + /// + public LayerEffect? Effect { get; } + + /// + /// Gets the rectangle containing the source pixels supplied to the operation. + /// + public Rectangle InputRect { get; } + + /// + /// Gets the bounds within which the processed pixels are written. + /// + public Rectangle OutputRect { get; } + + /// + /// Gets the offset subtracted from when reading the source pixels, so + /// a write-back recorded at an offset still reads the pre-offset region. + /// + public Point ReadOffset { get; } + + /// + /// Gets the encoded image-fill draw range inside the retained scene. + /// + public WebGPUSceneRange DrawRange { get; } +} + +/// +/// Retained WebGPU data for one native layer-effect operation. +/// +internal sealed class WebGPUShaderEffectSceneItem +{ + /// + /// Initializes a new instance of the class. + /// + /// The shader effect that owns the ordered GPU passes. + /// The rectangle represented by the effect working texture. + /// The bounds within which the working texture is written back. + /// The offset subtracted from when reading source pixels. + /// The encoded working-texture write-back range. + public WebGPUShaderEffectSceneItem(IWebGPUShaderEffectSource effect, Rectangle inputRect, Rectangle outputRect, Point readOffset, WebGPUSceneRange drawRange) + { + this.Effect = effect; + this.InputRect = inputRect; + this.OutputRect = outputRect; + this.ReadOffset = readOffset; + this.DrawRange = drawRange; + } + + /// + /// Gets the shader effect that owns the ordered GPU passes. + /// + public IWebGPUShaderEffectSource Effect { get; } + + /// + /// Gets the rectangle represented by the effect working texture. + /// + public Rectangle InputRect { get; } + + /// + /// Gets the bounds within which the working texture is written back. + /// + public Rectangle OutputRect { get; } + + /// + /// Gets the offset subtracted from when resolving the source snapshot. + /// + public Point ReadOffset { get; } + + /// + /// Gets the encoded working-texture write-back range. + /// + public WebGPUSceneRange DrawRange { get; } +} + +#pragma warning restore SA1649 +#pragma warning restore SA1201 diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs index 70a2a23eb..c3c87dfc6 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneResources.cs @@ -7,9 +7,8 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; using SixLabors.ImageSharp.PixelFormats; -using WgpuBuffer = Silk.NET.WebGPU.Buffer; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -21,6 +20,15 @@ internal static unsafe class WebGPUSceneResources /// /// Creates the flush-scoped GPU resources required by the staged scene pipeline. /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The encoded scene to stage. + /// The scene configuration. + /// The packed base color for the target. + /// The reusable resource arena for this staging operation. + /// The staged resource set. + /// The error message when resource creation fails. + /// when the resources were created. public static bool TryCreate( WebGPUFlushContext flushContext, WebGPUEncodedScene scene, @@ -30,22 +38,130 @@ public static bool TryCreate( out WebGPUSceneResourceSet resources, out string? error) where TPixel : unmanaged, IPixel + => TryCreate( + flushContext, + scene, + config, + baseColor, + externalTextureView: null, + ref arena, + out resources, + out error); + + /// + /// Creates the flush-scoped GPU resources required by the staged scene pipeline for one range. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The encoded scene to stage. + /// The scene range to stage. + /// The scene configuration. + /// The packed base color for the target. + /// The target texture view supplied by the caller. + /// The reusable resource arena for this staging operation. + /// The staged resource set. + /// The error message when resource creation fails. + /// when the resources were created. + public static bool TryCreate( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + WebGPUSceneRange range, + WebGPUSceneConfig config, + uint baseColor, + WGPUTextureViewImpl* externalTextureView, + [NotNullWhen(true)] ref WebGPUSceneResourceArena? arena, + out WebGPUSceneResourceSet resources, + out string? error) + where TPixel : unmanaged, IPixel + => TryCreateCore( + flushContext, + scene, + range, + config, + baseColor, + externalTextureView, + ref arena, + out resources, + out error); + + /// + /// Creates the flush-scoped GPU resources required by the staged scene pipeline. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The encoded scene to stage. + /// The scene configuration. + /// The packed base color for the target. + /// The target texture view supplied by the caller. + /// The reusable resource arena for this staging operation. + /// The staged resource set. + /// The error message when resource creation fails. + /// when the resources were created. + public static bool TryCreate( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + WebGPUSceneConfig config, + uint baseColor, + WGPUTextureViewImpl* externalTextureView, + [NotNullWhen(true)] ref WebGPUSceneResourceArena? arena, + out WebGPUSceneResourceSet resources, + out string? error) + where TPixel : unmanaged, IPixel + => TryCreateCore( + flushContext, + scene, + null, + config, + baseColor, + externalTextureView, + ref arena, + out resources, + out error); + + /// + /// Creates the staged-scene GPU resources, reusing an existing arena when its capacities fit. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The encoded scene to stage. + /// The scene range to stage, or for the full scene. + /// The scene configuration. + /// The packed base color for the target. + /// The target texture view supplied by the caller, or . + /// The reusable resource arena for this staging operation. + /// The staged resource set. + /// The error message when resource creation fails. + /// when the resources were created. + private static bool TryCreateCore( + WebGPUFlushContext flushContext, + WebGPUEncodedScene scene, + WebGPUSceneRange? range, + WebGPUSceneConfig config, + uint baseColor, + WGPUTextureViewImpl* externalTextureView, + [NotNullWhen(true)] ref WebGPUSceneResourceArena? arena, + out WebGPUSceneResourceSet resources, + out string? error) + where TPixel : unmanaged, IPixel { resources = default; + int infoWordCount = range?.InfoWordCount ?? scene.InfoWordCount; // Textures are scene-dependent and not pooled. - if (!TryCreateGradientTexture(flushContext, scene, out TextureView* gradientTextureView, out error)) + if (!TryCreateGradientTexture(flushContext, scene, out WGPUTextureViewImpl* gradientTextureView, out error)) { return false; } - if (!TryCreateImageAtlasTexture(flushContext, scene, flushContext.TextureFormat, out TextureView* imageAtlasTextureView, out error)) + if (!TryCreateImageAtlasTexture(flushContext, scene, range, flushContext.TextureFormat, externalTextureView, out WGPUTextureViewImpl* imageAtlasTextureView, out error)) { return false; } - // Compute byte lengths for the two variable-size data buffers. - nuint infoBinDataByteLength = checked(GetBindingByteLength(scene.InfoBufferWordCount) + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength); + // Compute byte lengths for the two variable-size data buffers. The combined + // info/bin-data buffer is laid out as [info | brush data | bin data | bin headers], + // matching config.bin_data_start and config.brush_data_base in Shared/config.wgsl. + nuint infoBinDataByteLength = checked(GetBindingByteLength(infoWordCount + scene.PathGradientDataWordCount) + config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength); nuint sceneByteLength = GetBindingByteLength(scene.SceneWordCount); // Reuse arena buffers if all capacities fit this scene. @@ -53,16 +169,58 @@ public static bool TryCreate( { // Upload new scene data and header into the existing arena buffers. using WebGPUHandle.HandleReference reuseQueueReference = flushContext.QueueHandle.AcquireReference(); - Queue* reuseQueue = (Queue*)reuseQueueReference.Handle; + WGPUQueueImpl* reuseQueue = (WGPUQueueImpl*)reuseQueueReference.Handle; + + // Consecutive ordered ranges stage the same encoded scene. The packed scene stream is + // range-independent apart from the image draw-data words patched by texture staging + // above, so a matching resident version skips the upload entirely and a stale version + // re-uploads only the mutated span. Queue ordering already ran the resident upload + // before any command submitted afterwards can read the buffer. ReadOnlySpan sceneData = scene.SceneData.Span; - fixed (uint* sceneDataPtr = sceneData) + if (!ReferenceEquals(arena.UploadedScene, scene)) + { + fixed (uint* sceneDataPtr = sceneData) + { + flushContext.Api.QueueWriteBuffer(reuseQueue, arena.SceneBuffer, 0, sceneDataPtr, (nuint)(sceneData.Length * sizeof(uint))); + } + } + else if (arena.UploadedSceneVersion != scene.SceneDataVersion) { - flushContext.Api.QueueWriteBuffer(reuseQueue, arena.SceneBuffer, 0, sceneDataPtr, (nuint)(sceneData.Length * sizeof(uint))); + // The resident stream was uploaded before this range's image staging rewrote its + // draw-data words. Patch the accumulated mutated span; rewriting words that + // already hold their final values is idempotent. + int mutatedStart = scene.MutatedSceneWordMin; + int mutatedLength = scene.MutatedSceneWordEnd - mutatedStart; + fixed (uint* sceneDataPtr = sceneData.Slice(mutatedStart, mutatedLength)) + { + flushContext.Api.QueueWriteBuffer( + reuseQueue, + arena.SceneBuffer, + checked((ulong)mutatedStart * sizeof(uint)), + sceneDataPtr, + (nuint)(mutatedLength * sizeof(uint))); + } } - GpuSceneConfig header = CreateHeader(scene, config, baseColor); + GpuSceneConfig header = range.HasValue + ? CreateHeader(scene, range.Value, config, baseColor) + : CreateHeader(scene, config, baseColor); flushContext.Api.QueueWriteBuffer(reuseQueue, arena.HeaderBuffer, 0, &header, (nuint)sizeof(GpuSceneConfig)); - UploadPathGradientData(flushContext, arena.InfoBinDataBuffer, scene.InfoWordCount, scene.PathGradientData.Span); + + // Brush words land immediately after the range's info prefix and recolor payloads are + // specialized per TPixel, so the brush upload can only be skipped when the scene, the + // prefix length, and the pixel specialization all match the bytes already resident. + if (!ReferenceEquals(arena.UploadedScene, scene) || + arena.UploadedBrushInfoWordCount != infoWordCount || + arena.UploadedPixelType != typeof(TPixel)) + { + UploadBrushData(flushContext, arena.InfoBinDataBuffer, infoWordCount, scene); + } + + arena.UploadedScene = scene; + arena.UploadedSceneVersion = scene.SceneDataVersion; + arena.UploadedBrushInfoWordCount = infoWordCount; + arena.UploadedPixelType = typeof(TPixel); resources = new WebGPUSceneResourceSet( arena.HeaderBuffer, @@ -89,60 +247,129 @@ public static bool TryCreate( return true; } - // Arena miss; create all buffers fresh and build a new arena. + // Arena miss; create all buffers fresh and build a new arena. Size every buffer to the + // union of the new requirement and the outgoing arena's capacity: consecutive ordered + // ranges of one scene can each dominate a different dimension, and exact-fit recreation + // would otherwise thrash the whole arena on every alternation. + WebGPUSceneBufferSizes createSizes = arena is null + ? config.BufferSizes + : MaxMergeArenaSizes(config.BufferSizes, arena.CapacitySizes); + nuint createInfoBinDataByteLength = arena is null + ? infoBinDataByteLength + : Math.Max(infoBinDataByteLength, arena.InfoBinDataByteCapacity); + nuint createSceneByteLength = arena is null + ? sceneByteLength + : Math.Max(sceneByteLength, arena.SceneByteCapacity); + WebGPUSceneResourceArena.Dispose(arena); arena = null; - WgpuBuffer* infoBinDataBuffer = CreateAndUploadCombinedInfoBinDataBuffer( - flushContext, - scene.InfoWordCount, - scene.PathGradientData.Span, - checked(config.BufferSizes.BinData.ByteLength + config.BufferSizes.BinHeaders.ByteLength)); - - WgpuBuffer* pathReducedBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReduced.Length); - WgpuBuffer* pathReduced2Buffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReduced2.Length); - WgpuBuffer* pathReducedScanBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathReducedScan.Length); - WgpuBuffer* pathMonoidBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathMonoids.Length); - WgpuBuffer* pathBboxBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.PathBboxes.Length); - WgpuBuffer* drawReducedBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawReduced.Length); - WgpuBuffer* drawMonoidBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawMonoids.Length); - WgpuBuffer* clipInputBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipInputs.Length); - WgpuBuffer* clipElementBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipElements.Length); - WgpuBuffer* clipBicBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipBics.Length); - WgpuBuffer* clipBboxBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.ClipBboxes.Length); - WgpuBuffer* drawBboxBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.DrawBboxes.Length); - WgpuBuffer* pathBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.Paths.Length); - WgpuBuffer* lineBuffer = CreateAndUploadBuffer(flushContext, [], config.BufferSizes.Lines.Length); - WgpuBuffer* sceneBuffer = CreateAndUploadBuffer(flushContext, scene.SceneData.Span, (uint)scene.SceneData.Length); - - GpuSceneConfig newHeader = CreateHeader(scene, config, baseColor); - WgpuBuffer* headerBuffer = CreateAndUploadScalarBuffer(flushContext, in newHeader); - - // Build the new arena from the freshly created buffers. - // These buffers are NOT tracked by the flush context; the arena owns them. - arena = new WebGPUSceneResourceArena( - flushContext.Api, - flushContext.DeviceHandle, - config.BufferSizes, - infoBinDataByteLength, - sceneByteLength, - headerBuffer, - sceneBuffer, - pathReducedBuffer, - pathReduced2Buffer, - pathReducedScanBuffer, - pathMonoidBuffer, - pathBboxBuffer, - drawReducedBuffer, - drawMonoidBuffer, - infoBinDataBuffer, - clipInputBuffer, - clipElementBuffer, - clipBicBuffer, - clipBboxBuffer, - drawBboxBuffer, - pathBuffer, - lineBuffer); + WGPUBufferImpl* infoBinDataBuffer = null; + WGPUBufferImpl* pathReducedBuffer = null; + WGPUBufferImpl* pathReduced2Buffer = null; + WGPUBufferImpl* pathReducedScanBuffer = null; + WGPUBufferImpl* pathMonoidBuffer = null; + WGPUBufferImpl* pathBboxBuffer = null; + WGPUBufferImpl* drawReducedBuffer = null; + WGPUBufferImpl* drawMonoidBuffer = null; + WGPUBufferImpl* clipInputBuffer = null; + WGPUBufferImpl* clipElementBuffer = null; + WGPUBufferImpl* clipBicBuffer = null; + WGPUBufferImpl* clipBboxBuffer = null; + WGPUBufferImpl* drawBboxBuffer = null; + WGPUBufferImpl* pathBuffer = null; + WGPUBufferImpl* lineBuffer = null; + WGPUBufferImpl* sceneBuffer = null; + WGPUBufferImpl* headerBuffer = null; + + try + { + infoBinDataBuffer = CreateAndUploadCombinedInfoBinDataBuffer( + flushContext, + infoWordCount, + scene, + checked(createSizes.BinData.ByteLength + createSizes.BinHeaders.ByteLength), + createInfoBinDataByteLength); + + pathReducedBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.PathReduced.Length); + pathReduced2Buffer = CreateAndUploadBuffer(flushContext, [], createSizes.PathReduced2.Length); + pathReducedScanBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.PathReducedScan.Length); + pathMonoidBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.PathMonoids.Length); + pathBboxBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.PathBboxes.Length); + drawReducedBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.DrawReduced.Length); + drawMonoidBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.DrawMonoids.Length); + clipInputBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.ClipInputs.Length); + clipElementBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.ClipElements.Length); + clipBicBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.ClipBics.Length); + clipBboxBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.ClipBboxes.Length); + drawBboxBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.DrawBboxes.Length); + pathBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.Paths.Length); + lineBuffer = CreateAndUploadBuffer(flushContext, [], createSizes.Lines.Length); + sceneBuffer = CreateAndUploadBuffer( + flushContext, + scene.SceneData.Span, + checked((uint)(createSceneByteLength / sizeof(uint)))); + + GpuSceneConfig newHeader = range.HasValue + ? CreateHeader(scene, range.Value, config, baseColor) + : CreateHeader(scene, config, baseColor); + headerBuffer = CreateAndUploadScalarBuffer(flushContext, in newHeader); + + // Build the new arena from the freshly created buffers. + // These buffers are NOT tracked by the flush context; the arena owns them. + arena = new WebGPUSceneResourceArena( + flushContext.Api, + flushContext.DeviceHandle, + createSizes, + createInfoBinDataByteLength, + createSceneByteLength, + headerBuffer, + sceneBuffer, + pathReducedBuffer, + pathReduced2Buffer, + pathReducedScanBuffer, + pathMonoidBuffer, + pathBboxBuffer, + drawReducedBuffer, + drawMonoidBuffer, + infoBinDataBuffer, + clipInputBuffer, + clipElementBuffer, + clipBicBuffer, + clipBboxBuffer, + drawBboxBuffer, + pathBuffer, + lineBuffer); + } + catch + { + // Nothing owns these buffers until the arena is constructed; a throw mid-sequence + // (a closed device handle, checked-size overflow) must release the ones created. + WebGPU api = flushContext.Api; + ReleaseArenaBuffer(api, infoBinDataBuffer); + ReleaseArenaBuffer(api, pathReducedBuffer); + ReleaseArenaBuffer(api, pathReduced2Buffer); + ReleaseArenaBuffer(api, pathReducedScanBuffer); + ReleaseArenaBuffer(api, pathMonoidBuffer); + ReleaseArenaBuffer(api, pathBboxBuffer); + ReleaseArenaBuffer(api, drawReducedBuffer); + ReleaseArenaBuffer(api, drawMonoidBuffer); + ReleaseArenaBuffer(api, clipInputBuffer); + ReleaseArenaBuffer(api, clipElementBuffer); + ReleaseArenaBuffer(api, clipBicBuffer); + ReleaseArenaBuffer(api, clipBboxBuffer); + ReleaseArenaBuffer(api, drawBboxBuffer); + ReleaseArenaBuffer(api, pathBuffer); + ReleaseArenaBuffer(api, lineBuffer); + ReleaseArenaBuffer(api, sceneBuffer); + ReleaseArenaBuffer(api, headerBuffer); + throw; + } + + arena.UploadedScene = scene; + arena.UploadedSceneVersion = scene.SceneDataVersion; + arena.UploadedBrushInfoWordCount = infoWordCount; + arena.UploadedPixelType = typeof(TPixel); resources = new WebGPUSceneResourceSet( headerBuffer, @@ -169,6 +396,42 @@ public static bool TryCreate( return true; } + /// + /// Merges the required sizes for the arena-owned buffers with the capacities of an outgoing + /// arena so recreation settles at the union of every range's dominant dimensions. + /// + /// The sizes required by the range being staged. + /// The capacities of the arena being replaced. + /// The sizes to create the replacement arena with. Fields that do not correspond to + /// an arena-owned buffer keep the required values. + private static WebGPUSceneBufferSizes MaxMergeArenaSizes(in WebGPUSceneBufferSizes required, in WebGPUSceneBufferSizes previousCapacities) + => new( + WebGPUSceneBufferSize.Create(Math.Max(required.PathReduced.Length, previousCapacities.PathReduced.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.PathReduced2.Length, previousCapacities.PathReduced2.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.PathReducedScan.Length, previousCapacities.PathReducedScan.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.PathMonoids.Length, previousCapacities.PathMonoids.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.PathBboxes.Length, previousCapacities.PathBboxes.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.DrawReduced.Length, previousCapacities.DrawReduced.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.DrawMonoids.Length, previousCapacities.DrawMonoids.Length)), + required.Info, + WebGPUSceneBufferSize.Create(Math.Max(required.ClipInputs.Length, previousCapacities.ClipInputs.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.ClipElements.Length, previousCapacities.ClipElements.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.ClipBics.Length, previousCapacities.ClipBics.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.ClipBboxes.Length, previousCapacities.ClipBboxes.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.DrawBboxes.Length, previousCapacities.DrawBboxes.Length)), + required.BumpAlloc, + WebGPUSceneBufferSize.Create(Math.Max(required.Paths.Length, previousCapacities.Paths.Length)), + WebGPUSceneBufferSize.Create(Math.Max(required.Lines.Length, previousCapacities.Lines.Length)), + required.BinHeaders, + required.BinData, + required.IndirectCount, + required.PathRows, + required.PathTiles, + required.SegCounts, + required.Segments, + required.BlendSpill, + required.Ptcl); + /// /// Creates the root config block uploaded to staged-scene shaders for one render attempt. /// @@ -178,12 +441,15 @@ public static bool TryCreate( /// The config block matching the WGSL Config layout. public static GpuSceneConfig CreateHeader(WebGPUEncodedScene scene, WebGPUSceneConfig config, uint baseColor) { + // The ptcl_dyn_start value (sixth layout argument) marks where the bump-allocated + // ptcl tail begins: after the fixed 64-word (WebGPUSceneDispatch.PtclInitialAlloc) + // reservation for every tile slot in the chunk window. GpuSceneLayout layout = new( scene.Layout.DrawObjectCount, scene.Layout.PathCount, scene.Layout.ClipCount, scene.Layout.BinDataStart, - scene.Layout.PathGradientDataBase, + scene.Layout.BrushDataBase, checked((uint)scene.TileCountX * config.ChunkWindow.TileBufferHeight * 64U), scene.Layout.PathTagBase, scene.Layout.PathDataBase, @@ -212,29 +478,156 @@ public static GpuSceneConfig CreateHeader(WebGPUEncodedScene scene, WebGPUSceneC scene.FineCoverageThreshold); } + /// + /// Creates the root config block uploaded to staged-scene shaders for one render range. + /// + /// The encoded scene whose global scene buffer is being rendered. + /// The range inside to render. + /// The attempt-specific dispatch, scratch, and chunk-window configuration. + /// The packed base color used by the fine pass. + /// The config block matching the WGSL Config layout. + public static GpuSceneConfig CreateHeader(WebGPUEncodedScene scene, WebGPUSceneRange range, WebGPUSceneConfig config, uint baseColor) + { + // Tiles are 16x16 pixels (TILE_WIDTH/TILE_HEIGHT in Shared/config.wgsl). + int tileCountX = (range.TargetBounds.Width + 15) / 16; + int tileCountY = (range.TargetBounds.Height + 15) / 16; + + // The combined info/bin-data buffer is [info | path-gradient | bin data | bin headers], + // so bin data starts after both ranges and the gradient data starts after the info + // words. The ptcl_dyn_start value (sixth layout argument) marks where the + // bump-allocated ptcl tail begins: after the fixed 64-word + // (WebGPUSceneDispatch.PtclInitialAlloc) reservation for every tile slot. + GpuSceneLayout layout = new( + checked((uint)range.DrawTagCount), + checked((uint)range.PathCount), + checked((uint)range.ClipCount), + checked((uint)(range.InfoWordCount + scene.PathGradientDataWordCount)), + checked((uint)range.InfoWordCount), + checked((uint)tileCountX * config.ChunkWindow.TileBufferHeight * 64U), + checked(scene.Layout.PathTagBase + (uint)range.PathTagWordStart), + checked(scene.Layout.PathDataBase + (uint)range.PathDataWordStart), + checked(scene.Layout.DrawTagBase + (uint)range.DrawTagStart), + checked(scene.Layout.DrawDataBase + (uint)range.DrawDataWordStart), + checked(scene.Layout.TransformBase + (uint)range.TransformWordStart), + checked(scene.Layout.StyleBase + (uint)range.StyleWordStart)); + + return new GpuSceneConfig( + checked((uint)tileCountX), + checked((uint)tileCountY), + checked((uint)range.TargetBounds.Width), + checked((uint)range.TargetBounds.Height), + config.ChunkWindow.TileYStart, + config.ChunkWindow.TileHeight, + baseColor, + layout, + config.BufferSizes.Lines.Length, + config.BumpSizes.Binning, + config.BumpSizes.PathRows, + config.BumpSizes.PathTiles, + config.BumpSizes.SegCounts, + config.BumpSizes.Segments, + config.BumpSizes.BlendSpill, + config.BumpSizes.Ptcl, + scene.FineCoverageThreshold); + } + + /// + /// Creates the sampled image-atlas texture and patches each image's draw-data words + /// with its atlas placement, extents, and sample-info word. + /// + /// + /// Entries are stacked vertically in a single column: the atlas is as wide as the + /// widest entry and as tall as the sum of entry heights. When an external texture view + /// is supplied it is bound directly and only the draw-data words are rewritten. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The encoded scene containing image descriptors. + /// The scene range to stage, or for the full scene. + /// The texture format to create. + /// The caller-supplied texture view, or . + /// The created or supplied texture view. + /// The error message when texture creation fails. + /// when the texture view was produced. private static bool TryCreateImageAtlasTexture( WebGPUFlushContext flushContext, WebGPUEncodedScene scene, - TextureFormat textureFormat, - out TextureView* textureView, + WebGPUSceneRange? range, + WGPUTextureFormat textureFormat, + WGPUTextureViewImpl* externalTextureView, + out WGPUTextureViewImpl* textureView, out string? error) where TPixel : unmanaged, IPixel { - if (scene.Images.Count == 0) + WebGPUTargetNumericEncoding pixelNumericEncoding = WebGPUDrawingBackend.CreateOffscreenTargetDescriptor( + flushContext.TargetDescriptor.Format, + flushContext.TargetDescriptor.AlphaRepresentation).NumericEncoding; + + if (externalTextureView is not null) + { + foreach (GpuImageDescriptor descriptor in scene.Images) + { + if (IsImageDescriptorInRange(descriptor, range) && + descriptor.Source.IsExternalTexture) + { + WebGPUTargetDescriptor sourceDescriptor = descriptor.Source.HasExplicitDescriptor + ? descriptor.Source.Descriptor + : flushContext.TargetDescriptor; + + int sceneIndex = (int)scene.Layout.DrawDataBase + descriptor.DrawDataWordOffset; + scene.SetSceneWord(sceneIndex, PackImageAtlasOffset(0, 0)); + scene.SetSceneWord(sceneIndex + 1, PackImageExtents(descriptor.Source.Size.Width, descriptor.Source.Size.Height)); + scene.SetSceneWord( + sceneIndex + 2, + PackImageSampleInfo( + MapImageWrapMode(descriptor.Source.WrapX), + MapImageWrapMode(descriptor.Source.WrapY), + sourceDescriptor.AlphaRepresentation, + sourceDescriptor.NumericEncoding)); + } + } + + textureView = externalTextureView; + error = null; + return true; + } + + int imageCount = 0; + foreach (GpuImageDescriptor descriptor in scene.Images) + { + if (IsImageDescriptorInRange(descriptor, range) && + descriptor.Source.Brush is ImageBrush or PatternBrush) + { + imageCount++; + } + } + + if (imageCount == 0) { - return TryCreateTransparentSampledTexture(flushContext, textureFormat, out _, out textureView, out error); + // Sampled image textures use the target pixel format's native numeric encoding. + // Constructing the placeholder through TPixel maps logical transparent black to the + // physical zero point required by signed-unit formats. + TPixel transparentPixel = TPixel.FromScaledVector4(Vector4.Zero); + + return TryCreateSinglePixelSampledTexture(flushContext, textureFormat, transparentPixel, out _, out textureView, out error); } int atlasWidth = 1; int atlasHeight = 0; foreach (GpuImageDescriptor descriptor in scene.Images) { - GetImageEntrySize(descriptor.Brush, out int width, out int height); + if (!IsImageDescriptorInRange(descriptor, range) || + descriptor.Source.Brush is not (ImageBrush or PatternBrush)) + { + continue; + } + + GetImageEntrySize(descriptor.Source.Brush, out int width, out int height); atlasWidth = Math.Max(atlasWidth, width); atlasHeight += height; } - if (!TryCreateTexture(flushContext, textureFormat, atlasWidth, atlasHeight, "image atlas", out Texture* texture, out textureView, out error)) + if (!TryCreateTexture(flushContext, textureFormat, atlasWidth, atlasHeight, "image atlas", out WGPUTextureImpl* texture, out textureView, out error)) { return false; } @@ -243,10 +636,16 @@ private static bool TryCreateImageAtlasTexture( int atlasY = 0; foreach (GpuImageDescriptor descriptor in scene.Images) { + if (!IsImageDescriptorInRange(descriptor, range) || + descriptor.Source.Brush is not (ImageBrush or PatternBrush)) + { + continue; + } + if (!TryUploadImageEntry( flushContext, texture, - descriptor.Brush, + descriptor.Source.Brush, atlasY, rowBuffer, out int entryWidth, @@ -256,10 +655,24 @@ private static bool TryCreateImageAtlasTexture( return false; } + WrapMode wrapX = WrapMode.Repeat; + WrapMode wrapY = WrapMode.Repeat; + if (descriptor.Source.Brush is ImageBrush imageBrush) + { + wrapX = imageBrush.WrapX; + wrapY = imageBrush.WrapY; + } + int sceneIndex = (int)scene.Layout.DrawDataBase + descriptor.DrawDataWordOffset; scene.SetSceneWord(sceneIndex, PackImageAtlasOffset(0, atlasY)); scene.SetSceneWord(sceneIndex + 1, PackImageExtents(entryWidth, entryHeight)); - scene.SetSceneWord(sceneIndex + 2, PackImageSampleInfo(textureFormat, xExtendMode: 1U, yExtendMode: 1U)); + scene.SetSceneWord( + sceneIndex + 2, + PackImageSampleInfo( + MapImageWrapMode(wrapX), + MapImageWrapMode(wrapY), + flushContext.TargetDescriptor.AlphaRepresentation, + pixelNumericEncoding)); atlasY += entryHeight; } @@ -267,83 +680,143 @@ private static bool TryCreateImageAtlasTexture( return true; } + /// + /// Determines whether an image descriptor's draw data falls inside the staged range. + /// + /// The image descriptor to test. + /// The scene range to stage, or for the full scene. + /// when the descriptor belongs to the staged range. + private static bool IsImageDescriptorInRange(GpuImageDescriptor descriptor, WebGPUSceneRange? range) + { + if (!range.HasValue) + { + return true; + } + + WebGPUSceneRange value = range.Value; + return descriptor.DrawDataWordOffset >= value.DrawDataWordStart && + descriptor.DrawDataWordOffset < value.DrawDataWordStart + value.DrawDataWordCount; + } + /// /// Creates and uploads the packed gradient-ramp texture used by gradient draw records. /// + /// + /// Each gradient occupies one 512-texel RGBA16Float row; the shader's ramp index selects the row. + /// + /// The active WebGPU flush context. + /// The encoded scene containing gradient rows. + /// The created texture view. + /// The error message when texture creation fails. + /// when the texture was created. private static bool TryCreateGradientTexture( WebGPUFlushContext flushContext, WebGPUEncodedScene scene, - out TextureView* textureView, + out WGPUTextureViewImpl* textureView, out string? error) { if (scene.GradientRowCount == 0) { - return TryCreateTransparentSampledTexture(flushContext, TextureFormat.Rgba8Unorm, out _, out textureView, out error); - } + // Gradient rows contain associated binary16 components, for which physical zero is transparent. + ulong transparentPixel = 0; - TextureDescriptor textureDescriptor = new() - { - Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D(512, (uint)scene.GradientRowCount, 1), - Format = TextureFormat.Rgba8Unorm, - MipLevelCount = 1, - SampleCount = 1 - }; + return TryCreateSinglePixelSampledTexture(flushContext, WGPUTextureFormat.RGBA16Float, transparentPixel, out _, out textureView, out error); + } - Texture* texture; - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) + // Gradient rows are scene-wide, so one uploaded texture serves every range of the flush. + if (flushContext.TryGetCachedGradientTextureView(scene, out textureView)) { - texture = flushContext.Api.DeviceCreateTexture((Device*)deviceReference.Handle, in textureDescriptor); + error = null; + return true; } - if (texture is null) + const TextureUsage usage = TextureUsage.TextureBinding | TextureUsage.CopyDst; + WGPUTextureImpl* texture; + + // Prefer a device-pooled texture. Extra rows in a larger entry are never addressed: the + // fine shader loads gradient texels by exact ramp row index. + if (flushContext.DeviceState.TryRentPooledTexture( + WGPUTextureFormat.RGBA16Float, + (ulong)usage, + 512, + (uint)scene.GradientRowCount, + out texture, + out textureView, + out uint rentedWidth, + out uint rentedHeight)) { - textureView = null; - error = "Failed to create a gradient texture."; - return false; + flushContext.TrackPooledTexture(texture, textureView, WGPUTextureFormat.RGBA16Float, (ulong)usage, rentedWidth, rentedHeight); } - - TextureViewDescriptor textureViewDescriptor = new() + else { - Format = TextureFormat.Rgba8Unorm, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; + WGPUTextureDescriptor textureDescriptor = new() + { + usage = (ulong)usage, + dimension = WGPUTextureDimension._2D, + + // 512 must match GRADIENT_WIDTH in fine.wgsl and the encoder's ramp width. + size = new WGPUExtent3D(512, (uint)scene.GradientRowCount, 1), + format = WGPUTextureFormat.RGBA16Float, + mipLevelCount = 1, + sampleCount = 1 + }; - textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); - if (textureView is null) - { - flushContext.Api.TextureRelease(texture); - error = "Failed to create a gradient texture view."; - return false; + using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) + { + texture = flushContext.Api.DeviceCreateTexture((WGPUDeviceImpl*)deviceReference.Handle, in textureDescriptor); + } + + if (texture is null) + { + textureView = null; + error = "Failed to create a gradient texture."; + return false; + } + + WGPUTextureViewDescriptor textureViewDescriptor = new() + { + format = WGPUTextureFormat.RGBA16Float, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All + }; + + textureView = flushContext.Api.TextureCreateView(texture, &textureViewDescriptor); + if (textureView is null) + { + flushContext.Api.TextureRelease(texture); + error = "Failed to create a gradient texture view."; + return false; + } + + // Returned to the device pool with the flush so later flushes rent instead of recreating. + flushContext.TrackPooledTexture(texture, textureView, WGPUTextureFormat.RGBA16Float, (ulong)usage, 512, (uint)scene.GradientRowCount); } - TextureDataLayout layout = new() + WGPUTexelCopyBufferLayout layout = new() { - Offset = 0, - BytesPerRow = 512 * 4, - RowsPerImage = (uint)scene.GradientRowCount + offset = 0, + bytesPerRow = 512 * 8, + rowsPerImage = (uint)scene.GradientRowCount }; - ImageCopyTexture destination = new() + WGPUTexelCopyTextureInfo destination = new() { - Texture = texture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All + texture = texture, + mipLevel = 0, + origin = new WGPUOrigin3D(0, 0, 0), + aspect = WGPUTextureAspect.All }; fixed (uint* pixelPtr = scene.GradientPixels.Span) { - Extent3D extent = new(512, (uint)scene.GradientRowCount, 1); + WGPUExtent3D extent = new(512, (uint)scene.GradientRowCount, 1); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); flushContext.Api.QueueWriteTexture( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, in destination, pixelPtr, (nuint)(scene.GradientPixels.Length * sizeof(uint)), @@ -351,8 +824,7 @@ private static bool TryCreateGradientTexture( in extent); } - flushContext.TrackTexture(texture); - flushContext.TrackTextureView(textureView); + flushContext.CacheGradientTextureView(scene, textureView); error = null; return true; } @@ -360,6 +832,9 @@ private static bool TryCreateGradientTexture( /// /// Gets the atlas footprint for one sampled image or pattern brush entry. /// + /// The sampled brush. + /// The atlas entry width in pixels. + /// The atlas entry height in pixels. private static void GetImageEntrySize(Brush brush, out int width, out int height) { if (brush is PatternBrush patternBrush) @@ -375,9 +850,22 @@ private static void GetImageEntrySize(Brush brush, out int width, out int height height = sourceRegion.Height; } + /// + /// Uploads one sampled brush entry into the image atlas at the given vertical offset. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The atlas texture. + /// The sampled brush; an or . + /// The vertical atlas offset of the entry in pixels. + /// The scratch row buffer used to convert pattern colors. + /// The uploaded entry width in pixels. + /// The uploaded entry height in pixels. + /// The error message when the upload fails. + /// when the entry was uploaded. private static bool TryUploadImageEntry( WebGPUFlushContext flushContext, - Texture* texture, + WGPUTextureImpl* texture, Brush brush, int atlasY, TPixel[] rowBuffer, @@ -396,9 +884,22 @@ private static bool TryUploadImageEntry( return TryUploadImageBrushEntry(flushContext, texture, (ImageBrush)brush, atlasY, out entryWidth, out entryHeight, out error); } + /// + /// Converts a pattern brush's color matrix to target pixels and uploads it row by row. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The atlas texture. + /// The pattern brush to upload. + /// The vertical atlas offset of the entry in pixels. + /// The scratch row buffer used to convert pattern colors. + /// The uploaded entry width in pixels. + /// The uploaded entry height in pixels. + /// The error message when the upload fails. + /// when the entry was uploaded. private static bool TryUploadPatternEntry( WebGPUFlushContext flushContext, - Texture* texture, + WGPUTextureImpl* texture, PatternBrush patternBrush, int atlasY, TPixel[] rowBuffer, @@ -429,9 +930,21 @@ private static bool TryUploadPatternEntry( return true; } + /// + /// Uploads the source region of an image brush into the atlas row by row. + /// + /// The pixel format of the target and of any sampled image brushes. + /// The active WebGPU flush context. + /// The atlas texture. + /// The image brush to upload. + /// The vertical atlas offset of the entry in pixels. + /// The uploaded entry width in pixels. + /// The uploaded entry height in pixels. + /// The error message when the upload fails. + /// when the entry was uploaded. private static bool TryUploadImageBrushEntry( WebGPUFlushContext flushContext, - Texture* texture, + WGPUTextureImpl* texture, ImageBrush imageBrush, int atlasY, out int entryWidth, @@ -461,130 +974,143 @@ private static bool TryUploadImageBrushEntry( /// /// Creates the combined info/bin-data scratch buffer expected by the scheduling passes. /// - private static WgpuBuffer* CreateAndUploadCombinedInfoBinDataBuffer( + /// + /// The buffer layout is [info | path-gradient | bin data | bin headers]; only the + /// path-gradient words are uploaded here, the rest is GPU-written scratch. + /// + /// The active WebGPU flush context. + /// The number of GPU-written info words preceding the gradient data. + /// The encoded scene supplying auxiliary brush data. + /// The combined bin-data and bin-header byte length appended after the gradient data. + /// The minimum buffer capacity, used to preserve an outgoing arena's high-water size. + /// The created buffer. + private static WGPUBufferImpl* CreateAndUploadCombinedInfoBinDataBuffer( WebGPUFlushContext flushContext, int infoWordCount, - ReadOnlySpan pathGradientData, - nuint dynamicBinByteLength) + WebGPUEncodedScene scene, + nuint dynamicBinByteLength, + nuint minimumTotalByteLength) + where TPixel : unmanaged, IPixel { - nuint infoByteLength = checked((nuint)(infoWordCount + pathGradientData.Length) * (nuint)Unsafe.SizeOf()); - nuint totalByteLength = checked(infoByteLength + dynamicBinByteLength); + ReadOnlySpan brushData = scene.PathGradientData.Span; + nuint infoByteLength = checked((nuint)(infoWordCount + brushData.Length) * (nuint)Unsafe.SizeOf()); + nuint totalByteLength = Math.Max(checked(infoByteLength + dynamicBinByteLength), minimumTotalByteLength); if (totalByteLength == 0) { totalByteLength = (nuint)Unsafe.SizeOf(); } - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = totalByteLength + usage = (ulong)(BufferUsage.Storage | BufferUsage.CopyDst), + size = totalByteLength }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - WgpuBuffer* buffer = flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); - UploadPathGradientData(flushContext, buffer, infoWordCount, pathGradientData); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); + UploadBrushData(flushContext, buffer, infoWordCount, scene); return buffer; } } - private static void UploadPathGradientData( + /// + /// Writes auxiliary brush data immediately after the info region, at the + /// word offset published as config.brush_data_base in Shared/config.wgsl. + /// + /// The active WebGPU flush context. + /// The combined info/bin-data buffer. + /// The number of info words preceding the brush data. + /// The encoded scene supplying static and target-specialized brush data. + private static void UploadBrushData( WebGPUFlushContext flushContext, - WgpuBuffer* buffer, + WGPUBufferImpl* buffer, int infoWordCount, - ReadOnlySpan pathGradientData) + WebGPUEncodedScene scene) + where TPixel : unmanaged, IPixel { - if (pathGradientData.IsEmpty) + ReadOnlySpan brushData = scene.PathGradientData.Span; + if (brushData.IsEmpty) { return; } nuint offset = checked((nuint)infoWordCount * (nuint)Unsafe.SizeOf()); - nuint byteLength = checked((nuint)pathGradientData.Length * (nuint)Unsafe.SizeOf()); + nuint byteLength = checked((nuint)brushData.Length * (nuint)Unsafe.SizeOf()); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); - fixed (uint* dataPtr = pathGradientData) + WGPUQueueImpl* queue = (WGPUQueueImpl*)queueReference.Handle; + fixed (uint* dataPtr = brushData) { - flushContext.Api.QueueWriteBuffer((Queue*)queueReference.Handle, buffer, offset, dataPtr, byteLength); + flushContext.Api.QueueWriteBuffer(queue, buffer, offset, dataPtr, byteLength); + } + + PixelAlphaRepresentation alphaRepresentation = TPixel.GetPixelTypeInfo().AlphaRepresentation; + IReadOnlyList recolors = scene.Recolors; + Span payload = stackalloc uint[9]; + for (int i = 0; i < recolors.Count; i++) + { + GpuRecolorDescriptor descriptor = recolors[i]; + + // These are the exact two CPU conversions performed when RecolorBrush creates its + // TPixel renderer. Specializing here retains Color precision without exposing its + // private storage-association state to the scene format or shader. + Vector4 source = descriptor.SourceColor.ToScaledVector4(alphaRepresentation); + TPixel targetPixel = descriptor.TargetColor.ToPixel(); + Vector4 target = targetPixel.ToScaledVector4(); + payload[0] = BitConverter.SingleToUInt32Bits(source.X); + payload[1] = BitConverter.SingleToUInt32Bits(source.Y); + payload[2] = BitConverter.SingleToUInt32Bits(source.Z); + payload[3] = BitConverter.SingleToUInt32Bits(source.W); + payload[4] = BitConverter.SingleToUInt32Bits(target.X); + payload[5] = BitConverter.SingleToUInt32Bits(target.Y); + payload[6] = BitConverter.SingleToUInt32Bits(target.Z); + payload[7] = BitConverter.SingleToUInt32Bits(target.W); + payload[8] = BitConverter.SingleToUInt32Bits(descriptor.Threshold); + + nuint payloadOffset = checked((nuint)(infoWordCount + descriptor.BrushDataWordOffset) * (nuint)Unsafe.SizeOf()); + fixed (uint* payloadPtr = payload) + { + flushContext.Api.QueueWriteBuffer(queue, buffer, payloadOffset, payloadPtr, 9U * (nuint)Unsafe.SizeOf()); + } } } /// - /// Creates a one-pixel transparent fallback texture so shader bindings stay valid when a scene omits that input. + /// Creates a one-pixel fallback texture so shader bindings stay valid when a scene omits that input. /// - private static bool TryCreateTransparentSampledTexture( + /// The active WebGPU flush context. + /// The texture format to create. + /// The physical texel value to upload. + /// The created texture. + /// The created texture view. + /// The error message when texture creation fails. + /// when the texture and view were created. + private static bool TryCreateSinglePixelSampledTexture( WebGPUFlushContext flushContext, - TextureFormat textureFormat, - out Texture* texture, - out TextureView* textureView, + WGPUTextureFormat textureFormat, + TPixel pixel, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, out string? error) + where TPixel : unmanaged { - TextureDescriptor textureDescriptor = new() - { - Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D(1, 1, 1), - Format = textureFormat, - MipLevelCount = 1, - SampleCount = 1 - }; - - using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) - { - texture = flushContext.Api.DeviceCreateTexture((Device*)deviceReference.Handle, in textureDescriptor); - } - - if (texture is null) - { - textureView = null; - error = "Failed to create a sampled scene texture."; - return false; - } - - TextureViewDescriptor textureViewDescriptor = new() - { - Format = textureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All - }; - - textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); - if (textureView is null) - { - flushContext.Api.TextureRelease(texture); - texture = null; - error = "Failed to create a sampled scene texture view."; - return false; - } - - uint pixel = 0; - ImageCopyTexture destination = new() - { - Texture = texture, - MipLevel = 0, - Origin = new Origin3D(0, 0, 0), - Aspect = TextureAspect.All - }; - - TextureDataLayout layout = new() - { - Offset = 0, - BytesPerRow = 4, - RowsPerImage = 1 - }; - - Extent3D size = new(1, 1, 1); + // The fallback content is immutable, so the view comes from the device-lifetime cache: + // it must not be tracked by the flush context, which would release a shared native object. + texture = null; using (WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference()) { - flushContext.Api.QueueWriteTexture((Queue*)queueReference.Handle, in destination, &pixel, 4, in layout, in size); + if (!flushContext.DeviceState.TryGetOrCreateSinglePixelTexture( + (WGPUQueueImpl*)queueReference.Handle, + textureFormat, + new ReadOnlySpan(&pixel, Unsafe.SizeOf()), + out textureView)) + { + error = "Failed to create a sampled scene texture."; + return false; + } } - flushContext.TrackTexture(texture); - flushContext.TrackTextureView(textureView); error = null; return true; } @@ -592,29 +1118,38 @@ private static bool TryCreateTransparentSampledTexture( /// /// Creates one sampled texture and its default 2D view. /// + /// The active WebGPU flush context. + /// The texture format to create. + /// The texture width in pixels. + /// The texture height in pixels. + /// The texture name used in error messages. + /// The created texture. + /// The created texture view. + /// The error message when texture creation fails. + /// when the texture and view were created. private static bool TryCreateTexture( WebGPUFlushContext flushContext, - TextureFormat textureFormat, + WGPUTextureFormat textureFormat, int width, int height, string textureName, - out Texture* texture, - out TextureView* textureView, + out WGPUTextureImpl* texture, + out WGPUTextureViewImpl* textureView, out string? error) { - TextureDescriptor textureDescriptor = new() + WGPUTextureDescriptor textureDescriptor = new() { - Usage = TextureUsage.TextureBinding | TextureUsage.CopyDst, - Dimension = TextureDimension.Dimension2D, - Size = new Extent3D((uint)width, (uint)height, 1), - Format = textureFormat, - MipLevelCount = 1, - SampleCount = 1 + usage = (ulong)(TextureUsage.TextureBinding | TextureUsage.CopyDst), + dimension = WGPUTextureDimension._2D, + size = new WGPUExtent3D((uint)width, (uint)height, 1), + format = textureFormat, + mipLevelCount = 1, + sampleCount = 1 }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - texture = flushContext.Api.DeviceCreateTexture((Device*)deviceReference.Handle, in textureDescriptor); + texture = flushContext.Api.DeviceCreateTexture((WGPUDeviceImpl*)deviceReference.Handle, in textureDescriptor); } if (texture is null) @@ -624,18 +1159,18 @@ private static bool TryCreateTexture( return false; } - TextureViewDescriptor textureViewDescriptor = new() + WGPUTextureViewDescriptor textureViewDescriptor = new() { - Format = textureFormat, - Dimension = TextureViewDimension.Dimension2D, - BaseMipLevel = 0, - MipLevelCount = 1, - BaseArrayLayer = 0, - ArrayLayerCount = 1, - Aspect = TextureAspect.All + format = textureFormat, + dimension = WGPUTextureViewDimension._2D, + baseMipLevel = 0, + mipLevelCount = 1, + baseArrayLayer = 0, + arrayLayerCount = 1, + aspect = WGPUTextureAspect.All }; - textureView = flushContext.Api.TextureCreateView(texture, in textureViewDescriptor); + textureView = flushContext.Api.TextureCreateView(texture, &textureViewDescriptor); if (textureView is null) { flushContext.Api.TextureRelease(texture); @@ -650,9 +1185,22 @@ private static bool TryCreateTexture( return true; } + /// + /// Writes a rectangular block of pixels into a texture through the queue. + /// + /// The unmanaged pixel type being uploaded. + /// The active WebGPU flush context. + /// The destination texture. + /// The destination x origin in pixels. + /// The destination y origin in pixels. + /// The region width in pixels. + /// The region height in pixels. + /// The tightly packed source pixels. + /// The error message when the write fails. + /// when the region was written. private static bool TryWriteTextureRegion( WebGPUFlushContext flushContext, - Texture* texture, + WGPUTextureImpl* texture, int x, int y, int width, @@ -661,27 +1209,27 @@ private static bool TryWriteTextureRegion( out string? error) where TPixel : unmanaged { - TextureDataLayout layout = new() + WGPUTexelCopyBufferLayout layout = new() { - Offset = 0, - BytesPerRow = (uint)(width * Unsafe.SizeOf()), - RowsPerImage = (uint)height + offset = 0, + bytesPerRow = (uint)(width * Unsafe.SizeOf()), + rowsPerImage = (uint)height }; fixed (TPixel* pixelPtr = pixels) { - ImageCopyTexture destination = new() + WGPUTexelCopyTextureInfo destination = new() { - Texture = texture, - MipLevel = 0, - Origin = new Origin3D((uint)x, (uint)y, 0), - Aspect = TextureAspect.All + texture = texture, + mipLevel = 0, + origin = new WGPUOrigin3D((uint)x, (uint)y, 0), + aspect = WGPUTextureAspect.All }; - Extent3D extent = new((uint)width, (uint)height, 1); + WGPUExtent3D extent = new((uint)width, (uint)height, 1); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); flushContext.Api.QueueWriteTexture( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, in destination, pixelPtr, (nuint)(pixels.Length * Unsafe.SizeOf()), @@ -695,51 +1243,97 @@ private static bool TryWriteTextureRegion( /// /// Packs the atlas-space origin stored in draw data for sampled image brushes. + /// Unpacked as x = high 16 bits, y = low 16 bits by read_image in fine.wgsl. /// + /// The atlas x origin in pixels. + /// The atlas y origin in pixels. + /// The packed origin word. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint PackImageAtlasOffset(int x, int y) => ((uint)x << 16) | (uint)y; /// /// Packs the sampled image extents stored in draw data for sampled image brushes. + /// Unpacked as width = high 16 bits, height = low 16 bits by read_image in fine.wgsl. /// + /// The entry width in pixels. + /// The entry height in pixels. + /// The packed extents word. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static uint PackImageExtents(int width, int height) => ((uint)width << 16) | (uint)height; /// - /// Packs the texture-format and extend-mode bits consumed by the image sampling shader path. + /// Maps an ImageSharp wrap mode to the shader atlas extend mode. + /// + /// The ImageSharp wrap mode. + /// The shader atlas extend mode. + private static uint MapImageWrapMode(WrapMode mode) + => mode switch + { + WrapMode.Clamp => 0U, // EXTEND_PAD (samples the nearest edge pixel) + WrapMode.Repeat => 1U, // EXTEND_REPEAT + WrapMode.Mirror => 2U, // EXTEND_REFLECT + _ => 3U, // None -> EXTEND_DECAL (transparent outside the source region) + }; + + /// + /// Packs the image sample-info word decoded by read_image in fine.wgsl: + /// bits 0-7 alpha, 8-9 y extend, 10-11 x extend, 14 alpha type, 15 pixel format, + /// and 16 signed-unit numeric encoding. /// - private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExtendMode, uint yExtendMode) + /// The horizontal atlas extend mode. + /// The vertical atlas extend mode. + /// The alpha representation stored by the atlas texels. + /// The mapping between the atlas's native and unit channel values. + /// The packed sample-info word. + private static uint PackImageSampleInfo( + uint xExtendMode, + uint yExtendMode, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) { const uint alpha = 0xFFU; - const uint alphaTypeStraight = 0U; - uint format = textureFormat == TextureFormat.Bgra8Unorm ? 1U : 0U; + const uint formatRgba = 0U; + uint alphaType = alphaRepresentation == PixelAlphaRepresentation.Associated ? 1U : 0U; + uint signedUnit = numericEncoding == WebGPUTargetNumericEncoding.SignedUnit ? 1U : 0U; + + // WebGPU texture sampling returns logical RGBA values regardless of the texture's byte layout. + // The Bgra32/Rgba32 distinction only belongs to CPU upload/readback memory, not shader colors. return alpha | (yExtendMode << 8) | (xExtendMode << 10) - | (alphaTypeStraight << 14) - | (format << 15); + | (alphaType << 14) + | (formatRgba << 15) + | (signedUnit << 16); } - private static WgpuBuffer* CreateAndUploadScalarBuffer( + /// + /// Creates a buffer sized for exactly one value and uploads it. + /// Used for the scene-config header, which binds as both storage and uniform. + /// + /// The unmanaged value type to upload. + /// The active WebGPU flush context. + /// The value to upload. + /// The created buffer. + private static WGPUBufferImpl* CreateAndUploadScalarBuffer( WebGPUFlushContext flushContext, in T value) where T : unmanaged { nuint byteLength = (nuint)Unsafe.SizeOf(); - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = BufferUsage.Storage | BufferUsage.Uniform | BufferUsage.CopyDst, - Size = byteLength + usage = (ulong)(BufferUsage.Storage | BufferUsage.Uniform | BufferUsage.CopyDst), + size = byteLength }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - WgpuBuffer* buffer = flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); using WebGPUHandle.HandleReference queueReference = flushContext.QueueHandle.AcquireReference(); flushContext.Api.QueueWriteBuffer( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, buffer, 0, Unsafe.AsPointer(ref Unsafe.AsRef(in value)), @@ -755,7 +1349,12 @@ private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExten /// Many staging buffers are scratch-only, so the upload branch is intentionally skipped for empty spans. /// The method still creates the buffer because later GPU passes depend on the binding existing for the full flush. /// - private static WgpuBuffer* CreateAndUploadBuffer( + /// The unmanaged element type of the buffer. + /// The active WebGPU flush context. + /// The initial contents to upload; may be empty for scratch buffers. + /// The minimum element capacity to reserve. + /// The created buffer. + private static WGPUBufferImpl* CreateAndUploadBuffer( WebGPUFlushContext flushContext, ReadOnlySpan values, uint minimumLength) @@ -763,15 +1362,15 @@ private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExten { uint elementCount = Math.Max(Math.Max((uint)values.Length, minimumLength), 1U); nuint byteLength = checked(elementCount * (nuint)Unsafe.SizeOf()); - BufferDescriptor descriptor = new() + WGPUBufferDescriptor descriptor = new() { - Usage = BufferUsage.Storage | BufferUsage.CopyDst, - Size = byteLength + usage = (ulong)(BufferUsage.Storage | BufferUsage.CopyDst), + size = byteLength }; using (WebGPUHandle.HandleReference deviceReference = flushContext.DeviceHandle.AcquireReference()) { - WgpuBuffer* buffer = flushContext.Api.DeviceCreateBuffer((Device*)deviceReference.Handle, in descriptor); + WGPUBufferImpl* buffer = flushContext.Api.DeviceCreateBuffer((WGPUDeviceImpl*)deviceReference.Handle, in descriptor); if (!values.IsEmpty) { nuint uploadByteLength = checked((nuint)values.Length * (nuint)Unsafe.SizeOf()); @@ -779,7 +1378,7 @@ private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExten fixed (T* dataPtr = values) { flushContext.Api.QueueWriteBuffer( - (Queue*)queueReference.Handle, + (WGPUQueueImpl*)queueReference.Handle, buffer, 0, dataPtr, @@ -795,10 +1394,26 @@ private static uint PackImageSampleInfo(TextureFormat textureFormat, uint xExten /// Gets the byte length required to bind unmanaged elements, /// preserving WebGPU's non-zero binding rule. /// + /// The unmanaged element type of the binding. + /// The element count; zero is clamped to one. + /// The binding size in bytes. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static nuint GetBindingByteLength(int count) + private static nuint GetBindingByteLength(int count) where T : unmanaged => checked((nuint)Math.Max(count, 1) * (nuint)Unsafe.SizeOf()); + + /// + /// Releases one arena-owned buffer, ignoring handles a failed creation never produced. + /// + /// The API facade used to release the buffer. + /// The buffer to release, or . + private static void ReleaseArenaBuffer(WebGPU api, WGPUBufferImpl* buffer) + { + if (buffer is not null) + { + api.BufferRelease(buffer); + } + } } /// @@ -809,26 +1424,45 @@ internal readonly unsafe struct WebGPUSceneResourceSet /// /// Initializes a new instance of the struct. /// + /// The root scene-config buffer. + /// The packed scene-data buffer. + /// The first pathtag-reduction scratch buffer. + /// The second pathtag-reduction scratch buffer. + /// The pathtag scan scratch buffer. + /// The final pathtag monoid buffer. + /// The per-path bounding-box buffer. + /// The draw reduction buffer. + /// The final draw monoid buffer. + /// The packed info-bin data buffer. + /// The clip input buffer. + /// The clip element buffer. + /// The clip bic (stack-monoid) reduction buffer. + /// The clip bounding-box buffer. + /// The draw bounding-box buffer. + /// The flattened path buffer. + /// The flattened line buffer. + /// The gradient texture view. + /// The image atlas texture view. public WebGPUSceneResourceSet( - WgpuBuffer* headerBuffer, - WgpuBuffer* sceneBuffer, - WgpuBuffer* pathReducedBuffer, - WgpuBuffer* pathReduced2Buffer, - WgpuBuffer* pathReducedScanBuffer, - WgpuBuffer* pathMonoidBuffer, - WgpuBuffer* pathBboxBuffer, - WgpuBuffer* drawReducedBuffer, - WgpuBuffer* drawMonoidBuffer, - WgpuBuffer* infoBinDataBuffer, - WgpuBuffer* clipInputBuffer, - WgpuBuffer* clipElementBuffer, - WgpuBuffer* clipBicBuffer, - WgpuBuffer* clipBboxBuffer, - WgpuBuffer* drawBboxBuffer, - WgpuBuffer* pathBuffer, - WgpuBuffer* lineBuffer, - TextureView* gradientTextureView, - TextureView* imageAtlasTextureView) + WGPUBufferImpl* headerBuffer, + WGPUBufferImpl* sceneBuffer, + WGPUBufferImpl* pathReducedBuffer, + WGPUBufferImpl* pathReduced2Buffer, + WGPUBufferImpl* pathReducedScanBuffer, + WGPUBufferImpl* pathMonoidBuffer, + WGPUBufferImpl* pathBboxBuffer, + WGPUBufferImpl* drawReducedBuffer, + WGPUBufferImpl* drawMonoidBuffer, + WGPUBufferImpl* infoBinDataBuffer, + WGPUBufferImpl* clipInputBuffer, + WGPUBufferImpl* clipElementBuffer, + WGPUBufferImpl* clipBicBuffer, + WGPUBufferImpl* clipBboxBuffer, + WGPUBufferImpl* drawBboxBuffer, + WGPUBufferImpl* pathBuffer, + WGPUBufferImpl* lineBuffer, + WGPUTextureViewImpl* gradientTextureView, + WGPUTextureViewImpl* imageAtlasTextureView) { this.HeaderBuffer = headerBuffer; this.SceneBuffer = sceneBuffer; @@ -854,97 +1488,97 @@ public WebGPUSceneResourceSet( /// /// Gets the root scene-config buffer bound at slot zero by most staged-scene shaders. /// - public WgpuBuffer* HeaderBuffer { get; } + public WGPUBufferImpl* HeaderBuffer { get; } /// /// Gets the packed scene-data buffer produced by the CPU encoder. /// - public WgpuBuffer* SceneBuffer { get; } + public WGPUBufferImpl* SceneBuffer { get; } /// /// Gets the first pathtag-reduction scratch buffer. /// - public WgpuBuffer* PathReducedBuffer { get; } + public WGPUBufferImpl* PathReducedBuffer { get; } /// /// Gets the second pathtag-reduction scratch buffer. /// - public WgpuBuffer* PathReduced2Buffer { get; } + public WGPUBufferImpl* PathReduced2Buffer { get; } /// /// Gets the pathtag-scan prefix scratch buffer. /// - public WgpuBuffer* PathReducedScanBuffer { get; } + public WGPUBufferImpl* PathReducedScanBuffer { get; } /// /// Gets the final pathtag monoid buffer. /// - public WgpuBuffer* PathMonoidBuffer { get; } + public WGPUBufferImpl* PathMonoidBuffer { get; } /// /// Gets the per-path bounding-box buffer. /// - public WgpuBuffer* PathBboxBuffer { get; } + public WGPUBufferImpl* PathBboxBuffer { get; } /// /// Gets the draw-reduction scratch buffer. /// - public WgpuBuffer* DrawReducedBuffer { get; } + public WGPUBufferImpl* DrawReducedBuffer { get; } /// /// Gets the final draw monoid buffer. /// - public WgpuBuffer* DrawMonoidBuffer { get; } + public WGPUBufferImpl* DrawMonoidBuffer { get; } /// /// Gets the combined info/bin-data scratch buffer. /// - public WgpuBuffer* InfoBinDataBuffer { get; } + public WGPUBufferImpl* InfoBinDataBuffer { get; } /// /// Gets the clip input buffer. /// - public WgpuBuffer* ClipInputBuffer { get; } + public WGPUBufferImpl* ClipInputBuffer { get; } /// /// Gets the clip element buffer. /// - public WgpuBuffer* ClipElementBuffer { get; } + public WGPUBufferImpl* ClipElementBuffer { get; } /// /// Gets the clip bic reduction buffer. /// - public WgpuBuffer* ClipBicBuffer { get; } + public WGPUBufferImpl* ClipBicBuffer { get; } /// /// Gets the reduced clip bounding-box buffer. /// - public WgpuBuffer* ClipBboxBuffer { get; } + public WGPUBufferImpl* ClipBboxBuffer { get; } /// /// Gets the per-draw bounding-box buffer. /// - public WgpuBuffer* DrawBboxBuffer { get; } + public WGPUBufferImpl* DrawBboxBuffer { get; } /// /// Gets the per-path scheduling buffer. /// - public WgpuBuffer* PathBuffer { get; } + public WGPUBufferImpl* PathBuffer { get; } /// /// Gets the flattened line buffer. /// - public WgpuBuffer* LineBuffer { get; } + public WGPUBufferImpl* LineBuffer { get; } /// /// Gets the sampled gradient-ramp texture view. /// - public TextureView* GradientTextureView { get; } + public WGPUTextureViewImpl* GradientTextureView { get; } /// /// Gets the sampled image-atlas texture view. /// - public TextureView* ImageAtlasTextureView { get; } + public WGPUTextureViewImpl* ImageAtlasTextureView { get; } } /// @@ -957,29 +1591,56 @@ public WebGPUSceneResourceSet( /// internal sealed unsafe class WebGPUSceneResourceArena { + private int disposed; + + /// + /// Initializes a new instance of the class. + /// + /// The WebGPU API instance used to release the buffers. + /// The device handle the buffers were created on. + /// The buffer sizes the arena was allocated with. + /// The combined info/bin-data buffer capacity in bytes. + /// The scene-data buffer capacity in bytes. + /// The root scene-config buffer. + /// The packed scene-data buffer. + /// The first pathtag-reduction scratch buffer. + /// The second pathtag-reduction scratch buffer. + /// The pathtag scan scratch buffer. + /// The final pathtag monoid buffer. + /// The per-path bounding-box buffer. + /// The draw reduction buffer. + /// The final draw monoid buffer. + /// The combined info/bin-data buffer. + /// The clip input buffer. + /// The clip element buffer. + /// The clip bic (stack-monoid) reduction buffer. + /// The clip bounding-box buffer. + /// The draw bounding-box buffer. + /// The per-path scheduling buffer. + /// The flattened line buffer. public WebGPUSceneResourceArena( WebGPU api, WebGPUDeviceHandle device, WebGPUSceneBufferSizes capacitySizes, nuint infoBinDataByteCapacity, nuint sceneByteCapacity, - WgpuBuffer* headerBuffer, - WgpuBuffer* sceneBuffer, - WgpuBuffer* pathReducedBuffer, - WgpuBuffer* pathReduced2Buffer, - WgpuBuffer* pathReducedScanBuffer, - WgpuBuffer* pathMonoidBuffer, - WgpuBuffer* pathBboxBuffer, - WgpuBuffer* drawReducedBuffer, - WgpuBuffer* drawMonoidBuffer, - WgpuBuffer* infoBinDataBuffer, - WgpuBuffer* clipInputBuffer, - WgpuBuffer* clipElementBuffer, - WgpuBuffer* clipBicBuffer, - WgpuBuffer* clipBboxBuffer, - WgpuBuffer* drawBboxBuffer, - WgpuBuffer* pathBuffer, - WgpuBuffer* lineBuffer) + WGPUBufferImpl* headerBuffer, + WGPUBufferImpl* sceneBuffer, + WGPUBufferImpl* pathReducedBuffer, + WGPUBufferImpl* pathReduced2Buffer, + WGPUBufferImpl* pathReducedScanBuffer, + WGPUBufferImpl* pathMonoidBuffer, + WGPUBufferImpl* pathBboxBuffer, + WGPUBufferImpl* drawReducedBuffer, + WGPUBufferImpl* drawMonoidBuffer, + WGPUBufferImpl* infoBinDataBuffer, + WGPUBufferImpl* clipInputBuffer, + WGPUBufferImpl* clipElementBuffer, + WGPUBufferImpl* clipBicBuffer, + WGPUBufferImpl* clipBboxBuffer, + WGPUBufferImpl* drawBboxBuffer, + WGPUBufferImpl* pathBuffer, + WGPUBufferImpl* lineBuffer) { this.Api = api; this.Device = device; @@ -1005,53 +1666,150 @@ public WebGPUSceneResourceArena( this.LineBuffer = lineBuffer; } + /// + /// Gets the WebGPU API instance used to release the buffers. + /// public WebGPU Api { get; } + /// + /// Gets the device handle the buffers were created on; reuse requires the same device. + /// public WebGPUDeviceHandle Device { get; } + /// + /// Gets the buffer sizes the arena was allocated with. + /// public WebGPUSceneBufferSizes CapacitySizes { get; } + /// + /// Gets the combined info/bin-data buffer capacity in bytes. + /// public nuint InfoBinDataByteCapacity { get; } + /// + /// Gets the scene-data buffer capacity in bytes. + /// public nuint SceneByteCapacity { get; } - public WgpuBuffer* HeaderBuffer { get; } + /// + /// Gets the root scene-config buffer. + /// + public WGPUBufferImpl* HeaderBuffer { get; private set; } - public WgpuBuffer* SceneBuffer { get; } + /// + /// Gets the packed scene-data buffer. + /// + public WGPUBufferImpl* SceneBuffer { get; private set; } - public WgpuBuffer* PathReducedBuffer { get; } + /// + /// Gets the first pathtag-reduction scratch buffer. + /// + public WGPUBufferImpl* PathReducedBuffer { get; private set; } - public WgpuBuffer* PathReduced2Buffer { get; } + /// + /// Gets the second pathtag-reduction scratch buffer. + /// + public WGPUBufferImpl* PathReduced2Buffer { get; private set; } - public WgpuBuffer* PathReducedScanBuffer { get; } + /// + /// Gets the pathtag scan scratch buffer. + /// + public WGPUBufferImpl* PathReducedScanBuffer { get; private set; } - public WgpuBuffer* PathMonoidBuffer { get; } + /// + /// Gets the final pathtag monoid buffer. + /// + public WGPUBufferImpl* PathMonoidBuffer { get; private set; } - public WgpuBuffer* PathBboxBuffer { get; } + /// + /// Gets the per-path bounding-box buffer. + /// + public WGPUBufferImpl* PathBboxBuffer { get; private set; } - public WgpuBuffer* DrawReducedBuffer { get; } + /// + /// Gets the draw reduction buffer. + /// + public WGPUBufferImpl* DrawReducedBuffer { get; private set; } - public WgpuBuffer* DrawMonoidBuffer { get; } + /// + /// Gets the final draw monoid buffer. + /// + public WGPUBufferImpl* DrawMonoidBuffer { get; private set; } - public WgpuBuffer* InfoBinDataBuffer { get; } + /// + /// Gets the combined info/bin-data buffer. + /// + public WGPUBufferImpl* InfoBinDataBuffer { get; private set; } - public WgpuBuffer* ClipInputBuffer { get; } + /// + /// Gets the clip input buffer. + /// + public WGPUBufferImpl* ClipInputBuffer { get; private set; } - public WgpuBuffer* ClipElementBuffer { get; } + /// + /// Gets the clip element buffer. + /// + public WGPUBufferImpl* ClipElementBuffer { get; private set; } - public WgpuBuffer* ClipBicBuffer { get; } + /// + /// Gets the clip bic (stack-monoid) reduction buffer. + /// + public WGPUBufferImpl* ClipBicBuffer { get; private set; } - public WgpuBuffer* ClipBboxBuffer { get; } + /// + /// Gets the clip bounding-box buffer. + /// + public WGPUBufferImpl* ClipBboxBuffer { get; private set; } - public WgpuBuffer* DrawBboxBuffer { get; } + /// + /// Gets the draw bounding-box buffer. + /// + public WGPUBufferImpl* DrawBboxBuffer { get; private set; } - public WgpuBuffer* PathBuffer { get; } + /// + /// Gets the per-path scheduling buffer. + /// + public WGPUBufferImpl* PathBuffer { get; private set; } - public WgpuBuffer* LineBuffer { get; } + /// + /// Gets the flattened line buffer. + /// + public WGPUBufferImpl* LineBuffer { get; private set; } + + /// + /// Gets or sets the encoded scene whose packed stream currently resides in . + /// + /// + /// Reference identity plus prove residency: the packed + /// stream only changes through the scene's tracked word mutations after encoding. + /// + public object? UploadedScene { get; set; } + + /// + /// Gets or sets the value captured when the + /// resident stream was last uploaded or patched. + /// + public int UploadedSceneVersion { get; set; } + + /// + /// Gets or sets the info-word prefix length used by the resident brush upload. Brush words + /// start immediately after this prefix, so a different prefix relocates them. + /// + public int UploadedBrushInfoWordCount { get; set; } + + /// + /// Gets or sets the pixel type the resident recolor payloads were specialized for. + /// + public Type? UploadedPixelType { get; set; } /// /// Returns true if every buffer fits the required sizes for this scene. /// + /// The active WebGPU flush context. + /// The required buffer sizes. + /// The required info-bin data length in bytes. + /// The required scene-data length in bytes. + /// when the arena can be reused. public bool CanReuse(WebGPUFlushContext flushContext, WebGPUSceneBufferSizes bufferSizes, nuint infoBinDataByteLength, nuint sceneByteLength) => ReferenceEquals(this.Device, flushContext.DeviceHandle) && this.HeaderBuffer is not null && @@ -1074,86 +1832,193 @@ this.SceneBuffer is not null && bufferSizes.Lines.ByteLength <= this.CapacitySizes.Lines.ByteLength; /// - /// Releases all GPU buffers owned by this arena. + /// Releases all GPU buffers owned by this arena. Arena caches hand instances between + /// owners with atomic exchanges, so disposal can race; the exchanged flag guarantees the + /// native buffers are released exactly once, and nulling each handle turns any stale + /// reference into a failed instead of a native use-after-free. /// + /// The arena to dispose. public static void Dispose(WebGPUSceneResourceArena? arena) { - if (arena is null || arena.HeaderBuffer is null) + if (arena is null || Interlocked.Exchange(ref arena.disposed, 1) == 1) { return; } WebGPU api = arena.Api; api.BufferRelease(arena.HeaderBuffer); + arena.HeaderBuffer = null; api.BufferRelease(arena.SceneBuffer); + arena.SceneBuffer = null; api.BufferRelease(arena.PathReducedBuffer); + arena.PathReducedBuffer = null; api.BufferRelease(arena.PathReduced2Buffer); + arena.PathReduced2Buffer = null; api.BufferRelease(arena.PathReducedScanBuffer); + arena.PathReducedScanBuffer = null; api.BufferRelease(arena.PathMonoidBuffer); + arena.PathMonoidBuffer = null; api.BufferRelease(arena.PathBboxBuffer); + arena.PathBboxBuffer = null; api.BufferRelease(arena.DrawReducedBuffer); + arena.DrawReducedBuffer = null; api.BufferRelease(arena.DrawMonoidBuffer); + arena.DrawMonoidBuffer = null; api.BufferRelease(arena.InfoBinDataBuffer); + arena.InfoBinDataBuffer = null; api.BufferRelease(arena.ClipInputBuffer); + arena.ClipInputBuffer = null; api.BufferRelease(arena.ClipElementBuffer); + arena.ClipElementBuffer = null; api.BufferRelease(arena.ClipBicBuffer); + arena.ClipBicBuffer = null; api.BufferRelease(arena.ClipBboxBuffer); + arena.ClipBboxBuffer = null; api.BufferRelease(arena.DrawBboxBuffer); + arena.DrawBboxBuffer = null; api.BufferRelease(arena.PathBuffer); + arena.PathBuffer = null; api.BufferRelease(arena.LineBuffer); + arena.LineBuffer = null; } } /// /// Flush-scoped bump allocator heads shared by the staged-scene scheduling passes. /// +/// +/// Mirrors BumpAllocators in Shared/bump.wgsl (each field is an atomic<u32> +/// there); field order must match. The C# side reads this block back to detect overflow +/// and grow the scratch buffers before retrying the render. +/// [StructLayout(LayoutKind.Sequential)] internal struct GpuSceneBumpAllocators { + /// + /// The bitmask of STAGE_* flags for stages that failed allocation. + /// public uint Failed; + + /// + /// The bin-data word allocation head within the combined info/bin-data buffer. + /// public uint Binning; + + /// + /// The per-tile command list word allocation head. + /// public uint Ptcl; + + /// + /// The sparse path-row record allocation head. + /// public uint PathRows; + + /// + /// The tile record allocation head. + /// public uint Tile; + + /// + /// The segment-count record allocation head. + /// public uint SegCounts; + + /// + /// The segment record allocation head. + /// public uint Segments; + + /// + /// The allocation head for blend stack slots spilled past BLEND_STACK_SPLIT. + /// public uint BlendSpill; + + /// + /// The flattened line record allocation head. + /// public uint Lines; } /// /// Prefix-scan monoid emitted from the packed path-tag stream. /// +/// +/// Mirrors TagMonoid in Shared/pathtag.wgsl; field order must match. +/// Each field counts the stream elements of its kind preceding the current position. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuTagMonoid { - public GpuTagMonoid(uint transIndex, uint pathSegmentIndex, uint pathSegmentOffset, uint styleIndex, uint pathIndex) + /// + /// Initializes a new instance of the struct. + /// + /// The transform marker count. + /// The path data stream offset in words. + /// The style marker count premultiplied by the style word size. + /// The path marker count. + public GpuTagMonoid(uint transIndex, uint pathSegmentOffset, uint styleIndex, uint pathIndex) { this.TransIndex = transIndex; - this.PathSegmentIndex = pathSegmentIndex; this.PathSegmentOffset = pathSegmentOffset; this.StyleIndex = styleIndex; this.PathIndex = pathIndex; } + /// + /// Gets the transform marker count (index into the transform stream). + /// public uint TransIndex { get; } - public uint PathSegmentIndex { get; } - + /// + /// Gets the offset into the path data stream, in u32 words. + /// public uint PathSegmentOffset { get; } + /// + /// Gets the style marker count premultiplied by STYLE_SIZE_IN_WORDS. + /// public uint StyleIndex { get; } + /// + /// Gets the path marker count (index of the current path). + /// public uint PathIndex { get; } } /// -/// Per-path bounding box and metadata consumed by later scheduling passes. +/// Per-path bounding box and scheduling data written by the flatten pass. /// +/// +/// Mirrors PathBbox in Shared/bbox.wgsl; field order and the 48-byte stride must +/// match. The padding word keeps at offset 32, satisfying the +/// 16-byte alignment WGSL requires for vec4<f32>. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuPathBbox { - public GpuPathBbox(int x0, int y0, int x1, int y1, uint drawFlags, uint transIndex) + /// + /// Initializes a new instance of the struct. + /// + /// The left edge of the transformed integer path bounds. + /// The top edge of the transformed integer path bounds. + /// The right edge of the transformed integer path bounds. + /// The bottom edge of the transformed integer path bounds. + /// The draw flags associated with the path. + /// The transform index associated with the path. + /// The aliased coverage threshold for the path. + /// The reserved layout slot matching PathBbox._padding in bbox.wgsl. + /// The root-target-local raster interest rectangle. + public GpuPathBbox( + int x0, + int y0, + int x1, + int y1, + uint drawFlags, + uint transIndex, + float coverageThreshold, + uint padding, + Vector4 interest) { this.X0 = x0; this.Y0 = y0; @@ -1161,124 +2026,288 @@ public GpuPathBbox(int x0, int y0, int x1, int y1, uint drawFlags, uint transInd this.Y1 = y1; this.DrawFlags = drawFlags; this.TransIndex = transIndex; + this.CoverageThreshold = coverageThreshold; + this.Padding = padding; + this.Interest = interest; } + /// + /// Gets the left edge of the transformed integer path bounds. + /// public int X0 { get; } + /// + /// Gets the top edge of the transformed integer path bounds. + /// public int Y0 { get; } + /// + /// Gets the right edge of the transformed integer path bounds. + /// public int X1 { get; } + /// + /// Gets the bottom edge of the transformed integer path bounds. + /// public int Y1 { get; } + /// + /// Gets the draw flags associated with this path. + /// public uint DrawFlags { get; } + /// + /// Gets the transform index associated with this path. + /// public uint TransIndex { get; } + + /// + /// Gets the aliased coverage threshold for this path. + /// + public float CoverageThreshold { get; } + + /// + /// Gets the reserved layout slot matching PathBbox._padding in bbox.wgsl. + /// + public uint Padding { get; } + + /// + /// Gets the root-target-local raster interest rectangle. + /// + public Vector4 Interest { get; } } /// /// Clip input record mapping one draw object to the path that defines its clip stack entry. /// +/// +/// Mirrors ClipInp in Shared/clip.wgsl; field order must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuClipInp { - public GpuClipInp(uint drawIndex, int pathIndex) + /// + /// Initializes a new instance of the struct. + /// + /// The draw object index for the clip record. + /// The begin-clip path index, or the bitwise-not end-clip draw object index. + /// The ImageSharp render clip operation for begin-clip records. + public GpuClipInp(uint drawIndex, int pathIndex, uint operation) { this.DrawIndex = drawIndex; this.PathIndex = pathIndex; + this.Operation = operation; } + /// + /// Gets the draw object index for the clip record. + /// public uint DrawIndex { get; } + /// + /// Gets the begin-clip path index, or the bitwise-not end-clip draw object index. + /// public int PathIndex { get; } + + /// + /// Gets the ImageSharp render clip operation. + /// + /// + /// Vello's source clip record stores only DrawIndex and PathIndex. + /// This field is ImageSharp's extension for . + /// + public uint Operation { get; } } /// -/// Binary interval combination record used by the clip reduction passes. +/// Stack-monoid element (the "bicyclic semigroup") used by the clip reduction passes. /// +/// +/// Mirrors Bic in Shared/clip.wgsl; field order must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuBic { + /// + /// Initializes a new instance of the struct. + /// + /// The number of unmatched end-clip records (pops) in the range. + /// The number of unmatched begin-clip records (pushes) in the range. public GpuBic(uint a, uint b) { this.A = a; this.B = b; } + /// + /// Gets the number of unmatched end-clip records (pops) in the range. + /// public uint A { get; } + /// + /// Gets the number of unmatched begin-clip records (pushes) in the range. + /// public uint B { get; } } /// /// Reduced clip element containing the parent link and accumulated clip bounds. /// +/// +/// Mirrors ClipEl in Shared/clip.wgsl; field order and the 32-byte stride must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuClipElement { + // ClipEl in clip.wgsl places bbox at offset 16 because vec4 has 16-byte + // alignment in storage buffers. These fields are part of the GPU ABI. + private readonly uint padding0; + private readonly uint padding1; + private readonly uint padding2; + + /// + /// Initializes a new instance of the struct. + /// + /// The index of the enclosing clip stream element. + /// The clip path bounds used to narrow descendant draw bounds. public GpuClipElement(uint parentIndex, Vector4 bbox) { this.ParentIndex = parentIndex; + this.padding0 = 0; + this.padding1 = 0; + this.padding2 = 0; this.Bbox = bbox; } + /// + /// Gets the index of the enclosing clip stream element. + /// public uint ParentIndex { get; } + /// + /// Gets the clip path bounds used to narrow descendant draw bounds. + /// public Vector4 Bbox { get; } } /// /// Bounding box emitted per draw object after draw reduction. /// +/// +/// Stored as the raw vec4<f32> elements of the draw_bboxes binding +/// written by draw_leaf.wgsl. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuDrawBbox { + /// + /// Initializes a new instance of the struct. + /// + /// The draw bounds as (x0, y0, x1, y1) in pixels. public GpuDrawBbox(Vector4 bbox) => this.Bbox = bbox; + /// + /// Gets the draw bounds as (x0, y0, x1, y1) in pixels. + /// public Vector4 Bbox { get; } } /// /// One bin-header entry describing how many elements belong to a scheduling chunk. /// +/// +/// Mirrors BinHeader in coarse.wgsl; the headers are stored as raw word pairs +/// in the tail of the combined info/bin-data buffer. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuSceneBinHeader { + /// + /// Initializes a new instance of the struct. + /// + /// The number of draw elements recorded for the bin. + /// The word offset of the bin's element list within the bin data. public GpuSceneBinHeader(uint elementCount, uint chunkOffset) { this.ElementCount = elementCount; this.ChunkOffset = chunkOffset; } + /// + /// Gets the number of draw elements recorded for the bin. + /// public uint ElementCount { get; } + /// + /// Gets the word offset of the bin's element list within the bin data. + /// public uint ChunkOffset { get; } } /// /// Indirect-dispatch argument buffer layout used by later scheduling passes. /// +/// +/// Mirrors IndirectCount in Shared/bump.wgsl, which has only the three count +/// words; is a C#-side trailing pad that rounds the binding size +/// up to 16 bytes and is never read by the shaders. +/// [StructLayout(LayoutKind.Sequential)] internal struct GpuSceneIndirectCount { + /// + /// The X workgroup count for the indirect dispatch. + /// public uint CountX; + + /// + /// The Y workgroup count for the indirect dispatch. + /// public uint CountY; + + /// + /// The Z workgroup count for the indirect dispatch. + /// public uint CountZ; + + /// + /// The unused trailing pad word; not part of the WGSL struct. + /// public uint Pad0; } /// /// Scene-buffer layout metadata consumed by every staged-scene shader. /// +/// +/// These fields are embedded inline in Config in Shared/config.wgsl, between +/// base_color and lines_size (n_drawobj through style_base); +/// field order must match. All offsets are in u32 words. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuSceneLayout { + /// + /// Initializes a new instance of the struct. + /// + /// The number of draw objects in the scene. + /// The number of paths in the scene. + /// The number of clip begin/end records in the scene. + /// The start of the bump-allocated bin data within the combined info/bin-data buffer. + /// The start of auxiliary brush data within the combined info/bin-data buffer. + /// The start of the bump-allocated region of the PTCL buffer. + /// The scene-buffer offset of the path tag stream. + /// The scene-buffer offset of the path data stream. + /// The scene-buffer offset of the draw tag stream. + /// The scene-buffer offset of the draw data stream. + /// The scene-buffer offset of the transform stream. + /// The scene-buffer offset of the style stream. public GpuSceneLayout( uint drawObjectCount, uint pathCount, uint clipCount, uint binDataStart, - uint pathGradientDataBase, + uint brushDataBase, uint ptclDynamicStart, uint pathTagBase, uint pathDataBase, @@ -1291,7 +2320,7 @@ public GpuSceneLayout( this.PathCount = pathCount; this.ClipCount = clipCount; this.BinDataStart = binDataStart; - this.PathGradientDataBase = pathGradientDataBase; + this.BrushDataBase = brushDataBase; this.PtclDynamicStart = ptclDynamicStart; this.PathTagBase = pathTagBase; this.PathDataBase = pathDataBase; @@ -1301,37 +2330,98 @@ public GpuSceneLayout( this.StyleBase = styleBase; } + /// + /// Gets the number of draw objects in the scene. + /// public uint DrawObjectCount { get; } + /// + /// Gets the number of paths in the scene. + /// public uint PathCount { get; } + /// + /// Gets the number of clip begin/end records in the scene. + /// public uint ClipCount { get; } + /// + /// Gets the start of the bump-allocated bin data within the combined info/bin-data buffer. + /// public uint BinDataStart { get; } - public uint PathGradientDataBase { get; } + /// + /// Gets the start of the path-gradient edge data within the combined info/bin-data buffer. + /// + public uint BrushDataBase { get; } + /// + /// Gets the start of the bump-allocated region of the PTCL buffer. + /// public uint PtclDynamicStart { get; } + /// + /// Gets the scene-buffer offset of the path tag stream. + /// public uint PathTagBase { get; } + /// + /// Gets the scene-buffer offset of the path data stream. + /// public uint PathDataBase { get; } + /// + /// Gets the scene-buffer offset of the draw tag stream. + /// public uint DrawTagBase { get; } + /// + /// Gets the scene-buffer offset of the draw data stream. + /// public uint DrawDataBase { get; } + /// + /// Gets the scene-buffer offset of the transform stream. + /// public uint TransformBase { get; } + /// + /// Gets the scene-buffer offset of the style stream. + /// public uint StyleBase { get; } } /// /// Root scene configuration block bound at slot zero for most staged-scene shaders. /// +/// +/// Mirrors Config in Shared/config.wgsl; field order and sizes must match. +/// The embedded expands to the n_drawobj through +/// style_base fields of that struct. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuSceneConfig { + /// + /// Initializes a new instance of the struct. + /// + /// The render width in 16x16 pixel tiles. + /// The render height in 16x16 pixel tiles. + /// The target width in pixels. + /// The target height in pixels. + /// The first global tile row rendered by this attempt. + /// The number of real tile rows rendered by this attempt. + /// The packed RGBA8 base color applied by the fine pass. + /// The scene-buffer layout metadata. + /// The flattened line buffer capacity in elements. + /// The bin-data scratch capacity in words. + /// The sparse path-row buffer capacity in elements. + /// The path-tile buffer capacity in elements. + /// The segment-count buffer capacity in elements. + /// The segment buffer capacity in elements. + /// The blend-spill buffer capacity in slots. + /// The PTCL buffer capacity in words. + /// The scene-wide aliased coverage threshold. public GpuSceneConfig( uint widthInTiles, uint heightInTiles, @@ -1370,12 +2460,24 @@ public GpuSceneConfig( this.FineCoverageThreshold = fineCoverageThreshold; } + /// + /// Gets the render width in 16x16 pixel tiles. + /// public uint WidthInTiles { get; } + /// + /// Gets the render height in 16x16 pixel tiles. + /// public uint HeightInTiles { get; } + /// + /// Gets the target width in pixels. + /// public uint TargetWidth { get; } + /// + /// Gets the target height in pixels. + /// public uint TargetHeight { get; } /// @@ -1388,12 +2490,24 @@ public GpuSceneConfig( /// public uint ChunkTileHeight { get; } + /// + /// Gets the packed RGBA8 (MSB order) base color applied by the fine pass. + /// public uint BaseColor { get; } + /// + /// Gets the scene-buffer layout metadata. + /// public GpuSceneLayout Layout { get; } + /// + /// Gets the flattened line buffer capacity in elements. + /// public uint LinesSize { get; } + /// + /// Gets the bin-data scratch capacity in words. + /// public uint BinningSize { get; } /// @@ -1401,14 +2515,29 @@ public GpuSceneConfig( /// public uint PathRowsSize { get; } + /// + /// Gets the path-tile buffer capacity in elements. + /// public uint TilesSize { get; } + /// + /// Gets the segment-count buffer capacity in elements. + /// public uint SegCountsSize { get; } + /// + /// Gets the segment buffer capacity in elements. + /// public uint SegmentsSize { get; } + /// + /// Gets the blend-spill buffer capacity in slots. + /// public uint BlendSize { get; } + /// + /// Gets the PTCL buffer capacity in words. + /// public uint PtclSize { get; } /// @@ -1417,82 +2546,29 @@ public GpuSceneConfig( public float FineCoverageThreshold { get; } } -/// -/// Encoded draw-tag constants matching the staged-scene shader contract. -/// -internal static class GpuSceneDrawTag -{ - // These values are not a plain enum because each word also encodes the path-count, clip-count, - // scene-word-count, and info-word-count increments consumed by the scan/reduction stages. - public const uint Nop = 0U; - public const uint FillColor = 0x44U; - public const uint FillRecolor = 0x4CU; - public const uint FillLinGradient = 0x114U; - public const uint FillRadGradient = 0x29CU; - public const uint FillEllipticGradient = 0x1DCU; - public const uint FillSweepGradient = 0x254U; - public const uint FillPathGradient = 0x50U; - public const uint FillImage = 0x294U; - public const uint BeginClip = 0x49U; - public const uint EndClip = 0x21U; - public const uint FillInfoFlagsFillRuleBit = 1U; - - /// - /// Decodes one packed draw tag into the additive monoid scanned by later scheduling passes. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static GpuSceneDrawMonoid Map(uint tagWord) - => new( - tagWord != Nop ? 1U : 0U, - tagWord & 1U, - (tagWord >> 2) & 0x07U, - (tagWord >> 6) & 0x0FU); -} - -/// -/// Additive monoid scanned over the draw-tag stream to derive scene offsets for later stages. -/// -[StructLayout(LayoutKind.Sequential)] -internal readonly struct GpuSceneDrawMonoid -{ - public GpuSceneDrawMonoid(uint pathIndex, uint clipIndex, uint sceneOffset, uint infoOffset) - { - this.PathIndex = pathIndex; - this.ClipIndex = clipIndex; - this.SceneOffset = sceneOffset; - this.InfoOffset = infoOffset; - } - - public uint PathIndex { get; } - - public uint ClipIndex { get; } - - public uint SceneOffset { get; } - - public uint InfoOffset { get; } - - /// - /// Combines two draw monoids by adding each offset/count component independently. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static GpuSceneDrawMonoid Combine(in GpuSceneDrawMonoid a, in GpuSceneDrawMonoid b) - => new( - a.PathIndex + b.PathIndex, - a.ClipIndex + b.ClipIndex, - a.SceneOffset + b.SceneOffset, - a.InfoOffset + b.InfoOffset); -} - /// /// Per-path scheduling record used after draw and clip reduction have established final bounds. /// +/// +/// Mirrors Path in Shared/tile.wgsl; field order and the 32-byte stride must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuScenePath { + // Path in tile.wgsl has a vec4 followed by one u32 and therefore a + // 32-byte array stride. The padding keeps reusable GPU buffer sizing exact. private readonly uint padding0; private readonly uint padding1; private readonly uint padding2; + /// + /// Initializes a new instance of the struct. + /// + /// The minimum x of the path bounds in tiles. + /// The minimum y of the path bounds in tiles. + /// The maximum x of the path bounds in tiles. + /// The maximum y of the path bounds in tiles. + /// The first sparse row record owned by the path. public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, uint rowOffset) { this.BboxMinX = bboxMinX; @@ -1505,12 +2581,24 @@ public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, this.padding2 = 0; } + /// + /// Gets the minimum x of the path bounds in tiles. + /// public uint BboxMinX { get; } + /// + /// Gets the minimum y of the path bounds in tiles. + /// public uint BboxMinY { get; } + /// + /// Gets the maximum x of the path bounds in tiles. + /// public uint BboxMaxX { get; } + /// + /// Gets the maximum y of the path bounds in tiles. + /// public uint BboxMaxY { get; } /// @@ -1522,6 +2610,10 @@ public GpuScenePath(uint bboxMinX, uint bboxMinY, uint bboxMaxX, uint bboxMaxY, /// /// Per-path sparse row record used to allocate tiles only for the x-span actually touched on one tile row. /// +/// +/// Mirrors PathRow (and its AtomicPathRow view) in Shared/tile.wgsl; +/// field order must match. +/// [StructLayout(LayoutKind.Sequential)] internal struct GpuPathRow { @@ -1564,11 +2656,22 @@ public GpuPathRow(uint minTileX, uint maxTileX, int backdrop, uint tileOffset) /// /// Flattened line record emitted from the path stream for segment-counting and tiling. /// +/// +/// Mirrors LineSoup in Shared/segment.wgsl; field order and the 24-byte stride must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuSceneLine { + // LineSoup in segment.wgsl has a u32 followed by vec2 values. WGSL + // aligns the first vec2 to offset 8, so this field is part of the buffer ABI. private readonly uint padding0; + /// + /// Initializes a new instance of the struct. + /// + /// The index of the path that produced the line. + /// The line start point in pixels. + /// The line end point in pixels. public GpuSceneLine(uint pathIndex, Vector2 point0, Vector2 point1) { this.PathIndex = pathIndex; @@ -1577,55 +2680,105 @@ public GpuSceneLine(uint pathIndex, Vector2 point0, Vector2 point1) this.Point1 = point1; } + /// + /// Gets the index of the path that produced this line. + /// public uint PathIndex { get; } + /// + /// Gets the line start point in pixels. + /// public Vector2 Point0 { get; } + /// + /// Gets the line end point in pixels. + /// public Vector2 Point1 { get; } } /// /// Per-tile path record containing the backdrop and either a segment count or a segment-list index. /// +/// +/// Mirrors Tile in Shared/tile.wgsl; field order must match. +/// [StructLayout(LayoutKind.Sequential)] internal struct GpuPathTile { + /// + /// Initializes a new instance of the struct. + /// + /// The winding number carried into the tile's left edge. + /// The segment count, or the bit-inverted segment-list index after coarse rasterization. public GpuPathTile(int backdrop, uint segmentCountOrIndex) { this.Backdrop = backdrop; this.SegmentCountOrIndex = segmentCountOrIndex; } + /// + /// The winding number carried into the tile's left edge. + /// public int Backdrop; + /// + /// The segment count up to coarse rasterization, then the bit-inverted segment-list + /// index; the inversion lets path tiling detect whether the tile was allocated. + /// public uint SegmentCountOrIndex; } /// /// Per-line segment-count record emitted by the path-count stage. /// +/// +/// Mirrors SegmentCount in Shared/segment.wgsl; field order must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuSegmentCount { + /// + /// Initializes a new instance of the struct. + /// + /// The index of the source line in the flattened line buffer. + /// The packed segment indices; see . public GpuSegmentCount(uint lineIndex, uint counts) { this.LineIndex = lineIndex; this.Counts = counts; } + /// + /// Gets the index of the source line in the flattened line buffer. + /// public uint LineIndex { get; } + /// + /// Gets two packed counts: the low 16 bits index the segment within its line and + /// the high 16 bits index the segment within its segment slice. + /// public uint Counts { get; } } /// /// Final per-segment record consumed by the fine rasterization stage. /// +/// +/// Mirrors Segment in Shared/segment.wgsl; field order and the 24-byte stride must match. +/// [StructLayout(LayoutKind.Sequential)] internal readonly struct GpuPathSegment { + // Segment in segment.wgsl has two vec2 values followed by one f32 and + // a 24-byte array stride. The final slot preserves that stride from C#. private readonly float padding0; + /// + /// Initializes a new instance of the struct. + /// + /// The segment start point relative to the tile origin. + /// The segment end point relative to the tile origin. + /// The tile-relative y at which the segment meets the tile's left edge, or the 1e9 sentinel. public GpuPathSegment(Vector2 point0, Vector2 point1, float yEdge) { this.Point0 = point0; @@ -1634,10 +2787,21 @@ public GpuPathSegment(Vector2 point0, Vector2 point1, float yEdge) this.padding0 = 0; } + /// + /// Gets the segment start point relative to the tile origin. + /// public Vector2 Point0 { get; } + /// + /// Gets the segment end point relative to the tile origin. + /// public Vector2 Point1 { get; } + /// + /// Gets the tile-relative y at which the segment meets the tile's left edge, or 1e9 + /// if it does not; fine accumulates the implied vertical edge there to keep winding + /// consistent after clipping. + /// public float YEdge { get; } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSceneTarget.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneTarget.cs new file mode 100644 index 000000000..f8d7d7f6c --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSceneTarget.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Render-time WebGPU target selected by the scene replay path. +/// +internal readonly unsafe struct WebGPUSceneTarget +{ + /// + /// Initializes a new instance of the struct. + /// + /// The target texture. + /// The target texture view. + /// The absolute logical bounds represented by the target. + /// The offset from absolute logical coordinates to texture coordinates. + public WebGPUSceneTarget( + WGPUTextureImpl* texture, + WGPUTextureViewImpl* textureView, + Rectangle bounds, + Point textureOffset) + { + this.Texture = texture; + this.TextureView = textureView; + this.Bounds = bounds; + this.TextureOffset = textureOffset; + } + + /// + /// Gets the target texture. + /// + public WGPUTextureImpl* Texture { get; } + + /// + /// Gets the target texture view. + /// + public WGPUTextureViewImpl* TextureView { get; } + + /// + /// Gets the absolute logical bounds represented by the target. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the offset from absolute logical coordinates to texture coordinates. + /// + public Point TextureOffset { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderCompilationException.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderCompilationException.cs new file mode 100644 index 000000000..af4349720 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderCompilationException.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// The exception thrown when a WebGPU layer-effect shader cannot be compiled. +/// +public sealed class WebGPUShaderCompilationException : Exception +{ + private readonly WebGPUShaderDiagnostic[] diagnostics; + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The compiler diagnostics associated with the failure. + internal WebGPUShaderCompilationException(string message, WebGPUShaderDiagnostic[] diagnostics) + : base(message) + => this.diagnostics = diagnostics; + + /// + /// Gets the compiler diagnostics associated with the failure. + /// + public IReadOnlyList Diagnostics => this.diagnostics; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnostic.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnostic.cs new file mode 100644 index 000000000..45425e880 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnostic.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes one message produced while compiling a WebGPU layer-effect shader. +/// +public readonly struct WebGPUShaderDiagnostic +{ + /// + /// Initializes a new instance of the struct. + /// + /// The diagnostic severity. + /// The compiler message. + /// The one-based user-source line, or zero when the message belongs to generated framework code. + /// The one-based source column, or zero when the compiler does not provide one. + public WebGPUShaderDiagnostic(WebGPUShaderDiagnosticSeverity severity, string message, int line, int column) + { + this.Severity = severity; + this.Message = message; + this.Line = line; + this.Column = column; + } + + /// + /// Gets the diagnostic severity. + /// + public WebGPUShaderDiagnosticSeverity Severity { get; } + + /// + /// Gets the compiler message. + /// + public string Message { get; } + + /// + /// Gets the one-based user-source line, or zero when the message belongs to generated framework code. + /// + public int Line { get; } + + /// + /// Gets the one-based source column, or zero when the compiler does not provide one. + /// + public int Column { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnosticSeverity.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnosticSeverity.cs new file mode 100644 index 000000000..4f464f643 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderDiagnosticSeverity.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies the severity of a WebGPU shader compiler message. +/// +public enum WebGPUShaderDiagnosticSeverity +{ + /// + /// An informational compiler message. + /// + Information, + + /// + /// A compiler warning. + /// + Warning, + + /// + /// A compiler error that prevents execution. + /// + Error +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderEffectWorkingTexture.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderEffectWorkingTexture.cs new file mode 100644 index 000000000..10070ba7d --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderEffectWorkingTexture.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Defines the representation shared by shader-effect passes and their retained write-back fills. +/// +internal static class WebGPUShaderEffectWorkingTexture +{ + /// + /// Gets the associated, unit-encoded floating-point representation used between shader-effect passes. + /// + /// + /// Shader arithmetic remains 32-bit floating point. Storing a pass result in this texture rounds + /// each component to IEEE 754 binary16 until the next pass loads it back into 32-bit registers. + /// Rgba16Float is used because it retains substantially more precision than normalized + /// 8-bit layer storage while consuming 8 bytes per pixel. Rgba32Float would consume + /// 16 bytes per pixel and would therefore double both the memory occupied by the two ping-pong + /// textures and the texture traffic generated by every effect pass. The binary16 store can still + /// select an adjacent final 8-bit value where the unrounded 32-bit result lies near a quantization + /// boundary, so shader effects are not guaranteed to be bit-identical to CPU operations that retain + /// 32-bit values until their final destination conversion. + /// + public static WebGPUTargetDescriptor Descriptor { get; } = new( + WebGPUTextureFormat.Rgba16Float, + PixelAlphaRepresentation.Associated, + WebGPUTargetNumericEncoding.Unit); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderFrameworkUniforms.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderFrameworkUniforms.cs new file mode 100644 index 000000000..249b3f554 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderFrameworkUniforms.cs @@ -0,0 +1,86 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Maps effect-local coordinates to the current source texture and its valid captured region. +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly struct WebGPUShaderFrameworkUniforms +{ + private readonly int sourceX; + private readonly int sourceY; + private readonly int validMinimumX; + private readonly int validMinimumY; + private readonly int validMaximumX; + private readonly int validMaximumY; + private readonly int inputWidth; + private readonly int inputHeight; + + /// + /// The byte size required by the matching WGSL uniform structure. + /// + public const ulong ByteLength = 32; + + /// + /// Initializes a new instance of the struct. + /// + /// The source texture coordinate corresponding to effect-local zero. + /// The inclusive minimum valid effect-local coordinate. + /// The exclusive maximum valid effect-local coordinate. + /// The complete effect input size. + public WebGPUShaderFrameworkUniforms(Point sourceOrigin, Point validMinimum, Point validMaximum, Size inputSize) + { + this.sourceX = sourceOrigin.X; + this.sourceY = sourceOrigin.Y; + this.validMinimumX = validMinimum.X; + this.validMinimumY = validMinimum.Y; + this.validMaximumX = validMaximum.X; + this.validMaximumY = validMaximum.Y; + this.inputWidth = inputSize.Width; + this.inputHeight = inputSize.Height; + } + + /// + /// Gets the source texture X coordinate corresponding to effect-local zero. + /// + public int SourceX => this.sourceX; + + /// + /// Gets the source texture Y coordinate corresponding to effect-local zero. + /// + public int SourceY => this.sourceY; + + /// + /// Gets the inclusive valid local X coordinate. + /// + public int ValidMinimumX => this.validMinimumX; + + /// + /// Gets the inclusive valid local Y coordinate. + /// + public int ValidMinimumY => this.validMinimumY; + + /// + /// Gets the exclusive valid local X coordinate. + /// + public int ValidMaximumX => this.validMaximumX; + + /// + /// Gets the exclusive valid local Y coordinate. + /// + public int ValidMaximumY => this.validMaximumY; + + /// + /// Gets the complete effect input width. + /// + public int InputWidth => this.inputWidth; + + /// + /// Gets the complete effect input height. + /// + public int InputHeight => this.inputHeight; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderLayerEffect.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderLayerEffect.cs new file mode 100644 index 000000000..50f4f1569 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderLayerEffect.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Defines a layer effect with complete WGSL shader passes and an equivalent ImageSharp CPU fallback. +/// +/// +/// +/// Instances work with both drawing backends. WebGPU executes the configured WGSL passes, while CPU rendering +/// executes the required fallback effect. Each pass receives the preceding image as its input. +/// +/// +/// Shader source must define fn layer_effect(position: vec2<f32>) -> vec4<f32>. Position uses +/// effect-local pixel-center coordinates. The return value is associated-alpha RGBA in [0, 1]. +/// ImageSharp supplies layer_load, layer_load_unassociated, layer_sample, and the declared +/// imagesharp_uniforms fields. Each source defines one complete shader pass. +/// +/// +public abstract class WebGPUShaderLayerEffect : LayerEffect, IWebGPUShaderEffect, IWebGPUShaderEffectSource +{ + private readonly string shaderSource; + private readonly WebGPUShaderUniformLayout uniformLayout; + private WebGPUShaderProgram? program; + private WebGPUShaderPass[] shaderPasses = []; + private int shaderPassCount; + + /// + /// Initializes a new instance of the class. + /// + /// The complete WGSL source for each pass added by this effect. + /// The named uniform values available to the shader. + /// The equivalent operation used by CPU rendering. + protected WebGPUShaderLayerEffect( + string shaderSource, + WebGPUShaderUniformLayout uniformLayout, + Action fallback) + : base(fallback) + { + this.shaderSource = shaderSource; + this.uniformLayout = uniformLayout; + } + + /// + /// Initializes a new instance of the class. + /// + /// The equivalent effect used by CPU rendering. + /// The complete WGSL source for each pass added by this effect. + /// The named uniform values available to the shader. + protected WebGPUShaderLayerEffect( + LayerEffect fallbackEffect, + string shaderSource, + WebGPUShaderUniformLayout uniformLayout) + : base(fallbackEffect) + { + if (fallbackEffect is BackdropLayerEffect) + { + throw new ArgumentException("A backdrop effect must be wrapped by WebGPUBackdropShaderLayerEffect.", nameof(fallbackEffect)); + } + + this.shaderSource = shaderSource; + this.uniformLayout = uniformLayout; + } + + /// + /// Gets the shared primary program when the first pass is configured. + /// + private WebGPUShaderProgram Program + => this.program ??= WebGPUShaderProgram.GetOrCreate(this.shaderSource, this.uniformLayout); + + /// + /// Configures and adds an invocation of the complete shader source to this effect's ordered pass sequence. + /// + /// The action that assigns this pass's named uniform values. + protected void AddShaderPass(Action configureUniforms) + { + Guard.NotNull(configureUniforms, nameof(configureUniforms)); + + // The base type owns layout, program, and immutable snapshot plumbing. Derived effects only + // assign the values their WGSL declares; the action executes synchronously and is not retained. + WebGPUShaderUniformBuilder uniforms = this.uniformLayout.CreateUniforms(); + configureUniforms(uniforms); + this.AddShaderPass(new WebGPUShaderPass(this.Program, uniforms.Build())); + } + + /// + /// Configures and adds an invocation whose filtered samples use the supplied border modes. + /// + /// The wrapping mode applied beyond the horizontal input borders. + /// The wrapping mode applied beyond the vertical input borders. + /// The action that assigns this pass's named uniform values. + protected void AddShaderPass( + BorderWrappingMode xBorderMode, + BorderWrappingMode yBorderMode, + Action configureUniforms) + { + Guard.NotNull(configureUniforms, nameof(configureUniforms)); + + // Border behavior belongs to an individual pass because one effect may combine shaders with + // different sampling contracts. Module specialization removes the selection from the pixel loop. + WebGPUShaderUniformBuilder uniforms = this.uniformLayout.CreateUniforms(); + configureUniforms(uniforms); + this.AddShaderPass(new WebGPUShaderPass(this.Program, uniforms.Build(), xBorderMode, yBorderMode)); + } + + /// + /// Adds internal passes owned by another built-in effect. + /// + private protected void AddShaderPasses(ReadOnlySpan shaderPasses) + { + this.EnsureShaderPassCapacity(shaderPasses.Length); + shaderPasses.CopyTo(this.shaderPasses.AsSpan(this.shaderPassCount)); + this.shaderPassCount += shaderPasses.Length; + } + + /// + /// Gets the ordered internal pass sequence. + /// + internal ReadOnlySpan GetShaderPasses() + => this.shaderPasses.AsSpan(0, this.shaderPassCount); + + /// + ReadOnlySpan IWebGPUShaderEffectSource.GetShaderPasses() + => this.GetShaderPasses(); + + /// + /// Adds one internal pass to the sequence. + /// + private void AddShaderPass(WebGPUShaderPass shaderPass) + { + this.EnsureShaderPassCapacity(1); + this.shaderPasses[this.shaderPassCount++] = shaderPass; + } + + /// + /// Ensures that the pass storage can append the requested number of entries. + /// + private void EnsureShaderPassCapacity(int additionalCount) + { + int requiredCapacity = checked(this.shaderPassCount + additionalCount); + if (requiredCapacity <= this.shaderPasses.Length) + { + return; + } + + // Four entries cover every built-in sequence in one allocation. Larger internal + // sequences double geometrically so repeated additions remain amortized O(1). + int grownCapacity = this.shaderPasses.Length == 0 ? 4 : checked(this.shaderPasses.Length * 2); + Array.Resize(ref this.shaderPasses, Math.Max(requiredCapacity, grownCapacity)); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs new file mode 100644 index 000000000..1bd4fc2a6 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs @@ -0,0 +1,438 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Text; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Contains one complete framework-wrapped WGSL module and its user-source location mapping. +/// +/// +/// A supplies a WGSL module fragment, not a parsed syntax tree. +/// This type combines that fragment with the bindings, load helpers, and pipeline entry points owned +/// by ImageSharp. The resulting module always has this order: +/// +/// leading user-authored WGSL module directives; +/// framework structures, bindings, and texture-access helpers; +/// the remaining user-authored declarations, including layer_effect; +/// framework vertex and fragment entry points. +/// +/// Only the leading directive prefix changes position. All other user source is appended verbatim, +/// and the vacated directive characters are represented by whitespace so compiler diagnostics can be +/// mapped back to the authored line and column positions. +/// +internal sealed class WebGPUShaderModuleSource +{ + // Incrementing this value invalidates pipeline keys whenever generated framework semantics + // change without requiring callers to alter their user WGSL. + private const int FrameworkContractVersion = 3; + + // WGSL section 3.2 defines LF, VT, FF, CR, NEL, LS, and PS as line breaks. Every value is one + // UTF-16 code unit, so SearchValues can use the runtime's optimized span search without decoding + // every source code point. CRLF folding remains explicit because it is one WGSL line break. + private static readonly SearchValues LineBreakCharacters = SearchValues.Create("\n\v\f\r\u0085\u2028\u2029"); + private readonly byte[] utf8Source; + + private WebGPUShaderModuleSource(string source, byte[] utf8Source, int userLineStart, int userLineCount) + { + this.Source = source; + this.utf8Source = utf8Source; + this.UserLineStart = userLineStart; + this.UserLineCount = userLineCount; + this.PrecomputedHashCode = HashCode.Combine( + FrameworkContractVersion, + StringComparer.Ordinal.GetHashCode(source), + WGPUTextureFormat.RGBA16Float); + } + + /// + /// Gets the complete WGSL module text. + /// + public string Source { get; } + + /// + /// Gets the null-terminated UTF-8 module text accepted by the native API. + /// + public ReadOnlySpan Utf8Source => this.utf8Source; + + /// + /// Gets the first one-based wrapper line occupied by user source. + /// + public int UserLineStart { get; } + + /// + /// Gets the number of lines occupied by user source. + /// + public int UserLineCount { get; } + + /// + /// Gets the exact pipeline-key hash computed once when the module is generated. + /// + public int PrecomputedHashCode { get; } + + /// + /// Generates the complete module for one source representation. + /// + /// The public user program. + /// The source texture representation. + /// The horizontal border mode used by filtered samples. + /// The vertical border mode used by filtered samples. + /// The generated module source. + /// + /// WGSL module directives must appear before global declarations. Since ImageSharp contributes + /// global declarations of its own, the leading directive prefix is emitted before the framework + /// prelude. The user fragment is otherwise preserved and the native WebGPU compiler remains + /// responsible for WGSL grammar, types, and directive semantics. + /// + public static WebGPUShaderModuleSource Create( + WebGPUShaderProgram program, + WebGPUTargetDescriptor sourceDescriptor, + BorderWrappingMode? xBorderMode, + BorderWrappingMode? yBorderMode) + { + StringBuilder builder = new(); + ReadOnlySpan userSource = program.Source.AsSpan(); + + // This is a lexical boundary, not a parsed or validated directive list. Only a contiguous + // directive prefix can be relocated without changing the relative order of user declarations. + // Malformed or non-leading directives stay in the body for the native compiler to reject. + int moduleDirectiveEnd = WebGPUShaderSourceValidator.GetModuleDirectiveEnd(program.Source); + + // Emit the directive prefix exactly once, before ImageSharp's first global declaration. + // The original characters are masked at their authored location below; retaining both + // copies would produce duplicate directives and invalid WGSL. + _ = builder.Append(userSource[..moduleDirectiveEnd]); + + // Framework declarations are generated rather than user supplied so resource bindings, + // texture representation conversion, and diagnostic source mapping remain authoritative. + _ = builder.AppendLine("struct ImageSharpFramework {"); + _ = builder.AppendLine(" imagesharp_source_origin: vec2,"); + _ = builder.AppendLine(" imagesharp_valid_min: vec2,"); + _ = builder.AppendLine(" imagesharp_valid_max: vec2,"); + _ = builder.AppendLine(" imagesharp_input_size: vec2,"); + _ = builder.AppendLine("};"); + _ = builder.Append(program.UniformLayout.WgslStructureDeclaration); + _ = builder.AppendLine("@group(0) @binding(0) var imagesharp_source: texture_2d;"); + _ = builder.AppendLine("@group(0) @binding(1) var imagesharp_framework: ImageSharpFramework;"); + _ = builder.AppendLine("@group(0) @binding(2) var imagesharp_uniforms: ImageSharpUniforms;"); + _ = builder.AppendLine("@group(0) @binding(3) var imagesharp_filtering_sampler: sampler;"); + _ = builder.AppendLine(); + _ = builder.AppendLine("fn imagesharp_layer_load_scaled(position: vec2) -> vec4 {"); + _ = builder.AppendLine(" if (any(position < imagesharp_framework.imagesharp_valid_min) || any(position >= imagesharp_framework.imagesharp_valid_max)) {"); + _ = builder.AppendLine(" return vec4(0.0);"); + _ = builder.AppendLine(" }"); + _ = builder.AppendLine(); + _ = builder.AppendLine(" let imagesharp_native = textureLoad(imagesharp_source, imagesharp_framework.imagesharp_source_origin + position, 0);"); + + if (sourceDescriptor.NumericEncoding == WebGPUTargetNumericEncoding.SignedUnit) + { + // Signed-unit targets store logical [0, 1] values in physical [-1, 1]. Convert before + // alpha association so every public load helper exposes the same logical component range. + _ = builder.AppendLine(" let imagesharp_scaled = (imagesharp_native + vec4(1.0)) * 0.5;"); + } + else + { + _ = builder.AppendLine(" let imagesharp_scaled = imagesharp_native;"); + } + + _ = builder.AppendLine(" return imagesharp_scaled;"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + _ = builder.AppendLine("fn layer_load(position: vec2) -> vec4 {"); + _ = builder.AppendLine(" let imagesharp_scaled = imagesharp_layer_load_scaled(position);"); + + if (sourceDescriptor.AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + _ = builder.AppendLine(" return imagesharp_scaled;"); + } + else + { + // The renderer's common effect space is associated alpha. Multiplying at this boundary + // prevents transparent RGB from bleeding through filtering and multi-pass effects. + _ = builder.AppendLine(" return vec4(imagesharp_scaled.rgb * imagesharp_scaled.a, imagesharp_scaled.a);"); + } + + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + _ = builder.AppendLine("fn layer_load_unassociated(position: vec2) -> vec4 {"); + _ = builder.AppendLine(" let imagesharp_scaled = imagesharp_layer_load_scaled(position);"); + + if (sourceDescriptor.AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + // Division is defined only for covered pixels. Transparent associated pixels have no + // recoverable straight RGB, so the logical transparent value is returned instead. + _ = builder.AppendLine(" if (imagesharp_scaled.a > 0.0) {"); + _ = builder.AppendLine(" return vec4(imagesharp_scaled.rgb / imagesharp_scaled.a, imagesharp_scaled.a);"); + _ = builder.AppendLine(" }"); + _ = builder.AppendLine(); + _ = builder.AppendLine(" return vec4(0.0);"); + } + else + { + _ = builder.AppendLine(" return imagesharp_scaled;"); + } + + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + + bool wrapsSamples = xBorderMode.HasValue || yBorderMode.HasValue; + if (wrapsSamples) + { + // Border mapping is generated into the module rather than selected by a shader uniform. + // Each pass therefore pays only for its declared rule inside the pixel-sampling loop. + AppendBorderCoordinateFunction(builder, 'x', xBorderMode); + AppendBorderCoordinateFunction(builder, 'y', yBorderMode); + _ = builder.AppendLine("fn imagesharp_wrap_position(position: vec2) -> vec2 {"); + _ = builder.AppendLine(" return vec2(imagesharp_wrap_x(position.x), imagesharp_wrap_y(position.y));"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + } + + _ = builder.AppendLine("fn imagesharp_layer_sample_bilinear(position: vec2) -> vec4 {"); + + // Fragment positions address pixel centers at half-integer coordinates. Subtracting one + // half converts them to the integer texel lattice used by the four explicit loads. + _ = builder.AppendLine(" let imagesharp_texel_position = position - vec2(0.5);"); + _ = builder.AppendLine(" let imagesharp_minimum = vec2(floor(imagesharp_texel_position));"); + _ = builder.AppendLine(" let imagesharp_fraction = fract(imagesharp_texel_position);"); + + if (wrapsSamples) + { + // Map each texel independently before interpolation. Mapping the floating-point sample + // position instead would produce incorrect weights at reflected and wrapped boundaries. + _ = builder.AppendLine(" let imagesharp_top_left = layer_load(imagesharp_wrap_position(imagesharp_minimum));"); + _ = builder.AppendLine(" let imagesharp_top_right = layer_load(imagesharp_wrap_position(imagesharp_minimum + vec2(1, 0)));"); + _ = builder.AppendLine(" let imagesharp_bottom_left = layer_load(imagesharp_wrap_position(imagesharp_minimum + vec2(0, 1)));"); + _ = builder.AppendLine(" let imagesharp_bottom_right = layer_load(imagesharp_wrap_position(imagesharp_minimum + vec2(1, 1)));"); + } + else + { + _ = builder.AppendLine(" let imagesharp_top_left = layer_load(imagesharp_minimum);"); + _ = builder.AppendLine(" let imagesharp_top_right = layer_load(imagesharp_minimum + vec2(1, 0));"); + _ = builder.AppendLine(" let imagesharp_bottom_left = layer_load(imagesharp_minimum + vec2(0, 1));"); + _ = builder.AppendLine(" let imagesharp_bottom_right = layer_load(imagesharp_minimum + vec2(1, 1));"); + } + + _ = builder.AppendLine(" let imagesharp_top = mix(imagesharp_top_left, imagesharp_top_right, imagesharp_fraction.x);"); + _ = builder.AppendLine(" let imagesharp_bottom = mix(imagesharp_bottom_left, imagesharp_bottom_right, imagesharp_fraction.x);"); + _ = builder.AppendLine(" return mix(imagesharp_top, imagesharp_bottom, imagesharp_fraction.y);"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + _ = builder.AppendLine("fn layer_sample(position: vec2) -> vec4 {"); + + if (sourceDescriptor.AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + // Hardware filtering is safe only when the complete bilinear footprint lies inside + // valid associated source data. Boundary footprints use explicit transparent loads. + _ = builder.AppendLine(" let imagesharp_texel_position = position - vec2(0.5);"); + _ = builder.AppendLine(" let imagesharp_minimum = vec2(floor(imagesharp_texel_position));"); + _ = builder.AppendLine(" let imagesharp_maximum = imagesharp_minimum + vec2(1);"); + _ = builder.AppendLine(); + _ = builder.AppendLine(" if (all(imagesharp_minimum >= imagesharp_framework.imagesharp_valid_min) && all(imagesharp_maximum < imagesharp_framework.imagesharp_valid_max)) {"); + _ = builder.AppendLine(" let imagesharp_source_position = vec2(imagesharp_framework.imagesharp_source_origin) + position;"); + _ = builder.AppendLine(" let imagesharp_coordinate = imagesharp_source_position / vec2(textureDimensions(imagesharp_source));"); + _ = builder.AppendLine(" let imagesharp_native = textureSampleLevel(imagesharp_source, imagesharp_filtering_sampler, imagesharp_coordinate, 0.0);"); + + if (sourceDescriptor.NumericEncoding == WebGPUTargetNumericEncoding.SignedUnit) + { + _ = builder.AppendLine(" return (imagesharp_native + vec4(1.0)) * 0.5;"); + } + else + { + _ = builder.AppendLine(" return imagesharp_native;"); + } + + _ = builder.AppendLine(" }"); + _ = builder.AppendLine(); + } + + // Hardware filtering would interpolate straight RGB before alpha association. Manually + // blending layer_load results instead preserves associated-alpha interpolation and also + // retains transparent reads when the bilinear footprint crosses the valid source bounds. + _ = builder.AppendLine(" return imagesharp_layer_sample_bilinear(position);"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + + int userLineStart = CountLines(builder); + + // The leading directives were emitted before the framework prelude and must not appear a + // second time here. Preserve every original line ending and replace only non-newline + // characters with spaces. The user body consequently begins on the same relative line, and + // tokens following a directive retain their authored columns in native compiler diagnostics. + // Appending directly avoids allocating a temporary character array or rewritten string. + for (int i = 0; i < moduleDirectiveEnd; i++) + { + char value = userSource[i]; + _ = builder.Append(value is '\r' or '\n' ? value : ' '); + } + + // Record the exact wrapper range occupied by user source before appending framework entry + // points. Masking preserves the original line count, so the immutable source can be counted + // directly without constructing an intermediate source-body string. + _ = builder.Append(userSource[moduleDirectiveEnd..]); + _ = builder.AppendLine(); + int userLineCount = CountLines(program.Source); + + _ = builder.AppendLine("@vertex"); + _ = builder.AppendLine("fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4 {"); + _ = builder.AppendLine(" let positions = array, 3>(vec2(-1.0, -1.0), vec2(3.0, -1.0), vec2(-1.0, 3.0));"); + _ = builder.AppendLine(" return vec4(positions[vertex_index], 0.0, 1.0);"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + _ = builder.AppendLine("@fragment"); + _ = builder.AppendLine("fn fs_main(@builtin(position) position: vec4) -> @location(0) vec4 {"); + _ = builder.AppendLine(" return layer_effect(position.xy);"); + _ = builder.AppendLine("}"); + + string source = builder.ToString(); + + // Native WebGPU consumes a null-terminated UTF-8 string view. Cache this encoding with the + // immutable module so pipeline creation performs no repeated string conversion. + byte[] utf8Source = new byte[Encoding.UTF8.GetByteCount(source) + 1]; + _ = Encoding.UTF8.GetBytes(source, utf8Source); + return new WebGPUShaderModuleSource(source, utf8Source, userLineStart, userLineCount); + } + + /// + /// Appends one axis of the pass-specific border-coordinate transform. + /// + /// The generated WGSL module. + /// The framework coordinate axis. + /// The ImageSharp convolution border mode, or transparent sampling when absent. + private static void AppendBorderCoordinateFunction( + StringBuilder builder, + char axis, + BorderWrappingMode? borderMode) + { + _ = builder.Append("fn imagesharp_wrap_").Append(axis).AppendLine("(value: i32) -> i32 {"); + + if (!borderMode.HasValue) + { + // An unconfigured axis retains layer_sample's public transparent-outside contract. + _ = builder.AppendLine(" return value;"); + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + return; + } + + _ = builder.Append(" let imagesharp_minimum = imagesharp_framework.imagesharp_valid_min.").Append(axis).AppendLine(";"); + _ = builder.Append(" let imagesharp_maximum = imagesharp_framework.imagesharp_valid_max.").Append(axis).AppendLine(" - 1;"); + + switch (borderMode.Value) + { + case BorderWrappingMode.Repeat: + // ImageSharp's Repeat convolution mode extends the nearest border sample. + _ = builder.AppendLine(" return clamp(value, imagesharp_minimum, imagesharp_maximum);"); + break; + + case BorderWrappingMode.Wrap: + _ = builder.AppendLine(" let imagesharp_extent = imagesharp_maximum - imagesharp_minimum + 1;"); + _ = builder.AppendLine(" let imagesharp_remainder = (value - imagesharp_minimum) % imagesharp_extent;"); + _ = builder.AppendLine(" let imagesharp_offset = (imagesharp_remainder + imagesharp_extent) % imagesharp_extent;"); + _ = builder.AppendLine(" return imagesharp_minimum + imagesharp_offset;"); + break; + + case BorderWrappingMode.Mirror: + _ = builder.AppendLine(" let imagesharp_extent = imagesharp_maximum - imagesharp_minimum + 1;"); + _ = builder.AppendLine(" let imagesharp_period = imagesharp_extent * 2;"); + _ = builder.AppendLine(" let imagesharp_remainder = (value - imagesharp_minimum) % imagesharp_period;"); + _ = builder.AppendLine(" let imagesharp_offset = (imagesharp_remainder + imagesharp_period) % imagesharp_period;"); + _ = builder.AppendLine(" let imagesharp_reflected = select(imagesharp_period - 1 - imagesharp_offset, imagesharp_offset, imagesharp_offset < imagesharp_extent);"); + _ = builder.AppendLine(" return imagesharp_minimum + imagesharp_reflected;"); + break; + + case BorderWrappingMode.Bounce: + _ = builder.AppendLine(" let imagesharp_extent = imagesharp_maximum - imagesharp_minimum + 1;"); + _ = builder.AppendLine(" if (imagesharp_extent == 1) {"); + _ = builder.AppendLine(" return imagesharp_minimum;"); + _ = builder.AppendLine(" }"); + _ = builder.AppendLine(); + _ = builder.AppendLine(" let imagesharp_period = (imagesharp_extent * 2) - 2;"); + _ = builder.AppendLine(" let imagesharp_remainder = (value - imagesharp_minimum) % imagesharp_period;"); + _ = builder.AppendLine(" let imagesharp_offset = (imagesharp_remainder + imagesharp_period) % imagesharp_period;"); + _ = builder.AppendLine(" let imagesharp_reflected = select(imagesharp_period - imagesharp_offset, imagesharp_offset, imagesharp_offset < imagesharp_extent);"); + _ = builder.AppendLine(" return imagesharp_minimum + imagesharp_reflected;"); + break; + } + + _ = builder.AppendLine("}"); + _ = builder.AppendLine(); + } + + /// + /// Counts one-based lines in a partially generated module without allocating an intermediate string. + /// + /// The generated source accumulated so far. + /// The one-based line count. + private static int CountLines(StringBuilder builder) + { + int count = 1; + bool previousWasCarriageReturn = false; + + // StringBuilder chunks avoid both the comparatively expensive indexer and a temporary + // flattened string. Carrying the CR state across chunks ensures a split CRLF is one line end. + foreach (ReadOnlyMemory chunk in builder.GetChunks()) + { + count += CountLineEndings(chunk.Span, ref previousWasCarriageReturn); + } + + return count; + } + + /// + /// Counts one-based lines in user WGSL for diagnostic source mapping. + /// + /// The source text to inspect. + /// The one-based line count. + private static int CountLines(string value) + { + bool previousWasCarriageReturn = false; + return 1 + CountLineEndings(value, ref previousWasCarriageReturn); + } + + /// + /// Counts Unicode newline indicators without allocating, treating CRLF as one line ending. + /// + /// The source segment to inspect. + /// + /// Indicates whether the preceding segment ended with CR so a leading LF completes the same line ending. + /// + /// The number of complete line endings represented by the segment. + private static int CountLineEndings(ReadOnlySpan value, ref bool previousWasCarriageReturn) + { + int count = 0; + + while (!value.IsEmpty) + { + int lineBreakIndex = value.IndexOfAny(LineBreakCharacters); + if (lineBreakIndex < 0) + { + // Ordinary text separates a trailing CR from any LF in the next builder chunk. + previousWasCarriageReturn = false; + break; + } + + if (lineBreakIndex > 0) + { + // A preceding non-line-break character means this cannot complete a cross-chunk CRLF. + previousWasCarriageReturn = false; + } + + char lineBreak = value[lineBreakIndex]; + if (lineBreak != '\n' || !previousWasCarriageReturn) + { + count++; + } + + previousWasCarriageReturn = lineBreak == '\r'; + value = value[(lineBreakIndex + 1)..]; + } + + return count; + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderPass.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderPass.cs new file mode 100644 index 000000000..726a4efbf --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderPass.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes one immutable invocation of a WebGPU layer-effect program. +/// +internal readonly struct WebGPUShaderPass +{ + /// + /// Initializes a new instance of the struct. + /// + /// The WGSL program to invoke. + /// The immutable values supplied to the program. + public WebGPUShaderPass(WebGPUShaderProgram program, WebGPUShaderUniforms uniforms) + : this(program, uniforms, null, null) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The WGSL program to invoke. + /// The immutable values supplied to the program. + /// The horizontal border mode used by layer_sample. + /// The vertical border mode used by layer_sample. + public WebGPUShaderPass( + WebGPUShaderProgram program, + WebGPUShaderUniforms uniforms, + BorderWrappingMode? xBorderMode, + BorderWrappingMode? yBorderMode) + { + Guard.NotNull(program, nameof(program)); + Guard.NotNull(uniforms, nameof(uniforms)); + + // Layout identity proves that the packed values use this program's exact offsets and types. + if (!ReferenceEquals(program.UniformLayout, uniforms.Layout)) + { + throw new ArgumentException("The supplied uniform values were built from a different layout.", nameof(uniforms)); + } + + this.Program = program; + this.Uniforms = uniforms; + this.XBorderMode = xBorderMode; + this.YBorderMode = yBorderMode; + } + + /// + /// Gets the reusable WGSL program. + /// + public WebGPUShaderProgram Program { get; } + + /// + /// Gets the immutable values supplied to the program. + /// + public WebGPUShaderUniforms Uniforms { get; } + + /// + /// Gets the horizontal border mode used by layer_sample, or for transparent samples. + /// + public BorderWrappingMode? XBorderMode { get; } + + /// + /// Gets the vertical border mode used by layer_sample, or for transparent samples. + /// + public BorderWrappingMode? YBorderMode { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs new file mode 100644 index 000000000..b73f925e7 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Convolution; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Stores one validated WGSL program shared by effect invocations using the same source and uniform layout. +/// +internal sealed class WebGPUShaderProgram +{ + /// + /// The maximum UTF-8 byte length accepted for one authored WGSL program. + /// + public const int MaximumSourceByteLength = 8 * 1024 * 1024; + + // Layouts are normally static declarations owned by an effect type. Weak keys allow dynamically + // created layouts and their cached programs to be collected together. + private static readonly ConditionalWeakTable ProgramsByLayout = []; + + private readonly object moduleSourceSync = new(); + private readonly Dictionary< + (PixelAlphaRepresentation AlphaRepresentation, WebGPUTargetNumericEncoding NumericEncoding, BorderWrappingMode? XBorderMode, BorderWrappingMode? YBorderMode), + WebGPUShaderModuleSource> moduleSources = []; + + /// + /// Initializes a new instance of the class. + /// + /// The WGSL module fragment defining the layer effect. + /// The named values exposed to the WGSL source. + public WebGPUShaderProgram(string source, WebGPUShaderUniformLayout uniformLayout) + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(uniformLayout, nameof(uniformLayout)); + + WebGPUShaderSourceValidator.Validate(source, nameof(source)); + this.Source = source; + this.UniformLayout = uniformLayout; + } + + /// + /// Gets the WGSL module fragment defining the layer effect. + /// + public string Source { get; } + + /// + /// Gets the named values exposed to the WGSL source. + /// + public WebGPUShaderUniformLayout UniformLayout { get; } + + /// + /// Gets the shared program for an exact source and layout contract. + /// + public static WebGPUShaderProgram GetOrCreate(string source, WebGPUShaderUniformLayout uniformLayout) + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(uniformLayout, nameof(uniformLayout)); + + ProgramCache cache = ProgramsByLayout.GetValue(uniformLayout, static _ => new ProgramCache()); + return cache.GetOrCreate(source, uniformLayout); + } + + /// + /// Gets the generated module specialized for one source representation. + /// + /// The physical and logical source texture representation. + /// The horizontal border mode used by filtered samples. + /// The vertical border mode used by filtered samples. + /// The generated module source. + public WebGPUShaderModuleSource GetModuleSource( + WebGPUTargetDescriptor sourceDescriptor, + BorderWrappingMode? xBorderMode, + BorderWrappingMode? yBorderMode) + { + (PixelAlphaRepresentation, WebGPUTargetNumericEncoding, BorderWrappingMode?, BorderWrappingMode?) key = ( + sourceDescriptor.AlphaRepresentation, + sourceDescriptor.NumericEncoding, + xBorderMode, + yBorderMode); + + lock (this.moduleSourceSync) + { + if (!this.moduleSources.TryGetValue(key, out WebGPUShaderModuleSource? moduleSource)) + { + // Wrapper generation includes both representation conversion and border-coordinate + // mapping. The immutable result is reused by every pass with that exact contract. + moduleSource = WebGPUShaderModuleSource.Create(this, sourceDescriptor, xBorderMode, yBorderMode); + this.moduleSources.Add(key, moduleSource); + } + + return moduleSource; + } + } + + /// + /// Owns the source variants associated with one live uniform layout. + /// + private sealed class ProgramCache + { + private readonly List programs = []; + + /// + /// Gets or creates the program for one source string under the owning layout. + /// + public WebGPUShaderProgram GetOrCreate(string source, WebGPUShaderUniformLayout uniformLayout) + { + lock (this.programs) + { + for (int i = 0; i < this.programs.Count; i++) + { + WebGPUShaderProgram candidate = this.programs[i]; + if (StringComparer.Ordinal.Equals(candidate.Source, source)) + { + return candidate; + } + } + + // Public WGSL validation occurs once per distinct source/layout contract. + WebGPUShaderProgram program = new(source, uniformLayout); + this.programs.Add(program); + return program; + } + } + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs new file mode 100644 index 000000000..c20f7dd8b --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderSourceValidator.cs @@ -0,0 +1,524 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Performs allocation-free lexical validation of user WGSL against names and declarations owned by the layer-effect framework. +/// +/// +/// This type deliberately does not parse WGSL grammar or types; the native WebGPU compiler remains +/// authoritative for those rules. The lexical pass rejects constructs that could shadow generated +/// resources, helpers, or entry points before untrusted source is combined with the framework module. +/// It also identifies the initial module-directive prefix because those directives must precede the +/// framework declarations in the final WGSL module. That scan reports source boundaries only; it does +/// not validate directive arguments or reorder directives found after the first non-directive token. +/// Successful validation allocates no managed memory. Validation failures allocate only their exception. +/// +internal static class WebGPUShaderSourceValidator +{ + /// + /// Finds the end of the leading WGSL module-directive prefix. + /// + /// The validated WGSL module fragment. + /// The exclusive end offset of the final leading directive, or zero when no directive is present. + /// + /// User source is accepted as a WGSL module fragment rather than as a parsed syntax tree. The + /// framework must therefore locate any initial enable, requires, and + /// diagnostic directives before it inserts its own global declarations. This method + /// recognizes only a contiguous leading sequence separated by WGSL whitespace or comments. + /// A malformed directive, or a directive appearing after another global declaration, remains in + /// the user body for the native WGSL compiler to diagnose. + /// + /// The returned offset lets module generation append slices of the original string. No substring + /// or rewritten source copy is allocated, and the original line endings remain available for + /// diagnostic source mapping. + /// + public static int GetModuleDirectiveEnd(string source) + { + // `position` identifies the next character to inspect. It may advance beyond trivia while + // probing for another directive, but that trivia is not committed unless a complete + // following directive is found. + int position = 0; + + // Track the last fully terminated directive separately from the current scan position. If + // the next apparent directive is malformed, the native compiler must see it in the user + // body rather than having an incomplete declaration moved ahead of the framework prelude. + int directiveEnd = 0; + + while (position < source.Length) + { + // WGSL permits whitespace, line comments, and nested block comments between module + // directives. Scan this trivia tentatively: when another directive follows, the + // returned prefix retains the trivia between them; otherwise directiveEnd remains + // unchanged and trailing trivia stays at its authored source location. + while (position < source.Length) + { + if (char.IsWhiteSpace(source[position])) + { + position++; + continue; + } + + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '/') + { + // Stop before the newline and let the whitespace branch consume it so CR, LF, + // and CRLF input all follow the same path. + position += 2; + while (position < source.Length && source[position] is not ('\r' or '\n')) + { + position++; + } + + continue; + } + + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '*') + { + // WGSL block comments may nest. Stopping at the first closing delimiter would + // expose outer-comment text and could misclassify it as a module directive. + int blockCommentDepth = 1; + position += 2; + + while (position < source.Length && blockCommentDepth > 0) + { + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '*') + { + blockCommentDepth++; + position += 2; + } + else if (position + 1 < source.Length && source[position] == '*' && source[position + 1] == '/') + { + blockCommentDepth--; + position += 2; + } + else + { + position++; + } + } + + continue; + } + + break; + } + + // Inspect the original source through a span so probing for each directive keyword + // creates no substring. + ReadOnlySpan remaining = source.AsSpan(position); + int keywordLength; + + // Recognize only module directives that WGSL requires ahead of global declarations. + // This deliberately stops at the first other token: relocating a later directive would + // also reorder the user's declarations and conceal the native compiler error. The + // identifier-boundary check prevents names such as `enabled_feature` from being treated + // as the `enable` directive. + if (StartsWithDirectiveKeyword(remaining, "enable")) + { + keywordLength = "enable".Length; + } + else if (StartsWithDirectiveKeyword(remaining, "requires")) + { + keywordLength = "requires".Length; + } + else if (StartsWithDirectiveKeyword(remaining, "diagnostic")) + { + keywordLength = "diagnostic".Length; + } + else + { + break; + } + + position += keywordLength; + + // This counter is initialized only when a block comment begins and is consumed before + // scanning resumes, so comment nesting never leaks into the next directive. + int blockDepth; + + // WGSL module directives end at the first semicolon outside a comment. WGSL has no + // string literal token, so comments are the only lexical construct that can hide it. + while (position < source.Length) + { + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '/') + { + // Semicolons inside a line comment cannot terminate the directive. + position += 2; + while (position < source.Length && source[position] is not ('\r' or '\n')) + { + position++; + } + + continue; + } + + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '*') + { + // Skip the complete nested comment before resuming the terminator search. + blockDepth = 1; + position += 2; + + while (position < source.Length && blockDepth > 0) + { + if (position + 1 < source.Length && source[position] == '/' && source[position + 1] == '*') + { + blockDepth++; + position += 2; + } + else if (position + 1 < source.Length && source[position] == '*' && source[position + 1] == '/') + { + blockDepth--; + position += 2; + } + else + { + position++; + } + } + + continue; + } + + if (source[position++] == ';') + { + // Commit only after finding a real terminator. The exclusive offset includes + // the semicolon and any leading or inter-directive trivia before it. + directiveEnd = position; + break; + } + } + + if (directiveEnd != position) + { + // The native WGSL compiler reports the malformed unterminated directive. Treating + // it as source body here avoids moving unrelated declarations ahead of the prelude. + // Equality holds only when this scan committed a semicolon at the current position. + break; + } + } + + // Returning an offset lets module generation append slices of the original source and + // preserve diagnostic coordinates without allocating rewritten strings in this validator. + return directiveEnd; + } + + /// + /// Validates source constructs that would collide with the generated layer-effect module. + /// + /// The user WGSL module fragment. + /// The public parameter name reported by validation failures. + public static void Validate(string source, string parameterName) + { + // The limit is defined in encoded bytes because that is what the native API consumes. + // Encoding.GetByteCount scans the existing string without creating an encoded copy. + if (Encoding.UTF8.GetByteCount(source) > WebGPUShaderProgram.MaximumSourceByteLength) + { + throw new ArgumentException( + $"WGSL source cannot exceed {WebGPUShaderProgram.MaximumSourceByteLength} UTF-8 bytes.", + parameterName); + } + + // This is a single-pass lexical boundary check rather than a WGSL parser. The flags retain + // only the context required to identify declarations and framework-owned references: + // + // - attribute: the preceding significant character was '@', so the next identifier names + // a pipeline attribute owned either by the user or by the framework. + // - declarationNameExpected/functionNameExpected: the preceding token was a declaration + // keyword whose next identifier introduces a symbol. + // - variableNameExpected/variableTemplateDepth: `var` may place an address-space template + // between its keyword and declared identifier, for example `var value`. + // - uniformMemberAccessExpected: the public uniform object may only be followed by `.` so + // callers cannot pass, index, take the address of, or redeclare the complete binding. + // - reservedNameAwaitingColon: function parameters and structure fields have no declaration + // keyword, so a following colon identifies a declaration that must also be rejected. + // + // Comments are skipped before token interpretation. WGSL permits nested block comments, so + // blockCommentDepth prevents comment text from being mistaken for executable declarations. + bool attribute = false; + bool declarationNameExpected = false; + bool functionNameExpected = false; + bool uniformMemberAccessExpected = false; + bool variableNameExpected = false; + bool reservedNameAwaitingColon = false; + int blockCommentDepth = 0; + int layerEffectDeclarationCount = 0; + int variableTemplateDepth = 0; + + for (int i = 0; i < source.Length;) + { + char value = source[i]; + if (value == '\0') + { + // Native string views use a null terminator. An embedded null could make the native + // compiler validate a different prefix from the source inspected here. + throw new ArgumentException("WGSL source cannot contain a null character.", parameterName); + } + + if (blockCommentDepth > 0) + { + if (i + 1 < source.Length && value == '/' && source[i + 1] == '*') + { + blockCommentDepth++; + i += 2; + } + else if (i + 1 < source.Length && value == '*' && source[i + 1] == '/') + { + blockCommentDepth--; + i += 2; + } + else + { + i++; + } + + continue; + } + + if (i + 1 < source.Length && value == '/' && source[i + 1] == '*') + { + blockCommentDepth = 1; + i += 2; + continue; + } + + if (i + 1 < source.Length && value == '/' && source[i + 1] == '/') + { + i += 2; + while (i < source.Length && source[i] is not ('\r' or '\n')) + { + i++; + } + + continue; + } + + if (value == '@') + { + if (uniformMemberAccessExpected) + { + throw new ArgumentException("imagesharp_uniforms must be accessed through a directly named field.", parameterName); + } + + // Remember the attribute marker across whitespace; the next identifier determines + // whether the user is attempting to claim a framework-owned pipeline construct. + reservedNameAwaitingColon = false; + attribute = true; + i++; + continue; + } + + if (IsIdentifierStart(value)) + { + if (uniformMemberAccessExpected) + { + throw new ArgumentException("imagesharp_uniforms must be accessed through a directly named field.", parameterName); + } + + int start = i++; + while (i < source.Length && IsIdentifierPart(source[i])) + { + i++; + } + + ReadOnlySpan name = source.AsSpan(start, i - start); + reservedNameAwaitingColon = false; + + if (attribute) + { + if (name.StartsWith("imagesharp_", StringComparison.Ordinal)) + { + throw new ArgumentException("WGSL identifiers beginning with 'imagesharp_' are reserved by ImageSharp.", parameterName); + } + + if (name.SequenceEqual("group") || name.SequenceEqual("binding")) + { + throw new ArgumentException("Layer-effect WGSL cannot declare bind groups or bindings.", parameterName); + } + + if (name.SequenceEqual("vertex") || name.SequenceEqual("fragment") || name.SequenceEqual("compute")) + { + throw new ArgumentException("Layer-effect WGSL cannot declare shader entry points.", parameterName); + } + + attribute = false; + continue; + } + + bool isDeclarationName = false; + bool isLayerEffectFunction = false; + if (functionNameExpected) + { + functionNameExpected = false; + isDeclarationName = true; + + if (name.SequenceEqual("layer_effect")) + { + isLayerEffectFunction = true; + layerEffectDeclarationCount++; + if (layerEffectDeclarationCount > 1) + { + throw new ArgumentException("Layer-effect WGSL must declare exactly one layer_effect function.", parameterName); + } + } + } + else if (declarationNameExpected) + { + declarationNameExpected = false; + isDeclarationName = true; + } + else if (variableNameExpected && variableTemplateDepth == 0) + { + variableNameExpected = false; + isDeclarationName = true; + } + else if (!variableNameExpected) + { + // These WGSL declarations place their name immediately after the keyword. + // Variable declarations are handled separately because var may include an + // address-space template before its name. + if (name.SequenceEqual("fn")) + { + functionNameExpected = true; + } + else if (name.SequenceEqual("alias") || + name.SequenceEqual("const") || + name.SequenceEqual("let") || + name.SequenceEqual("override") || + name.SequenceEqual("struct")) + { + declarationNameExpected = true; + } + else if (name.SequenceEqual("var")) + { + variableNameExpected = true; + } + } + + if (IsFrameworkPrivateName(name)) + { + throw new ArgumentException($"The WGSL identifier '{name}' is owned by ImageSharp.", parameterName); + } + + if (isDeclarationName && IsFrameworkOwnedName(name) && !isLayerEffectFunction) + { + throw new ArgumentException($"The WGSL declaration name '{name}' is owned by ImageSharp.", parameterName); + } + + bool hasReservedPrefix = name.StartsWith("imagesharp_", StringComparison.Ordinal); + bool isPublicUniformBinding = name.SequenceEqual("imagesharp_uniforms"); + if (hasReservedPrefix && (!isPublicUniformBinding || isDeclarationName)) + { + throw new ArgumentException("WGSL identifiers beginning with 'imagesharp_' are reserved by ImageSharp; only imagesharp_uniforms may be read by user source.", parameterName); + } + + // Function parameters and structure members have no declaration keyword. Retain + // the public uniform binding name across whitespace and comments so a following + // colon cannot redeclare it while ordinary member reads remain valid. + reservedNameAwaitingColon = !isDeclarationName && isPublicUniformBinding; + uniformMemberAccessExpected = !isDeclarationName && isPublicUniformBinding; + continue; + } + + if (!char.IsWhiteSpace(value)) + { + if (uniformMemberAccessExpected) + { + if (value != '.') + { + throw new ArgumentException("imagesharp_uniforms must be accessed through a directly named field.", parameterName); + } + + uniformMemberAccessExpected = false; + } + + attribute = false; + + if (variableNameExpected) + { + if (value == '<') + { + variableTemplateDepth++; + } + else if (value == '>' && variableTemplateDepth > 0) + { + variableTemplateDepth--; + } + } + + if (value == ':' && reservedNameAwaitingColon) + { + throw new ArgumentException("WGSL declarations beginning with 'imagesharp_' are reserved by ImageSharp.", parameterName); + } + + reservedNameAwaitingColon = false; + } + + i++; + } + + if (uniformMemberAccessExpected) + { + throw new ArgumentException("imagesharp_uniforms must be accessed through a directly named field.", parameterName); + } + + if (layerEffectDeclarationCount == 0) + { + throw new ArgumentException("Layer-effect WGSL must declare exactly one layer_effect function.", parameterName); + } + } + + /// + /// Tests whether a character may begin a WGSL identifier. + /// + /// The character to test. + /// when the character is valid. + private static bool IsIdentifierStart(char value) + => value is '_' or (>= 'A' and <= 'Z') or (>= 'a' and <= 'z'); + + /// + /// Tests whether a character may follow the first character of a WGSL identifier. + /// + /// The character to test. + /// when the character is valid. + private static bool IsIdentifierPart(char value) + => IsIdentifierStart(value) || (value >= '0' && value <= '9'); + + /// + /// Tests whether source begins with one complete WGSL directive keyword. + /// + /// The source beginning at the next non-trivia token. + /// The directive keyword to test. + /// when the keyword is not an identifier prefix. + private static bool StartsWithDirectiveKeyword(ReadOnlySpan source, ReadOnlySpan keyword) + => source.StartsWith(keyword, StringComparison.Ordinal) + && (source.Length == keyword.Length || !IsIdentifierPart(source[keyword.Length])); + + /// + /// Tests whether a declaration name is supplied by the generated layer-effect module. + /// + /// The WGSL identifier to test. + /// when ImageSharp owns the declaration. + private static bool IsFrameworkOwnedName(ReadOnlySpan name) + => name.SequenceEqual("layer_effect") + || name.SequenceEqual("layer_load") + || name.SequenceEqual("layer_load_unassociated") + || name.SequenceEqual("layer_sample") + || name.SequenceEqual("vs_main") + || name.SequenceEqual("fs_main") + || name.SequenceEqual("ImageSharpFramework") + || name.SequenceEqual("ImageSharpUniforms"); + + /// + /// Tests whether an identifier belongs to the generated module but is not a public shader helper. + /// + /// The WGSL identifier to test. + /// when user source cannot reference the identifier. + private static bool IsFrameworkPrivateName(ReadOnlySpan name) + => name.SequenceEqual("vs_main") + || name.SequenceEqual("fs_main") + || name.SequenceEqual("ImageSharpFramework") + || name.SequenceEqual("ImageSharpUniforms"); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniform.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniform.cs new file mode 100644 index 000000000..f87dfd97a --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniform.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes one named value or fixed-size array supplied to a WebGPU layer-effect shader. +/// +public readonly struct WebGPUShaderUniform +{ + /// + /// Initializes a new instance of the struct. + /// + /// The WGSL member name used to access the value. + /// The value type. + /// The fixed number of elements. Use one for a scalar, vector, or matrix value. + public WebGPUShaderUniform(string name, WebGPUShaderUniformType type, int elementCount) + { + Guard.NotNull(name, nameof(name)); + Guard.MustBeGreaterThan(elementCount, 0, nameof(elementCount)); + + this.Name = name; + this.Type = type; + this.ElementCount = elementCount; + } + + /// + /// Gets the WGSL member name used to access the value. + /// + public string Name { get; } + + /// + /// Gets the value type. + /// + public WebGPUShaderUniformType Type { get; } + + /// + /// Gets the fixed number of elements. + /// + public int ElementCount { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformBuilder.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformBuilder.cs new file mode 100644 index 000000000..92f8c3422 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformBuilder.cs @@ -0,0 +1,275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Builds a set of named values for a . +/// +public sealed class WebGPUShaderUniformBuilder +{ + private readonly WebGPUShaderUniformLayout layout; + private readonly byte[] data; + + /// + /// Initializes a new instance of the class. + /// + /// The layout that defines the accepted names and value types. + internal WebGPUShaderUniformBuilder(WebGPUShaderUniformLayout layout) + { + this.layout = layout; + this.data = new byte[layout.ByteLength]; + } + + /// + /// Sets a named 32-bit floating-point value. + /// + /// The declared member name. + /// The value to set. + public void SetFloat32(string name, float value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Float32, isArray: false); + WriteFloat32(this.data, member.Offset, value); + } + + /// + /// Sets a named signed 32-bit integer value. + /// + /// The declared member name. + /// The value to set. + public void SetInt32(string name, int value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Int32, isArray: false); + BinaryPrimitives.WriteInt32LittleEndian(this.data.AsSpan(member.Offset, sizeof(int)), value); + } + + /// + /// Sets a named unsigned 32-bit integer value. + /// + /// The declared member name. + /// The value to set. + public void SetUInt32(string name, uint value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.UInt32, isArray: false); + BinaryPrimitives.WriteUInt32LittleEndian(this.data.AsSpan(member.Offset, sizeof(uint)), value); + } + + /// + /// Sets a named two-component floating-point vector. + /// + /// The declared member name. + /// The value to set. + public void SetVector2(string name, Vector2 value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Vector2, isArray: false); + WriteVector2(this.data, member.Offset, value); + } + + /// + /// Sets a named three-component floating-point vector. + /// + /// The declared member name. + /// The value to set. + public void SetVector3(string name, Vector3 value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Vector3, isArray: false); + WriteVector3(this.data, member.Offset, value); + } + + /// + /// Sets a named four-component floating-point vector. + /// + /// The declared member name. + /// The value to set. + public void SetVector4(string name, Vector4 value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Vector4, isArray: false); + WriteVector4(this.data, member.Offset, value); + } + + /// + /// Sets a named four-by-four floating-point matrix. + /// + /// The declared member name. + /// The value to set. + public void SetMatrix4x4(string name, Matrix4x4 value) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, WebGPUShaderUniformType.Matrix4x4, isArray: false); + WriteMatrix4x4(this.data, member.Offset, value); + } + + /// + /// Sets a named fixed-size array of 32-bit floating-point values. + /// + /// The declared member name. + /// The values to set. + public void SetFloat32Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Float32, values.Length); + for (int i = 0; i < values.Length; i++) + { + WriteFloat32(this.data, member.Offset + (i * member.Stride), values[i]); + } + } + + /// + /// Sets a named fixed-size array of signed 32-bit integer values. + /// + /// The declared member name. + /// The values to set. + public void SetInt32Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Int32, values.Length); + for (int i = 0; i < values.Length; i++) + { + BinaryPrimitives.WriteInt32LittleEndian(this.data.AsSpan(member.Offset + (i * member.Stride), sizeof(int)), values[i]); + } + } + + /// + /// Sets a named fixed-size array of unsigned 32-bit integer values. + /// + /// The declared member name. + /// The values to set. + public void SetUInt32Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.UInt32, values.Length); + for (int i = 0; i < values.Length; i++) + { + BinaryPrimitives.WriteUInt32LittleEndian(this.data.AsSpan(member.Offset + (i * member.Stride), sizeof(uint)), values[i]); + } + } + + /// + /// Sets a named fixed-size array of two-component floating-point vectors. + /// + /// The declared member name. + /// The values to set. + public void SetVector2Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Vector2, values.Length); + for (int i = 0; i < values.Length; i++) + { + WriteVector2(this.data, member.Offset + (i * member.Stride), values[i]); + } + } + + /// + /// Sets a named fixed-size array of three-component floating-point vectors. + /// + /// The declared member name. + /// The values to set. + public void SetVector3Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Vector3, values.Length); + for (int i = 0; i < values.Length; i++) + { + WriteVector3(this.data, member.Offset + (i * member.Stride), values[i]); + } + } + + /// + /// Sets a named fixed-size array of four-component floating-point vectors. + /// + /// The declared member name. + /// The values to set. + public void SetVector4Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Vector4, values.Length); + for (int i = 0; i < values.Length; i++) + { + WriteVector4(this.data, member.Offset + (i * member.Stride), values[i]); + } + } + + /// + /// Sets a named fixed-size array of four-by-four floating-point matrices. + /// + /// The declared member name. + /// The values to set. + public void SetMatrix4x4Array(string name, ReadOnlySpan values) + { + WebGPUShaderUniformMember member = this.GetArrayMember(name, WebGPUShaderUniformType.Matrix4x4, values.Length); + for (int i = 0; i < values.Length; i++) + { + WriteMatrix4x4(this.data, member.Offset + (i * member.Stride), values[i]); + } + } + + /// + /// Creates an immutable snapshot of the current values. + /// + /// The immutable uniform values. + /// The snapshot copies the packed bytes so subsequent builder changes cannot mutate a retained scene. + internal WebGPUShaderUniforms Build() => new(this.layout, (byte[])this.data.Clone()); + + /// + /// Resolves an array member and verifies that the caller supplied its complete fixed-size value. + /// + /// The declared member name. + /// The required element type. + /// The supplied element count. + /// The matching packed member. + private WebGPUShaderUniformMember GetArrayMember(string name, WebGPUShaderUniformType type, int elementCount) + { + WebGPUShaderUniformMember member = this.layout.GetMember(name, type, isArray: true); + if (member.Uniform.ElementCount != elementCount) + { + throw new ArgumentException($"The uniform '{name}' requires exactly {member.Uniform.ElementCount} elements.", nameof(elementCount)); + } + + return member; + } + + /// + /// Writes one floating-point value using WGSL's little-endian host-shareable representation. + /// + private static void WriteFloat32(Span destination, int offset, float value) + => BinaryPrimitives.WriteInt32LittleEndian(destination.Slice(offset, sizeof(float)), BitConverter.SingleToInt32Bits(value)); + + /// + /// Writes one two-component vector at its calculated member offset. + /// + private static void WriteVector2(Span destination, int offset, Vector2 value) + { + WriteFloat32(destination, offset, value.X); + WriteFloat32(destination, offset + 4, value.Y); + } + + /// + /// Writes one three-component vector without overwriting its trailing layout padding. + /// + private static void WriteVector3(Span destination, int offset, Vector3 value) + { + WriteFloat32(destination, offset, value.X); + WriteFloat32(destination, offset + 4, value.Y); + WriteFloat32(destination, offset + 8, value.Z); + } + + /// + /// Writes one four-component vector at its calculated member offset. + /// + private static void WriteVector4(Span destination, int offset, Vector4 value) + { + WriteFloat32(destination, offset, value.X); + WriteFloat32(destination, offset + 4, value.Y); + WriteFloat32(destination, offset + 8, value.Z); + WriteFloat32(destination, offset + 12, value.W); + } + + /// + /// Writes a row-addressed into WGSL's column-major matrix representation. + /// + private static void WriteMatrix4x4(Span destination, int offset, Matrix4x4 value) + { + // WGSL matrices are stored column-major. Writing each System.Numerics row/column element + // explicitly preserves the matrix's M[row][column] values across the representation change. + WriteVector4(destination, offset, new Vector4(value.M11, value.M21, value.M31, value.M41)); + WriteVector4(destination, offset + 16, new Vector4(value.M12, value.M22, value.M32, value.M42)); + WriteVector4(destination, offset + 32, new Vector4(value.M13, value.M23, value.M33, value.M43)); + WriteVector4(destination, offset + 48, new Vector4(value.M14, value.M24, value.M34, value.M44)); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs new file mode 100644 index 000000000..c8ed499d5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformLayout.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes the names, types, and fixed array lengths of values supplied to a WebGPU layer-effect shader. +/// +public sealed class WebGPUShaderUniformLayout +{ + /// + /// The maximum packed byte length accepted for a shader uniform layout. + /// + public const int MaximumByteLength = 64 * 1024; + + // WGSL reserves these identifiers for the language and future language evolution. The + // framework prefix is checked separately so generated names cannot collide with user names. + private static readonly HashSet DisallowedNames = new(StringComparer.Ordinal) + { + "NULL", "Self", "abstract", "active", "alias", "alignas", "alignof", "as", "asm", "asm_fragment", "async", "attribute", "auto", "await", + "become", "break", "case", "cast", "catch", "class", "co_await", "co_return", "co_yield", "coherent", "column_major", "common", "compile", + "compile_fragment", "concept", "const", "const_assert", "const_cast", "consteval", "constexpr", "constinit", "continue", "continuing", "crate", + "debugger", "decltype", "default", "delete", "demote", "demote_to_helper", "diagnostic", "discard", "do", "dynamic_cast", "else", "enable", + "enum", "explicit", "export", "extends", "extern", "external", "fallthrough", "false", "filter", "final", "finally", "fn", "for", "friend", + "from", "fxgroup", "get", "goto", "groupshared", "highp", "if", "impl", "implements", "import", "inline", "instanceof", "interface", "layout", + "let", "loop", "lowp", "macro", "macro_rules", "match", "mediump", "meta", "mod", "module", "move", "mut", "mutable", "namespace", "new", + "nil", "noexcept", "noinline", "nointerpolation", "non_coherent", "noncoherent", "noperspective", "null", "nullptr", "of", "operator", "override", + "package", "packoffset", "partition", "pass", "patch", "pixelfragment", "precise", "precision", "premerge", "priv", "protected", "pub", "public", + "readonly", "ref", "regardless", "register", "reinterpret_cast", "require", "requires", "resource", "restrict", "return", "self", "set", "shared", + "sizeof", "smooth", "snorm", "static", "static_assert", "static_cast", "std", "struct", "subroutine", "super", "switch", "target", "template", "this", + "thread_local", "throw", "trait", "true", "try", "type", "typedef", "typeid", "typename", "typeof", "union", "unless", "unorm", "unsafe", + "unsized", "use", "using", "var", "varying", "virtual", "volatile", "wgsl", "where", "while", "with", "writeonly", "yield" + }; + + private readonly WebGPUShaderUniform[] uniforms; + private readonly WebGPUShaderUniformMember[] members; + private readonly Dictionary memberIndices; + + /// + /// Initializes a new instance of the class. + /// + /// The ordered uniform declarations. + public WebGPUShaderUniformLayout(ReadOnlySpan uniforms) + { + this.uniforms = uniforms.ToArray(); + this.members = new WebGPUShaderUniformMember[uniforms.Length]; + this.memberIndices = new Dictionary(uniforms.Length, StringComparer.Ordinal); + + int offset = 0; + int structureAlignment = 4; + + // Calculate the host-shareable uniform layout once. The builder and generated WGSL both + // consume these members, preventing the CPU byte offsets from drifting from shader types. + for (int i = 0; i < uniforms.Length; i++) + { + WebGPUShaderUniform uniform = uniforms[i]; + ValidateName(uniform.Name, nameof(uniforms), "uniform"); + if (uniform.ElementCount <= 0) + { + throw new ArgumentException("Uniform element counts must be greater than zero.", nameof(uniforms)); + } + + if (!this.memberIndices.TryAdd(uniform.Name, i)) + { + throw new ArgumentException($"The uniform name '{uniform.Name}' is declared more than once.", nameof(uniforms)); + } + + GetTypeLayout(uniform.Type, out int alignment, out int size, out string wgslType); + int memberAlignment = alignment; + int stride = AlignUp(size, alignment); + if (uniform.ElementCount > 1) + { + // Uniform-address-space arrays have a minimum 16-byte alignment and stride on + // every WebGPU implementation. Using that portable layout avoids an optional + // WGSL feature changing whether the same public program can run on a device. + memberAlignment = Math.Max(16, alignment); + stride = AlignUp(size, memberAlignment); + } + + offset = AlignUp(offset, memberAlignment); + this.members[i] = new WebGPUShaderUniformMember(uniform, offset, stride, wgslType); + int memberSize = uniform.ElementCount == 1 ? size : checked(stride * uniform.ElementCount); + offset = checked(offset + memberSize); + structureAlignment = Math.Max(structureAlignment, memberAlignment); + } + + // Uniform structs also have a minimum 16-byte alignment. Capping the final binding at + // WebGPU's guaranteed limit makes a valid layout portable instead of device-dependent. + this.ByteLength = Math.Max(16, AlignUp(offset, Math.Max(16, structureAlignment))); + + // WebGPU guarantees at least 64 KiB for a uniform buffer binding. Enforcing the public + // limit here keeps every successfully constructed program portable across conforming devices. + if (this.ByteLength > MaximumByteLength) + { + throw new ArgumentException($"The uniform layout requires {this.ByteLength} bytes; WebGPU layer-effect uniforms are limited to {MaximumByteLength} bytes.", nameof(uniforms)); + } + + this.WgslStructureDeclaration = CreateWgslStructureDeclaration(this.members); + } + + /// + /// Gets the ordered uniform declarations. + /// + public ReadOnlySpan Uniforms => this.uniforms; + + /// + /// Gets the calculated members in declaration order. + /// + internal ReadOnlySpan Members => this.members; + + /// + /// Gets the packed byte length required for one set of values. + /// + internal int ByteLength { get; } + + /// + /// Gets the WGSL structure declaration generated from this layout. + /// + internal string WgslStructureDeclaration { get; } + + /// + /// Creates a mutable builder initialized with zero values for every declared uniform. + /// + /// A builder accepting the names and types declared by this layout. + internal WebGPUShaderUniformBuilder CreateUniforms() => new(this); + + /// + /// Gets the packed member matching the supplied name and type. + /// + /// The declared member name. + /// The required member type. + /// Whether the caller requires an array member. + /// The matching packed member. + internal WebGPUShaderUniformMember GetMember(string name, WebGPUShaderUniformType type, bool isArray) + { + Guard.NotNull(name, nameof(name)); + if (!this.memberIndices.TryGetValue(name, out int index)) + { + throw new ArgumentException($"The uniform layout does not contain a member named '{name}'.", nameof(name)); + } + + WebGPUShaderUniformMember member = this.members[index]; + if (member.Uniform.Type != type || (member.Uniform.ElementCount > 1) != isArray) + { + string expected = isArray ? $"an array of {type}" : type.ToString(); + throw new ArgumentException($"The uniform '{name}' is not declared as {expected}.", nameof(name)); + } + + return member; + } + + /// + /// Validates that a public name is a WGSL identifier which cannot collide with framework or reserved names. + /// + /// The name to validate. + /// The public parameter reported by validation failures. + /// The declaration kind described by validation failures. + internal static void ValidateName(string name, string parameterName, string declarationKind) + { + Guard.NotNull(name, parameterName); + if (!IsValidPublicIdentifier(name)) + { + throw new ArgumentException($"'{name}' cannot be used as a WGSL {declarationKind} name.", parameterName); + } + } + + /// + /// Gets the occupied byte length of one uniform member, including fixed-array stride. + /// + /// The calculated member. + /// The member byte length. + internal static int GetMemberByteLength(WebGPUShaderUniformMember member) + { + if (member.Uniform.ElementCount > 1) + { + return checked(member.Stride * member.Uniform.ElementCount); + } + + return member.Uniform.Type switch + { + WebGPUShaderUniformType.Float32 => sizeof(float), + WebGPUShaderUniformType.Int32 => sizeof(int), + WebGPUShaderUniformType.UInt32 => sizeof(uint), + WebGPUShaderUniformType.Vector2 => 8, + WebGPUShaderUniformType.Vector3 => 12, + WebGPUShaderUniformType.Vector4 => 16, + WebGPUShaderUniformType.Matrix4x4 => 64, + _ => throw new ArgumentOutOfRangeException(nameof(member)) + }; + } + + /// + /// Tests whether a name is available to a public shader declaration. + /// + /// The name to test. + /// when the name is a non-reserved WGSL identifier. + internal static bool IsValidPublicIdentifier(string name) + { + if (name.Length == 0 || !IsIdentifierStart(name[0])) + { + return false; + } + + for (int i = 1; i < name.Length; i++) + { + if (!IsIdentifierPart(name[i])) + { + return false; + } + } + + return !name.StartsWith("imagesharp_", StringComparison.Ordinal) && !DisallowedNames.Contains(name); + } + + /// + /// Tests whether a character may begin a WGSL identifier. + /// + /// The character to test. + /// when the character is valid. + private static bool IsIdentifierStart(char value) + => value == '_' || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z'); + + /// + /// Tests whether a character may follow the first character of a WGSL identifier. + /// + /// The character to test. + /// when the character is valid. + private static bool IsIdentifierPart(char value) + => IsIdentifierStart(value) || (value >= '0' && value <= '9'); + + /// + /// Rounds a byte offset up to the requested power-of-two alignment. + /// + /// The byte offset to align. + /// The required power-of-two alignment. + /// The aligned byte offset. + private static int AlignUp(int value, int alignment) + => checked((value + alignment - 1) & -alignment); + + /// + /// Maps one public uniform type to its WGSL host-shareable alignment, size, and spelling. + /// + /// The public uniform type. + /// Receives its required byte alignment. + /// Receives its unpadded byte size. + /// Receives its WGSL type spelling. + private static void GetTypeLayout(WebGPUShaderUniformType type, out int alignment, out int size, out string wgslType) + { + switch (type) + { + case WebGPUShaderUniformType.Float32: + alignment = 4; + size = 4; + wgslType = "f32"; + break; + case WebGPUShaderUniformType.Int32: + alignment = 4; + size = 4; + wgslType = "i32"; + break; + case WebGPUShaderUniformType.UInt32: + alignment = 4; + size = 4; + wgslType = "u32"; + break; + case WebGPUShaderUniformType.Vector2: + alignment = 8; + size = 8; + wgslType = "vec2"; + break; + case WebGPUShaderUniformType.Vector3: + alignment = 16; + size = 12; + wgslType = "vec3"; + break; + case WebGPUShaderUniformType.Vector4: + alignment = 16; + size = 16; + wgslType = "vec4"; + break; + case WebGPUShaderUniformType.Matrix4x4: + alignment = 16; + size = 64; + wgslType = "mat4x4"; + break; + default: + throw new ArgumentOutOfRangeException(nameof(type)); + } + } + + /// + /// Generates the WGSL structure that exactly mirrors the calculated host byte layout. + /// + /// The calculated members in declaration order. + /// The complete WGSL structure declaration. + private static string CreateWgslStructureDeclaration(ReadOnlySpan members) + { + StringBuilder builder = new(); + builder.AppendLine("struct ImageSharpUniforms {"); + if (members.IsEmpty) + { + // WGSL structures cannot be empty. This private member keeps a stable binding layout + // for programs that do not expose user values. + builder.AppendLine(" imagesharp_padding: vec4,"); + } + else + { + for (int i = 0; i < members.Length; i++) + { + WebGPUShaderUniformMember member = members[i]; + builder.Append(" ").Append(member.Uniform.Name).Append(": "); + if (member.Uniform.ElementCount == 1) + { + builder.Append(member.WgslType); + } + else + { + builder.Append("array<").Append(member.WgslType).Append(", ").Append(member.Uniform.ElementCount).Append('>'); + } + + builder.AppendLine(","); + } + } + + builder.AppendLine("};"); + return builder.ToString(); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformMember.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformMember.cs new file mode 100644 index 000000000..4fb54ad23 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformMember.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes the packed location of one shader uniform member. +/// +internal readonly struct WebGPUShaderUniformMember +{ + /// + /// Initializes a new instance of the struct. + /// + /// The public declaration. + /// The byte offset of the first element. + /// The byte stride between elements. + /// The WGSL element type. + public WebGPUShaderUniformMember(WebGPUShaderUniform uniform, int offset, int stride, string wgslType) + { + this.Uniform = uniform; + this.Offset = offset; + this.Stride = stride; + this.WgslType = wgslType; + } + + /// + /// Gets the public declaration. + /// + public WebGPUShaderUniform Uniform { get; } + + /// + /// Gets the byte offset of the first element. + /// + public int Offset { get; } + + /// + /// Gets the byte stride between elements. + /// + public int Stride { get; } + + /// + /// Gets the WGSL element type. + /// + public string WgslType { get; } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformType.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformType.cs new file mode 100644 index 000000000..33e06e137 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniformType.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies a value type that can be supplied to a WebGPU layer-effect shader. +/// +public enum WebGPUShaderUniformType +{ + /// + /// A 32-bit floating-point value. + /// + Float32, + + /// + /// A signed 32-bit integer value. + /// + Int32, + + /// + /// An unsigned 32-bit integer value. + /// + UInt32, + + /// + /// A two-component 32-bit floating-point vector. + /// + Vector2, + + /// + /// A three-component 32-bit floating-point vector. + /// + Vector3, + + /// + /// A four-component 32-bit floating-point vector. + /// + Vector4, + + /// + /// A four-by-four 32-bit floating-point matrix. + /// + Matrix4x4 +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniforms.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniforms.cs new file mode 100644 index 000000000..28f494eba --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUShaderUniforms.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Contains an immutable snapshot of values supplied to a WebGPU layer-effect shader. +/// +public sealed class WebGPUShaderUniforms +{ + private readonly byte[] data; + + /// + /// Initializes a new instance of the class. + /// + /// The layout that describes the values. + /// The packed immutable value snapshot. + internal WebGPUShaderUniforms(WebGPUShaderUniformLayout layout, byte[] data) + { + this.Layout = layout; + this.data = data; + } + + /// + /// Gets the layout that describes the values. + /// + public WebGPUShaderUniformLayout Layout { get; } + + /// + /// Gets the packed immutable value bytes. + /// + internal ReadOnlySpan Data => this.data; +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFactory.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFactory.cs new file mode 100644 index 000000000..ae5f9b9ef --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFactory.cs @@ -0,0 +1,261 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Silk.NET.Core.Contexts; +using Silk.NET.GLFW; +using Silk.NET.SDL; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Creates WebGPU surfaces from native window handles. +/// +internal static unsafe class WebGPUSurfaceFactory +{ + /// + /// Creates a WebGPU surface for a native window source. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The native window source. + /// The created surface, or on failure. + public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, INativeWindowSource source) => + + // Every backend entry point supplies an initialized window source. The Silk contract makes + // Native nullable only for source implementations that have not created their window yet. + Create(api, instance, source.Native!, true); + + /// + /// Creates a WebGPU surface for an externally-owned host. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The external native host. + /// The created surface, or on failure. + public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, WebGPUSurfaceHost host) => + + // External hosts model the descriptors accepted by wgpu-native directly. GLFW and SDL are + // the only toolkit-level entries and are translated before reaching the native C API. + host.Kind switch + { + WebGPUSurfaceHostKind.Glfw => Create(api, instance, new GlfwNativeWindow(GlfwProvider.GLFW.Value, (WindowHandle*)host.Handle0), false), + WebGPUSurfaceHostKind.Sdl => Create(api, instance, new SdlNativeWindow(SdlProvider.SDL.Value, (Window*)host.Handle0), false), + WebGPUSurfaceHostKind.Win32 => CreateWindowsSurface(api, instance, host.Handle0, host.Handle1), + WebGPUSurfaceHostKind.X11 => CreateXlibSurface(api, instance, host.Handle0, host.Number0), + WebGPUSurfaceHostKind.Cocoa => CreateCocoaSurface(api, instance, host.Handle0), + WebGPUSurfaceHostKind.MetalLayer => CreateMetalLayerSurface(api, instance, host.Handle0), + WebGPUSurfaceHostKind.Wayland => CreateWaylandSurface(api, instance, host.Handle0, host.Handle1), + WebGPUSurfaceHostKind.SwapChainPanel => CreateSwapChainPanelSurface(api, instance, host.Handle0), + WebGPUSurfaceHostKind.Android => CreateAndroidSurface(api, instance, host.Handle0), + _ => null, + }; + + /// + /// Resolves a Silk native window to one of the platform handles represented by the WebGPU C surface descriptors. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The native window information exposed by Silk. + /// + /// Whether a GLFW or SDL toolkit handle may be translated to its underlying platform handles. Recursive calls disable + /// translation so a toolkit that cannot expose a supported platform source terminates without recursing indefinitely. + /// + /// The created surface, or when Silk exposes no compatible platform source. + private static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, INativeWindow native, bool translateToolkitHandle) + { + if (native.X11 is { } x11) + { + return CreateXlibSurface(api, instance, x11.Display, x11.Window); + } + + if (native.Cocoa is { } cocoa) + { + return CreateCocoaSurface(api, instance, cocoa); + } + + if (native.Wayland is { } wayland) + { + return CreateWaylandSurface(api, instance, wayland.Display, wayland.Surface); + } + + if (native.Win32 is { } win32) + { + return CreateWindowsSurface(api, instance, win32.Hwnd, win32.HInstance); + } + + if (native.Android is { } android) + { + return CreateAndroidSurface(api, instance, android.Window); + } + + if (translateToolkitHandle && native.Glfw is { } glfw) + { + // GLFWwindow* is a toolkit handle rather than a WebGPU surface source. Resolve the + // platform window once, then create the surface from the resulting native handles. + GlfwNativeWindow platformWindow = new(GlfwProvider.GLFW.Value, (WindowHandle*)glfw); + return Create(api, instance, platformWindow, false); + } + + if (translateToolkitHandle && native.Sdl is { } sdl) + { + // SDL_Window* follows the same rule. SDL_GetWindowWMInfo exposes the underlying + // platform handles required by the WebGPU C surface descriptor. + SdlNativeWindow platformWindow = new(SdlProvider.SDL.Value, (Window*)sdl); + return Create(api, instance, platformWindow, false); + } + + return null; + } + + /// + /// Creates a surface from an Xlib display and window identifier. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The Xlib Display*. + /// The Xlib window identifier. + /// The created surface. + private static WGPUSurfaceImpl* CreateXlibSurface(WebGPU api, WGPUInstanceImpl* instance, nint display, nuint window) + { + WGPUSurfaceSourceXlibWindow source = new() + { + chain = new WGPUChainedStruct { sType = WGPUSType.SurfaceSourceXlibWindow }, + display = (void*)display, + window = window + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Creates and attaches the metal layer required to present through an AppKit window. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The AppKit NSWindow*. + /// The created surface. + private static WGPUSurfaceImpl* CreateCocoaSurface(WebGPU api, WGPUInstanceImpl* instance, nint nsWindow) + { + nint metalLayer = MacOSMetalLayerFactory.Create(nsWindow); + + try + { + return CreateMetalLayerSurface(api, instance, metalLayer); + } + finally + { + // The NSView retains its layer property, so release the ownership acquired by alloc/init. + MacOSMetalLayerFactory.Release(metalLayer); + } + } + + /// + /// Creates a surface from an existing Core Animation metal layer. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The CAMetalLayer* used for presentation. + /// The created surface. + private static WGPUSurfaceImpl* CreateMetalLayerSurface(WebGPU api, WGPUInstanceImpl* instance, nint metalLayer) + { + WGPUSurfaceSourceMetalLayer source = new() + { + chain = new WGPUChainedStruct { sType = WGPUSType.SurfaceSourceMetalLayer }, + layer = (void*)metalLayer + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Creates a surface from a Wayland display and surface pair. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The Wayland wl_display*. + /// The Wayland wl_surface*. + /// The created surface. + private static WGPUSurfaceImpl* CreateWaylandSurface(WebGPU api, WGPUInstanceImpl* instance, nint display, nint surface) + { + WGPUSurfaceSourceWaylandSurface source = new() + { + chain = new WGPUChainedStruct { sType = WGPUSType.SurfaceSourceWaylandSurface }, + display = (void*)display, + surface = (void*)surface + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Creates a surface from a Win32 window and its module instance. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The Win32 HWND. + /// The Win32 HINSTANCE. + /// The created surface. + private static WGPUSurfaceImpl* CreateWindowsSurface(WebGPU api, WGPUInstanceImpl* instance, nint hwnd, nint hinstance) + { + WGPUSurfaceSourceWindowsHWND source = new() + { + chain = new WGPUChainedStruct { sType = WGPUSType.SurfaceSourceWindowsHWND }, + hwnd = (void*)hwnd, + hinstance = (void*)hinstance + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Creates a surface from an Android native window. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The Android ANativeWindow*. + /// The created surface. + private static WGPUSurfaceImpl* CreateAndroidSurface(WebGPU api, WGPUInstanceImpl* instance, nint window) + { + WGPUSurfaceSourceAndroidNativeWindow source = new() + { + chain = new WGPUChainedStruct { sType = WGPUSType.SurfaceSourceAndroidNativeWindow }, + window = (void*)window + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Creates a surface from the native interface of a WinUI swap-chain panel. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The panel's ISwapChainPanelNative*. + /// The created surface. + private static WGPUSurfaceImpl* CreateSwapChainPanelSurface(WebGPU api, WGPUInstanceImpl* instance, nint panelNative) + { + WGPUSurfaceSourceSwapChainPanel source = new() + { + chain = new WGPUChainedStruct { sType = (WGPUSType)WGPUNativeSType.WGPUSType_SurfaceSourceSwapChainPanel }, + panelNative = (void*)panelNative + }; + + return CreateSurface(api, instance, (WGPUChainedStruct*)&source); + } + + /// + /// Passes a platform-specific chained source descriptor to wgpu-native. + /// + /// The WebGPU API facade. + /// The instance that owns the surface. + /// The platform source descriptor at the head of the chain. + /// The created surface. + private static WGPUSurfaceImpl* CreateSurface(WebGPU api, WGPUInstanceImpl* instance, WGPUChainedStruct* source) + { + // Every source descriptor is stack allocated by its owning helper. wgpuInstanceCreateSurface + // consumes the chain synchronously, so no pointer to that storage escapes this call. + WGPUSurfaceDescriptor descriptor = new() { nextInChain = source }; + return api.InstanceCreateSurface(instance, in descriptor); + } +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFrame.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFrame.cs index fcf070ee0..3e104f379 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFrame.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFrame.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -9,28 +9,58 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// A single acquired drawable frame returned by a WebGPU surface. /// Use the to draw the frame contents, then dispose the frame to show it on screen. /// +/// +/// Disposal submits the recorded canvas work, presents the surface, releases the per-frame texture and view, +/// and signals the owning surface so the next TryAcquireFrame call can succeed. Only one frame can be +/// outstanding per surface at a time. +/// public sealed unsafe class WebGPUSurfaceFrame : IDisposable { private readonly WebGPU api; + private readonly WebGPUDeviceContext deviceContext; + private readonly WebGPUTargetDescriptor targetDescriptor; + + // Mutable struct: Dispose nulls its internal owner guard, so the field must not be readonly + // or disposal would run on a defensive copy and leave the field's guard intact. private WebGPUHandle.HandleReference surfaceReference; private readonly WebGPUTextureHandle textureHandle; private readonly WebGPUTextureViewHandle textureViewHandle; + private readonly WebGPUPresentationRenderer presentationRenderer; private readonly Action? onDisposed; private bool isDisposed; + /// + /// Initializes a new instance of the class taking ownership of the per-frame + /// texture and view handles and holding an acquired reference on the surface handle until disposal. + /// + /// The shared WebGPU API loader. + /// The device context the frame renders through. + /// The swapchain texture format and alpha representation used when creating matching render targets. + /// The surface handle presented on disposal. The frame holds a reference, not ownership. + /// The owned per-frame texture handle, released on disposal. + /// The owned per-frame texture-view handle, released on disposal. + /// The drawing canvas targeting the persistent ImageSharp-owned frame texture. + /// The renderer that transfers the completed frame into the acquired surface texture. + /// Invoked after disposal completes; the owning surface uses this to allow the next acquire. internal WebGPUSurfaceFrame( WebGPU api, + WebGPUDeviceContext deviceContext, + WebGPUTargetDescriptor targetDescriptor, WebGPUSurfaceHandle surfaceHandle, WebGPUTextureHandle textureHandle, WebGPUTextureViewHandle textureViewHandle, DrawingCanvas canvas, + WebGPUPresentationRenderer presentationRenderer, Action? onDisposed = null) { this.api = api; + this.deviceContext = deviceContext; this.surfaceReference = surfaceHandle.AcquireReference(); this.textureHandle = textureHandle; this.textureViewHandle = textureViewHandle; + this.targetDescriptor = targetDescriptor; this.Canvas = canvas; + this.presentationRenderer = presentationRenderer; this.onDisposed = onDisposed; } @@ -39,6 +69,25 @@ internal WebGPUSurfaceFrame( /// public DrawingCanvas Canvas { get; } + /// + /// Creates an empty render target with the same texture format as this frame. + /// + /// The target width in pixels. + /// The target height in pixels. + /// The created render target. + /// + /// The created target does not contain a copy of this frame's pixels. + /// + public WebGPURenderTarget CreateRenderTarget(int width, int height) + { + this.ThrowIfDisposed(); + this.deviceContext.ThrowIfDisposed(); + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + return new WebGPURenderTarget(this.deviceContext, false, this.targetDescriptor, width, height, isPresentationSurface: false); + } + /// /// Disposes the frame, rendering and presenting it, then releasing the per-frame WebGPU resources. /// @@ -51,13 +100,16 @@ public void Dispose() try { - // Canvas disposal replays the recorded timeline. Present only after rendering has submitted work for - // this acquired surface texture. + // Queue ordering makes the selected copy or render-attachment transfer consume the + // completed canvas texture without a CPU wait. this.Canvas.Dispose(); - this.api.SurfacePresent((Surface*)this.surfaceReference.Handle); + this.presentationRenderer.Present(this.textureHandle, this.textureViewHandle); + _ = this.api.SurfacePresent((WGPUSurfaceImpl*)this.surfaceReference.Handle); } finally { + // Release the per-frame resources even when submit or present throws, and signal the + // owner last so a new frame can only be acquired after this one is fully torn down. this.textureViewHandle.Dispose(); this.textureHandle.Dispose(); this.surfaceReference.Dispose(); @@ -65,4 +117,10 @@ public void Dispose() this.onDisposed?.Invoke(); } } + + /// + /// Throws when this frame has been disposed. + /// + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHandle.cs index 4142bd5a5..f913433a8 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -12,19 +12,6 @@ internal sealed unsafe class WebGPUSurfaceHandle : WebGPUHandle { private readonly WebGPU? api; - /// - /// Initializes a new instance of the class. - /// - /// The WebGPU surface handle value. - /// - /// when this wrapper owns the surface and must release it; - /// when the caller retains ownership. - /// - internal WebGPUSurfaceHandle(nint surfaceHandle, bool ownsHandle) - : this(ownsHandle ? WebGPURuntime.GetApi() : null, surfaceHandle, ownsHandle) - { - } - /// /// Initializes a new instance of the class. /// @@ -37,7 +24,7 @@ internal WebGPUSurfaceHandle(nint surfaceHandle, bool ownsHandle) /// when this wrapper owns the surface and must release it; /// when the caller retains ownership. /// - internal WebGPUSurfaceHandle(WebGPU? api, nint surfaceHandle, bool ownsHandle) + public WebGPUSurfaceHandle(WebGPU? api, nint surfaceHandle, bool ownsHandle) : base(surfaceHandle, ownsHandle) => this.api = api; @@ -46,7 +33,7 @@ protected override bool ReleaseHandle() { try { - this.api?.SurfaceRelease((Surface*)this.handle); + this.api?.SurfaceRelease((WGPUSurfaceImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHost.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHost.cs index 663b4f23c..9721d446f 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHost.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceHost.cs @@ -8,17 +8,53 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal enum WebGPUSurfaceHostKind { + /// + /// A GLFW-owned window (GLFWwindow* in ). + /// Glfw, + + /// + /// An SDL-owned window (SDL_Window* in ). + /// Sdl, + + /// + /// A Win32 window (HWND in and HINSTANCE in + /// ). + /// Win32, + + /// + /// An X11 window (Display* in , window id in + /// ). + /// X11, + + /// + /// A Cocoa window (NSWindow* in ). + /// Cocoa, - UIKit, + + /// + /// A Core Animation metal layer (CAMetalLayer* in ). + /// + MetalLayer, + + /// + /// A Wayland surface (wl_display* in , wl_surface* in + /// ). + /// Wayland, - WinRT, + + /// + /// A WinUI swap-chain panel (ISwapChainPanelNative* in ). + /// + SwapChainPanel, + + /// + /// An Android native window (ANativeWindow* in ). + /// Android, - Vivante, - EGL, } /// @@ -28,6 +64,8 @@ internal enum WebGPUSurfaceHostKind /// /// Construct via the platform-specific factory methods. The caller retains ownership of the underlying handles; /// the external surface never releases them. +/// GLFW and SDL handles are translated to their underlying platform source during surface creation. The remaining +/// factories correspond directly to surface source descriptors accepted by the bundled wgpu-native API. /// public readonly struct WebGPUSurfaceHost { @@ -35,48 +73,48 @@ public readonly struct WebGPUSurfaceHost // to the internal surface adapter, keeping backend windowing details out of the public API. private readonly nint handle0; private readonly nint handle1; - private readonly nint handle2; private readonly nuint number0; - private readonly uint number1; - private readonly uint number2; - private readonly uint number3; + /// + /// Initializes a new instance of the struct. Only the slots meaningful + /// for are populated; see the public factory methods for the per-platform mapping. + /// + /// The native platform surface kind. + /// The first pointer-sized handle slot. + /// The second pointer-sized handle slot. + /// The pointer-sized numeric slot. private WebGPUSurfaceHost( WebGPUSurfaceHostKind kind, nint handle0 = 0, nint handle1 = 0, - nint handle2 = 0, - nuint number0 = 0, - uint number1 = 0, - uint number2 = 0, - uint number3 = 0) + nuint number0 = 0) { this.Kind = kind; this.handle0 = handle0; this.handle1 = handle1; - this.handle2 = handle2; this.number0 = number0; - this.number1 = number1; - this.number2 = number2; - this.number3 = number3; } + /// + /// Gets the native platform surface kind that defines how the handle and number slots are interpreted. + /// internal WebGPUSurfaceHostKind Kind { get; } + /// + /// Gets the first pointer-sized handle slot; its meaning depends on . + /// internal nint Handle0 => this.handle0; + /// + /// Gets the second pointer-sized handle slot; its meaning depends on . + /// internal nint Handle1 => this.handle1; - internal nint Handle2 => this.handle2; - + /// + /// Gets the pointer-sized numeric slot; its meaning depends on . + /// internal nuint Number0 => this.number0; - internal uint Number1 => this.number1; - - internal uint Number2 => this.number2; - - internal uint Number3 => this.number3; - /// /// Creates a host descriptor for a GLFW-owned window. /// @@ -94,15 +132,13 @@ public static WebGPUSurfaceHost Sdl(nint sdlWindow) => new(WebGPUSurfaceHostKind.Sdl, handle0: sdlWindow); /// - /// Creates a host descriptor for a Win32 window. The device context handle is optional; when omitted, - /// the surface layer derives one from the window as needed. + /// Creates a host descriptor for a Win32 window. /// /// The Win32 window handle (HWND). /// The module instance handle (HINSTANCE) associated with the window. - /// The device context handle (HDC); pass if unavailable. /// A Win32 host descriptor. - public static WebGPUSurfaceHost Win32(nint hwnd, nint hinstance, nint hdc = 0) - => new(WebGPUSurfaceHostKind.Win32, handle0: hwnd, handle1: hdc, handle2: hinstance); + public static WebGPUSurfaceHost Win32(nint hwnd, nint hinstance) + => new(WebGPUSurfaceHostKind.Win32, handle0: hwnd, handle1: hinstance); /// /// Creates a host descriptor for an X11 window. @@ -122,15 +158,13 @@ public static WebGPUSurfaceHost Cocoa(nint nsWindow) => new(WebGPUSurfaceHostKind.Cocoa, handle0: nsWindow); /// - /// Creates a host descriptor for a UIKit window with its framebuffer objects. + /// Creates a host descriptor for a Core Animation metal layer. /// - /// The UIKit window pointer (UIWindow*). - /// The framebuffer object id. - /// The color buffer object id. - /// The resolve framebuffer object id. - /// A UIKit host descriptor. - public static WebGPUSurfaceHost UIKit(nint uiWindow, uint framebuffer, uint colorbuffer, uint resolveFramebuffer) - => new(WebGPUSurfaceHostKind.UIKit, handle0: uiWindow, number1: framebuffer, number2: colorbuffer, number3: resolveFramebuffer); + /// The Core Animation metal layer pointer (CAMetalLayer*). + /// A metal-layer host descriptor. + /// The caller owns the layer and must keep it valid for the lifetime of the external surface. + public static WebGPUSurfaceHost MetalLayer(nint metalLayer) + => new(WebGPUSurfaceHostKind.MetalLayer, handle0: metalLayer); /// /// Creates a host descriptor for a Wayland surface. @@ -142,37 +176,19 @@ public static WebGPUSurfaceHost Wayland(nint display, nint surface) => new(WebGPUSurfaceHostKind.Wayland, handle0: display, handle1: surface); /// - /// Creates a host descriptor for a WinRT window. + /// Creates a host descriptor for a WinUI swap-chain panel. /// - /// The WinRT window's IInspectable*. - /// A WinRT host descriptor. - public static WebGPUSurfaceHost WinRT(nint inspectable) - => new(WebGPUSurfaceHostKind.WinRT, handle0: inspectable); + /// The swap-chain panel's ISwapChainPanelNative* interface pointer. + /// A swap-chain-panel host descriptor. + /// The caller owns the interface pointer and must keep it valid for the lifetime of the external surface. + public static WebGPUSurfaceHost SwapChainPanel(nint panelNative) + => new(WebGPUSurfaceHostKind.SwapChainPanel, handle0: panelNative); /// /// Creates a host descriptor for an Android native window. /// /// The Android native window pointer (ANativeWindow*). - /// The associated EGL surface (optional). /// An Android host descriptor. - public static WebGPUSurfaceHost Android(nint aNativeWindow, nint eglSurface = 0) - => new(WebGPUSurfaceHostKind.Android, handle0: aNativeWindow, handle1: eglSurface); - - /// - /// Creates a host descriptor for a Vivante-backed window. - /// - /// The Vivante EGL display type (EGLNativeDisplayType). - /// The Vivante EGL window type (EGLNativeWindowType). - /// A Vivante host descriptor. - public static WebGPUSurfaceHost Vivante(nint display, nint window) - => new(WebGPUSurfaceHostKind.Vivante, handle0: display, handle1: window); - - /// - /// Creates a host descriptor for an EGL display and surface. - /// - /// The EGL display handle ( if unspecified). - /// The EGL surface handle ( if unspecified). - /// An EGL host descriptor. - public static WebGPUSurfaceHost EGL(nint eglDisplay, nint eglSurface) - => new(WebGPUSurfaceHostKind.EGL, handle0: eglDisplay, handle1: eglSurface); + public static WebGPUSurfaceHost Android(nint aNativeWindow) + => new(WebGPUSurfaceHostKind.Android, handle0: aNativeWindow); } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs index 8f98f7a89..f3dc8af7d 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceResources.cs @@ -1,81 +1,88 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Silk.NET.Core.Contexts; -using Silk.NET.WebGPU; -using SilkPresentMode = Silk.NET.WebGPU.PresentMode; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Owning container for the per-surface WebGPU stack: instance, surface, adapter, device, queue, device context, -/// and the negotiated swapchain texture format. +/// Owns one native presentation surface and its negotiated swapchain state. /// /// /// -/// Provides a single static factory that bootstraps every handle in order and leaves the surface -/// initially configured against initialPresentMode and initialFramebufferSize. +/// Provides static factory methods that attach a native surface to a shared +/// and leaves it initially configured. /// Callers hold the returned instance for the lifetime of the rendering surface and dispose it when the surface tears down. /// /// -/// Shared by the owned-window and external-surface entry points. Both provide a native surface source while this class owns the WebGPU -/// handles, surface creation, per-frame texture acquisition, and swapchain reconfiguration. +/// Shared by the owned-window and external-surface entry points. Both provide a native surface source while this class owns surface +/// creation, per-frame texture acquisition, and swapchain reconfiguration. /// /// -/// All handle fields are non-null after successful construction. releases them in reverse -/// acquisition order. +/// The adapter, device, queue, drawing context, and compiled pipelines belong to the session and outlive this surface. /// /// internal sealed unsafe class WebGPUSurfaceResources : IDisposable { + // WebGPU guarantees render-attachment support for presentable textures. CopyDst is optional, + // but enables the native-copy presentation path when the surface reports it. + private const TextureUsage RequiredSurfaceUsage = TextureUsage.RenderAttachment; + /// - /// Upper bound for the asynchronous adapter and device request callbacks. Exceeding this throws. + /// Upper bound for polling a presented frame's queue-completion callback. /// private const int CallbackTimeoutMilliseconds = 10_000; private bool isDisposed; + + // True while an acquired WebGPUSurfaceFrame has not yet been disposed. Guards TryAcquireFrame + // against overlapping acquires; cleared by the frame's onDisposed callback. private bool frameInFlight; - private readonly FeatureName requiredFeature; - private readonly Configuration configuration; - private WebGPUDeviceContext deviceContext; + + // Work-done signal registered when a frame is presented. Waiting on it at the next acquire + // caps the CPU at one frame in flight: without a governor the CPU submits ahead until the + // driver throttles it inside queue submit at unpredictable mid-frame points, which paces + // worse than one deliberate wait at the frame boundary. + private FramePacingSignal? previousFrameWorkDone; + private readonly WebGPUSurfaceSession session; + private readonly TextureUsage surfaceUsage; + private WebGPURenderTarget? presentationTarget; + private WebGPUPresentationRenderer? presentationRenderer; + private readonly uint supportedPresentModes; /// - /// Initializes a new instance of the class with already-acquired handles. - /// Only invoked by after every handle has been successfully bootstrapped. + /// Initializes a new instance of the class with an acquired surface. /// /// The shared WebGPU API loader. - /// The ImageSharp configuration the device context uses for its rendering backend. - /// The owned WebGPU instance handle. + /// The session that owns the shared device resources. /// The owned WebGPU surface handle attached to the native host. - /// The owned adapter handle selected for . - /// The owned device handle requested from . - /// The owned default queue handle paired with . - /// The device context bound to and . /// The negotiated swapchain texture format. - /// The optional WebGPU feature required by the selected texture format. + /// How the native compositor interprets the swapchain alpha channel. + /// The negotiated swapchain presentation mode. + /// A bit field containing the presentation modes supported by the surface. + /// The texture usages selected from the capabilities reported by the surface. private WebGPUSurfaceResources( WebGPU api, - Configuration configuration, - WebGPUInstanceHandle instanceHandle, + WebGPUSurfaceSession session, WebGPUSurfaceHandle surfaceHandle, - WebGPUAdapterHandle adapterHandle, - WebGPUDeviceHandle deviceHandle, - WebGPUQueueHandle queueHandle, - WebGPUDeviceContext deviceContext, WebGPUTextureFormat format, - FeatureName requiredFeature) + WebGPUCompositeAlphaMode alphaMode, + WebGPUPresentMode presentMode, + uint supportedPresentModes, + TextureUsage surfaceUsage) { this.Api = api; - this.configuration = configuration; - this.InstanceHandle = instanceHandle; + this.session = session; this.SurfaceHandle = surfaceHandle; - this.AdapterHandle = adapterHandle; - this.DeviceHandle = deviceHandle; - this.QueueHandle = queueHandle; - this.deviceContext = deviceContext; this.Format = format; - this.requiredFeature = requiredFeature; + this.AlphaMode = alphaMode; + this.PresentMode = presentMode; + this.supportedPresentModes = supportedPresentModes; + this.surfaceUsage = surfaceUsage; } /// @@ -83,11 +90,6 @@ private WebGPUSurfaceResources( /// public WebGPU Api { get; } - /// - /// Gets the WebGPU instance owned by this stack. Released on . - /// - public WebGPUInstanceHandle InstanceHandle { get; } - /// /// Gets the WebGPU surface attached to the native host. The surface is reconfigured whenever /// runs and released on . @@ -95,33 +97,41 @@ private WebGPUSurfaceResources( public WebGPUSurfaceHandle SurfaceHandle { get; } /// - /// Gets the adapter selected for the current surface. Released on . + /// Gets the negotiated swapchain texture format. + /// + public WebGPUTextureFormat Format { get; private set; } + + /// + /// Gets the negotiated composite alpha mode. /// - public WebGPUAdapterHandle AdapterHandle { get; private set; } + public WebGPUCompositeAlphaMode AlphaMode { get; } /// - /// Gets the logical device requested from . Released on . + /// Gets the texture format and alpha representation negotiated for acquired frames. /// - public WebGPUDeviceHandle DeviceHandle { get; private set; } + public WebGPUTargetDescriptor TargetDescriptor + => WebGPUDrawingBackend.CreateNativeTargetDescriptor( + this.Format, + this.AlphaMode == WebGPUCompositeAlphaMode.Premultiplied ? PixelAlphaRepresentation.Associated : PixelAlphaRepresentation.Unassociated); /// - /// Gets the default queue for . Released on . + /// Gets the device context owned by the surface session. /// - public WebGPUQueueHandle QueueHandle { get; private set; } + public WebGPUDeviceContext DeviceContext => this.session.DeviceContext; /// - /// Gets the swapchain texture format chosen at construction time. - /// Stable for the lifetime of this instance. + /// Gets the negotiated swapchain presentation mode. /// - public WebGPUTextureFormat Format { get; } + public WebGPUPresentMode PresentMode { get; private set; } /// - /// Bootstraps the full per-surface WebGPU stack bound to and leaves the surface - /// configured against and . + /// Creates a native presentation surface bound to and leaves it configured against + /// and . /// - /// The ImageSharp configuration the device context will use for its rendering backend. + /// The session that owns the shared adapter, device, queue, and drawing context. /// The native surface source that provides the platform handles for surface creation. /// The swapchain texture format. + /// How the native compositor interprets the swapchain alpha channel. /// The present mode to apply to the first surface configuration. /// The framebuffer size to apply to the first surface configuration. Zero-area /// sizes are permitted and leave the surface unconfigured until the caller invokes @@ -133,72 +143,116 @@ private WebGPUSurfaceResources( /// before the exception propagates. /// public static WebGPUSurfaceResources Create( - Configuration configuration, + WebGPUSurfaceSession session, INativeWindowSource nativeSource, WebGPUTextureFormat format, + WebGPUCompositeAlphaMode alphaMode, + WebGPUPresentMode initialPresentMode, + Size initialFramebufferSize) + { + WebGPU api = session.Api; + WGPUInstanceImpl* instance = WebGPURuntime.GetOrCreateSharedInstance(); + WGPUSurfaceImpl* surface = WebGPUSurfaceFactory.Create(api, instance, nativeSource); + return Create(session, surface, format, alphaMode, initialPresentMode, initialFramebufferSize); + } + + /// + /// Creates a native presentation surface for an externally-owned host. + /// + /// The session that owns the shared adapter, device, queue, and drawing context. + /// The native surface host. + /// The swapchain texture format. + /// How the native compositor interprets the swapchain alpha channel. + /// The present mode to apply to the first surface configuration. + /// The initial framebuffer size. + /// The fully initialized resource container. + public static WebGPUSurfaceResources Create( + WebGPUSurfaceSession session, + WebGPUSurfaceHost host, + WebGPUTextureFormat format, + WebGPUCompositeAlphaMode alphaMode, WebGPUPresentMode initialPresentMode, Size initialFramebufferSize) { - WebGPUDrawingBackend.GetCompositeTextureFormatInfo(format, out _, out FeatureName requiredFeature); + WebGPU api = session.Api; + WGPUInstanceImpl* instance = WebGPURuntime.GetOrCreateSharedInstance(); + WGPUSurfaceImpl* surface = WebGPUSurfaceFactory.Create(api, instance, host); + return Create(session, surface, format, alphaMode, initialPresentMode, initialFramebufferSize); + } - WebGPU api = WebGPURuntime.GetApi(); - Instance* instance = null; - Surface* surface = null; - WebGPUInstanceHandle? instanceHandle = null; + /// + /// Completes surface negotiation and transfers ownership of the raw native surface to the returned resource container. + /// + /// The session that owns the shared device resources. + /// The raw surface returned by wgpu-native. + /// The requested swapchain texture format. + /// The requested compositor alpha mode. + /// The requested initial presentation mode. + /// The initial framebuffer size. + /// The initialized surface resources. + private static WebGPUSurfaceResources Create( + WebGPUSurfaceSession session, + WGPUSurfaceImpl* surface, + WebGPUTextureFormat format, + WebGPUCompositeAlphaMode alphaMode, + WebGPUPresentMode initialPresentMode, + Size initialFramebufferSize) + { + WebGPU api = session.Api; WebGPUSurfaceHandle? surfaceHandle = null; - DeviceResources? deviceResources = null; + uint supportedPresentModes = 0; + TextureUsage surfaceUsage = RequiredSurfaceUsage; try { - InstanceDescriptor instanceDescriptor = default; - instance = api.CreateInstance(&instanceDescriptor); - if (instance is null) - { - throw new InvalidOperationException("WebGPU instance creation failed."); - } - - instanceHandle = new WebGPUInstanceHandle(api, (nint)instance, ownsHandle: true); - surface = nativeSource.CreateWebGPUSurface(api, instance); if (surface is null) { throw new InvalidOperationException("WebGPU surface creation failed."); } surfaceHandle = new WebGPUSurfaceHandle(api, (nint)surface, ownsHandle: true); - deviceResources = CreateDeviceResources(api, configuration, instance, surface, requiredFeature); + + // The first surface selects the presentation-compatible adapter and provisions the + // shared device. Later surfaces reuse it and only query their own capabilities. + session.EnsureInitialized(surface); + + using (WebGPUHandle.HandleReference adapterReference = session.AdapterHandle.AcquireReference()) + { + NegotiateSurfaceConfiguration( + api, + (WGPUAdapterImpl*)adapterReference.Handle, + surface, + ref format, + ref alphaMode, + ref initialPresentMode, + out supportedPresentModes, + out surfaceUsage); + } WebGPUSurfaceResources resources = new( api, - configuration, - instanceHandle, + session, surfaceHandle, - deviceResources.AdapterHandle, - deviceResources.DeviceHandle, - deviceResources.QueueHandle, - deviceResources.DeviceContext, format, - requiredFeature); + alphaMode, + initialPresentMode, + supportedPresentModes, + surfaceUsage); resources.ConfigureSurface(initialPresentMode, initialFramebufferSize); - deviceResources = null; return resources; } catch { - deviceResources?.Dispose(); surfaceHandle?.Dispose(); - instanceHandle?.Dispose(); + // Once the raw pointer is wrapped, SafeHandle owns its native reference. Release + // directly only if wrapper construction failed before that ownership transfer. if (surfaceHandle is null && surface is not null) { api.SurfaceRelease(surface); } - if (instanceHandle is null && instance is not null) - { - api.InstanceRelease(instance); - } - throw; } } @@ -212,12 +266,29 @@ public static WebGPUSurfaceResources Create( /// The new framebuffer size in pixels. public void ConfigureSurface(WebGPUPresentMode presentMode, Size framebufferSize) { + presentMode = presentMode switch + { + WebGPUPresentMode.Fifo => WebGPUPresentMode.Fifo, + WebGPUPresentMode.Immediate => WebGPUPresentMode.Immediate, + WebGPUPresentMode.Mailbox => WebGPUPresentMode.Mailbox, + _ => WebGPUPresentMode.Fifo + }; + + // The public enum values occupy consecutive bit positions, allowing the capabilities + // retained at creation to resolve later property changes without another native query. + if ((this.supportedPresentModes & (1U << (int)presentMode)) == 0) + { + presentMode = WebGPUPresentMode.Fifo; + } + + this.PresentMode = presentMode; + if (framebufferSize.Width <= 0 || framebufferSize.Height <= 0) { return; } - this.ConfigureSurfaceCore(presentMode, framebufferSize, this.DeviceHandle); + this.ConfigureSurfaceCore(presentMode, framebufferSize, this.session.DeviceHandle, this.Format, this.AlphaMode); } /// @@ -226,37 +297,64 @@ public void ConfigureSurface(WebGPUPresentMode presentMode, Size framebufferSize /// The present mode applied to the swapchain. /// The framebuffer size in pixels. /// The device handle used by the new surface configuration. - private void ConfigureSurfaceCore(WebGPUPresentMode presentMode, Size framebufferSize, WebGPUDeviceHandle deviceHandle) + /// The texture format applied to the swapchain. + /// The composite alpha mode applied to the swapchain. + private void ConfigureSurfaceCore( + WebGPUPresentMode presentMode, + Size framebufferSize, + WebGPUDeviceHandle deviceHandle, + WebGPUTextureFormat format, + WebGPUCompositeAlphaMode alphaMode) { if (framebufferSize.Width <= 0 || framebufferSize.Height <= 0) { return; } - SurfaceConfiguration surfaceConfiguration = new() + WGPUSurfaceConfiguration surfaceConfiguration = new() { - Usage = TextureUsage.RenderAttachment | TextureUsage.CopySrc | TextureUsage.CopyDst | TextureUsage.TextureBinding, - Format = WebGPUTextureFormatMapper.ToNative(this.Format), - PresentMode = ToNative(presentMode), - Width = (uint)framebufferSize.Width, - Height = (uint)framebufferSize.Height, + usage = (ulong)this.surfaceUsage, + format = WebGPUTextureFormatMapper.ToNative(format), + alphaMode = ToNative(alphaMode), + presentMode = ToNative(presentMode), + width = (uint)framebufferSize.Width, + height = (uint)framebufferSize.Height, }; using WebGPUHandle.HandleReference surfaceReference = this.SurfaceHandle.AcquireReference(); using WebGPUHandle.HandleReference deviceReference = deviceHandle.AcquireReference(); - surfaceConfiguration.Device = (Device*)deviceReference.Handle; - this.Api.SurfaceConfigure((Surface*)surfaceReference.Handle, ref surfaceConfiguration); + surfaceConfiguration.device = (WGPUDeviceImpl*)deviceReference.Handle; + this.Api.SurfaceConfigure((WGPUSurfaceImpl*)surfaceReference.Handle, in surfaceConfiguration); } - private static SilkPresentMode ToNative(WebGPUPresentMode presentMode) + /// + /// Maps the public present-mode enumeration to the native WebGPU value. + /// + /// The present mode to map. + /// The native present mode. + private static WGPUPresentMode ToNative(WebGPUPresentMode presentMode) => presentMode switch { - WebGPUPresentMode.Fifo => SilkPresentMode.Fifo, - WebGPUPresentMode.Immediate => SilkPresentMode.Immediate, - WebGPUPresentMode.Mailbox => SilkPresentMode.Mailbox, + WebGPUPresentMode.Fifo => WGPUPresentMode.Fifo, + WebGPUPresentMode.Immediate => WGPUPresentMode.Immediate, + WebGPUPresentMode.Mailbox => WGPUPresentMode.Mailbox, _ => throw new InvalidOperationException("The WebGPU present mode mapping is incomplete.") }; + /// + /// Maps the public composite-alpha enumeration to the native WebGPU value. + /// + /// The composite-alpha mode to map. + /// The native composite-alpha mode. + private static WGPUCompositeAlphaMode ToNative(WebGPUCompositeAlphaMode alphaMode) + => alphaMode switch + { + WebGPUCompositeAlphaMode.Auto => WGPUCompositeAlphaMode.Auto, + WebGPUCompositeAlphaMode.Opaque => WGPUCompositeAlphaMode.Opaque, + WebGPUCompositeAlphaMode.Premultiplied => WGPUCompositeAlphaMode.Premultiplied, + _ => throw new InvalidOperationException("The WebGPU composite alpha mode mapping is incomplete.") + }; + /// /// Acquires the next presentable frame from and wraps it as a /// with a ready-to-use . @@ -269,13 +367,40 @@ private static SilkPresentMode ToNative(WebGPUPresentMode presentMode) /// The drawing options that seed the canvas on the returned frame. /// Receives the acquired frame on success. /// when a frame is available; when no drawable frame is - /// available right now (zero-area framebuffer, recoverable surface acquisition status, or recovered device loss). - /// Thrown on an out-of-memory acquire status, or when texture-view - /// creation or device recovery fails. + /// available right now (zero-area framebuffer or recoverable surface acquisition status). + /// Thrown when surface texture or texture-view acquisition fails. + public bool TryAcquireFrame( + WebGPUPresentMode presentMode, + Size framebufferSize, + DrawingOptions options, + [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) + => this.TryAcquireFrameCore(presentMode, framebufferSize, options, textCache: null, out frame); + + /// + /// Acquires the next presentable frame using a caller-owned text cache. + /// + /// The present mode used when reconfiguring the surface. + /// The current framebuffer size in pixels. + /// The drawing options that seed the canvas on the returned frame. + /// The text cache used by the returned frame canvas. + /// Receives the acquired frame on success. + /// when a frame is available; otherwise . public bool TryAcquireFrame( WebGPUPresentMode presentMode, Size framebufferSize, DrawingOptions options, + DrawingTextCache textCache, + [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) + => this.TryAcquireFrameCore(presentMode, framebufferSize, options, textCache, out frame); + + /// + /// Acquires a presentable frame and selects the cache ownership used by its canvas. + /// + private bool TryAcquireFrameCore( + WebGPUPresentMode presentMode, + Size framebufferSize, + DrawingOptions options, + DrawingTextCache? textCache, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) { frame = null; @@ -293,47 +418,53 @@ public bool TryAcquireFrame( return false; } - SurfaceTexture surfaceTexture = default; + // Frame pacing: block here until the previously presented frame's GPU work completes. + // This is the one deliberate CPU-GPU synchronization point per frame; every flush-side + // readback is deferred, so this wait alone bounds how far the CPU runs ahead. + this.WaitForPreviousFrameWorkDone(); + + WGPUSurfaceTexture surfaceTexture = default; using (WebGPUHandle.HandleReference surfaceReference = this.SurfaceHandle.AcquireReference()) { - this.Api.SurfaceGetCurrentTexture((Surface*)surfaceReference.Handle, &surfaceTexture); + this.Api.SurfaceGetCurrentTexture((WGPUSurfaceImpl*)surfaceReference.Handle, &surfaceTexture); } - switch (surfaceTexture.Status) + switch (surfaceTexture.status) { - case SurfaceGetCurrentTextureStatus.Timeout: - case SurfaceGetCurrentTextureStatus.Outdated: - case SurfaceGetCurrentTextureStatus.Lost: - if (surfaceTexture.Texture is not null) - { - this.Api.TextureRelease(surfaceTexture.Texture); - } + case WGPUSurfaceGetCurrentTextureStatus.SuccessOptimal: + case WGPUSurfaceGetCurrentTextureStatus.SuccessSuboptimal: + break; - this.ConfigureSurface(presentMode, framebufferSize); + case (WGPUSurfaceGetCurrentTextureStatus)WGPUNativeSurfaceGetCurrentTextureStatus.WGPUSurfaceGetCurrentTextureStatus_Occluded: + // wgpu-native uses this extension status when the compositor has no drawable + // surface. The surface remains configured and can be retried on the next frame. return false; - case SurfaceGetCurrentTextureStatus.DeviceLost: - if (surfaceTexture.Texture is not null) + case WGPUSurfaceGetCurrentTextureStatus.Timeout: + case WGPUSurfaceGetCurrentTextureStatus.Outdated: + case WGPUSurfaceGetCurrentTextureStatus.Lost: + if (surfaceTexture.texture is not null) { - this.Api.TextureRelease(surfaceTexture.Texture); + this.Api.TextureRelease(surfaceTexture.texture); } - this.RecoverDeviceResources(presentMode, framebufferSize); + this.ConfigureSurface(presentMode, framebufferSize); return false; - case SurfaceGetCurrentTextureStatus.OutOfMemory: - if (surfaceTexture.Texture is not null) + case WGPUSurfaceGetCurrentTextureStatus.Error: + default: + if (surfaceTexture.texture is not null) { - this.Api.TextureRelease(surfaceTexture.Texture); + this.Api.TextureRelease(surfaceTexture.texture); } - throw new InvalidOperationException($"Surface texture error: {surfaceTexture.Status}"); + throw new InvalidOperationException($"Surface texture error: {surfaceTexture.status}"); } - TextureView* textureView = this.Api.TextureCreateView(surfaceTexture.Texture, null); + WGPUTextureViewImpl* textureView = this.Api.TextureCreateView(surfaceTexture.texture, null); if (textureView is null) { - this.Api.TextureRelease(surfaceTexture.Texture); + this.Api.TextureRelease(surfaceTexture.texture); throw new InvalidOperationException("WebGPU texture view creation failed."); } @@ -341,24 +472,71 @@ public bool TryAcquireFrame( WebGPUTextureViewHandle? textureViewHandle = null; try { - textureHandle = new WebGPUTextureHandle(this.Api, (nint)surfaceTexture.Texture, ownsHandle: true); + textureHandle = new WebGPUTextureHandle(this.Api, (nint)surfaceTexture.texture, ownsHandle: true); textureViewHandle = new WebGPUTextureViewHandle(this.Api, (nint)textureView, ownsHandle: true); - DrawingCanvas canvas = this.deviceContext.CreateCanvas( - options, - textureHandle, - textureViewHandle, - this.Format, - framebufferSize.Width, - framebufferSize.Height); - this.frameInFlight = true; + if (this.presentationTarget is null || + this.presentationTarget.Width != framebufferSize.Width || + this.presentationTarget.Height != framebufferSize.Height) + { + this.presentationRenderer?.Dispose(); + this.presentationTarget?.Dispose(); + this.presentationRenderer = null; + this.presentationTarget = null; + + // Keep one fully capable texture per surface. Reusing it avoids driver allocations + // per frame and isolates the drawing pipeline from optional swapchain usages. + WebGPURenderTarget createdTarget = new( + this.session.DeviceContext, + ownsDeviceContext: false, + this.TargetDescriptor, + framebufferSize.Width, + framebufferSize.Height, + isPresentationSurface: true); + + try + { + this.presentationRenderer = new WebGPUPresentationRenderer( + this.Api, + this.session.DeviceContext, + createdTarget, + (this.surfaceUsage & TextureUsage.CopyDst) != 0); + + this.presentationTarget = createdTarget; + } + catch + { + createdTarget.Dispose(); + throw; + } + } + + // A supplied cache survives frame disposal and retains glyph/run geometry. The null + // path preserves the existing frame-local cache behavior for every current caller. + DrawingCanvas canvas = textCache is null + ? this.presentationTarget.CreateCanvas(options) + : this.presentationTarget.CreateCanvas(options, textCache); + frame = new WebGPUSurfaceFrame( this.Api, + this.session.DeviceContext, + this.TargetDescriptor, this.SurfaceHandle, textureHandle, textureViewHandle, canvas, - onDisposed: () => this.frameInFlight = false); + this.presentationRenderer!, + onDisposed: () => + { + // The frame has just been submitted and presented; register the pacing + // signal its successor will wait on, then allow the next acquire. + this.RegisterFramePacingSignal(); + this.frameInFlight = false; + }); + + // Mark in-flight only once the frame exists. Setting the guard before construction + // would leave it stuck on a constructor throw, wedging every subsequent acquire. + this.frameInFlight = true; return true; } @@ -367,6 +545,7 @@ public bool TryAcquireFrame( textureViewHandle?.Dispose(); textureHandle?.Dispose(); + // Raw pointers are released directly only when wrapping into an owning handle never happened. if (textureViewHandle is null) { this.Api.TextureViewRelease(textureView); @@ -374,7 +553,7 @@ public bool TryAcquireFrame( if (textureHandle is null) { - this.Api.TextureRelease(surfaceTexture.Texture); + this.Api.TextureRelease(surfaceTexture.texture); } throw; @@ -382,267 +561,323 @@ public bool TryAcquireFrame( } /// - /// Releases every owned handle in reverse acquisition order (device context, queue, device, adapter, surface, instance). - /// Idempotent; subsequent calls are no-ops. + /// Waits (bounded) for the previously presented frame's GPU work to complete, then retires + /// the signal. No-op when no frame has been presented since the last wait. /// - public void Dispose() + private void WaitForPreviousFrameWorkDone() { - if (this.isDisposed) + FramePacingSignal? signal = this.previousFrameWorkDone; + if (signal is null) { return; } - this.deviceContext.Dispose(); - this.QueueHandle.Dispose(); - this.DeviceHandle.Dispose(); - this.AdapterHandle.Dispose(); - this.SurfaceHandle.Dispose(); - this.InstanceHandle.Dispose(); - this.isDisposed = true; + this.previousFrameWorkDone = null; + using (WebGPUHandle.HandleReference deviceReference = this.session.DeviceHandle.AcquireReference()) + { + _ = signal.Wait(this.Api, (WGPUDeviceImpl*)deviceReference.Handle); + } + + signal.Dispose(); } /// - /// Requests the adapter, device, queue, and device context for an existing surface. + /// Registers a queue work-done signal covering everything submitted for the frame that was + /// just presented. The next acquire waits on it. Failure to register (for example during + /// device loss) simply skips pacing for one frame. /// - /// The shared WebGPU API loader. - /// The ImageSharp configuration the device context will use. - /// The instance that owns the surface. - /// The surface the adapter must be compatible with. - /// The optional WebGPU feature required by the selected texture format. - /// The acquired device resources. The caller owns the returned handles. - private static DeviceResources CreateDeviceResources( - WebGPU api, - Configuration configuration, - Instance* instance, - Surface* surface, - FeatureName requiredFeature) + private void RegisterFramePacingSignal() { - Adapter* adapter = null; - WebGPUAdapterHandle? adapterHandle = null; - WebGPUDeviceHandle? deviceHandle = null; - WebGPUQueueHandle? queueHandle = null; - WebGPUDeviceContext? deviceContext = null; - - try + if (this.isDisposed || this.session.QueueHandle.IsInvalid) { - adapter = RequestAdapter(api, instance, surface); - adapterHandle = new WebGPUAdapterHandle(api, (nint)adapter, ownsHandle: true); - - Device* device = RequestDevice(api, adapter, requiredFeature); - deviceHandle = new WebGPUDeviceHandle(api, (nint)device, ownsHandle: true); - Queue* queue = api.DeviceGetQueue(device); - if (queue is null) - { - throw new InvalidOperationException("The WebGPU device did not provide a default queue."); - } + return; + } - queueHandle = new WebGPUQueueHandle(api, (nint)queue, ownsHandle: true); - deviceContext = new WebGPUDeviceContext(configuration, deviceHandle, queueHandle); + // A previous signal should already have been consumed at acquire; retire a leftover + // (an acquire that never happened) before installing the new one. + this.WaitForPreviousFrameWorkDone(); - DeviceResources resources = new(adapterHandle, deviceHandle, queueHandle, deviceContext); - adapterHandle = null; - deviceHandle = null; - queueHandle = null; - deviceContext = null; - return resources; + try + { + using WebGPUHandle.HandleReference queueReference = this.session.QueueHandle.AcquireReference(); + this.previousFrameWorkDone = new FramePacingSignal(this.Api, (WGPUQueueImpl*)queueReference.Handle); } - catch + catch (ObjectDisposedException) { - deviceContext?.Dispose(); - queueHandle?.Dispose(); - deviceHandle?.Dispose(); - adapterHandle?.Dispose(); - - if (adapterHandle is null && adapter is not null) - { - api.AdapterRelease(adapter); - } - - throw; + // The queue was torn down between the check and the acquire; skip pacing. } } /// - /// Recovers the device-owned portion of the surface stack after device loss. - /// The existing instance and surface remain valid; only adapter, device, queue, and device context are replaced. + /// Releases the owned native presentation surface after retiring its frame-pacing callback. + /// Idempotent; subsequent calls are no-ops. /// - /// The present mode applied to the recovered swapchain. - /// The framebuffer size in pixels. - private void RecoverDeviceResources(WebGPUPresentMode presentMode, Size framebufferSize) + public void Dispose() { - DeviceResources? deviceResources = null; - - try - { - using WebGPUHandle.HandleReference instanceReference = this.InstanceHandle.AcquireReference(); - using WebGPUHandle.HandleReference surfaceReference = this.SurfaceHandle.AcquireReference(); - deviceResources = CreateDeviceResources( - this.Api, - this.configuration, - (Instance*)instanceReference.Handle, - (Surface*)surfaceReference.Handle, - this.requiredFeature); - - this.ConfigureSurfaceCore(presentMode, framebufferSize, deviceResources.DeviceHandle); - - WebGPUDeviceContext oldDeviceContext = this.deviceContext; - WebGPUQueueHandle oldQueueHandle = this.QueueHandle; - WebGPUDeviceHandle oldDeviceHandle = this.DeviceHandle; - WebGPUAdapterHandle oldAdapterHandle = this.AdapterHandle; - - this.deviceContext = deviceResources.DeviceContext; - this.QueueHandle = deviceResources.QueueHandle; - this.DeviceHandle = deviceResources.DeviceHandle; - this.AdapterHandle = deviceResources.AdapterHandle; - deviceResources = null; - - oldDeviceContext.Dispose(); - oldQueueHandle.Dispose(); - oldDeviceHandle.Dispose(); - oldAdapterHandle.Dispose(); - } - catch + if (this.isDisposed) { - deviceResources?.Dispose(); - throw; + return; } + + // Retire the pacing signal before the device goes away. Its callback wrapper remains + // self-rooted if native WebGPU still owes the registered completion invocation. + this.WaitForPreviousFrameWorkDone(); + + this.presentationRenderer?.Dispose(); + this.presentationTarget?.Dispose(); + this.SurfaceHandle.Dispose(); + this.isDisposed = true; } /// - /// Requests a high-performance adapter compatible with . + /// Negotiates the format, alpha mode, and present mode for one surface on the session adapter. /// /// The shared WebGPU API loader. - /// The WebGPU instance the adapter is requested from. - /// The surface the adapter must be compatible with. - /// The requested adapter. - private static Adapter* RequestAdapter( + /// The adapter shared by the surface session. + /// The surface whose capabilities are queried. + /// The requested texture format. Receives the negotiated format. + /// The requested composite alpha mode. Receives when the requested mode is unavailable. + /// The requested presentation mode. Receives when the requested mode is unavailable. + /// Receives a bit field containing the presentation modes supported by the surface. + /// Receives the guaranteed render-attachment usage and any supported optional usages selected by the backend. + private static void NegotiateSurfaceConfiguration( WebGPU api, - Instance* instance, - Surface* surface) + WGPUAdapterImpl* adapter, + WGPUSurfaceImpl* surface, + ref WebGPUTextureFormat format, + ref WebGPUCompositeAlphaMode alphaMode, + ref WebGPUPresentMode presentMode, + out uint supportedPresentModes, + out TextureUsage surfaceUsage) { - RequestAdapterStatus callbackStatus = RequestAdapterStatus.Unknown; - Adapter* callbackAdapter = null; - using ManualResetEventSlim callbackReady = new(false); + supportedPresentModes = 0; + surfaceUsage = RequiredSurfaceUsage; - void Callback(RequestAdapterStatus status, Adapter* adapterPtr, byte* message, void* userData) + WGPUSurfaceCapabilities capabilities = default; + WGPUStatus capabilityStatus = api.SurfaceGetCapabilities(surface, adapter, &capabilities); + + if (capabilityStatus != WGPUStatus.Success) { - _ = message; - _ = userData; - callbackStatus = status; - callbackAdapter = adapterPtr; - callbackReady.Set(); + throw new InvalidOperationException($"The WebGPU runtime could not query surface capabilities. Status: '{capabilityStatus}'."); } - using PfnRequestAdapterCallback callbackPtr = PfnRequestAdapterCallback.From(Callback); - RequestAdapterOptions options = new() + try { - CompatibleSurface = surface, - PowerPreference = PowerPreference.HighPerformance, - }; + TextureUsage supportedUsage = (TextureUsage)capabilities.usages; + if ((supportedUsage & RequiredSurfaceUsage) != RequiredSurfaceUsage) + { + throw new NotSupportedException($"The WebGPU surface does not support the texture usages required by the drawing backend. Supported usages: '{supportedUsage}'."); + } - api.InstanceRequestAdapter(instance, in options, callbackPtr, null); - if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) - { - throw new InvalidOperationException("Timed out while waiting for the WebGPU adapter request callback."); - } + // A same-format native copy avoids the presentation shader entirely. Surfaces that + // omit CopyDst retain the guaranteed render-attachment presentation fallback. + surfaceUsage |= supportedUsage & TextureUsage.CopyDst; - if (callbackStatus != RequestAdapterStatus.Success || callbackAdapter is null) - { - throw new InvalidOperationException($"The WebGPU runtime failed to acquire a surface-compatible adapter. Status: '{callbackStatus}'."); - } + format = format switch + { + WebGPUTextureFormat.Rgba8Unorm => WebGPUTextureFormat.Rgba8Unorm, + WebGPUTextureFormat.Rgba8Snorm => WebGPUTextureFormat.Rgba8Snorm, + WebGPUTextureFormat.Bgra8Unorm => WebGPUTextureFormat.Bgra8Unorm, + WebGPUTextureFormat.Rgba16Float => WebGPUTextureFormat.Rgba16Float, + _ => WebGPUTextureFormat.Rgba8Unorm + }; - return callbackAdapter; - } + WebGPUDrawingBackend.GetCompositeTextureFormatInfo(format, out WGPUTextureFormat nativeFormat, out WGPUFeatureName requiredFeature); + bool supportsFormat = requiredFeature == default || api.AdapterHasFeature(adapter, requiredFeature); - /// - /// Requests a device from with enabled (when not - /// ) and waits synchronously on the asynchronous callback, up to - /// . - /// - /// The shared WebGPU API loader. - /// The adapter the device is requested from. - /// The feature to enable on the requested device, or for none. - /// The requested device. - private static Device* RequestDevice( - WebGPU api, - Adapter* adapter, - FeatureName requiredFeature) - { - if (requiredFeature != FeatureName.Undefined && !api.AdapterHasFeature(adapter, requiredFeature)) - { - throw new NotSupportedException($"The selected WebGPU adapter does not support required feature '{requiredFeature}'."); - } + if (supportsFormat) + { + supportsFormat = false; - RequestDeviceStatus callbackStatus = RequestDeviceStatus.Unknown; - Device* callbackDevice = null; - using ManualResetEventSlim callbackReady = new(false); + for (nuint i = 0; i < capabilities.formatCount; i++) + { + if (capabilities.formats[i] == nativeFormat) + { + supportsFormat = true; + break; + } + } + } - void Callback(RequestDeviceStatus status, Device* devicePtr, byte* message, void* userData) - { - _ = message; - _ = userData; - callbackStatus = status; - callbackDevice = devicePtr; - callbackReady.Set(); - } + if (!supportsFormat) + { + // Prefer the public default, then the remaining formats in their documented + // order. This keeps fallback deterministic rather than depending on the + // platform-specific order of the native capability array. + ReadOnlySpan fallbackFormats = + [ + WebGPUTextureFormat.Rgba8Unorm, + WebGPUTextureFormat.Rgba8Snorm, + WebGPUTextureFormat.Bgra8Unorm, + WebGPUTextureFormat.Rgba16Float + ]; + + bool foundFallback = false; + + foreach (WebGPUTextureFormat fallbackFormat in fallbackFormats) + { + WebGPUDrawingBackend.GetCompositeTextureFormatInfo(fallbackFormat, out WGPUTextureFormat fallbackNativeFormat, out WGPUFeatureName fallbackFeature); + + if (fallbackFeature != default && !api.AdapterHasFeature(adapter, fallbackFeature)) + { + continue; + } + + for (nuint i = 0; i < capabilities.formatCount; i++) + { + if (capabilities.formats[i] == fallbackNativeFormat) + { + format = fallbackFormat; + foundFallback = true; + break; + } + } + + if (foundFallback) + { + break; + } + } - using PfnRequestDeviceCallback callbackPtr = PfnRequestDeviceCallback.From(Callback); - DeviceDescriptor descriptor = default; - if (requiredFeature != FeatureName.Undefined) - { - FeatureName requestedFeature = requiredFeature; - descriptor = new DeviceDescriptor + if (!foundFallback) + { + throw new NotSupportedException("The WebGPU surface does not expose a texture format supported by the drawing backend."); + } + } + + // Each public present-mode value owns the matching bit. Retaining this compact + // mask lets runtime property changes degrade without another native allocation. + for (nuint i = 0; i < capabilities.presentModeCount; i++) + { + supportedPresentModes |= capabilities.presentModes[i] switch + { + WGPUPresentMode.Fifo => 1U << (int)WebGPUPresentMode.Fifo, + WGPUPresentMode.Immediate => 1U << (int)WebGPUPresentMode.Immediate, + WGPUPresentMode.Mailbox => 1U << (int)WebGPUPresentMode.Mailbox, + _ => 0 + }; + } + + presentMode = presentMode switch { - RequiredFeatureCount = 1, - RequiredFeatures = &requestedFeature, + WebGPUPresentMode.Fifo => WebGPUPresentMode.Fifo, + WebGPUPresentMode.Immediate => WebGPUPresentMode.Immediate, + WebGPUPresentMode.Mailbox => WebGPUPresentMode.Mailbox, + _ => WebGPUPresentMode.Fifo }; - } - api.AdapterRequestDevice(adapter, in descriptor, callbackPtr, null); - if (!callbackReady.Wait(CallbackTimeoutMilliseconds)) - { - throw new InvalidOperationException("Timed out while waiting for the WebGPU device request callback."); - } + if ((supportedPresentModes & (1U << (int)presentMode)) == 0) + { + presentMode = WebGPUPresentMode.Fifo; + } + + alphaMode = alphaMode switch + { + WebGPUCompositeAlphaMode.Auto => WebGPUCompositeAlphaMode.Auto, + WebGPUCompositeAlphaMode.Opaque => WebGPUCompositeAlphaMode.Opaque, + WebGPUCompositeAlphaMode.Premultiplied => WebGPUCompositeAlphaMode.Premultiplied, + _ => WebGPUCompositeAlphaMode.Auto + }; + + WGPUCompositeAlphaMode nativeAlphaMode = ToNative(alphaMode); + + if (nativeAlphaMode != WGPUCompositeAlphaMode.Auto) + { + bool supportsAlphaMode = false; - if (callbackStatus != RequestDeviceStatus.Success || callbackDevice is null) + for (nuint i = 0; i < capabilities.alphaModeCount; i++) + { + if (capabilities.alphaModes[i] == nativeAlphaMode) + { + supportsAlphaMode = true; + break; + } + } + + // Auto is the WebGPU-defined compatibility fallback for an unavailable + // explicit mode and resolves to opaque or inherited composition. + if (!supportsAlphaMode) + { + alphaMode = WebGPUCompositeAlphaMode.Auto; + } + } + } + finally { - throw new InvalidOperationException($"The WebGPU runtime failed to acquire a device. Status: '{callbackStatus}'."); + // The capability arrays are native allocations owned by the query result. + api.SurfaceCapabilitiesFreeMembers(capabilities); } - - return callbackDevice; } /// - /// Owns one acquired adapter/device/queue/context set while it is being transferred into the surface resources. + /// A queue work-done callback and its completion event for one presented frame. The callback + /// wrapper roots itself while native WebGPU owes an invocation and suppresses access to the + /// event after a bounded wait retires its managed owner. /// - private sealed class DeviceResources : IDisposable + private sealed class FramePacingSignal : IDisposable { - public DeviceResources( - WebGPUAdapterHandle adapterHandle, - WebGPUDeviceHandle deviceHandle, - WebGPUQueueHandle queueHandle, - WebGPUDeviceContext deviceContext) + /// + /// Set by the work-done callback once the frame's submissions have completed. + /// + private readonly ManualResetEventSlim workDone = new(false); + + /// + /// The self-rooting work-done callback wrapper registered with native WebGPU. + /// + private readonly WebGPUQueueWorkDoneCallback callback; + + /// + /// Initializes a new instance of the class and registers + /// the work-done callback for everything submitted to the queue so far. + /// + /// The WebGPU API used to register the callback. + /// The queue whose submitted work the signal covers. + public FramePacingSignal(WebGPU api, WGPUQueueImpl* queue) { - this.AdapterHandle = adapterHandle; - this.DeviceHandle = deviceHandle; - this.QueueHandle = queueHandle; - this.DeviceContext = deviceContext; + this.callback = WebGPUQueueWorkDoneCallback.From(this.OnWorkDone); + api.QueueOnSubmittedWorkDone(queue, this.callback, null); } - public WebGPUAdapterHandle AdapterHandle { get; } - - public WebGPUDeviceHandle DeviceHandle { get; } - - public WebGPUQueueHandle QueueHandle { get; } + /// + /// Waits (bounded) for the work-done callback while polling the owning device. + /// + /// The WebGPU API used to poll the device. + /// The device that owns the queue. + /// when the callback completed before the timeout; otherwise, . + public bool Wait(WebGPU api, WGPUDeviceImpl* device) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + while (!this.workDone.IsSet && stopwatch.ElapsedMilliseconds < CallbackTimeoutMilliseconds) + { + // The queue callback already captures the work boundary registered by the + // constructor. A blocking poll parks this thread in the native fence wait for the + // duration of the GPU work instead of spinning tens of thousands of non-blocking + // polls per frame; the callback is delivered during the poll that observes its + // boundary. wgpu bounds the native wait internally, so the stopwatch still + // enforces the overall timeout when the device is lost or stalled. + _ = api.DevicePoll(device, true, null); + } - internal WebGPUDeviceContext DeviceContext { get; } + return this.workDone.IsSet; + } + /// public void Dispose() { - this.DeviceContext.Dispose(); - this.QueueHandle.Dispose(); - this.DeviceHandle.Dispose(); - this.AdapterHandle.Dispose(); + this.callback.Dispose(); + this.workDone.Dispose(); + } + + /// + /// The queue work-done callback; any status counts as completion for pacing purposes. + /// + /// The completion status reported by the implementation. + /// Unused user data pointer. + private void OnWorkDone(WGPUQueueWorkDoneStatus status, void* userData) + { + _ = status; + _ = userData; + this.workDone.Set(); } } } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceSession.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceSession.cs new file mode 100644 index 000000000..c3a163de2 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceSession.cs @@ -0,0 +1,254 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Provides shared WebGPU resources for a group of external presentation surfaces. +/// +/// +/// Create one session for related windows and popups so they use the same GPU device and drawing context. +/// Dispose every surface and render target created by the session before disposing the session. +/// +public sealed unsafe class WebGPUSurfaceSession : IDisposable +{ + private readonly Configuration configuration; + private WebGPU? api; + private WebGPUAdapterHandle? adapterHandle; + private WebGPUDeviceHandle? deviceHandle; + private WebGPUQueueHandle? queueHandle; + private WebGPUDeviceContext? deviceContext; + private WebGPURuntime.DeviceSharedState? deviceState; + private bool isDisposed; + + /// + /// Initializes a new instance of the class. + /// + public WebGPUSurfaceSession() + : this(Configuration.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration used by surfaces and render targets created by this session. + public WebGPUSurfaceSession(Configuration configuration) + { + Guard.NotNull(configuration, nameof(configuration)); + + this.configuration = configuration; + } + + /// + /// Gets a value indicating whether the shared WebGPU device has been lost. + /// + public bool IsLost => this.deviceState?.IsLost ?? false; + + /// + /// Gets the shared WebGPU API loader. + /// + internal WebGPU Api => this.api ??= WebGPURuntime.GetApi(); + + /// + /// Gets the adapter selected for the first presentation surface. + /// + internal WebGPUAdapterHandle AdapterHandle => this.adapterHandle!; + + /// + /// Gets the device shared by every surface in the session. + /// + internal WebGPUDeviceHandle DeviceHandle => this.deviceHandle!; + + /// + /// Gets the queue shared by every surface in the session. + /// + internal WebGPUQueueHandle QueueHandle => this.queueHandle!; + + /// + /// Gets the device context shared by every surface and render target in this session. + /// + /// Thrown when no presentation surface has initialized the session. + /// The session owns the returned context; callers must not dispose it separately. + public WebGPUDeviceContext DeviceContext + { + get + { + this.ThrowIfDisposed(); + + return this.deviceContext + ?? throw new InvalidOperationException("Create a presentation surface before accessing its WebGPU device context."); + } + } + + /// + /// Creates a presentation surface attached to an externally owned native host. + /// + /// The native surface host. + /// The initial framebuffer size in pixels. + /// The created presentation surface. + public WebGPUExternalSurface CreateSurface(WebGPUSurfaceHost host, Size framebufferSize) + => this.CreateSurface(host, framebufferSize, new WebGPUExternalSurfaceOptions()); + + /// + /// Creates a presentation surface attached to an externally owned native host. + /// + /// The native surface host. + /// The initial framebuffer size in pixels. + /// The presentation options. + /// The created presentation surface. + public WebGPUExternalSurface CreateSurface(WebGPUSurfaceHost host, Size framebufferSize, WebGPUExternalSurfaceOptions options) + { + this.ThrowIfDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.MustBeGreaterThan(framebufferSize.Width, 0, nameof(framebufferSize)); + Guard.MustBeGreaterThan(framebufferSize.Height, 0, nameof(framebufferSize)); + + return new WebGPUExternalSurface(this, host, framebufferSize, options); + } + + /// + /// Creates an offscreen render target on the shared WebGPU device. + /// + /// The texture format. + /// The alpha representation stored by the target. + /// The target width in pixels. + /// The target height in pixels. + /// The created render target. + /// Thrown when no presentation surface has initialized the session. + public WebGPURenderTarget CreateRenderTarget(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, int width, int height) + { + this.ThrowIfDisposed(); + + if (this.deviceContext is null) + { + throw new InvalidOperationException("Create a presentation surface before creating an offscreen target from its WebGPU session."); + } + + Guard.MustBeGreaterThan(width, 0, nameof(width)); + Guard.MustBeGreaterThan(height, 0, nameof(height)); + + WebGPUTargetDescriptor targetDescriptor = WebGPUDrawingBackend.CreateOffscreenTargetDescriptor(format, alphaRepresentation); + + return new WebGPURenderTarget(this.deviceContext, false, targetDescriptor, width, height, isPresentationSurface: false); + } + + /// + /// Initializes the session against its first compatible presentation surface. + /// + /// The surface used to select the presentation adapter. + internal void EnsureInitialized(WGPUSurfaceImpl* compatibleSurface) + { + this.ThrowIfDisposed(); + + if (this.deviceContext is not null) + { + return; + } + + WGPUAdapterImpl* adapter = null; + WGPUDeviceImpl* device = null; + WebGPUAdapterHandle? acquiredAdapter = null; + WebGPUDeviceHandle? acquiredDevice = null; + WebGPUQueueHandle? acquiredQueue = null; + WebGPUDeviceContext? acquiredContext = null; + + try + { + WGPUInstanceImpl* instance = WebGPURuntime.GetOrCreateSharedInstance(); + + if (!WebGPURuntime.TryRequestAdapter(this.Api, instance, compatibleSurface, out adapter, out WebGPUEnvironmentError adapterError)) + { + throw new InvalidOperationException(WebGPURuntime.CreateEnvironmentExceptionMessage(adapterError)); + } + + acquiredAdapter = new WebGPUAdapterHandle(this.Api, (nint)adapter, ownsHandle: true); + + if (!WebGPURuntime.TryRequestDevice(this.Api, adapter, out device, out WebGPUEnvironmentError deviceError)) + { + throw new InvalidOperationException(WebGPURuntime.CreateEnvironmentExceptionMessage(deviceError)); + } + + acquiredDevice = new WebGPUDeviceHandle(this.Api, (nint)device, ownsHandle: true); + WGPUQueueImpl* queue = this.Api.DeviceGetQueue(device); + + if (queue is null) + { + throw new InvalidOperationException("The WebGPU device did not provide a default queue."); + } + + acquiredQueue = new WebGPUQueueHandle(this.Api, (nint)queue, ownsHandle: true); + acquiredContext = new WebGPUDeviceContext(this.configuration, acquiredDevice, acquiredQueue); + WebGPURuntime.DeviceSharedState acquiredState = WebGPURuntime.GetOrCreateDeviceState(this.Api, acquiredDevice); + + // Device-state construction queries native limits and can fail. Publish the session + // only after that final fallible operation so a retry cannot observe disposed handles + // left behind by this method's exception cleanup. + this.adapterHandle = acquiredAdapter; + this.deviceHandle = acquiredDevice; + this.queueHandle = acquiredQueue; + this.deviceContext = acquiredContext; + this.deviceState = acquiredState; + acquiredAdapter = null; + acquiredDevice = null; + acquiredQueue = null; + acquiredContext = null; + } + catch + { + acquiredContext?.Dispose(); + acquiredQueue?.Dispose(); + acquiredDevice?.Dispose(); + acquiredAdapter?.Dispose(); + + // A raw callback result is released directly only when constructing its owning + // SafeHandle failed before ownership could transfer to managed code. + if (acquiredDevice is null && device is not null && this.deviceHandle is null) + { + this.Api.DeviceRelease(device); + } + + if (acquiredAdapter is null && adapter is not null && this.adapterHandle is null) + { + this.Api.AdapterRelease(adapter); + } + + throw; + } + } + + /// + /// Releases the shared WebGPU resources. + /// + public void Dispose() + { + if (this.isDisposed) + { + return; + } + + this.deviceContext?.Dispose(); + + if (this.deviceHandle is not null) + { + // DeviceSharedState owns the compiled pipelines and a reference to the device. + // Remove it while the owning device handle is still live, after the shared backend + // has drained its pending work and before releasing the native queue/device pair. + WebGPURuntime.RemoveDeviceState(this.deviceHandle.DangerousGetHandle()); + } + + this.queueHandle?.Dispose(); + this.deviceHandle?.Dispose(); + this.adapterHandle?.Dispose(); + this.isDisposed = true; + } + + /// + /// Throws when this session has been disposed. + /// + private void ThrowIfDisposed() + => ObjectDisposedException.ThrowIf(this.isDisposed, this); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTargetDescriptor.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTargetDescriptor.cs new file mode 100644 index 000000000..45edf80b5 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTargetDescriptor.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Identifies the physical storage, numeric encoding, and alpha representation of a WebGPU target. +/// +internal readonly struct WebGPUTargetDescriptor : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The physical texture format. + /// The alpha representation stored by the texture. + /// The mapping between native channel values and ImageSharp unit values. + public WebGPUTargetDescriptor( + WebGPUTextureFormat format, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) + { + this.Format = format; + this.AlphaRepresentation = alphaRepresentation == PixelAlphaRepresentation.Associated + ? PixelAlphaRepresentation.Associated + : PixelAlphaRepresentation.Unassociated; + this.NumericEncoding = numericEncoding; + } + + /// + /// Gets the physical texture format. + /// + public WebGPUTextureFormat Format { get; } + + /// + /// Gets the alpha representation stored by the texture. + /// + public PixelAlphaRepresentation AlphaRepresentation { get; } + + /// + /// Gets the mapping between native channel values and ImageSharp unit values. + /// + public WebGPUTargetNumericEncoding NumericEncoding { get; } + + /// + /// Compares two descriptors for equality. + /// + /// The left descriptor. + /// The right descriptor. + /// when both descriptors identify the same target type. + public static bool operator ==(WebGPUTargetDescriptor left, WebGPUTargetDescriptor right) => left.Equals(right); + + /// + /// Compares two descriptors for inequality. + /// + /// The left descriptor. + /// The right descriptor. + /// when the descriptors identify different target types. + public static bool operator !=(WebGPUTargetDescriptor left, WebGPUTargetDescriptor right) => !left.Equals(right); + + /// + public bool Equals(WebGPUTargetDescriptor other) + => this.Format == other.Format && + this.AlphaRepresentation == other.AlphaRepresentation && + this.NumericEncoding == other.NumericEncoding; + + /// + public override bool Equals(object? obj) => obj is WebGPUTargetDescriptor other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Format, this.AlphaRepresentation, this.NumericEncoding); +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTargetNumericEncoding.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTargetNumericEncoding.cs new file mode 100644 index 000000000..c5d65d183 --- /dev/null +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTargetNumericEncoding.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Describes how a WebGPU target's native channel values represent ImageSharp's unit color values. +/// +internal enum WebGPUTargetNumericEncoding +{ + /// + /// Native channel values already use ImageSharp's unit color range. + /// + Unit, + + /// + /// Native channel values map ImageSharp's unit color range onto the signed unit range. + /// + SignedUnit +} diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormat.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormat.cs index ad23653cd..0c6088fc2 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormat.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormat.cs @@ -9,7 +9,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// Supported texture format identifiers for native WebGPU targets. /// /// -/// Only formats with storage texture binding support are included. +/// The compute pipeline writes its result through a storage texture binding, so a target format +/// must be storage-bindable. Most formats here are storage-capable in core WebGPU; +/// is the exception and requires the optional bgra8unorm-storage +/// device feature, which the backend enables when the adapter reports it. /// public enum WebGPUTextureFormat { @@ -25,11 +28,12 @@ public enum WebGPUTextureFormat /// /// Four-channel 8-bit normalized unsigned BGRA format, mapped to . + /// Storage binding for this format requires the optional bgra8unorm-storage device feature. /// Bgra8Unorm, /// - /// Four-channel 16-bit floating-point RGBA format, mapped to . + /// Four-channel 16-bit floating-point RGBA format, mapped to . /// Rgba16Float } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs index 798384689..ee543f6e3 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureFormatMapper.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -14,14 +14,14 @@ internal static class WebGPUTextureFormatMapper /// Converts a public WebGPU texture format identifier to the corresponding native texture format. /// /// The public texture format identifier. - /// The matching value. - public static TextureFormat ToNative(WebGPUTextureFormat formatId) + /// The matching value. + public static WGPUTextureFormat ToNative(WebGPUTextureFormat formatId) => formatId switch { - WebGPUTextureFormat.Rgba8Unorm => TextureFormat.Rgba8Unorm, - WebGPUTextureFormat.Rgba8Snorm => TextureFormat.Rgba8Snorm, - WebGPUTextureFormat.Bgra8Unorm => TextureFormat.Bgra8Unorm, - WebGPUTextureFormat.Rgba16Float => TextureFormat.Rgba16float, + WebGPUTextureFormat.Rgba8Unorm => WGPUTextureFormat.RGBA8Unorm, + WebGPUTextureFormat.Rgba8Snorm => WGPUTextureFormat.RGBA8Snorm, + WebGPUTextureFormat.Bgra8Unorm => WGPUTextureFormat.BGRA8Unorm, + WebGPUTextureFormat.Rgba16Float => WGPUTextureFormat.RGBA16Float, _ => throw new InvalidOperationException("The WebGPU texture format mapping is incomplete.") }; @@ -30,13 +30,13 @@ public static TextureFormat ToNative(WebGPUTextureFormat formatId) /// /// The native texture format. /// The matching value. - public static WebGPUTextureFormat FromNative(TextureFormat textureFormat) + public static WebGPUTextureFormat FromNative(WGPUTextureFormat textureFormat) => textureFormat switch { - TextureFormat.Rgba8Unorm => WebGPUTextureFormat.Rgba8Unorm, - TextureFormat.Rgba8Snorm => WebGPUTextureFormat.Rgba8Snorm, - TextureFormat.Bgra8Unorm => WebGPUTextureFormat.Bgra8Unorm, - TextureFormat.Rgba16float => WebGPUTextureFormat.Rgba16Float, + WGPUTextureFormat.RGBA8Unorm => WebGPUTextureFormat.Rgba8Unorm, + WGPUTextureFormat.RGBA8Snorm => WebGPUTextureFormat.Rgba8Snorm, + WGPUTextureFormat.BGRA8Unorm => WebGPUTextureFormat.Bgra8Unorm, + WGPUTextureFormat.RGBA16Float => WebGPUTextureFormat.Rgba16Float, _ => throw new InvalidOperationException("The native texture format mapping is incomplete.") }; } diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureHandle.cs index 44d7f3b5c..41095dd36 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -20,7 +20,7 @@ internal sealed unsafe class WebGPUTextureHandle : WebGPUHandle /// when this wrapper owns the texture and must release it; /// when the caller retains ownership. /// - internal WebGPUTextureHandle(nint textureHandle, bool ownsHandle) + public WebGPUTextureHandle(nint textureHandle, bool ownsHandle) : this(ownsHandle ? WebGPURuntime.GetApi() : null, textureHandle, ownsHandle) { } @@ -37,7 +37,7 @@ internal WebGPUTextureHandle(nint textureHandle, bool ownsHandle) /// when this wrapper owns the texture and must release it; /// when the caller retains ownership. /// - internal WebGPUTextureHandle(WebGPU? api, nint textureHandle, bool ownsHandle) + public WebGPUTextureHandle(WebGPU? api, nint textureHandle, bool ownsHandle) : base(textureHandle, ownsHandle) => this.api = api; @@ -46,7 +46,7 @@ protected override bool ReleaseHandle() { try { - this.api?.TextureRelease((Texture*)this.handle); + this.api?.TextureRelease((WGPUTextureImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureViewHandle.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureViewHandle.cs index 803af5a0c..2e6d1493e 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUTextureViewHandle.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUTextureViewHandle.cs @@ -1,7 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -20,23 +20,21 @@ internal sealed unsafe class WebGPUTextureViewHandle : WebGPUHandle /// when this wrapper owns the texture view and must release it; /// when the caller retains ownership. /// - internal WebGPUTextureViewHandle(nint textureViewHandle, bool ownsHandle) + public WebGPUTextureViewHandle(nint textureViewHandle, bool ownsHandle) : this(ownsHandle ? WebGPURuntime.GetApi() : null, textureViewHandle, ownsHandle) { } - internal WebGPUTextureViewHandle(WebGPU? api, nint textureViewHandle, bool ownsHandle) + public WebGPUTextureViewHandle(WebGPU? api, nint textureViewHandle, bool ownsHandle) : base(textureViewHandle, ownsHandle) - { - this.api = api; - } + => this.api = api; /// protected override bool ReleaseHandle() { try { - this.api?.TextureViewRelease((TextureView*)this.handle); + this.api?.TextureViewRelease((WGPUTextureViewImpl*)this.handle); return true; } catch diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUWindow.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUWindow.cs index 2a161cc68..c8d60b43c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUWindow.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUWindow.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using Silk.NET.Maths; using Silk.NET.Windowing; +using Silk.NET.Windowing.Glfw; +using Silk.NET.Windowing.Sdl; using NativeWindowBorder = Silk.NET.Windowing.WindowBorder; using NativeWindowState = Silk.NET.Windowing.WindowState; @@ -22,9 +24,18 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; public sealed class WebGPUWindow : IDisposable { private readonly IWindow window; + private readonly WebGPUSurfaceSession session; private readonly WebGPUSurfaceResources resources; private bool isDisposed; - private WebGPUPresentMode presentMode; + + static WebGPUWindow() + { + // Silk.NET discovers window platforms by scanning assemblies with reflection, which + // trimming removes under Native AOT. Registering the shipped platforms explicitly keeps + // window creation working in every deployment model. + GlfwWindowing.RegisterPlatform(); + SdlWindowing.RegisterPlatform(); + } /// /// Initializes a new instance of the class. @@ -52,21 +63,28 @@ public WebGPUWindow(Configuration configuration, WebGPUWindowOptions options) { this.window = Window.Create(CreateSilkOptions(options)); this.Configuration = configuration; - this.Format = options.Format; - this.presentMode = options.PresentMode; + WebGPUSurfaceSession? session = null; try { this.window.Initialize(); + session = new WebGPUSurfaceSession(configuration); this.resources = WebGPUSurfaceResources.Create( - configuration, + session, this.window, - this.Format, - this.presentMode, + options.Format, + options.AlphaMode, + options.PresentMode, ToSize(this.window.FramebufferSize)); + + this.session = session; + session = null; } catch { + // The platform window is the only resource acquired before the WebGPU stack; + // release it when surface bootstrap fails so the constructor leaks nothing. + session?.Dispose(); this.window.Dispose(); throw; } @@ -126,6 +144,20 @@ public WebGPUWindow(Configuration configuration, WebGPUWindowOptions options) /// public Configuration Configuration { get; } + /// + /// Gets the WebGPU device context used to render this window. + /// + /// The window owns the returned context; callers must not dispose it separately. + public WebGPUDeviceContext DeviceContext + { + get + { + this.ThrowIfDisposed(); + + return this.session.DeviceContext; + } + } + /// /// Gets or sets the window title. /// @@ -163,6 +195,8 @@ public float RenderScale return 1F; } + // The per-axis ratios can differ when the platform rounds the two sizes + // independently; report the larger so content is never rendered undersized. return MathF.Max( (float)framebufferSize.Width / clientSize.Width, (float)framebufferSize.Height / clientSize.Height); @@ -255,18 +289,19 @@ public WebGPUWindowBorder WindowBorder /// public WebGPUPresentMode PresentMode { - get => this.presentMode; - set - { - this.presentMode = value; - this.resources.ConfigureSurface(this.presentMode, this.FramebufferSize); - } + get => this.resources.PresentMode; + set => this.resources.ConfigureSurface(value, this.FramebufferSize); } /// /// Gets the swapchain texture format. /// - public WebGPUTextureFormat Format { get; } + public WebGPUTextureFormat Format => this.resources.Format; + + /// + /// Gets how the native compositor interprets the window surface alpha channel. + /// + public WebGPUCompositeAlphaMode AlphaMode => this.resources.AlphaMode; /// /// Tries to acquire the next drawable frame using default drawing options. @@ -292,12 +327,32 @@ public bool TryAcquireFrame([NotNullWhen(true)] out WebGPUSurfaceFrame? frame) /// /// Use this overload when you are driving the render loop yourself and need explicit drawing options. /// A result means no drawable frame is available right now, for example because the - /// surface was lost, outdated, timed out, has a zero-sized framebuffer, or the window recovered from device loss. + /// surface was lost, outdated, timed out, or has a zero-sized framebuffer. /// Dispose the returned frame when you are done with it to present it and release its per-frame resources. /// public bool TryAcquireFrame(DrawingOptions options, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) => this.TryAcquireFrameCore(options, out frame); + /// + /// Tries to acquire the next drawable frame using a caller-owned text cache. + /// + /// The drawing options for the acquired frame. + /// The text cache shared by acquired frame canvases. + /// Receives the acquired frame on success. + /// when a frame is available; otherwise . + public bool TryAcquireFrame(DrawingOptions options, DrawingTextCache textCache, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) + { + Guard.NotNull(textCache, nameof(textCache)); + this.ThrowIfDisposed(); + + return this.resources.TryAcquireFrame( + this.resources.PresentMode, + this.FramebufferSize, + options, + textCache, + out frame); + } + /// /// Runs the window's event loop and renders one WebGPU frame per render callback. /// @@ -316,6 +371,39 @@ public void Run(DrawingOptions options, Action render) this.Run(options, (frame, _) => render(frame)); } + /// + /// Runs the window's event loop using a caller-owned text cache shared by every frame canvas. + /// + /// The drawing options applied to each acquired frame. + /// The text cache shared by acquired frame canvases. + /// The per-frame render callback. + public void Run(DrawingOptions options, DrawingTextCache textCache, Action render) + { + Guard.NotNull(textCache, nameof(textCache)); + Guard.NotNull(render, nameof(render)); + this.ThrowIfDisposed(); + + void OnRender(double deltaTime) + { + if (!this.resources.TryAcquireFrame( + this.resources.PresentMode, + this.FramebufferSize, + options, + textCache, + out WebGPUSurfaceFrame? frame)) + { + return; + } + + using (frame) + { + render(frame); + } + } + + this.RunCore(OnRender); + } + /// /// Runs the window's event loop and renders one WebGPU frame per render callback. /// @@ -346,9 +434,22 @@ void OnRender(double deltaTime) } } - this.window.Render += OnRender; + this.RunCore(OnRender); + } + + /// + /// Runs the native event pump with the supplied frame callback. + /// + /// The callback registered with the native render event. + private void RunCore(Action render) + { + this.window.Render += render; + try { + // Pump events, update, and render explicitly instead of using the default loop so a + // close request raised during DoEvents or DoUpdate skips the remaining stages of that + // iteration and never renders into a window that is tearing down. this.window.Run(() => { this.window.DoEvents(); @@ -368,7 +469,7 @@ void OnRender(double deltaTime) } finally { - this.window.Render -= OnRender; + this.window.Render -= render; } } @@ -411,7 +512,11 @@ public void ContinueEvents() /// /// Requests that the window close. /// - public void RequestClose() => this.window.IsClosing = true; + public void RequestClose() + { + this.ThrowIfDisposed(); + this.window.IsClosing = true; + } /// /// Closes the window immediately. @@ -475,64 +580,127 @@ public void Dispose() return; } + // The WebGPU surface attaches to the native window, so release the GPU stack first. this.resources.Dispose(); + this.session.Dispose(); this.window.Dispose(); this.isDisposed = true; } + /// + /// Acquires the next drawable frame from the shared surface resources using the window's current framebuffer size. + /// + /// The drawing options for the acquired frame. + /// Receives the acquired frame on success. + /// when a frame is available; otherwise . private bool TryAcquireFrameCore( DrawingOptions options, [NotNullWhen(true)] out WebGPUSurfaceFrame? frame) { this.ThrowIfDisposed(); return this.resources.TryAcquireFrame( - this.presentMode, + this.resources.PresentMode, this.FramebufferSize, options, out frame); } + /// + /// Handles native framebuffer resize notifications, reconfiguring the swapchain and raising + /// . + /// + /// The new framebuffer size in pixels. private void OnFramebufferResize(Vector2D size) { + // A zero-area framebuffer (minimize, mid-layout) must not reconfigure the swapchain, + // but the managed event is still raised so listeners can observe the transition. if (size.X > 0 && size.Y > 0) { - this.resources.ConfigureSurface(this.presentMode, ToSize(size)); + this.resources.ConfigureSurface(this.resources.PresentMode, ToSize(size)); } this.FramebufferResized?.Invoke(ToSize(size)); } + /// + /// Throws when this window has been disposed. + /// private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); + /// + /// Maps the public window options to the native Silk.NET window options. + /// + /// The window creation options. + /// The native window options. private static WindowOptions CreateSilkOptions(WebGPUWindowOptions options) { WindowOptions silkOptions = WindowOptions.Default; + Size size = options.Size.Width > 0 && options.Size.Height > 0 + ? options.Size + : new Size(1280, 720); + + double framesPerSecond = double.IsFinite(options.FramesPerSecond) && options.FramesPerSecond >= 0 + ? options.FramesPerSecond + : 0; + + double updatesPerSecond = double.IsFinite(options.UpdatesPerSecond) && options.UpdatesPerSecond >= 0 + ? options.UpdatesPerSecond + : 0; + + // WebGPU owns the device and presentation, so the window must not create a GL + // context, swap buffers, or apply vsync itself; the swapchain present mode does that. silkOptions.API = GraphicsAPI.None; silkOptions.ShouldSwapAutomatically = false; silkOptions.IsContextControlDisabled = true; silkOptions.VSync = false; - silkOptions.Title = options.Title; - silkOptions.Size = ToVector(options.Size); + silkOptions.Title = options.Title ?? "ImageSharp.Drawing WebGPU"; + silkOptions.Size = ToVector(size); silkOptions.Position = ToVector(options.Position); silkOptions.IsVisible = options.IsVisible; - silkOptions.FramesPerSecond = options.FramesPerSecond; - silkOptions.UpdatesPerSecond = options.UpdatesPerSecond; + silkOptions.FramesPerSecond = framesPerSecond; + silkOptions.UpdatesPerSecond = updatesPerSecond; silkOptions.IsEventDriven = options.IsEventDriven; silkOptions.WindowState = ToNative(options.WindowState); silkOptions.WindowBorder = ToNative(options.WindowBorder); silkOptions.TopMost = options.IsTopMost; + return silkOptions; } + /// + /// Converts a size to a native vector. + /// + /// The size to convert. + /// The converted vector. private static Vector2D ToVector(Size size) => new(size.Width, size.Height); + /// + /// Converts a point to a native vector. + /// + /// The point to convert. + /// The converted vector. private static Vector2D ToVector(Point point) => new(point.X, point.Y); + /// + /// Converts a native vector to a size. + /// + /// The vector to convert. + /// The converted size. private static Size ToSize(Vector2D value) => new(value.X, value.Y); + /// + /// Converts a native vector to a point. + /// + /// The vector to convert. + /// The converted point. private static Point ToPoint(Vector2D value) => new(value.X, value.Y); + /// + /// Maps the public window state to the native Silk.NET value. + /// + /// The window state to map. + /// The native window state. private static NativeWindowState ToNative(WebGPUWindowState state) => state switch { @@ -540,9 +708,14 @@ private static NativeWindowState ToNative(WebGPUWindowState state) WebGPUWindowState.Minimized => NativeWindowState.Minimized, WebGPUWindowState.Maximized => NativeWindowState.Maximized, WebGPUWindowState.Fullscreen => NativeWindowState.Fullscreen, - _ => throw new InvalidOperationException("The WebGPU window state mapping is incomplete.") + _ => NativeWindowState.Normal }; + /// + /// Maps the native Silk.NET window state to the public value. + /// + /// The native window state to map. + /// The public window state. private static WebGPUWindowState FromNative(NativeWindowState state) => state switch { @@ -553,15 +726,25 @@ private static WebGPUWindowState FromNative(NativeWindowState state) _ => throw new InvalidOperationException("The native window state mapping is incomplete.") }; + /// + /// Maps the public window border mode to the native Silk.NET value. + /// + /// The window border mode to map. + /// The native window border mode. private static NativeWindowBorder ToNative(WebGPUWindowBorder border) => border switch { WebGPUWindowBorder.Resizable => NativeWindowBorder.Resizable, WebGPUWindowBorder.Fixed => NativeWindowBorder.Fixed, WebGPUWindowBorder.Hidden => NativeWindowBorder.Hidden, - _ => throw new InvalidOperationException("The WebGPU window border mapping is incomplete.") + _ => NativeWindowBorder.Resizable }; + /// + /// Maps the native Silk.NET window border mode to the public value. + /// + /// The native window border mode to map. + /// The public window border mode. private static WebGPUWindowBorder FromNative(NativeWindowBorder border) => border switch { diff --git a/src/ImageSharp.Drawing.WebGPU/WebGPUWindowOptions.cs b/src/ImageSharp.Drawing.WebGPU/WebGPUWindowOptions.cs index 2a07004ad..d064c783c 100644 --- a/src/ImageSharp.Drawing.WebGPU/WebGPUWindowOptions.cs +++ b/src/ImageSharp.Drawing.WebGPU/WebGPUWindowOptions.cs @@ -20,6 +20,9 @@ public sealed class WebGPUWindowOptions /// /// Gets or sets the initial client-area size in window coordinates. /// + /// + /// A size with a non-positive dimension uses the default initial size. + /// public Size Size { get; set; } = new(1280, 720); /// @@ -38,6 +41,7 @@ public sealed class WebGPUWindowOptions /// /// This is a scheduling hint for the underlying window loop rather than a guarantee of presented frame rate. /// The chosen and the platform's display timing still influence what the user sees. + /// Negative or non-finite values use the default unrestricted rate. /// public double FramesPerSecond { get; set; } @@ -46,6 +50,7 @@ public sealed class WebGPUWindowOptions /// /// /// Use this when you want simulation or input updates to run at a different cadence from rendering. + /// Negative or non-finite values use the default unrestricted rate. /// public double UpdatesPerSecond { get; set; } @@ -61,11 +66,17 @@ public sealed class WebGPUWindowOptions /// /// Gets or sets the initial window state such as normal, maximized, or fullscreen. /// + /// + /// Unrecognized values use . + /// public WebGPUWindowState WindowState { get; set; } = WebGPUWindowState.Normal; /// /// Gets or sets the initial window border mode. /// + /// + /// Unrecognized values use . + /// public WebGPUWindowBorder WindowBorder { get; set; } = WebGPUWindowBorder.Resizable; /// @@ -80,11 +91,23 @@ public sealed class WebGPUWindowOptions /// Choose for the usual v-synced behavior, /// for the lowest latency with possible tearing, or /// when you want newer-frame-wins behavior and the backend supports it. + /// When the requested mode is unavailable, is used. /// public WebGPUPresentMode PresentMode { get; set; } = WebGPUPresentMode.Fifo; /// /// Gets or sets the swapchain texture format used by acquired frames. /// + /// + /// The requested format is used when available. Otherwise, a compatible format is selected automatically. + /// public WebGPUTextureFormat Format { get; set; } = WebGPUTextureFormat.Rgba8Unorm; + + /// + /// Gets or sets how the native compositor interprets the window surface alpha channel. + /// + /// + /// The requested mode is used when available. Otherwise, a compatible mode is selected automatically. + /// + public WebGPUCompositeAlphaMode AlphaMode { get; set; } = WebGPUCompositeAlphaMode.Auto; } diff --git a/src/ImageSharp.Drawing.WebGPU/buildTransitive/SixLabors.ImageSharp.Drawing.WebGPU.targets b/src/ImageSharp.Drawing.WebGPU/buildTransitive/SixLabors.ImageSharp.Drawing.WebGPU.targets deleted file mode 100644 index 391a0bcee..000000000 --- a/src/ImageSharp.Drawing.WebGPU/buildTransitive/SixLabors.ImageSharp.Drawing.WebGPU.targets +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - <_SixLaborsImageSharpDrawingWebGPUNativeLibs Include="@(RuntimeTargetsCopyLocalItems)" - Condition="'%(RuntimeTargetsCopyLocalItems.AssetType)' == 'native'" /> - - - - - - diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm/native/libwgpu_native.so new file mode 100644 index 000000000..a1aa3d2a9 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm64/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm64/native/libwgpu_native.so new file mode 100644 index 000000000..afae69a43 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/android-arm64/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/android-x64/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/android-x64/native/libwgpu_native.so new file mode 100644 index 000000000..e63017650 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/android-x64/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/android-x86/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/android-x86/native/libwgpu_native.so new file mode 100644 index 000000000..6b3caa77e Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/android-x86/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/ios-arm64/native/libwgpu_native.a b/src/ImageSharp.Drawing.WebGPU/runtimes/ios-arm64/native/libwgpu_native.a new file mode 100644 index 000000000..6772a6d05 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/ios-arm64/native/libwgpu_native.a differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-arm64/native/libwgpu_native.a b/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-arm64/native/libwgpu_native.a new file mode 100644 index 000000000..9447c513d Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-arm64/native/libwgpu_native.a differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-x64/native/libwgpu_native.a b/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-x64/native/libwgpu_native.a new file mode 100644 index 000000000..1b017906e Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/iossimulator-x64/native/libwgpu_native.a differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/linux-arm64/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/linux-arm64/native/libwgpu_native.so new file mode 100644 index 000000000..66a6f7ab2 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/linux-arm64/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/linux-x64/native/libwgpu_native.so b/src/ImageSharp.Drawing.WebGPU/runtimes/linux-x64/native/libwgpu_native.so new file mode 100644 index 000000000..0fdf5bcd6 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/linux-x64/native/libwgpu_native.so differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/osx-arm64/native/libwgpu_native.dylib b/src/ImageSharp.Drawing.WebGPU/runtimes/osx-arm64/native/libwgpu_native.dylib new file mode 100644 index 000000000..41f7ff4b9 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/osx-arm64/native/libwgpu_native.dylib differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/osx-x64/native/libwgpu_native.dylib b/src/ImageSharp.Drawing.WebGPU/runtimes/osx-x64/native/libwgpu_native.dylib new file mode 100644 index 000000000..8a088d18b Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/osx-x64/native/libwgpu_native.dylib differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxcompiler.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxcompiler.dll new file mode 100644 index 000000000..d3192d60e Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxcompiler.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxil.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxil.dll new file mode 100644 index 000000000..e78a1bc10 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/dxil.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/wgpu_native.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/wgpu_native.dll new file mode 100644 index 000000000..0304eee43 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-arm64/native/wgpu_native.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxcompiler.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxcompiler.dll new file mode 100644 index 000000000..a52c4ab72 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxcompiler.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxil.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxil.dll new file mode 100644 index 000000000..c3d1ebf90 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/dxil.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/wgpu_native.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/wgpu_native.dll new file mode 100644 index 000000000..42df84fef Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x64/native/wgpu_native.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxcompiler.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxcompiler.dll new file mode 100644 index 000000000..ca78b1d56 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxcompiler.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxil.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxil.dll new file mode 100644 index 000000000..82ee82e42 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/dxil.dll differ diff --git a/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/wgpu_native.dll b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/wgpu_native.dll new file mode 100644 index 000000000..10f1e4b21 Binary files /dev/null and b/src/ImageSharp.Drawing.WebGPU/runtimes/win-x86/native/wgpu_native.dll differ diff --git a/src/ImageSharp.Drawing/ArcLineSegment.cs b/src/ImageSharp.Drawing/ArcLineSegment.cs index bde3f6971..f10bf8545 100644 --- a/src/ImageSharp.Drawing/ArcLineSegment.cs +++ b/src/ImageSharp.Drawing/ArcLineSegment.cs @@ -7,11 +7,18 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents a line segment that contains radii and angles that will be rendered as a elliptical arc. +/// Represents a line segment that contains radii and angles that will be rendered as an elliptical arc. /// public class ArcLineSegment : ILineSegment { + /// + /// The tolerance below which radii and squared distances are treated as zero. + /// private const float ZeroTolerance = 1e-05F; + + /// + /// The retained linearized arc points, baked in local space at construction. + /// private readonly PointF[] linePoints; /// @@ -84,6 +91,11 @@ public ArcLineSegment(PointF center, SizeF radius, float rotation, float startAn this.Bounds = CalculateBounds(this.linePoints); } + /// + /// Initializes a new instance of the class. + /// Used to wrap pre-linearized points produced by . + /// + /// The retained linearized arc points. private ArcLineSegment(PointF[] linePoints) { this.linePoints = linePoints; @@ -147,6 +159,8 @@ public ILineSegment Transform(Matrix4x4 matrix) /// /// Computes the bounds for the retained linearized arc points. /// + /// The linearized arc points. + /// The axis-aligned bounds enclosing the points. private static RectangleF CalculateBounds(ReadOnlySpan points) { float minX = float.MaxValue; @@ -166,6 +180,17 @@ private static RectangleF CalculateBounds(ReadOnlySpan points) return RectangleF.FromLTRB(minX, minY, maxX, maxY); } + /// + /// Linearizes an elliptical arc described in SVG endpoint parameterization. Degenerate arcs + /// (coincident endpoints or zero radii) collapse to a straight line between the endpoints. + /// + /// The arc start point. + /// The arc end point. + /// The ellipse radii. + /// The ellipse x-axis rotation, in radians. + /// Whether the arc spans more than 180 degrees. + /// Whether the arc sweeps through increasing angles. + /// The linearized arc points. private static PointF[] EllipticArcFromEndParams( PointF from, PointF to, @@ -185,6 +210,14 @@ private static PointF[] EllipticArcFromEndParams( return EllipticArcToBezierCurve(from, center, absRadius, rotation, angles.X, angles.Y); } + /// + /// Detects the SVG F.6.2 out-of-range cases where the endpoint arc parameters cannot + /// describe an arc and the segment degenerates to a straight line. + /// + /// The arc start point. + /// The arc end point. + /// The ellipse radii. + /// when the parameters are out of range; otherwise, . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radius) { @@ -204,18 +237,44 @@ private static bool EllipticArcOutOfRange(Vector2 from, Vector2 to, Vector2 radi return false; } + /// + /// Computes the derivative of the rotated ellipse parameterization at angle . + /// + /// The ellipse radii. + /// The ellipse x-axis rotation, in radians. + /// The parametric angle, in radians. + /// The tangent vector at the given angle. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 EllipticArcDerivative(Vector2 r, float xAngle, float t) => new( (-r.X * MathF.Cos(xAngle) * MathF.Sin(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Cos(t)), (-r.X * MathF.Sin(xAngle) * MathF.Sin(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Cos(t))); + /// + /// Evaluates the rotated ellipse parameterization at angle . + /// + /// The ellipse center. + /// The ellipse radii. + /// The ellipse x-axis rotation, in radians. + /// The parametric angle, in radians. + /// The point on the ellipse at the given angle. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector2 EllipticArcPoint(Vector2 c, Vector2 r, float xAngle, float t) => new( c.X + (r.X * MathF.Cos(xAngle) * MathF.Cos(t)) - (r.Y * MathF.Sin(xAngle) * MathF.Sin(t)), c.Y + (r.X * MathF.Sin(xAngle) * MathF.Cos(t)) + (r.Y * MathF.Cos(xAngle) * MathF.Sin(t))); + /// + /// Approximates the elliptical arc with cubic bezier spans of at most 45 degrees each and + /// flattens those spans into a single contiguous point run. + /// + /// The arc start point. + /// The ellipse center. + /// The ellipse radii. + /// The ellipse x-axis rotation, in radians. + /// The arc start angle, in radians. + /// The signed arc sweep, in radians. + /// The linearized arc points. private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, Vector2 radius, float xAngle, float startAngle, float sweepAngle) { float s = startAngle; @@ -260,6 +319,18 @@ private static PointF[] EllipticArcToBezierCurve(Vector2 from, Vector2 center, V return points.Detach(); } + /// + /// Converts SVG endpoint arc parameterization to center parameterization following + /// SVG spec section F.6.5, scaling up too-small radii as required by F.6.6. + /// + /// The arc start point. + /// The arc end point. + /// The ellipse radii; scaled up on return when too small to span both endpoints. + /// The ellipse x-axis rotation, in radians. + /// The large arc flag. + /// The sweep flag. + /// When this method returns, contains the ellipse center. + /// When this method returns, contains the start angle (X) and sweep delta (Y), in radians. private static void EndpointToCenterArcParams( Vector2 p1, Vector2 p2, @@ -335,6 +406,13 @@ private static void EndpointToCenterArcParams( angles = new Vector2((float)theta, (float)delta); } + /// + /// Clamps to the inclusive range [, ]. + /// + /// The value to clamp. + /// The minimum allowed value. + /// The maximum allowed value. + /// The clamped value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float Clamp(float val, float min, float max) { @@ -352,6 +430,14 @@ private static float Clamp(float val, float min, float max) } } + /// + /// Computes the signed angle between two vectors as defined by SVG spec equation F.6.5.4. + /// + /// The x-component of the first vector. + /// The y-component of the first vector. + /// The x-component of the second vector. + /// The y-component of the second vector. + /// The signed angle, in radians. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float SvgAngle(double ux, double uy, double vx, double vy) { diff --git a/src/ImageSharp.Drawing/ClipOperation.cs b/src/ImageSharp.Drawing/ClipOperation.cs new file mode 100644 index 000000000..5bc211df6 --- /dev/null +++ b/src/ImageSharp.Drawing/ClipOperation.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Specifies how a clip shape is combined with the current clip region. +/// +public enum ClipOperation +{ + /// + /// Keeps only the area covered by both the current clip region and the clip shape. + /// + Intersection = 0, + + /// + /// Removes the clip shape from the current clip region. + /// + Difference = 1 +} diff --git a/src/ImageSharp.Drawing/ClipPathExtensions.cs b/src/ImageSharp.Drawing/ClipPathExtensions.cs index 9ad53bbff..f6ec3809a 100644 --- a/src/ImageSharp.Drawing/ClipPathExtensions.cs +++ b/src/ImageSharp.Drawing/ClipPathExtensions.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.PolygonGeometry; -using SixLabors.ImageSharp.Drawing.Processing; namespace SixLabors.ImageSharp.Drawing; @@ -11,49 +10,75 @@ namespace SixLabors.ImageSharp.Drawing; /// public static class ClipPathExtensions { - private static readonly ShapeOptions DefaultOptions = new(); - /// - /// Clips the specified subject path with the provided clipping paths. + /// Clips the specified subject path with the provided clipping paths using + /// and . /// /// The subject path. /// The clipping paths. /// The clipped . public static IPath Clip(this IPath subjectPath, params IPath[] clipPaths) - => subjectPath.Clip(DefaultOptions, clipPaths); + => subjectPath.Clip(BooleanOperation.Intersection, IntersectionRule.NonZero, clipPaths); /// - /// Clips the specified subject path with the provided clipping paths. + /// Clips the specified subject path with the provided clipping paths using + /// and . /// /// The subject path. - /// The shape options. /// The clipping paths. /// The clipped . - public static IPath Clip( - this IPath subjectPath, - ShapeOptions options, - params IPath[] clipPaths) - => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); + public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) + => subjectPath.Clip(BooleanOperation.Intersection, IntersectionRule.NonZero, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths using + /// . + /// + /// The subject path. + /// The boolean operation to perform. + /// The clipping paths. + /// The clipped . + public static IPath Clip(this IPath subjectPath, BooleanOperation operation, params IPath[] clipPaths) + => subjectPath.Clip(operation, IntersectionRule.NonZero, clipPaths); + + /// + /// Clips the specified subject path with the provided clipping paths using + /// . + /// + /// The subject path. + /// The boolean operation to perform. + /// The clipping paths. + /// The clipped . + public static IPath Clip(this IPath subjectPath, BooleanOperation operation, IEnumerable clipPaths) + => subjectPath.Clip(operation, IntersectionRule.NonZero, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. /// /// The subject path. + /// The boolean operation to perform. + /// The fill rule used to interpret the subject and clipping paths. /// The clipping paths. /// The clipped . - public static IPath Clip(this IPath subjectPath, IEnumerable clipPaths) - => subjectPath.Clip(DefaultOptions, clipPaths); + public static IPath Clip( + this IPath subjectPath, + BooleanOperation operation, + IntersectionRule intersectionRule, + params IPath[] clipPaths) + => ClippedShapeGenerator.GenerateClippedShapes(operation, intersectionRule, subjectPath, clipPaths); /// /// Clips the specified subject path with the provided clipping paths. /// /// The subject path. - /// The shape options. + /// The boolean operation to perform. + /// The fill rule used to interpret the subject and clipping paths. /// The clipping paths. /// The clipped . public static IPath Clip( this IPath subjectPath, - ShapeOptions options, + BooleanOperation operation, + IntersectionRule intersectionRule, IEnumerable clipPaths) - => ClippedShapeGenerator.GenerateClippedShapes(options.BooleanOperation, subjectPath, clipPaths); + => ClippedShapeGenerator.GenerateClippedShapes(operation, intersectionRule, subjectPath, clipPaths); } diff --git a/src/ImageSharp.Drawing/ComplexPolygon.cs b/src/ImageSharp.Drawing/ComplexPolygon.cs index 036557f3c..d58a05701 100644 --- a/src/ImageSharp.Drawing/ComplexPolygon.cs +++ b/src/ImageSharp.Drawing/ComplexPolygon.cs @@ -8,23 +8,41 @@ namespace SixLabors.ImageSharp.Drawing; /// /// Represents a complex polygon made up of one or more shapes overlayed on each other, -/// where overlaps causes holes. +/// where overlaps cause holes. /// /// -public sealed class ComplexPolygon : IPath, IPathInternals, IInternalPathOwner +public sealed class ComplexPolygon : IPath, IInternalPathOwner { + /// + /// The child paths that make up the shape. + /// private readonly IPath[] paths; + + /// + /// The lazily initialized internal representation of each flattened ring. + /// private List? internalPaths; - private float length; + + /// + /// The lazily computed union of the child path bounds. + /// private RectangleF? bounds; + + /// + /// The cached result of . + /// private IPath? closedPath; + + /// + /// The per-scale cache of retained linear geometry. + /// private LinearGeometryCache geometryCache; /// /// Initializes a new instance of the class. /// - /// The contour path. - /// The hole path. + /// The points defining the outer contour. + /// The points defining the hole. public ComplexPolygon(PointF[] contour, PointF[] hole) : this(new Path(new LinearLineSegment(contour)), new Path(new LinearLineSegment(hole))) { @@ -105,13 +123,33 @@ public LinearGeometry ToLinearGeometry(Vector2 scale) ? hit : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + /// + public float ComputeLength(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeLength(); + + /// + public float ComputeArea(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeArea(); + + /// + public bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale) + { + PointF scaledPoint = new(point.X * scale.X, point.Y * scale.Y); + + return this.ToLinearGeometry(scale).Contains(scaledPoint, intersectionRule); + } + + /// + /// Concatenates the child path geometries into a single retained geometry, rebasing each + /// child's point, contour and segment indices onto the combined arrays. + /// + /// The X/Y scale at which curves are flattened. + /// The combined linear geometry. private LinearGeometry BuildLinearGeometry(Vector2 scale) { int pointCount = 0; int contourCount = 0; int segmentCount = 0; - int nonHorizontalSegmentCountPixelBoundary = 0; - int nonHorizontalSegmentCountPixelCenter = 0; bool hasBounds = false; float minX = float.MaxValue; @@ -138,8 +176,6 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) pointCount += geometry.Info.PointCount; contourCount += geometry.Info.ContourCount; segmentCount += geometry.Info.SegmentCount; - nonHorizontalSegmentCountPixelBoundary += geometry.Info.NonHorizontalSegmentCountPixelBoundary; - nonHorizontalSegmentCountPixelCenter += geometry.Info.NonHorizontalSegmentCountPixelCenter; } PointF[] points = new PointF[pointCount]; @@ -168,6 +204,7 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) { PointStart = pointStart + contour.PointStart, PointCount = contour.PointCount, + Bounds = contour.Bounds, SegmentStart = segmentStart + contour.SegmentStart, SegmentCount = contour.SegmentCount, IsClosed = contour.IsClosed @@ -187,9 +224,7 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) Bounds = bounds, ContourCount = contours.Length, PointCount = points.Length, - SegmentCount = segmentCount, - NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, - NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + SegmentCount = segmentCount }, contours, points); @@ -219,25 +254,16 @@ public IPath AsClosedPath() } /// - SegmentInfo IPathInternals.PointAlongPath(float distance) - { - this.EnsureInternalPaths(); - - distance %= this.length; - foreach (InternalPath p in this.internalPaths) - { - if (p.Length >= distance) - { - return p.PointAlongPath(distance); - } + public bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistance(distance, out pathPoint); - // Reduce it before trying the next path - distance -= p.Length; - } + /// + public bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistanceUnbounded(distance, out pathPoint); - ThrowOutOfRange(); - return default; - } + /// + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath path) + => this.ToLinearGeometry(scale).TryGetSegment(startDistance, stopDistance, startOnBeginFigure, out path); /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() @@ -246,6 +272,9 @@ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() return this.internalPaths; } + /// + /// Ensures is initialized. + /// [MemberNotNull(nameof(internalPaths))] private void EnsureInternalPaths() { @@ -258,25 +287,27 @@ private void EnsureInternalPaths() } /// - /// Initializes and . + /// Initializes . /// [MemberNotNull(nameof(internalPaths))] private void InitInternalPaths() { this.internalPaths = new List(this.paths.Length); - this.length = 0; foreach (IPath p in this.paths) { foreach (ISimplePath s in p.Flatten()) { InternalPath ip = new(s.Points, s.IsClosed); - this.length += ip.Length; this.internalPaths.Add(ip); } } } + /// + /// Computes the union of the child path bounds. + /// + /// The axis-aligned bounds enclosing all child paths. private RectangleF CalcBounds() { float minX = float.MaxValue; @@ -296,6 +327,4 @@ private RectangleF CalcBounds() return new RectangleF(minX, minY, maxX - minX, maxY - minY); } - - private static InvalidOperationException ThrowOutOfRange() => new("Should not be possible to reach this line"); } diff --git a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs index 53a0af990..540a69d34 100644 --- a/src/ImageSharp.Drawing/CubicBezierLineSegment.cs +++ b/src/ImageSharp.Drawing/CubicBezierLineSegment.cs @@ -7,22 +7,39 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents a line segment that contains a lists of control points that will be rendered as a cubic bezier curve +/// Represents a line segment that contains a list of control points that will be rendered as a cubic bezier curve. /// /// public sealed class CubicBezierLineSegment : ILineSegment { // Code for this taken from + + /// + /// The squared distance between span endpoints below which subdivision stops. + /// private const float MinimumSqrDistance = 1.75f; + + /// + /// The dot-product threshold used to decide whether a span is flat enough. Directions from the + /// midpoint to each endpoint are nearly opposite (dot close to -1) when the span is straight; + /// values above this threshold indicate curvature that requires further subdivision. + /// private const float DivisionThreshold = -.9995f; + /// + /// The bezier control points; the length is a multiple of 3 plus 1. + /// private readonly PointF[] controlPoints; + + /// + /// The most recently flattened point run, keyed by the scale it was baked at. + /// private FlattenedCache? flattenedCache; /// /// Initializes a new instance of the class. /// - /// The points. + /// The control points. The length must be a multiple of 3 plus 1 (4, 7, 10...). public CubicBezierLineSegment(PointF[] points) { Guard.NotNull(points, nameof(points)); @@ -34,11 +51,14 @@ public CubicBezierLineSegment(PointF[] points) /// /// Initializes a new instance of the class. /// - /// The start. - /// The control point1. - /// The control point2. - /// The end. - /// The additional points. + /// The start point of the curve. + /// The first control point. + /// The second control point. + /// The end point of the curve. + /// + /// Additional points appended after ; each group of three + /// (two control points and an end point) defines a further cubic span. + /// public CubicBezierLineSegment(PointF start, PointF controlPoint1, PointF controlPoint2, PointF end, params PointF[] additionalPoints) : this(new[] { start, controlPoint1, controlPoint2, end }.Concat(additionalPoints)) { @@ -83,6 +103,8 @@ public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale) /// Publication uses so a concurrent reader either observes /// or a fully-constructed entry. /// + /// The X/Y scale at which the curve is flattened. + /// The flattened points at the requested scale. private PointF[] GetFlattenedPoints(Vector2 scale) { FlattenedCache? hit = Volatile.Read(ref this.flattenedCache); @@ -133,6 +155,9 @@ public CubicBezierLineSegment Transform(Matrix4x4 matrix) /// into a single contiguous point run. Subdivision density is evaluated /// against the scaled control points so the polyline adapts to rendering scale. /// + /// The bezier control points; the length is a multiple of 3 plus 1. + /// The X/Y scale at which the curve is flattened. + /// The flattened points. private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) { int curveCount = (controlPoints.Length - 1) / 3; @@ -164,6 +189,14 @@ private static PointF[] FlattenCurve(PointF[] controlPoints, Vector2 scale) /// /// Recursively subdivides the scaled cubic segment, appending midpoints in left-to-right order. /// + /// The curve parameter at the start of the span. + /// The curve parameter at the end of the span. + /// The start point of the cubic. + /// The first control point of the cubic. + /// The second control point of the cubic. + /// The end point of the cubic. + /// The builder receiving the appended points. + /// The current recursion depth; used to bound the subdivision. private static void SubdivideAndAppend( float t0, float t1, @@ -205,10 +238,10 @@ private static void SubdivideAndAppend( /// Calculates the bezier point along the line. /// /// The position within the line. - /// The p 0. - /// The p 1. - /// The p 2. - /// The p 3. + /// The start point of the cubic. + /// The first control point of the cubic. + /// The second control point of the cubic. + /// The end point of the cubic. /// /// The . /// @@ -232,6 +265,8 @@ private static Vector2 CalculateBezierPoint(float t, Vector2 p0, Vector2 p1, Vec /// /// Computes the bounds for the cached linearized bezier points. /// + /// The linearized bezier points. + /// The axis-aligned bounds enclosing the points. private static RectangleF CalculateBounds(ReadOnlySpan points) { float minX = float.MaxValue; @@ -251,10 +286,21 @@ private static RectangleF CalculateBounds(ReadOnlySpan points) return RectangleF.FromLTRB(minX, minY, maxX, maxY); } + /// + /// An immutable pairing of a flatten scale with the point run baked at that scale. + /// + /// The scale the points were baked at. + /// The baked points. private sealed class FlattenedCache(Vector2 scale, PointF[] points) { + /// + /// Gets the scale the points were baked at. + /// public Vector2 Scale { get; } = scale; + /// + /// Gets the baked points. + /// public PointF[] Points { get; } = points; } } diff --git a/src/ImageSharp.Drawing/EllipsePolygon.cs b/src/ImageSharp.Drawing/EllipsePolygon.cs index 4456c4bf5..51cfd8b60 100644 --- a/src/ImageSharp.Drawing/EllipsePolygon.cs +++ b/src/ImageSharp.Drawing/EllipsePolygon.cs @@ -6,9 +6,9 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// An elliptical shape made up of a single path made up of one of more s. +/// An elliptical shape made up of a single path made up of one or more s. /// -public sealed class EllipsePolygon : Polygon, IPathInternals +public sealed class EllipsePolygon : Polygon { /// /// Initializes a new instance of the class. @@ -24,7 +24,7 @@ public EllipsePolygon(PointF location, SizeF size) /// Initializes a new instance of the class. /// /// The location the center of the circle will be placed. - /// The radius final circle. + /// The radius of the final circle. public EllipsePolygon(PointF location, float radius) : this(location, new SizeF(radius * 2, radius * 2)) { @@ -42,6 +42,11 @@ public EllipsePolygon(float x, float y, float width, float height) { } + /// + /// Initializes a new instance of the class. + /// Used by to wrap already-transformed segments. + /// + /// The transformed segments; ownership passes to the new instance. private EllipsePolygon(ILineSegment[] segments) : base(segments, true) { @@ -52,7 +57,7 @@ private EllipsePolygon(ILineSegment[] segments) /// /// The x-coordinate of the center of the circle. /// The y-coordinate of the center of the circle. - /// The radius final circle. + /// The radius of the final circle. public EllipsePolygon(float x, float y, float radius) : this(new PointF(x, y), new SizeF(radius * 2, radius * 2)) { @@ -76,11 +81,13 @@ public override IPath Transform(Matrix4x4 matrix) return new EllipsePolygon(segments); } - /// - // TODO switch this out to a calculated algorithm - SegmentInfo IPathInternals.PointAlongPath(float distance) - => this.InnerPath.PointAlongPath(distance); - + /// + /// Builds the closed four-arc cubic bezier approximation of the ellipse using the + /// standard kappa constant. + /// + /// The center of the ellipse. + /// The width and height of the ellipse. + /// The bezier segment describing the ellipse. private static CubicBezierLineSegment CreateSegment(Vector2 location, SizeF size) { Guard.MustBeGreaterThan(size.Width, 0, "width"); diff --git a/src/ImageSharp.Drawing/EmptyPath.cs b/src/ImageSharp.Drawing/EmptyPath.cs index a694babe2..27dd01c00 100644 --- a/src/ImageSharp.Drawing/EmptyPath.cs +++ b/src/ImageSharp.Drawing/EmptyPath.cs @@ -10,28 +10,33 @@ namespace SixLabors.ImageSharp.Drawing; /// public sealed class EmptyPath : IPath { + /// + /// The shared zero-content geometry returned for every scale. + /// private static readonly LinearGeometry EmptyGeometry = new( new LinearGeometryInfo { Bounds = RectangleF.Empty, ContourCount = 0, PointCount = 0, - SegmentCount = 0, - NonHorizontalSegmentCountPixelBoundary = 0, - NonHorizontalSegmentCountPixelCenter = 0 + SegmentCount = 0 }, [], []); + /// + /// Initializes a new instance of the class. + /// + /// The path type the empty path reports. private EmptyPath(PathTypes pathType) => this.PathType = pathType; /// - /// Gets the closed path instance of the empty path + /// Gets the closed path instance of the empty path. /// public static EmptyPath ClosedPath { get; } = new(PathTypes.Closed); /// - /// Gets the open path instance of the empty path + /// Gets the open path instance of the empty path. /// public static EmptyPath OpenPath { get; } = new(PathTypes.Open); @@ -50,6 +55,36 @@ public sealed class EmptyPath : IPath /// public LinearGeometry ToLinearGeometry(Vector2 scale) => EmptyGeometry; + /// + public float ComputeLength(Vector2 scale) => 0; + + /// + public float ComputeArea(Vector2 scale) => 0; + + /// + public bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale) => false; + + /// + public bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint) + { + pathPoint = default; + return false; + } + + /// + public bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint) + { + pathPoint = default; + return false; + } + + /// + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath path) + { + path = this; + return false; + } + /// public IPath Transform(Matrix4x4 matrix) => this; } diff --git a/src/ImageSharp.Drawing/FlattenedPointBuilder.cs b/src/ImageSharp.Drawing/FlattenedPointBuilder.cs index da5d8c868..9000c6db0 100644 --- a/src/ImageSharp.Drawing/FlattenedPointBuilder.cs +++ b/src/ImageSharp.Drawing/FlattenedPointBuilder.cs @@ -12,7 +12,14 @@ namespace SixLabors.ImageSharp.Drawing; /// internal struct FlattenedPointBuilder { + /// + /// The owned backing array. Grows geometrically; trimmed to by . + /// private PointF[] points; + + /// + /// The number of points appended so far. + /// private int count; /// @@ -53,8 +60,12 @@ public Span GetAppendSpan(int length) public void Advance(int length) => this.count += length; /// - /// Returns the owned point array. + /// Returns the owned point array, trimmed to the appended count. /// + /// + /// Ownership of the array transfers to the caller. The builder must not be appended to afterwards as further + /// writes could mutate the detached array. + /// /// The tightly-sized retained point array. public PointF[] Detach() { diff --git a/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs index a22343005..ac0d44935 100644 --- a/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs +++ b/src/ImageSharp.Drawing/Helpers/ArrayExtensions.cs @@ -16,7 +16,7 @@ internal static class ArrayExtensions /// The second source array. /// /// A new array containing the elements of both source arrays, or - /// when is empty. + /// when is or empty. /// public static T[] Concat(this T[] source1, T[] source2) { diff --git a/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs index 6d0168044..42eea6ac9 100644 --- a/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs +++ b/src/ImageSharp.Drawing/Helpers/MatrixUtilities.cs @@ -7,9 +7,14 @@ namespace SixLabors.ImageSharp.Drawing.Helpers; /// -/// Provides helper methods for extracting properties from transformation matrices. +/// Provides helper methods for extracting scale properties from transformation matrices. /// -internal static class MatrixUtilities +/// +/// Scale extraction operates on the 2D linear part of the matrix (M11, M12, M21, M22). +/// Scale magnitudes are the lengths of the transformed X and Y basis vectors, so they are +/// rotation-invariant and always non-negative. Translation does not affect any result. +/// +public static class MatrixUtilities { /// /// Extracts the average 2D scale factor from a . @@ -25,4 +30,76 @@ public static float GetAverageScale(in Matrix4x4 matrix) float sy = MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22)); return (sx + sy) * 0.5f; } + + /// + /// Returns a value indicating whether the matrix is affine in 2D: no perspective terms, + /// so every position is transformed by the same linear part and shapes are mapped + /// identically wherever they sit. Projective matrices fail this test because their + /// perspective divisor varies with the input position. + /// + /// The transformation matrix. + /// when the matrix contains no perspective terms; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsAffine(in Matrix4x4 matrix) + => matrix.M14 == 0F && matrix.M24 == 0F && matrix.M34 == 0F && matrix.M44 == 1F; + + /// + /// Returns a value indicating whether the matrix is a pure 2D translation: an identity + /// linear part with no perspective terms, so geometry moves by the M41 and M42 offsets + /// without any scale, rotation, or skew. + /// + /// The transformation matrix. + /// when the matrix only translates; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsTranslationOnly(in Matrix4x4 matrix) + => matrix.M11 == 1F && matrix.M12 == 0F && matrix.M13 == 0F && matrix.M14 == 0F && + matrix.M21 == 0F && matrix.M22 == 1F && matrix.M23 == 0F && matrix.M24 == 0F && + matrix.M31 == 0F && matrix.M32 == 0F && matrix.M33 == 1F && matrix.M34 == 0F && + matrix.M44 == 1F; + + /// + /// Returns a value indicating whether the matrix maps axis-aligned rectangles to axis-aligned rectangles. + /// + /// + /// Translation, scaling, reflection, and 90 degree rotations (axis swaps) preserve axis + /// alignment. Skew and free rotation do not. + /// + /// The transformation matrix. + /// when axis-aligned rectangles remain axis-aligned; otherwise, . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool PreservesAxisAlignedRectangles(in Matrix4x4 matrix) => + + // Either each output axis depends on the matching input axis, or the axes are swapped. + // Once both terms in an output axis are non-zero, rectangle edges become rotated or skewed. + (matrix.M12 == 0 && matrix.M21 == 0) || (matrix.M11 == 0 && matrix.M22 == 0); + + /// + /// Extracts the X and Y scale magnitudes from a 2D transform matrix. + /// + /// + /// The magnitudes are the lengths of the transformed X and Y basis vectors. They are + /// always non-negative; reflection and rotation are not represented in the result. + /// + /// The transformation matrix. + /// The X and Y scale magnitudes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector2 GetScale(in Matrix4x4 matrix) + => new( + MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)), + MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22))); + + /// + /// Computes the transform remaining after the X and Y scale magnitudes have been baked into geometry. + /// + /// + /// The invariant is Matrix4x4.CreateScale(scale.X, scale.Y, 1) * residual == matrix: + /// applying the residual to geometry that has already been scaled reproduces the original + /// transform. Rotation, reflection, skew, and translation all remain in the residual. + /// + /// The scale magnitudes baked into geometry. Components must be non-zero. + /// The original transformation matrix. + /// The residual transform. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Matrix4x4 GetResidual(Vector2 scale, Matrix4x4 matrix) + => Matrix4x4.CreateScale(1F / scale.X, 1F / scale.Y, 1F) * matrix; } diff --git a/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs index e035fc940..67f66f9d1 100644 --- a/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs +++ b/src/ImageSharp.Drawing/Helpers/PolygonUtilities.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Drawing; namespace SixLabors.ImageSharp.Drawing.Helpers; @@ -43,6 +44,16 @@ public static void EnsureOrientation(Span polygon, int expectedOrientati } } + /// + /// Returns the 2D cross product scalar of the supplied vectors. + /// + /// The left operand. + /// The right operand. + /// The 2D cross product scalar. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float CrossProduct(Vector2 left, Vector2 right) + => (left.X * right.Y) - (left.Y * right.X); + /// /// Returns the orientation sign of a closed polygon ring using the shoelace sum. /// diff --git a/src/ImageSharp.Drawing/IInternalPathOwner.cs b/src/ImageSharp.Drawing/IInternalPathOwner.cs index b9c54685e..52dab69d9 100644 --- a/src/ImageSharp.Drawing/IInternalPathOwner.cs +++ b/src/ImageSharp.Drawing/IInternalPathOwner.cs @@ -12,6 +12,6 @@ internal interface IInternalPathOwner /// /// Returns the rings as a readonly collection of elements. /// - /// The . + /// The of rings. public IReadOnlyList GetRingsAsInternalPath(); } diff --git a/src/ImageSharp.Drawing/ILineSegment.cs b/src/ImageSharp.Drawing/ILineSegment.cs index 6619c45d5..7f0bffafd 100644 --- a/src/ImageSharp.Drawing/ILineSegment.cs +++ b/src/ImageSharp.Drawing/ILineSegment.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents a simple path segment +/// Represents a simple path segment. /// public interface ILineSegment { @@ -46,9 +46,9 @@ public interface ILineSegment public void CopyTo(Span destination, bool skipFirstPoint, Vector2 scale); /// - /// Transforms the current LineSegment using specified matrix. + /// Transforms the current line segment using the specified matrix. /// - /// The matrix. + /// The transformation matrix. /// A line segment with the matrix applied to it. public ILineSegment Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/IPath.cs b/src/ImageSharp.Drawing/IPath.cs index a6ceb36b5..bebb6a037 100644 --- a/src/ImageSharp.Drawing/IPath.cs +++ b/src/ImageSharp.Drawing/IPath.cs @@ -34,6 +34,69 @@ public interface IPath /// The retained linear geometry. public LinearGeometry ToLinearGeometry(Vector2 scale); + /// + /// Calculates the path length after flattening curves at the supplied scale. + /// + /// The X/Y scale at which curves are flattened for length measurement. + /// The path length. + public float ComputeLength(Vector2 scale); + + /// + /// Calculates the non-negative path area after flattening curves at the supplied scale. + /// + /// The X/Y scale at which curves are flattened for area measurement. + /// The path area. + public float ComputeArea(Vector2 scale); + + /// + /// Returns whether the supplied point is inside the path. + /// + /// The point to test. + /// The fill rule used for containment. + /// The X/Y scale at which curves are flattened for the containment test. + /// when the point is inside or on the path boundary. + public bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale); + + /// + /// Gets path information at the specified distance along the path. + /// + /// The distance along the path. + /// The X/Y scale at which curves are flattened for distance measurement. + /// When this method returns, contains the path information at if the distance resolves to a point on the path; otherwise, the default value. + /// + /// if resolves to a point on the path; + /// otherwise, . + /// + public bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint); + + /// + /// Gets path information at the specified distance along the path, extrapolating along the + /// boundary tangents when the distance falls before the start or beyond the end of an open path. + /// + /// + /// Unlike , out-of-range + /// distances resolve to a point on the virtual straight-line continuation of the path's first + /// or last segment. Text layout uses this to place aligned glyphs that overflow their layout + /// path. The method returns only when the distance is not a finite + /// number or the path has no measurable length. + /// + /// The distance along the path. + /// The X/Y scale at which curves are flattened for distance measurement. + /// When this method returns, contains the path information at if the path is measurable; otherwise, the default value. + /// if the point could be resolved; otherwise, . + public bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint); + + /// + /// Creates a path segment between two distances along the path. + /// + /// The segment start distance. + /// The segment stop distance. + /// Whether the returned segment starts a new figure at the first segment point. + /// The X/Y scale at which curves are flattened for segment extraction. + /// When this method returns, contains the segment path if one was created; otherwise, an empty path. + /// when a segment path was created. + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath path); + /// /// Transforms the path using the specified matrix. /// @@ -44,6 +107,6 @@ public interface IPath /// /// Returns this path with all figures closed. /// - /// A new close . + /// A new closed . public IPath AsClosedPath(); } diff --git a/src/ImageSharp.Drawing/IPathCollection.cs b/src/ImageSharp.Drawing/IPathCollection.cs index 5d2780235..7774554c1 100644 --- a/src/ImageSharp.Drawing/IPathCollection.cs +++ b/src/ImageSharp.Drawing/IPathCollection.cs @@ -6,19 +6,19 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents a logic path that can be drawn +/// Represents a collection of paths that can be enumerated and transformed as a single unit. /// public interface IPathCollection : IEnumerable { /// - /// Gets the bounds enclosing the path + /// Gets the bounds enclosing all paths in the collection. /// public RectangleF Bounds { get; } /// - /// Transforms the path using the specified matrix. + /// Transforms all paths in the collection using the specified matrix. /// - /// The matrix. + /// The transformation matrix. /// A new path collection with the matrix applied to it. public IPathCollection Transform(Matrix4x4 matrix); } diff --git a/src/ImageSharp.Drawing/IPathInternals.cs b/src/ImageSharp.Drawing/IPathInternals.cs deleted file mode 100644 index 164ccc45e..000000000 --- a/src/ImageSharp.Drawing/IPathInternals.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// An interface for internal operations we don't want to expose on . -/// -internal interface IPathInternals : IPath -{ - /// - /// Returns information about a point at a given distance along a path. - /// - /// The distance along the path to return details for. - /// - /// The segment information. - /// - SegmentInfo PointAlongPath(float distance); -} diff --git a/src/ImageSharp.Drawing/IRegionPath.cs b/src/ImageSharp.Drawing/IRegionPath.cs new file mode 100644 index 000000000..f5c17a3fe --- /dev/null +++ b/src/ImageSharp.Drawing/IRegionPath.cs @@ -0,0 +1,23 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Represents a boundary path exported from an integer region while retaining its rectangle-set form. +/// +/// +/// Region clipping has two valid representations in this codebase: ordinary path geometry +/// and an exact integer rectangle set. must return an +/// , but callers that are still in untransformed integer region space +/// can use this marker to continue exact region operations without converting the region +/// into generic polygon clipping. Once the path is transformed, the marker is dropped and +/// the result is treated as normal path geometry. +/// +internal interface IRegionPath : IPath +{ + /// + /// Gets the normalized non-overlapping rectangles that cover the same area as the boundary path. + /// + public IReadOnlyList Rectangles { get; } +} diff --git a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj index 8eae47b5b..60d2088df 100644 --- a/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj +++ b/src/ImageSharp.Drawing/ImageSharp.Drawing.csproj @@ -40,9 +40,19 @@ - - - + + + + + + + + + + diff --git a/src/ImageSharp.Drawing/InternalPath.cs b/src/ImageSharp.Drawing/InternalPath.cs index 489784039..5ade5ff46 100644 --- a/src/ImageSharp.Drawing/InternalPath.cs +++ b/src/ImageSharp.Drawing/InternalPath.cs @@ -8,18 +8,24 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Internal logic for integrating linear paths. +/// The legacy path simplification engine. Reduces a segment or point sequence to a simplified vertex list +/// (removing coincident and collinear vertices while preserving user-intended direction reversals) and +/// computes the bounds and total length of the result. /// internal class InternalPath { /// - /// The epsilon for float comparison + /// The epsilon used for orientation (collinearity) and zero-length vector tests. /// private const float Epsilon = 0.003f; + + /// + /// The per-axis epsilon used to merge near-coincident vertices during simplification. + /// private const float Epsilon2 = 0.2f; /// - /// The points. + /// The simplified vertices together with their orientation and incoming edge length. /// private readonly PointData[] points; @@ -29,17 +35,17 @@ internal class InternalPath private PointF[]? materializedPoints; /// - /// The closed path. + /// Whether the path is closed. /// private readonly bool closedPath; /// /// Initializes a new instance of the class. /// - /// The segments. - /// if set to true [is closed path]. - /// Whether to remove close and collinear vertices - internal InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true) + /// The segments to flatten and simplify. + /// Whether the path is closed. + /// Whether to remove close and collinear vertices. + public InternalPath(IReadOnlyList segments, bool isClosedPath, bool removeCloseAndCollinear = true) : this(Simplify(segments, isClosedPath, removeCloseAndCollinear), isClosedPath) { } @@ -47,9 +53,9 @@ internal InternalPath(IReadOnlyList segments, bool isClosedPath, b /// /// Initializes a new instance of the class. /// - /// The points. - /// if set to true [is closed path]. - internal InternalPath(ReadOnlyMemory points, bool isClosedPath) + /// The points to simplify. + /// Whether the path is closed. + public InternalPath(ReadOnlyMemory points, bool isClosedPath) : this(Simplify(points.Span, isClosedPath, true), isClosedPath) { } @@ -57,8 +63,8 @@ internal InternalPath(ReadOnlyMemory points, bool isClosedPath) /// /// Initializes a new instance of the class. /// - /// The points. - /// if set to true [is closed path]. + /// The simplified vertex data. + /// Whether the path is closed. private InternalPath(PointData[] points, bool isClosedPath) { this.points = points; @@ -91,7 +97,7 @@ private InternalPath(PointData[] points, bool isClosedPath) } /// - /// Gets the bounds. + /// Gets the axis-aligned bounds of the simplified vertices. /// /// /// The bounds. @@ -99,7 +105,7 @@ private InternalPath(PointData[] points, bool isClosedPath) public RectangleF Bounds { get; } /// - /// Gets the length. + /// Gets the total length of the simplified path. /// /// /// The length. @@ -107,74 +113,30 @@ private InternalPath(PointData[] points, bool isClosedPath) public float Length { get; } /// - /// Gets the length. + /// Gets the number of simplified vertices. /// public int PointCount => this.points.Length; /// - /// Gets the points. + /// Gets the simplified points, materializing and caching them on first use. /// - /// The - internal ReadOnlyMemory Points() => this.materializedPoints ??= this.CreatePoints(); + /// The of simplified points. + public ReadOnlyMemory Points() => this.materializedPoints ??= this.CreatePoints(); /// - /// Calculates the point a certain distance a path. + /// Wraps an index that is at most one length beyond the end back into array range. /// - /// The distance along the path to find details of. - /// - /// Returns details about a point along a path. - /// - /// Thrown if no points found. - internal SegmentInfo PointAlongPath(float distanceAlongPath) - { - int pointCount = this.PointCount; - if (this.closedPath) - { - // Move the distance back to the beginning since this is a closed polygon. - distanceAlongPath %= this.Length; - pointCount--; - } - - for (int i = 0; i < pointCount; i++) - { - int next = WrapArrayIndex(i + 1, this.PointCount); - if (distanceAlongPath < this.points[next].Length) - { - float t = distanceAlongPath / this.points[next].Length; - Vector2 point = Vector2.Lerp(this.points[i].Point, this.points[next].Point, t); - Vector2 diff = this.points[i].Point - this.points[next].Point; - - return new SegmentInfo - { - Point = point, - Angle = (float)(Math.Atan2(diff.Y, diff.X) % (Math.PI * 2)) - }; - } - - distanceAlongPath -= this.points[next].Length; - } - - // Closed paths will never reach this point. - // For open paths we're going to create a new virtual point that extends past the path. - // The position and angle for that point are calculated based upon the last two points. - PointF a = this.points[Math.Max(this.points.Length - 2, 0)].Point; - PointF b = this.points[^1].Point; - Vector2 delta = a - b; - float angle = (float)(Math.Atan2(delta.Y, delta.X) % (Math.PI * 2)); - - Matrix4x4 transform = Matrix4x4.CreateRotationZ(angle - MathF.PI) * Matrix4x4.CreateTranslation(b.X, b.Y, 0); - - return new SegmentInfo - { - Point = PointF.Transform(new PointF(distanceAlongPath, 0), transform), - Angle = angle - }; - } - + /// The candidate index. Must be less than twice . + /// The array length. + /// The wrapped index. // Modulo is a very slow operation. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int WrapArrayIndex(int i, int arrayLength) => i < arrayLength ? i : i - arrayLength; + /// + /// Projects the simplified vertex data to a plain point array. + /// + /// The projected points. private PointF[] CreatePoints() { PointF[] result = new PointF[this.points.Length]; @@ -186,6 +148,13 @@ private PointF[] CreatePoints() return result; } + /// + /// Calculates the orientation of the ordered point triplet (p, q, r). + /// + /// The first point. + /// The second (middle) point. + /// The third point. + /// The of the triplet. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vector2 r) { @@ -197,18 +166,18 @@ private static PointOrientation CalculateOrientation(Vector2 p, Vector2 q, Vecto if (val is > -Epsilon and < Epsilon) { - return PointOrientation.Collinear; // colinear + return PointOrientation.Collinear; // collinear } return (val > 0) ? PointOrientation.Clockwise : PointOrientation.Counterclockwise; // clock or counterclock wise } /// - /// Simplifies the collection of segments. + /// Flattens the collection of segments and simplifies the resulting points. /// - /// The segments. - /// Weather the path is closed or open. - /// Whether to remove close and collinear vertices + /// The segments to flatten. + /// Whether the path is closed or open. + /// Whether to remove close and collinear vertices. /// /// The . /// @@ -225,7 +194,7 @@ private static PointData[] Simplify(IReadOnlyList segments, bool i // Track indices where collinear direction reversals represent user-intended // geometry: interior points of multi-point linear segments, and junction - // points between two linear segments (e.g. PathBuilder LineTo → LineTo). + // points between two linear segments (e.g. PathBuilder LineTo -> LineTo). // Reversals at all other indices (flattened curves, curve junctions) are // artifacts and should be removed normally. HashSet? linearReversalIndices = null; @@ -251,7 +220,7 @@ private static PointData[] Simplify(IReadOnlyList segments, bool i } } - // Junction between two linear segments (e.g. PathBuilder LineTo → LineTo). + // Junction between two linear segments (e.g. PathBuilder LineTo -> LineTo). if (prevSeg is LinearLineSegment && start > 0) { linearReversalIndices ??= []; @@ -265,6 +234,19 @@ private static PointData[] Simplify(IReadOnlyList segments, bool i return Simplify(CollectionsMarshal.AsSpan(simplified), isClosed, removeCloseAndCollinear, linearReversalIndices); } + /// + /// Simplifies a point sequence into vertex data annotated with orientation and incoming edge length. + /// + /// The points to simplify. + /// Whether the path is closed or open. + /// Whether to remove close and collinear vertices. + /// + /// Indices whose collinear direction reversals are user-intended and must be preserved. + /// When , reversals are preserved at every index. + /// + /// + /// The . + /// private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, bool removeCloseAndCollinear, HashSet? linearReversalIndices = null) { int polyCorners = points.Length; @@ -328,9 +310,9 @@ private static PointData[] Simplify(ReadOnlySpan points, bool isClosed, if (removeCloseAndCollinear && or == PointOrientation.Collinear && next != 0) { // Preserve collinear points that represent a direction reversal (U-turn) - // within a single segment. E.g. (10,10)→(90,10)→(20,10): the middle point + // within a single segment. E.g. (10,10) -> (90,10) -> (20,10): the middle point // is collinear but the stroker needs to see the reversal. - // Don't preserve reversals at segment boundaries — these arise from joining + // Don't preserve reversals at segment boundaries; these arise from joining // different path segments (e.g. arc-to-arc) and are not user-intended. bool preserve = false; if (linearReversalIndices == null || linearReversalIndices.Contains(i)) @@ -386,10 +368,24 @@ private static bool Equivalent(PointF source1, PointF source2, float threshold) return abs.X < threshold && abs.Y < threshold; } + /// + /// A simplified vertex together with its derived metadata. + /// private struct PointData { + /// + /// The vertex position. + /// public PointF Point; + + /// + /// The orientation of the triplet formed by the previous vertex, this vertex, and the next vertex. + /// public PointOrientation Orientation; + + /// + /// The length of the edge from the previous vertex to this vertex. Zero for the first vertex of an open path. + /// public float Length; } } diff --git a/src/ImageSharp.Drawing/LinearContour.cs b/src/ImageSharp.Drawing/LinearContour.cs index c69c894f8..ca855b4af 100644 --- a/src/ImageSharp.Drawing/LinearContour.cs +++ b/src/ImageSharp.Drawing/LinearContour.cs @@ -22,6 +22,11 @@ public readonly struct LinearContour /// public required int PointCount { get; init; } + /// + /// Gets the bounds of the stored contour points. + /// + public required RectangleF Bounds { get; init; } + /// /// Gets the zero-based index of the first derived segment belonging to this contour. /// diff --git a/src/ImageSharp.Drawing/LinearGeometry.cs b/src/ImageSharp.Drawing/LinearGeometry.cs index e6fd7d19b..43a9eff6d 100644 --- a/src/ImageSharp.Drawing/LinearGeometry.cs +++ b/src/ImageSharp.Drawing/LinearGeometry.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; @@ -28,9 +29,20 @@ namespace SixLabors.ImageSharp.Drawing; /// public sealed class LinearGeometry { + /// + /// The contour metadata partitioning . + /// private readonly LinearContour[] contours; + + /// + /// The concatenated point storage for every contour. + /// private readonly PointF[] points; + // Computed measurements are finite; NaN keeps the memoized state in one field without nullable value-type overhead. + private float length = float.NaN; + private float area = float.NaN; + /// /// Initializes a new instance of the class. /// @@ -49,6 +61,27 @@ public LinearGeometry(LinearGeometryInfo info, IReadOnlyList cont this.Points = this.points; } + /// + /// The classification of a point against a single closed contour. + /// + private enum PointContainment + { + /// + /// The point is outside the contour. + /// + Outside, + + /// + /// The point is inside the contour. + /// + Inside, + + /// + /// The point lies exactly on a contour edge. + /// + OnBoundary + } + /// /// Gets geometry-wide metadata for this retained result. /// @@ -71,14 +104,27 @@ public LinearGeometry(LinearGeometryInfo info, IReadOnlyList cont /// public IReadOnlyList Points { get; } + /// + /// Gets the retained contour metadata as a span for backend hot paths. + /// + /// The retained contour metadata. internal ReadOnlySpan GetContours() => this.contours; + /// + /// Gets the retained point run for the supplied contour. + /// + /// The contour whose points are returned. + /// The retained points belonging to . internal ReadOnlySpan GetContourPoints(in LinearContour contour) => this.points.AsSpan(contour.PointStart, contour.PointCount); /// /// Creates retained geometry for one open polyline, baked under the supplied device-space . /// + /// + /// When is the input array is retained directly without + /// copying, so the caller must not mutate it afterwards. + /// /// The polyline points. /// The X/Y scale at which the polyline is baked. /// The retained open polyline geometry. @@ -103,22 +149,6 @@ public static LinearGeometry CreateOpenPolyline(PointF[] points, Vector2 scale) RectangleF bounds = GetPointBounds(retained); int segmentCount = retained.Length - 1; - int nonHorizontalBoundary = 0; - int nonHorizontalCenter = 0; - for (int i = 0; i < segmentCount; i++) - { - PointF start = retained[i]; - PointF end = retained[i + 1]; - if ((int)MathF.Floor(start.Y) != (int)MathF.Floor(end.Y)) - { - nonHorizontalBoundary++; - } - - if ((int)MathF.Floor(start.Y + 0.5F) != (int)MathF.Floor(end.Y + 0.5F)) - { - nonHorizontalCenter++; - } - } return new LinearGeometry( new LinearGeometryInfo @@ -126,14 +156,13 @@ public static LinearGeometry CreateOpenPolyline(PointF[] points, Vector2 scale) Bounds = bounds, ContourCount = 1, PointCount = retained.Length, - SegmentCount = segmentCount, - NonHorizontalSegmentCountPixelBoundary = nonHorizontalBoundary, - NonHorizontalSegmentCountPixelCenter = nonHorizontalCenter + SegmentCount = segmentCount }, [new LinearContour { PointStart = 0, PointCount = retained.Length, + Bounds = bounds, SegmentStart = 0, SegmentCount = segmentCount, IsClosed = false @@ -158,7 +187,368 @@ public static LinearGeometry CreateOpenPolyline(PointF[] points) /// public SegmentEnumerator GetSegments() => new(this); - private static RectangleF GetPointBounds(PointF[] points) + /// + /// Calculates the total length of all derived linear segments. + /// + /// + /// The result is memoized. Concurrent first calls may compute it more than once, which is benign because + /// the computation is deterministic. + /// + /// The total segment length. + public float ComputeLength() + { + if (float.IsNaN(this.length)) + { + float calculatedLength = 0; + SegmentEnumerator segments = this.GetSegments(); + + while (segments.MoveNext()) + { + LinearSegment segment = segments.Current; + + calculatedLength += Vector2.Distance(segment.Start, segment.End); + } + + this.length = calculatedLength; + } + + return this.length; + } + + /// + /// Calculates the total absolute area of all contour point runs. + /// + /// + /// Each contour with at least three points contributes the absolute value of its shoelace area, regardless of + /// whether it is marked closed. The result is memoized. + /// + /// The total contour area. + public float ComputeArea() + { + if (float.IsNaN(this.area)) + { + float calculatedArea = 0; + + for (int i = 0; i < this.contours.Length; i++) + { + LinearContour contour = this.contours[i]; + if (contour.PointCount < 3) + { + // Open line contours are valid retained geometry, but cannot enclose area. + continue; + } + + ReadOnlySpan points = this.GetContourPoints(contour); + float signedArea = 0; + Vector2 previous = points[^1]; + + for (int p = 0; p < points.Length; p++) + { + Vector2 current = points[p]; + signedArea += PolygonUtilities.CrossProduct(previous, current); + previous = current; + } + + calculatedArea += MathF.Abs(signedArea) * .5F; + } + + this.area = calculatedArea; + } + + return this.area; + } + + /// + /// Returns whether the supplied point is inside the geometry. + /// + /// + /// Only closed contours participate in the test; open contours never contain points. A point lying exactly on + /// a closed contour edge is treated as contained under both fill rules. + /// + /// The point to test. + /// The fill rule used for containment. + /// when the point is inside or on the geometry boundary. + public bool Contains(PointF point, IntersectionRule intersectionRule) + { + RectangleF bounds = this.Info.Bounds; + if (point.X < bounds.Left || + point.X > bounds.Right || + point.Y < bounds.Top || + point.Y > bounds.Bottom) + { + return false; + } + + int winding = 0; + bool inside = false; + + for (int i = 0; i < this.contours.Length; i++) + { + LinearContour contour = this.contours[i]; + if (!contour.IsClosed) + { + continue; + } + + RectangleF contourBounds = contour.Bounds; + if (point.X < contourBounds.Left || + point.X > contourBounds.Right || + point.Y < contourBounds.Top || + point.Y > contourBounds.Bottom) + { + continue; + } + + if (intersectionRule == IntersectionRule.NonZero) + { + winding += this.GetWindingContribution(contour, point, out bool onBoundary); + if (onBoundary) + { + return true; + } + + continue; + } + + PointContainment containment = this.ClassifyEvenOdd(contour, point); + if (containment == PointContainment.OnBoundary) + { + return true; + } + + inside ^= containment == PointContainment.Inside; + } + + return intersectionRule == IntersectionRule.NonZero ? winding != 0 : inside; + } + + /// + /// Gets path information at the specified distance along the geometry. + /// + /// + /// + /// The distance must resolve to a point on the geometry itself: negative or non-finite distances fail, and when + /// any contour is open a distance beyond the total length fails. When every contour is closed the distance + /// wraps modulo the total length instead. + /// + /// + /// At a distance that lands exactly on a shared vertex the outgoing segment wins, so the reported tangent and + /// angle are those of the segment leaving the vertex. + /// + /// + /// The distance along the geometry. + /// When this method returns, contains the path information at if the distance resolves to a point on the geometry; otherwise, the default value. + /// + /// if resolves to a point on the geometry; + /// otherwise, . + /// + public bool TryGetPathPointAtDistance(float distance, out PathPoint pathPoint) + { + pathPoint = default; + + float length = this.ComputeLength(); + if (length <= 0 || !float.IsFinite(distance) || distance < 0) + { + return false; + } + + bool allContoursClosed = true; + for (int i = 0; i < this.contours.Length; i++) + { + allContoursClosed &= this.contours[i].IsClosed; + } + + if (allContoursClosed) + { + distance %= length; + } + else if (distance > length) + { + return false; + } + + SegmentEnumerator segments = this.GetSegments(); + LinearSegment lastSegment = default; + + while (segments.MoveNext()) + { + LinearSegment segment = segments.Current; + float segmentLength = Vector2.Distance(segment.Start, segment.End); + + if (segmentLength <= 0) + { + continue; + } + + if (distance < segmentLength) + { + Vector2 tangent = Vector2.Normalize((Vector2)segment.End - (Vector2)segment.Start); + float angle = (float)(Math.Atan2(tangent.Y, tangent.X) % (Math.PI * 2)); + + pathPoint = new PathPoint + { + // Advance from the segment start along the unit tangent. This is exact for + // axis-aligned segments, unlike interpolating by the fractional distance. + // The strict comparison above makes the outgoing segment win at an exact + // shared vertex. + Point = (Vector2)segment.Start + (tangent * distance), + Tangent = tangent, + Angle = GeometryUtilities.RadianToDegree(angle) + }; + + return true; + } + + distance -= segmentLength; + lastSegment = segment; + } + + Vector2 endTangent = Vector2.Normalize((Vector2)lastSegment.End - (Vector2)lastSegment.Start); + float endAngle = (float)(Math.Atan2(endTangent.Y, endTangent.X) % (Math.PI * 2)); + + pathPoint = new PathPoint + { + Point = lastSegment.End, + Tangent = endTangent, + Angle = GeometryUtilities.RadianToDegree(endAngle) + }; + + return true; + } + + /// + /// Gets path information at the specified distance along the geometry, extrapolating along the + /// boundary tangents when the distance falls before the start or beyond the end of an open geometry. + /// + /// + /// Unlike , out-of-range distances + /// resolve to a point on the virtual straight-line continuation of the geometry's first or last + /// segment. The method returns only when the distance is not a finite + /// number or the geometry has no measurable length. + /// + /// The distance along the geometry. + /// When this method returns, contains the path information at if the geometry is measurable; otherwise, the default value. + /// if the point could be resolved; otherwise, . + public bool TryGetPathPointAtDistanceUnbounded(float distance, out PathPoint pathPoint) + { + if (this.TryGetPathPointAtDistance(distance, out pathPoint)) + { + return true; + } + + if (!float.IsFinite(distance)) + { + return false; + } + + if (distance < 0) + { + if (!this.TryGetPathPointAtDistance(0, out PathPoint start)) + { + return false; + } + + pathPoint = new PathPoint + { + Point = (Vector2)start.Point + ((Vector2)start.Tangent * distance), + Tangent = start.Tangent, + Angle = start.Angle + }; + + return true; + } + + float length = this.ComputeLength(); + if (length <= 0 || !this.TryGetPathPointAtDistance(length, out PathPoint end)) + { + return false; + } + + pathPoint = new PathPoint + { + Point = (Vector2)end.Point + ((Vector2)end.Tangent * (distance - length)), + Tangent = end.Tangent, + Angle = end.Angle + }; + + return true; + } + + /// + /// Creates a path segment between two distances along the geometry. + /// + /// The segment start distance. + /// The segment stop distance. + /// Whether the returned segment starts a new figure at the first segment point. + /// When this method returns, contains the segment path if one was created; otherwise, an empty path. + /// when a segment path was created. + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, out IPath path) + { + float length = this.ComputeLength(); + if (stopDistance <= startDistance || length <= 0) + { + path = EmptyPath.OpenPath; + return false; + } + + PathBuilder builder = new(); + List points = []; + bool added = false; + + float start = Math.Clamp(startDistance, 0, length); + float stop = Math.Clamp(stopDistance, 0, length); + float currentDistance = 0; + int activeContour = -1; + SegmentEnumerator segments = this.GetSegments(); + + while (segments.MoveNext()) + { + LinearSegment segment = segments.Current; + + if (activeContour != segment.ContourIndex) + { + // The enumerator owns segment derivation, but extracted paths still need + // figure boundaries when the requested range crosses between contours. + FlushSegmentPoints(builder, points, ref added, startOnBeginFigure); + activeContour = segment.ContourIndex; + } + + float segmentLength = Vector2.Distance(segment.Start, segment.End); + float segmentEndDistance = currentDistance + segmentLength; + + if (segmentEndDistance >= start && currentDistance <= stop && segmentLength > 0) + { + float localStart = Math.Clamp((start - currentDistance) / segmentLength, 0, 1); + float localStop = Math.Clamp((stop - currentDistance) / segmentLength, 0, 1); + + PointF startPoint = Vector2.Lerp(segment.Start, segment.End, localStart); + if (points.Count == 0 || points[^1] != startPoint) + { + points.Add(startPoint); + } + + PointF stopPoint = Vector2.Lerp(segment.Start, segment.End, localStop); + if (points.Count == 0 || points[^1] != stopPoint) + { + points.Add(stopPoint); + } + } + + currentDistance = segmentEndDistance; + } + + FlushSegmentPoints(builder, points, ref added, startOnBeginFigure); + + path = added ? builder.Build() : EmptyPath.OpenPath; + return added; + } + + /// + /// Calculates the bounds of a retained point array. + /// + /// The points to bound. + /// The bounds enclosing . + private static RectangleF GetPointBounds(ReadOnlySpan points) { float minX = points[0].X; float minY = points[0].Y; @@ -176,4 +566,145 @@ private static RectangleF GetPointBounds(PointF[] points) return RectangleF.FromLTRB(minX, minY, maxX, maxY); } + + /// + /// Classifies a point against a closed contour using the even-odd rule. + /// + /// The closed contour to test. + /// The point to classify. + /// The point classification for . + private PointContainment ClassifyEvenOdd(in LinearContour contour, PointF point) + { + bool inside = false; + PointF previous = this.points[contour.PointStart + contour.PointCount - 1]; + + for (int i = 0; i < contour.PointCount; i++) + { + PointF current = this.points[contour.PointStart + i]; + + if (IsPointOnSegment(point, previous, current)) + { + return PointContainment.OnBoundary; + } + + bool crossesY = (previous.Y > point.Y) != (current.Y > point.Y); + if (crossesY) + { + Vector2 edge = (Vector2)current - (Vector2)previous; + Vector2 pointOffset = (Vector2)point - (Vector2)previous; + float yRatio = pointOffset.Y / edge.Y; + float intersectionX = previous.X + (edge.X * yRatio); + + if (point.X < intersectionX) + { + inside = !inside; + } + } + + previous = current; + } + + return inside ? PointContainment.Inside : PointContainment.Outside; + } + + /// + /// Calculates one closed contour's winding contribution for a point. + /// + /// The closed contour to test. + /// The point to classify. + /// When this method returns, indicates whether lies on the contour boundary. + /// The contour winding contribution. + private int GetWindingContribution(in LinearContour contour, PointF point, out bool onBoundary) + { + onBoundary = false; + + int winding = 0; + PointF previous = this.points[contour.PointStart + contour.PointCount - 1]; + + for (int i = 0; i < contour.PointCount; i++) + { + PointF current = this.points[contour.PointStart + i]; + + if (IsPointOnSegment(point, previous, current)) + { + onBoundary = true; + return 0; + } + + Vector2 edge = (Vector2)current - (Vector2)previous; + Vector2 pointOffset = (Vector2)point - (Vector2)previous; + float crossProduct = PolygonUtilities.CrossProduct(edge, pointOffset); + + if (previous.Y <= point.Y) + { + if (current.Y > point.Y && crossProduct > 0) + { + winding++; + } + } + else if (current.Y <= point.Y && crossProduct < 0) + { + winding--; + } + + previous = current; + } + + return winding; + } + + /// + /// Returns whether a point lies on a line segment. + /// + /// The point to test. + /// The segment start point. + /// The segment end point. + /// when lies on the segment. + private static bool IsPointOnSegment(PointF point, PointF start, PointF end) + { + Vector2 startVector = start; + Vector2 segment = (Vector2)end - startVector; + Vector2 pointOffset = (Vector2)point - startVector; + float cross = PolygonUtilities.CrossProduct(segment, pointOffset); + + if (cross != 0) + { + return false; + } + + float dot = Vector2.Dot(pointOffset, segment); + if (dot < 0) + { + return false; + } + + return dot <= segment.LengthSquared(); + } + + /// + /// Flushes the current segment extraction buffer to a path builder. + /// + /// The path builder receiving the extracted segment. + /// The segment extraction buffer. + /// Whether a previous figure has already been added. + /// Whether the first emitted segment should start a new figure. + private static void FlushSegmentPoints(PathBuilder builder, List points, ref bool added, bool startOnBeginFigure) + { + // The buffer is reused across contour boundaries. Empty or single-point buffers are valid + // when the requested range misses a contour or only touches it at one exact boundary. + if (points.Count < 2) + { + points.Clear(); + return; + } + + if (added || !startOnBeginFigure) + { + builder.StartFigure(); + } + + builder.AddLines([.. points]); + points.Clear(); + added = true; + } } diff --git a/src/ImageSharp.Drawing/LinearGeometryCache.cs b/src/ImageSharp.Drawing/LinearGeometryCache.cs index 204b24e0c..fa231574b 100644 --- a/src/ImageSharp.Drawing/LinearGeometryCache.cs +++ b/src/ImageSharp.Drawing/LinearGeometryCache.cs @@ -8,17 +8,27 @@ namespace SixLabors.ImageSharp.Drawing; /// /// Single-entry memoization slot for a scale-baked derived from an . -/// Entries are keyed on the X/Y scale so text and panning workloads — which re-render the same shapes at a fixed -/// zoom level while their rotation/translation/perspective drift — hit a stable cached bake. +/// Entries are keyed on the X/Y scale so text and panning workloads, which re-render the same shapes at a fixed +/// zoom level while their rotation/translation/perspective drift, hit a stable cached bake. /// /// /// Safe for concurrent readers and writers. Publication uses so a reader -/// either observes or a fully-constructed entry. +/// either observes or a fully-constructed entry. Storing a new scale replaces the previous +/// entry; last writer wins. /// internal struct LinearGeometryCache { + /// + /// The single cached entry, or when nothing has been baked yet. + /// private Entry? entry; + /// + /// Attempts to get the cached geometry baked at the supplied scale. + /// + /// The X/Y scale key. + /// When this method returns, contains the cached geometry if the scale matched; otherwise, . + /// when a geometry baked at was cached. public bool TryGet(Vector2 scale, [NotNullWhen(true)] out LinearGeometry? value) { Entry? hit = Volatile.Read(ref this.entry); @@ -32,16 +42,34 @@ public bool TryGet(Vector2 scale, [NotNullWhen(true)] out LinearGeometry? value) return false; } + /// + /// Stores geometry baked at the supplied scale, replacing any previous entry. + /// + /// The X/Y scale key. + /// The baked geometry to cache. + /// The cached , returned for caller convenience. public LinearGeometry Store(Vector2 scale, LinearGeometry value) { Volatile.Write(ref this.entry, new Entry(scale, value)); return value; } + /// + /// An immutable scale/geometry pair. A class (not a struct) so the pair publishes atomically via a single + /// reference write. + /// + /// The X/Y scale key. + /// The baked geometry. private sealed class Entry(Vector2 scale, LinearGeometry value) { + /// + /// Gets the X/Y scale the geometry was baked at. + /// public Vector2 Scale { get; } = scale; + /// + /// Gets the baked geometry. + /// public LinearGeometry Value { get; } = value; } } diff --git a/src/ImageSharp.Drawing/LinearGeometryInfo.cs b/src/ImageSharp.Drawing/LinearGeometryInfo.cs index 4ed9ea6c0..f1754d6be 100644 --- a/src/ImageSharp.Drawing/LinearGeometryInfo.cs +++ b/src/ImageSharp.Drawing/LinearGeometryInfo.cs @@ -31,22 +31,4 @@ public readonly struct LinearGeometryInfo /// Gets the total number of derived linear segments across all contours. /// public required int SegmentCount { get; init; } - - /// - /// Gets the number of derived segments that remain non-horizontal when sampled on pixel boundaries. - /// - /// - /// A segment contributes to this count when its start and end sample into different rows under - /// pixel-boundary sampling. - /// - public required int NonHorizontalSegmentCountPixelBoundary { get; init; } - - /// - /// Gets the number of derived segments that remain non-horizontal when sampled at pixel centers. - /// - /// - /// A segment contributes to this count when its start and end sample into different rows after the - /// half-pixel center-sampling offset is applied. - /// - public required int NonHorizontalSegmentCountPixelCenter { get; init; } } diff --git a/src/ImageSharp.Drawing/LinearLineSegment.cs b/src/ImageSharp.Drawing/LinearLineSegment.cs index d08296901..b3a5dc112 100644 --- a/src/ImageSharp.Drawing/LinearLineSegment.cs +++ b/src/ImageSharp.Drawing/LinearLineSegment.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents a series of control points that will be joined by straight lines +/// Represents a series of control points that will be joined by straight lines. /// /// public sealed class LinearLineSegment : ILineSegment @@ -20,8 +20,8 @@ public sealed class LinearLineSegment : ILineSegment /// /// Initializes a new instance of the class. /// - /// The start. - /// The end. + /// The start point. + /// The end point. public LinearLineSegment(PointF start, PointF end) : this([start, end]) { @@ -30,9 +30,9 @@ public LinearLineSegment(PointF start, PointF end) /// /// Initializes a new instance of the class. /// - /// The point1. - /// The point2. - /// Additional points + /// The first point. + /// The second point. + /// Additional points appended after . public LinearLineSegment(PointF point1, PointF point2, params PointF[] additionalPoints) : this(new[] { point1, point2 }.Concat(additionalPoints)) { @@ -41,7 +41,7 @@ public LinearLineSegment(PointF point1, PointF point2, params PointF[] additiona /// /// Initializes a new instance of the class. /// - /// The points. + /// The points; at least two are required. public LinearLineSegment(PointF[] points) { Guard.NotNull(points, nameof(points)); @@ -122,6 +122,8 @@ public LinearLineSegment Transform(Matrix4x4 matrix) /// /// Computes the bounds for the retained linear point run. /// + /// The retained points. + /// The axis-aligned bounds enclosing the points. private static RectangleF CalculateBounds(ReadOnlySpan points) { float minX = float.MaxValue; diff --git a/src/ImageSharp.Drawing/LinearSegment.cs b/src/ImageSharp.Drawing/LinearSegment.cs index 9fcb141c0..4a97482df 100644 --- a/src/ImageSharp.Drawing/LinearSegment.cs +++ b/src/ImageSharp.Drawing/LinearSegment.cs @@ -22,6 +22,11 @@ public readonly struct LinearSegment /// public required PointF End { get; init; } + /// + /// Gets the zero-based index of the contour that owns this segment. + /// + public required int ContourIndex { get; init; } + /// /// Gets the smaller of . and .. /// diff --git a/src/ImageSharp.Drawing/NormalizePathExtensions.cs b/src/ImageSharp.Drawing/NormalizePathExtensions.cs new file mode 100644 index 000000000..8e8a15431 --- /dev/null +++ b/src/ImageSharp.Drawing/NormalizePathExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.PolygonGeometry; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Provides extension methods to that allow paths to be normalized. +/// +public static class NormalizePathExtensions +{ + /// + /// Creates a normalized positive-winding polygon representation of the specified path. + /// + /// The path to normalize. + /// + /// A path whose contours represent the normalized filled area of using positive winding. + /// + public static IPath Normalize(this IPath path) => NormalizedShapeGenerator.NormalizeShape(path); +} diff --git a/src/ImageSharp.Drawing/OutlinePathExtensions.cs b/src/ImageSharp.Drawing/OutlinePathExtensions.cs index 77f40996f..00b6948d4 100644 --- a/src/ImageSharp.Drawing/OutlinePathExtensions.cs +++ b/src/ImageSharp.Drawing/OutlinePathExtensions.cs @@ -16,7 +16,7 @@ public static class OutlinePathExtensions /// /// Generates an outline of the path. /// - /// The path to outline + /// The path to outline. /// The outline width. /// A new representing the outline. public static IPath GenerateOutline(this IPath path, float width) @@ -25,7 +25,7 @@ public static IPath GenerateOutline(this IPath path, float width) /// /// Generates an outline of the path. /// - /// The path to outline + /// The path to outline. /// The outline width. /// The stroke geometry options. /// A new representing the outline. @@ -42,7 +42,7 @@ public static IPath GenerateOutline(this IPath path, float width, StrokeOptions /// /// Generates an outline of the path with alternating on and off segments based on the pattern. /// - /// The path to outline + /// The path to outline. /// The outline width. /// The pattern made of multiples of the width. /// A new representing the outline. @@ -52,7 +52,18 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan /// Generates an outline of the path with alternating on and off segments based on the pattern. /// - /// The path to outline + /// The path to outline. + /// The outline width. + /// The pattern made of multiples of the width. + /// The distance into the dash pattern, expressed as a multiple of . + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, float offset) + => path.GenerateOutline(width, pattern, false, offset); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline. /// The outline width. /// The pattern made of multiples of the width. /// The stroke geometry options. @@ -63,7 +74,24 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan /// Generates an outline of the path with alternating on and off segments based on the pattern. /// - /// The path to outline + /// The path to outline. + /// The outline width. + /// The pattern made of multiples of the width. + /// The distance into the dash pattern, expressed as a multiple of . + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + float offset, + StrokeOptions strokeOptions) + => GenerateOutline(path, width, pattern, false, offset, strokeOptions); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline. /// The outline width. /// The pattern made of multiples of the width. /// Whether the first item in the pattern is on or off. @@ -74,10 +102,40 @@ public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan /// Generates an outline of the path with alternating on and off segments based on the pattern. /// - /// The path to outline + /// The path to outline. + /// The outline width. + /// The pattern made of multiples of the width. + /// Whether the first item in the pattern is on or off. + /// The distance into the dash pattern, expressed as a multiple of . + /// A new representing the outline. + public static IPath GenerateOutline(this IPath path, float width, ReadOnlySpan pattern, bool startOff, float offset) + => GenerateOutline(path, width, pattern, startOff, offset, DefaultOptions); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline. + /// The outline width. + /// The pattern made of multiples of the width. + /// Whether the first item in the pattern is on or off. + /// The stroke geometry options. + /// A new representing the outline. + public static IPath GenerateOutline( + this IPath path, + float width, + ReadOnlySpan pattern, + bool startOff, + StrokeOptions strokeOptions) + => GenerateOutline(path, width, pattern, startOff, 0, strokeOptions); + + /// + /// Generates an outline of the path with alternating on and off segments based on the pattern. + /// + /// The path to outline. /// The outline width. /// The pattern made of multiples of the width. /// Whether the first item in the pattern is on or off. + /// The distance into the dash pattern, expressed as a multiple of . /// The stroke geometry options. /// A new representing the outline. public static IPath GenerateOutline( @@ -85,6 +143,7 @@ public static IPath GenerateOutline( float width, ReadOnlySpan pattern, bool startOff, + float offset, StrokeOptions strokeOptions) { if (width <= 0) @@ -97,7 +156,7 @@ public static IPath GenerateOutline( return path.GenerateOutline(width, strokeOptions); } - IPath dashed = path.GenerateDashes(width, pattern, startOff); + IPath dashed = path.GenerateDashes(width, pattern, startOff, offset); // GenerateDashes returns the original path when the pattern is degenerate // or when segmentation would exceed safety limits; stroke it as solid. diff --git a/src/ImageSharp.Drawing/Path.cs b/src/ImageSharp.Drawing/Path.cs index f90a6f3af..0d0912e2f 100644 --- a/src/ImageSharp.Drawing/Path.cs +++ b/src/ImageSharp.Drawing/Path.cs @@ -8,16 +8,39 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// A aggregate of s making a single logical path. +/// An aggregate of s making a single logical path. /// /// -public class Path : IPath, ISimplePath, IPathInternals, IInternalPathOwner +public class Path : IPath, ISimplePath, IInternalPathOwner { + /// + /// The segments that make up the path. + /// private readonly ILineSegment[] lineSegments; + + /// + /// The lazily initialized internal representation of the path. + /// private InternalPath? innerPath; + + /// + /// The lazily initialized single-ring list returned by . + /// private IReadOnlyList? internalPathRings; + + /// + /// The cached result of . + /// private IPath? closedPath; + + /// + /// The per-scale cache of retained linear geometry. + /// private LinearGeometryCache geometryCache; + + /// + /// The lazily computed union of the segment bounds. + /// private RectangleF? bounds; /// @@ -92,6 +115,9 @@ public Path(params ILineSegment[] segments) /// internal bool RemoveCloseAndCollinearPoints { get; set; } = true; + /// + /// Gets the lazily initialized internal representation of the path. + /// private protected InternalPath InnerPath => this.innerPath ??= new InternalPath(this.lineSegments, this.IsClosed, this.RemoveCloseAndCollinearPoints); @@ -136,6 +162,28 @@ public virtual LinearGeometry ToLinearGeometry(Vector2 scale) ? hit : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + /// + public virtual float ComputeLength(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeLength(); + + /// + public virtual float ComputeArea(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeArea(); + + /// + public virtual bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale) + { + PointF scaledPoint = new(point.X * scale.X, point.Y * scale.Y); + + return this.ToLinearGeometry(scale).Contains(scaledPoint, intersectionRule); + } + + /// + /// Flattens the segment run into a single-contour retained geometry, deduplicating the + /// shared vertex where one segment starts exactly where the previous one ended. + /// + /// The X/Y scale at which curves are flattened. + /// The retained linear geometry. private LinearGeometry BuildLinearGeometry(Vector2 scale) { if (this.lineSegments.Length == 0) @@ -146,9 +194,7 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) Bounds = RectangleF.Empty, ContourCount = 0, PointCount = 0, - SegmentCount = 0, - NonHorizontalSegmentCountPixelBoundary = 0, - NonHorizontalSegmentCountPixelCenter = 0 + SegmentCount = 0 }, [], []); @@ -173,8 +219,6 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) float minY = float.MaxValue; float maxX = float.MinValue; float maxY = float.MinValue; - int nonHorizontalSegmentCountPixelBoundary = 0; - int nonHorizontalSegmentCountPixelCenter = 0; int pointIndex = 0; lastEndPoint = null; @@ -202,7 +246,7 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) } int segmentCount = pointCount == 0 ? 0 : this.IsClosed ? pointCount : pointCount - 1; - CountNonHorizontalSegments(points, pointCount, this.IsClosed, ref nonHorizontalSegmentCountPixelBoundary, ref nonHorizontalSegmentCountPixelCenter); + RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty; if (pointCount > 0) { @@ -210,31 +254,36 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) { PointStart = 0, PointCount = pointCount, + Bounds = bounds, SegmentStart = 0, SegmentCount = segmentCount, IsClosed = this.IsClosed }; } - RectangleF bounds = hasBounds ? RectangleF.FromLTRB(minX, minY, maxX, maxY) : RectangleF.Empty; - return new LinearGeometry( new LinearGeometryInfo { Bounds = bounds, ContourCount = contours.Length, PointCount = points.Length, - SegmentCount = segmentCount, - NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, - NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + SegmentCount = segmentCount }, contours, points); } /// - SegmentInfo IPathInternals.PointAlongPath(float distance) - => this.InnerPath.PointAlongPath(distance); + public virtual bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistance(distance, out pathPoint); + + /// + public virtual bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistanceUnbounded(distance, out pathPoint); + + /// + public virtual bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath path) + => this.ToLinearGeometry(scale).TryGetSegment(startDistance, stopDistance, startOnBeginFigure, out path); /// IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() @@ -243,6 +292,7 @@ IReadOnlyList IInternalPathOwner.GetRingsAsInternalPath() /// /// Computes path bounds directly from segment bounds without materializing . /// + /// The axis-aligned bounds enclosing all segments. private RectangleF CalculateBounds() { if (this.lineSegments.Length == 0) @@ -271,57 +321,6 @@ private static ILineSegment[] GetSegmentArray(IEnumerable segments return segments as ILineSegment[] ?? [.. segments]; } - /// - /// Counts how many derived segments survive as non-horizontal raster work for each sampling origin. - /// - /// The retained contour point run. - /// The number of retained points in the contour. - /// Whether the contour closes back to its first point. - /// The accumulated pixel-boundary count to update. - /// The accumulated pixel-center count to update. - private static void CountNonHorizontalSegments( - ReadOnlySpan points, - int pointCount, - bool isClosed, - ref int nonHorizontalSegmentCountPixelBoundary, - ref int nonHorizontalSegmentCountPixelCenter) - { - if (pointCount <= 1) - { - return; - } - - int segmentCount = isClosed ? pointCount : pointCount - 1; - for (int i = 0; i < segmentCount; i++) - { - PointF start = points[i]; - PointF end = points[(i + 1) == pointCount ? 0 : i + 1]; - if (ToFixedBoundary(start.Y) != ToFixedBoundary(end.Y)) - { - nonHorizontalSegmentCountPixelBoundary++; - } - - if (ToFixedCenter(start.Y) != ToFixedCenter(end.Y)) - { - nonHorizontalSegmentCountPixelCenter++; - } - } - } - - /// - /// Converts a coordinate to the fixed-point row space used by boundary-sampled raster work. - /// - /// The coordinate to convert. - /// The rounded 24.8 fixed-point value. - private static int ToFixedBoundary(float value) => (int)MathF.Round(value * 256F); - - /// - /// Converts a coordinate to the fixed-point row space used by center-sampled raster work. - /// - /// The coordinate to convert. - /// The rounded 24.8 fixed-point value after the half-pixel sampling offset is applied. - private static int ToFixedCenter(float value) => (int)MathF.Round((value + 0.5F) * 256F); - /// /// Converts an SVG path string into an . /// @@ -569,6 +568,12 @@ public static bool TryParseSvgPath(ReadOnlySpan svgPath, [NotNullWhen(true return true; } + /// + /// Reads a single SVG arc flag ("0" or "1") from the front of . + /// + /// The remaining path data; advanced past the flag on success. + /// When this method returns, contains the parsed flag value. + /// if a flag was read; otherwise, . private static bool TryFindFlag(ref ReadOnlySpan str, out bool value) { str = TrimSeparator(str); @@ -587,6 +592,11 @@ private static bool TryFindFlag(ref ReadOnlySpan str, out bool value) return true; } + /// + /// Trims leading separators from . + /// + /// The remaining path data; advanced past leading separators on success. + /// if the trimmed result is a suffix of the input; otherwise, . private static bool TryTrimSeparator(ref ReadOnlySpan str) { // SVG separators are optional in places where the next token can be @@ -601,6 +611,12 @@ private static bool TryTrimSeparator(ref ReadOnlySpan str) return false; } + /// + /// Reads a single finite number from the front of . + /// + /// The remaining path data; advanced past the number on success. + /// When this method returns, contains the parsed number. + /// if a finite number was read; otherwise, . private static bool TryFindScaler(ref ReadOnlySpan str, out float value) { ReadOnlySpan source = TrimSeparator(str); @@ -614,6 +630,15 @@ private static bool TryFindScaler(ref ReadOnlySpan str, out float value) return false; } + /// + /// Reads a coordinate pair from the front of , resolving relative + /// coordinates against . + /// + /// The remaining path data; advanced past the coordinates on success. + /// Whether the coordinates are relative to the current point. + /// The current point used to resolve relative coordinates. + /// When this method returns, contains the absolute point. + /// if a finite point was read; otherwise, . private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, PointF current, out PointF value) { if (TryFindScaler(ref str, out float x) && TryFindScaler(ref str, out float y)) @@ -640,6 +665,13 @@ private static bool TryFindPoint(ref ReadOnlySpan str, bool relative, Poin return false; } + /// + /// Scans the length of the leading number token and parses it. + /// + /// The character data starting at the number. + /// When this method returns, contains the parsed number. + /// When this method returns, contains the number of characters consumed. + /// if a finite number was parsed; otherwise, . private static bool TryReadScalar(ReadOnlySpan str, out float scaler, out int length) { // SVG path numbers can be tightly packed: "10-20" is two numbers, as is @@ -689,9 +721,19 @@ private static bool TryReadScalar(ReadOnlySpan str, out float scaler, out return TryParseFloat(str, out scaler); } + /// + /// Returns whether the character is an SVG token separator (whitespace or comma). + /// + /// The character to test. + /// if the character is a separator; otherwise, . private static bool IsSeparator(char ch) => char.IsWhiteSpace(ch) || ch == ','; + /// + /// Returns with all leading separators removed. + /// + /// The character data to trim. + /// The trimmed data. private static ReadOnlySpan TrimSeparator(ReadOnlySpan data) { if (data.Length == 0) @@ -711,6 +753,12 @@ private static ReadOnlySpan TrimSeparator(ReadOnlySpan data) return data[idx..]; } + /// + /// Parses a culture-invariant float, rejecting non-finite values. + /// + /// The character data to parse. + /// When this method returns, contains the parsed value. + /// if a finite value was parsed; otherwise, . private static bool TryParseFloat(ReadOnlySpan str, out float value) => float.TryParse(str, CultureInfo.InvariantCulture, out value) && float.IsFinite(value); } diff --git a/src/ImageSharp.Drawing/PathBuilder.cs b/src/ImageSharp.Drawing/PathBuilder.cs index a59add5e6..7c9786f07 100644 --- a/src/ImageSharp.Drawing/PathBuilder.cs +++ b/src/ImageSharp.Drawing/PathBuilder.cs @@ -7,7 +7,8 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Allow you to derivatively build shapes and paths. +/// Provides a fluent API for incrementally building complex shapes and paths +/// from figures composed of lines, arcs, and bezier curves. /// public class PathBuilder { @@ -29,7 +30,10 @@ public PathBuilder() /// /// Initializes a new instance of the class. /// - /// The default transform. + /// + /// The default transform. This is always composed with any transform set via + /// or . + /// public PathBuilder(Matrix4x4 defaultTransform) { this.defaultTransform = defaultTransform; @@ -49,9 +53,10 @@ public PathBuilder(Matrix4x4 defaultTransform) public Matrix4x4 Transform => this.currentTransform; /// - /// Sets the translation to be applied to all items to follow being applied to the . + /// Sets the transform to apply to all subsequently added segments. + /// The supplied transform is composed with the default transform provided at construction. /// - /// The transform. + /// The transform to apply. /// The . public PathBuilder SetTransform(Matrix4x4 transform) { @@ -61,7 +66,7 @@ public PathBuilder SetTransform(Matrix4x4 transform) } /// - /// Sets the origin all subsequent point should be relative to. + /// Sets the origin that all subsequent points are relative to. /// /// The origin. /// The . @@ -99,9 +104,9 @@ public PathBuilder ResetOrigin() } /// - /// Moves to current point to the supplied vector. + /// Moves the current point to the supplied point, starting a new figure. /// - /// The point. + /// The point to move to. /// The . public PathBuilder MoveTo(PointF point) { @@ -111,55 +116,55 @@ public PathBuilder MoveTo(PointF point) } /// - /// Moves to current point to the supplied vector. + /// Moves the current point to the supplied point, starting a new figure. /// - /// The x-coordinate. - /// The y-coordinate. + /// The x-coordinate of the point to move to. + /// The y-coordinate of the point to move to. /// The public PathBuilder MoveTo(float x, float y) => this.MoveTo(new PointF(x, y)); /// - /// Draws the line connecting the current the current point to the new point. + /// Draws a line connecting the current point to the new point. /// - /// The point. + /// The end point of the line. /// The . public PathBuilder LineTo(PointF point) => this.AddLine(this.currentPoint, point); /// - /// Draws the line connecting the current the current point to the new point. + /// Draws a line connecting the current point to the new point. /// - /// The x. - /// The y. + /// The x-coordinate of the end point. + /// The y-coordinate of the end point. /// The public PathBuilder LineTo(float x, float y) => this.LineTo(new PointF(x, y)); /// - /// Adds the line connecting the current point to the new point. + /// Adds a line segment connecting the two points to the current figure. /// - /// The start. - /// The end. + /// The start point of the line. + /// The end point of the line. /// The . public PathBuilder AddLine(PointF start, PointF end) => this.AddSegment(new LinearLineSegment(start, end)); /// - /// Adds the line connecting the current point to the new point. + /// Adds a line segment connecting the two points to the current figure. /// - /// The x1. - /// The y1. - /// The x2. - /// The y2. + /// The x-coordinate of the start point. + /// The y-coordinate of the start point. + /// The x-coordinate of the end point. + /// The y-coordinate of the end point. /// The . public PathBuilder AddLine(float x1, float y1, float x2, float y2) => this.AddLine(new PointF(x1, y1), new PointF(x2, y2)); /// - /// Adds a series of line segments connecting the current point to the new points. + /// Adds a series of line segments connecting the supplied points to the current figure. /// - /// The points. + /// The points to connect. /// The . public PathBuilder AddLines(IEnumerable points) { @@ -168,9 +173,9 @@ public PathBuilder AddLines(IEnumerable points) } /// - /// Adds a series of line segments connecting the current point to the new points. + /// Adds a series of line segments connecting the supplied points to the current figure. /// - /// The points. + /// The points to connect. /// The . public PathBuilder AddLines(params PointF[] points) { @@ -179,9 +184,10 @@ public PathBuilder AddLines(params PointF[] points) } /// - /// Adds the segment. + /// Adds a segment to the current figure. The segment is transformed by the current transform + /// and the current point is moved to the segment end point. /// - /// The segment. + /// The segment to add. /// The . public PathBuilder AddSegment(ILineSegment segment) { @@ -196,18 +202,18 @@ public PathBuilder AddSegment(ILineSegment segment) /// /// Draws a quadratic bezier from the current point to the /// - /// The second control point. - /// The point. + /// The second control point. The current point acts as the first. + /// The end point of the curve. /// The . public PathBuilder QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) => this.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); /// - /// Draws a quadratic bezier from the current point to the + /// Draws a cubic bezier from the current point to the /// - /// The second control point. + /// The second control point. The current point acts as the first. /// The third control point. - /// The point. + /// The end point of the curve. /// The . public PathBuilder CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) => this.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); @@ -216,7 +222,7 @@ public PathBuilder CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdContro /// Adds a quadratic bezier curve to the current figure joining the point to the . /// /// The start point. - /// The control point1. + /// The control point. /// The end point. /// The . public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, PointF endPoint) @@ -225,6 +231,8 @@ public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, Po Vector2 controlPointVector = controlPoint; Vector2 endPointVector = endPoint; + // Exact degree elevation: a quadratic bezier is representable as a cubic whose inner + // control points sit two thirds of the way from each end point to the quadratic control point. Vector2 c1 = ((controlPointVector - startPointVector) * 2 / 3) + startPointVector; Vector2 c2 = ((controlPointVector - endPointVector) * 2 / 3) + endPointVector; @@ -235,8 +243,8 @@ public PathBuilder AddQuadraticBezier(PointF startPoint, PointF controlPoint, Po /// Adds a cubic bezier curve to the current figure joining the point to the . /// /// The start point. - /// The control point1. - /// The control point2. + /// The first control point. + /// The second control point. /// The end point. /// The . public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, PointF controlPoint2, PointF endPoint) @@ -254,8 +262,8 @@ public PathBuilder AddCubicBezier(PointF startPoint, PointF controlPoint1, Point /// are greater than zero but too small to describe an arc. /// /// - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The rotation along the X-axis; measured in degrees clockwise. /// /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees @@ -283,8 +291,8 @@ public PathBuilder ArcTo(float radiusX, float radiusY, float rotation, bool larg /// /// /// The start point of the arc. - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The rotation along the X-axis; measured in degrees clockwise. /// /// The large arc flag, and is if an arc spanning less than or equal to 180 degrees @@ -329,8 +337,8 @@ public PathBuilder AddArc(Rectangle rectangle, int rotation, int startAngle, int /// Adds an elliptical arc to the current figure. /// /// The center of the ellipse from which the arc is taken. - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). @@ -344,8 +352,8 @@ public PathBuilder AddArc(PointF center, float radiusX, float radiusY, float rot /// Adds an elliptical arc to the current figure. /// /// The center of the ellipse from which the arc is taken. - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). @@ -360,8 +368,8 @@ public PathBuilder AddArc(Point center, int radiusX, int radiusY, int rotation, /// /// The x-coordinate of the center point of the ellipse from which the arc is taken. /// The y-coordinate of the center point of the ellipse from which the arc is taken. - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). @@ -376,8 +384,8 @@ public PathBuilder AddArc(int x, int y, int radiusX, int radiusY, int rotation, /// /// The x-coordinate of the center point of the ellipse from which the arc is taken. /// The y-coordinate of the center point of the ellipse from which the arc is taken. - /// The x-radius of the ellipsis. - /// The y-radius of the ellipsis. + /// The x-radius of the ellipse. + /// The y-radius of the ellipse. /// The angle, in degrees, from the x-axis of the current coordinate system to the x-axis of the ellipse. /// /// The start angle of the elliptical arc prior to the stretch and rotate operations. (0 is at the 3 o'clock position of the arc's circle). @@ -694,6 +702,8 @@ public PathBuilder StartFigure() } else { + // Reuse the empty figure but clear any closed flag left by CloseFigure so + // repeated Start/Close calls cannot produce a closed empty figure. this.currentFigure.IsClosed = false; } @@ -713,7 +723,7 @@ public PathBuilder CloseFigure() } /// - /// Closes the current figure. + /// Closes all figures in the path, including the current figure. /// /// The . public PathBuilder CloseAllFigures() @@ -729,9 +739,12 @@ public PathBuilder CloseAllFigures() } /// - /// Builds a complex polygon from the current working set of working operations. + /// Builds a path from the current working set of figures. /// - /// The current set of operations as a complex polygon + /// + /// The built . A single non-empty figure is returned directly; + /// multiple figures are combined into a . + /// public IPath Build() { IPath[] paths = [.. this.figures.Where(x => !x.IsEmpty).Select(x => x.Build())]; @@ -757,7 +770,7 @@ public PathBuilder Reset() } /// - /// Clears all drawn paths, Leaving any applied transforms. + /// Clears all drawn paths, leaving any applied transforms. /// [MemberNotNull(nameof(currentFigure))] public void Clear() @@ -767,16 +780,33 @@ public void Clear() this.figures.Add(this.currentFigure); } + /// + /// Represents one figure under construction: an ordered list of pre-transformed segments. + /// private class Figure { private readonly List segments = []; + /// + /// Gets or sets a value indicating whether the figure is closed. + /// public bool IsClosed { get; set; } + /// + /// Gets a value indicating whether the figure contains no segments. + /// public bool IsEmpty => this.segments.Count == 0; + /// + /// Appends a segment to the figure. + /// + /// The segment to append. It must already be transformed. public void AddSegment(ILineSegment segment) => this.segments.Add(segment); + /// + /// Builds the figure into a path. + /// + /// A closed or an open . public IPath Build() => this.IsClosed ? new Polygon([.. this.segments], true) diff --git a/src/ImageSharp.Drawing/PathCollection.cs b/src/ImageSharp.Drawing/PathCollection.cs index 60a71c20e..c2793cfdd 100644 --- a/src/ImageSharp.Drawing/PathCollection.cs +++ b/src/ImageSharp.Drawing/PathCollection.cs @@ -7,18 +7,25 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// A aggregate of s to apply common operations to them. +/// An aggregate of s to apply common operations to them. /// /// public class PathCollection : IPathCollection { + /// + /// The paths in the collection. + /// private readonly IPath[] paths; + + /// + /// The lazily computed union of the path bounds. + /// private RectangleF? bounds; /// /// Initializes a new instance of the class. /// - /// The collection of paths + /// The collection of paths. public PathCollection(IEnumerable paths) : this(GetPathArray(paths)) { @@ -27,7 +34,7 @@ public PathCollection(IEnumerable paths) /// /// Initializes a new instance of the class. /// - /// The collection of paths + /// The collection of paths. public PathCollection(params IPath[] paths) { Guard.NotNull(paths, nameof(paths)); @@ -42,6 +49,10 @@ public PathCollection(params IPath[] paths) /// public RectangleF Bounds => this.bounds ??= this.CalcBounds(); + /// + /// Computes the union of the contained path bounds. + /// + /// The axis-aligned bounds enclosing all paths. private RectangleF CalcBounds() { float minX, minY, maxX, maxY; @@ -79,6 +90,11 @@ public IPathCollection Transform(Matrix4x4 matrix) /// IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)this.paths).GetEnumerator(); + /// + /// Materializes the path sequence into the retained array used by the collection. + /// + /// The path sequence to materialize. + /// The retained path array. private static IPath[] GetPathArray(IEnumerable paths) { Guard.NotNull(paths, nameof(paths)); diff --git a/src/ImageSharp.Drawing/PathExtensions.Internal.cs b/src/ImageSharp.Drawing/PathExtensions.Internal.cs index 40329b910..37e9e7a53 100644 --- a/src/ImageSharp.Drawing/PathExtensions.Internal.cs +++ b/src/ImageSharp.Drawing/PathExtensions.Internal.cs @@ -26,6 +26,11 @@ internal static IPath Reverse(this IPath path) return closed ? new Polygon(segments) : new Path(segments); } + /// + /// Copies the supplied points into a new array in reverse order. + /// + /// The points to reverse. + /// The reversed point array. private static PointF[] ReversePoints(ReadOnlySpan points) { PointF[] reversed = new PointF[points.Length]; diff --git a/src/ImageSharp.Drawing/PathExtensions.cs b/src/ImageSharp.Drawing/PathExtensions.cs index a0dcd38ca..95c92fb10 100644 --- a/src/ImageSharp.Drawing/PathExtensions.cs +++ b/src/ImageSharp.Drawing/PathExtensions.cs @@ -11,58 +11,58 @@ namespace SixLabors.ImageSharp.Drawing; public static partial class PathExtensions { /// - /// Creates a path rotated by the specified radians around its center. + /// Creates a path collection rotated by the specified radians around its center. /// - /// The path to rotate. - /// The radians to rotate the path. - /// A with a rotate transform applied. + /// The path collection to rotate. + /// The radians to rotate the paths. + /// An with a rotate transform applied. public static IPathCollection Rotate(this IPathCollection path, float radians) => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); /// - /// Creates a path rotated by the specified degrees around its center. + /// Creates a path collection rotated by the specified degrees around its center. /// - /// The path to rotate. - /// The degree to rotate the path. - /// A with a rotate transform applied. + /// The path collection to rotate. + /// The degree to rotate the paths. + /// An with a rotate transform applied. public static IPathCollection RotateDegree(this IPathCollection shape, float degree) => shape.Rotate(GeometryUtilities.DegreeToRadian(degree)); /// - /// Creates a path translated by the supplied position + /// Creates a path collection translated by the supplied position. /// - /// The path to translate. + /// The path collection to translate. /// The translation position. - /// A with a translate transform applied. + /// An with a translate transform applied. public static IPathCollection Translate(this IPathCollection path, PointF position) => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// - /// Creates a path translated by the supplied position + /// Creates a path collection translated by the supplied position. /// - /// The path to translate. + /// The path collection to translate. /// The amount to translate along the X axis. /// The amount to translate along the Y axis. - /// A with a translate transform applied. + /// An with a translate transform applied. public static IPathCollection Translate(this IPathCollection path, float x, float y) => path.Translate(new PointF(x, y)); /// - /// Creates a path translated by the supplied position + /// Creates a path collection scaled by the supplied amounts around its center. /// - /// The path to translate. + /// The path collection to scale. /// The amount to scale along the X axis. /// The amount to scale along the Y axis. - /// A with a translate transform applied. + /// An with a scale transform applied. public static IPathCollection Scale(this IPathCollection path, float scaleX, float scaleY) => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// - /// Creates a path translated by the supplied position + /// Creates a path collection scaled by the supplied amount around its center. /// - /// The path to translate. + /// The path collection to scale. /// The amount to scale along both the x and y axis. - /// A with a translate transform applied. + /// An with a scale transform applied. public static IPathCollection Scale(this IPathCollection path, float scale) => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); @@ -71,7 +71,7 @@ public static IPathCollection Scale(this IPathCollection path, float scale) /// /// The path to rotate. /// The radians to rotate the path. - /// A with a rotate transform applied. + /// An with a rotate transform applied. public static IPath Rotate(this IPath path, float radians) => path.Transform(new Matrix4x4(Matrix3x2.CreateRotation(radians, RectangleF.Center(path.Bounds)))); @@ -80,89 +80,114 @@ public static IPath Rotate(this IPath path, float radians) /// /// The path to rotate. /// The degree to rotate the path. - /// A with a rotate transform applied. + /// An with a rotate transform applied. public static IPath RotateDegree(this IPath shape, float degree) => shape.Rotate(GeometryUtilities.DegreeToRadian(degree)); /// - /// Creates a path translated by the supplied position + /// Creates a path translated by the supplied position. /// /// The path to translate. /// The translation position. - /// A with a translate transform applied. + /// An with a translate transform applied. public static IPath Translate(this IPath path, PointF position) => path.Transform(Matrix4x4.CreateTranslation(position.X, position.Y, 0)); /// - /// Creates a path translated by the supplied position + /// Creates a path translated by the supplied position. /// /// The path to translate. /// The amount to translate along the X axis. /// The amount to translate along the Y axis. - /// A with a translate transform applied. + /// An with a translate transform applied. public static IPath Translate(this IPath path, float x, float y) => path.Translate(new Vector2(x, y)); /// - /// Creates a path translated by the supplied position + /// Creates a path scaled by the supplied amounts around its center. /// - /// The path to translate. + /// The path to scale. /// The amount to scale along the X axis. /// The amount to scale along the Y axis. - /// A with a translate transform applied. + /// An with a scale transform applied. public static IPath Scale(this IPath path, float scaleX, float scaleY) => path.Transform(Matrix4x4.CreateScale(scaleX, scaleY, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// - /// Creates a path translated by the supplied position + /// Creates a path scaled by the supplied amount around its center. /// - /// The path to translate. + /// The path to scale. /// The amount to scale along both the x and y axis. - /// A with a translate transform applied. + /// An with a scale transform applied. public static IPath Scale(this IPath path, float scale) => path.Transform(Matrix4x4.CreateScale(scale, scale, 1, new Vector3(RectangleF.Center(path.Bounds), 0))); /// - /// Calculates the approximate length of the path as though each segment were unrolled into a line. + /// Returns whether the supplied point is inside the path. /// - /// The path to compute the length for. + /// The path to test. + /// The point to test. + /// The fill rule used for containment. + /// when the point is inside or on the path boundary. + public static bool Contains(this IPath path, PointF point, IntersectionRule intersectionRule) + => path.Contains(point, intersectionRule, Vector2.One); + + /// + /// Gets path information at the specified distance along the path. + /// + /// The path to measure. + /// The distance along the path. + /// When this method returns, contains the path information at if the distance resolves to a point on the path; otherwise, the default value. /// - /// The representing the unrolled length. - /// For closed paths, the length includes an implicit closing segment. + /// if resolves to a point on the path; + /// otherwise, . /// - public static float ComputeLength(this IPath path) - { - float dist = 0; - foreach (ISimplePath s in path.Flatten()) - { - ReadOnlySpan points = s.Points.Span; - if (points.Length < 2) - { - // Only a single point - continue; - } - - for (int i = 1; i < points.Length; i++) - { - dist += Vector2.Distance(points[i - 1], points[i]); - } - - if (s.IsClosed) - { - dist += Vector2.Distance(points[0], points[^1]); - } - } + public static bool TryGetPathPointAtDistance(this IPath path, float distance, out PathPoint pathPoint) + => path.TryGetPathPointAtDistance(distance, Vector2.One, out pathPoint); - return dist; - } + /// + /// Gets path information at the specified distance along the path, extrapolating along the + /// boundary tangents when the distance falls before the start or beyond the end of an open path. + /// + /// + /// Unlike , out-of-range + /// distances resolve to a point on the virtual straight-line continuation of the path's first + /// or last segment. Text layout uses this to place aligned glyphs that overflow their layout + /// path. The method returns only when the distance is not a finite + /// number or the path has no measurable length. + /// + /// The path to measure. + /// The distance along the path. + /// When this method returns, contains the path information at if the path is measurable; otherwise, the default value. + /// if the point could be resolved; otherwise, . + public static bool TryGetPathPointAtDistanceUnbounded(this IPath path, float distance, out PathPoint pathPoint) + => path.TryGetPathPointAtDistanceUnbounded(distance, Vector2.One, out pathPoint); + + /// + /// Creates a path segment between two distances along the path. + /// + /// The path to measure. + /// The segment start distance. + /// The segment stop distance. + /// Whether the returned segment starts a new figure at the first segment point. + /// When this method returns, contains the segment path if one was created; otherwise, an empty path. + /// when a segment path was created. + public static bool TryGetSegment(this IPath path, float startDistance, float stopDistance, bool startOnBeginFigure, out IPath segment) + => path.TryGetSegment(startDistance, stopDistance, startOnBeginFigure, Vector2.One, out segment); + + /// + /// Calculates the path length after flattening curves at local-space precision. + /// + /// The path to compute the length for. + /// The path length. + public static float ComputeLength(this IPath path) + => path.ComputeLength(Vector2.One); /// /// Calculates the total area of all paths in the specified collection. /// /// A collection of paths for which to compute the combined area. Cannot be null. - /// - /// The total area, in square units, enclosed by all paths in the collection. - /// + /// The total area, in square units, represented by all paths in the collection. public static float ComputeArea(this IPathCollection paths) { float area = 0; @@ -175,44 +200,10 @@ public static float ComputeArea(this IPathCollection paths) } /// - /// Calculates the total area enclosed by the specified path. + /// Calculates the total area represented by the specified path. /// - /// - /// This method sums the areas of all subpaths within the path. Subpaths with fewer than three - /// points are ignored, as they do not form a closed region. The result is always non-negative, regardless of the - /// winding direction of the subpaths. - /// - /// - /// The path for which to compute the enclosed area. Must contain at least one subpath with three or more points to - /// contribute to the area calculation. - /// - /// - /// The total area, in square units, enclosed by all subpaths of the path. Returns 0 if the path does not contain - /// any subpaths with at least three points. - /// + /// The path for which to compute the area. + /// The total path area, in square units. public static float ComputeArea(this IPath path) - { - float area = 0; - foreach (ISimplePath s in path.Flatten()) - { - ReadOnlySpan points = s.Points.Span; - if (points.Length < 3) - { - // Not enough points to form an area - continue; - } - - float subArea = 0; - for (int i = 0; i < points.Length; i++) - { - PointF p1 = points[i]; - PointF p2 = points[(i + 1) % points.Length]; - subArea += (p1.X * p2.Y) - (p2.X * p1.Y); - } - - area += MathF.Abs(subArea) * .5F; - } - - return area; - } + => path.ComputeArea(Vector2.One); } diff --git a/src/ImageSharp.Drawing/PathPoint.cs b/src/ImageSharp.Drawing/PathPoint.cs new file mode 100644 index 000000000..024cf3961 --- /dev/null +++ b/src/ImageSharp.Drawing/PathPoint.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Represents metadata about a point along a path. +/// +public readonly struct PathPoint +{ + /// + /// Gets the point on the path. + /// + public PointF Point { get; init; } + + /// + /// Gets the normalized forward tangent of the segment. + /// + public PointF Tangent { get; init; } + + /// + /// Gets the forward angle of the segment, in degrees. + /// + public float Angle { get; init; } +} diff --git a/src/ImageSharp.Drawing/PathTypes.cs b/src/ImageSharp.Drawing/PathTypes.cs index b69e0b735..ea8f27197 100644 --- a/src/ImageSharp.Drawing/PathTypes.cs +++ b/src/ImageSharp.Drawing/PathTypes.cs @@ -9,12 +9,12 @@ namespace SixLabors.ImageSharp.Drawing; public enum PathTypes { /// - /// Denotes a path containing a single simple open path + /// Denotes a path containing a single simple open path. /// Open, /// - /// Denotes a path describing a single simple closed shape + /// Denotes a path describing a single simple closed shape. /// Closed, diff --git a/src/ImageSharp.Drawing/PiePolygon.cs b/src/ImageSharp.Drawing/PiePolygon.cs index f0b8b8e91..ee2459f96 100644 --- a/src/ImageSharp.Drawing/PiePolygon.cs +++ b/src/ImageSharp.Drawing/PiePolygon.cs @@ -64,6 +64,11 @@ public PiePolygon(float x, float y, float radiusX, float radiusY, float startAng { } + /// + /// Initializes a new instance of the class. + /// Used by to wrap already-transformed segments. + /// + /// The transformed segments; ownership passes to the new instance. private PiePolygon(ILineSegment[] segments) : base(segments, true) { @@ -87,6 +92,16 @@ public override IPath Transform(Matrix4x4 matrix) return new PiePolygon(segments); } + /// + /// Builds the pie contour: a line from the center to the arc start, the elliptical arc, + /// and a line from the arc end back to the center. The polygon base closes the figure. + /// + /// The center point of the pie sector. + /// The x and y radii of the pie ellipse. + /// The ellipse rotation in degrees. + /// The pie start angle in degrees. + /// The pie sweep angle in degrees. + /// The segments describing the pie sector. private static ILineSegment[] CreateSegments(PointF center, SizeF radius, float rotation, float startAngle, float sweepAngle) { Guard.MustBeGreaterThan(radius.Width, 0, "radiusX"); @@ -103,6 +118,14 @@ private static ILineSegment[] CreateSegments(PointF center, SizeF radius, float ]; } + /// + /// Evaluates the rotated ellipse parameterization at the given angle. + /// + /// The ellipse center. + /// The ellipse radii. + /// The ellipse rotation in degrees. + /// The parametric angle in degrees. + /// The point on the ellipse at the given angle. private static PointF GetArcPoint(PointF center, SizeF radius, float rotation, float angle) { float rotationRadians = rotation * (MathF.PI / 180F); diff --git a/src/ImageSharp.Drawing/PointOrientation.cs b/src/ImageSharp.Drawing/PointOrientation.cs index dec17ebc4..c3b6e0d06 100644 --- a/src/ImageSharp.Drawing/PointOrientation.cs +++ b/src/ImageSharp.Drawing/PointOrientation.cs @@ -4,22 +4,23 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// Represents the orientation of a point from a line. +/// Represents the orientation of an ordered triplet of points: the turn taken at the middle point +/// when travelling from the first point to the third. /// internal enum PointOrientation { /// - /// The point is collinear. + /// The three points lie on (or within tolerance of) a single line. /// Collinear = 0, /// - /// The point is clockwise. + /// The triplet makes a clockwise turn. /// Clockwise = 1, /// - /// The point is counter-clockwise. + /// The triplet makes a counter-clockwise turn. /// Counterclockwise = 2 } diff --git a/src/ImageSharp.Drawing/Polygon.cs b/src/ImageSharp.Drawing/Polygon.cs index ef9e690e9..e5f2feb13 100644 --- a/src/ImageSharp.Drawing/Polygon.cs +++ b/src/ImageSharp.Drawing/Polygon.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// A shape made up of a single closed path made up of one of more s +/// A shape made up of a single closed path made up of one or more s. /// public class Polygon : Path { diff --git a/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs index 6f2e36f75..1397e9f43 100644 --- a/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/ClippedShapeGenerator.cs @@ -12,14 +12,15 @@ namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; /// /// /// This class provides a high-level wrapper around the low-level . -/// It accumulates subject and clip polygons, applies the specified , -/// and converts the resulting polygon contours back into instances suitable -/// for rendering or further processing. +/// It converts subject and clip paths to clipper polygons, applies the specified +/// , and converts the resulting polygon contours back into +/// instances suitable for rendering or further processing. +/// clip operands are handled as rect-sets on dedicated fast paths. /// internal static class ClippedShapeGenerator { /// - /// Generates the final clipped shapes from the previously provided subject and clip paths. + /// Generates the final clipped shapes from the specified subject and clip paths. /// /// /// The boolean operation to perform, such as , @@ -34,21 +35,225 @@ public static ComplexPolygon GenerateClippedShapes( BooleanOperation operation, IPath subject, IEnumerable clip) + => GenerateClippedShapes(operation, IntersectionRule.NonZero, subject, clip); + + /// + /// Generates the final clipped shapes from the specified subject and clip paths. + /// + /// The boolean operation to perform. + /// The fill rule used to interpret both the subject and clipping paths. + /// The subject path. + /// The clipping paths. + /// + /// The representing the result of the boolean operation. + /// + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IntersectionRule intersectionRule, + IPath subject, + IEnumerable clip) + => GenerateClippedShapes(operation, intersectionRule, subject, clip, intersectionRule); + + /// + /// Generates the final clipped shapes from the specified subject and clip paths. + /// + /// The boolean operation to perform. + /// The fill rule used to interpret the subject path. + /// The subject path. + /// The clipping paths. + /// The fill rule used to interpret the clipping paths. + /// + /// The representing the result of the boolean operation. + /// + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IntersectionRule intersectionRule, + IPath subject, + IEnumerable clip, + IntersectionRule clipIntersectionRule) + { + Guard.NotNull(subject); + Guard.NotNull(clip); + + IRegionPath? singleRegionPath = clip is IReadOnlyList { Count: 1 } clipList && + clipList[0] is IRegionPath candidate + ? candidate + : null; + + if (singleRegionPath is not null && + TryClipRectangleByRegion(operation, subject, singleRegionPath, out ComplexPolygon clippedRectangle)) + { + return clippedRectangle; + } + + PCPolygon s = PolygonClipperFactory.FromClosedPath(subject, intersectionRule); + + if (singleRegionPath is not null) + { + return GenerateRegionClippedShapes(operation, s, singleRegionPath); + } + + PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip, clipIntersectionRule); + + return GenerateClippedShapes(operation, s, c); + } + + /// + /// Generates the final clipped shape from the specified subject and clip path. + /// + /// The boolean operation to perform. + /// The fill rule used to interpret the subject path. + /// The subject path. + /// The clipping path. + /// The fill rule used to interpret the clipping path. + /// + /// The representing the result of the boolean operation. + /// + public static ComplexPolygon GenerateClippedShapes( + BooleanOperation operation, + IntersectionRule intersectionRule, + IPath subject, + IPath clip, + IntersectionRule clipIntersectionRule) { Guard.NotNull(subject); Guard.NotNull(clip); - PCPolygon s = PolygonClipperFactory.FromSimpleClosedPaths(subject.Flatten()); - PCPolygon c = PolygonClipperFactory.FromClosedPaths(clip); + if (clip is IRegionPath regionPath && + TryClipRectangleByRegion(operation, subject, regionPath, out ComplexPolygon clippedRectangle)) + { + return clippedRectangle; + } + + PCPolygon s = PolygonClipperFactory.FromClosedPath(subject, intersectionRule); + if (clip is IRegionPath regionPathAfterFastPath) + { + return GenerateRegionClippedShapes(operation, s, regionPathAfterFastPath); + } + + PCPolygon c = PolygonClipperFactory.FromClosedPath(clip, clipIntersectionRule); + + return GenerateClippedShapes(operation, s, c); + } + + /// + /// Attempts the rectangle-by-region fast path: intersecting an axis-aligned rectangle + /// subject with a rect-set region needs no polygon clipper at all. + /// + /// The boolean operation. Only qualifies. + /// The subject path. Only qualifies. + /// The rect-set clip operand. + /// When this method returns , contains the clipped result. + /// when the fast path applied; otherwise, . + private static bool TryClipRectangleByRegion( + BooleanOperation operation, + IPath subject, + IRegionPath regionPath, + out ComplexPolygon clipped) + { + clipped = null!; + if (operation != BooleanOperation.Intersection || + subject is not RectanglePolygon rectangle) + { + return false; + } + + RectangleF subjectBounds = rectangle.Bounds; + IReadOnlyList rectangles = regionPath.Rectangles; + List paths = []; + + for (int i = 0; i < rectangles.Count; i++) + { + RectangleF intersection = RectangleF.Intersect(subjectBounds, ToRectangleF(rectangles[i])); + if (intersection.Width > 0 && intersection.Height > 0) + { + paths.Add(new RectanglePolygon(intersection)); + } + } + + clipped = new ComplexPolygon(paths); + return true; + } + + /// + /// Converts an integer rectangle to its floating-point equivalent. + /// + /// The rectangle to convert. + /// The converted rectangle. + private static RectangleF ToRectangleF(Rectangle rectangle) + => new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + /// + /// Applies a boolean operation between a subject polygon and a rect-set region operand. + /// + /// The boolean operation to perform. + /// The converted subject polygon. + /// The rect-set clip operand. + /// The representing the result of the boolean operation. + private static ComplexPolygon GenerateRegionClippedShapes(BooleanOperation operation, PCPolygon subject, IRegionPath regionPath) + { + IReadOnlyList rectangles = regionPath.Rectangles; + if (operation == BooleanOperation.Intersection) + { + PCPolygon result = []; + + // RegionPath is a rect-set operand: S & (r0 | r1 | ...) is the union of + // each per-rectangle intersection. Keeping that algebra explicit avoids + // asking the generic polygon clipper to infer region union semantics from + // adjacent/disjoint rectangle contours. + for (int i = 0; i < rectangles.Count; i++) + { + PCPolygon clip = PolygonClipperFactory.FromRectangle(rectangles[i]); + result.Join(PolygonClipperAction.Intersection(subject, clip)); + } + + return ToComplexPolygon(result); + } + + PCPolygon current = subject; + for (int i = 0; i < rectangles.Count; i++) + { + PCPolygon clip = PolygonClipperFactory.FromRectangle(rectangles[i]); + current = operation switch + { + BooleanOperation.Xor => PolygonClipperAction.Xor(current, clip), + BooleanOperation.Difference => PolygonClipperAction.Difference(current, clip), + BooleanOperation.Union => PolygonClipperAction.Union(current, clip), + _ => PolygonClipperAction.Intersection(current, clip), + }; + } + + return ToComplexPolygon(current); + } + + /// + /// Applies a boolean operation between two converted polygons. + /// + /// The boolean operation to perform. + /// The converted subject polygon. + /// The converted clip polygon. + /// The representing the result of the boolean operation. + private static ComplexPolygon GenerateClippedShapes(BooleanOperation operation, PCPolygon subject, PCPolygon clip) + { PCPolygon result = operation switch { - BooleanOperation.Xor => PolygonClipperAction.Xor(s, c), - BooleanOperation.Difference => PolygonClipperAction.Difference(s, c), - BooleanOperation.Union => PolygonClipperAction.Union(s, c), - _ => PolygonClipperAction.Intersection(s, c), + BooleanOperation.Xor => PolygonClipperAction.Xor(subject, clip), + BooleanOperation.Difference => PolygonClipperAction.Difference(subject, clip), + BooleanOperation.Union => PolygonClipperAction.Union(subject, clip), + _ => PolygonClipperAction.Intersection(subject, clip), }; + return ToComplexPolygon(result); + } + + /// + /// Converts a PolygonClipper result polygon into a . + /// + /// The polygon produced by the clipper. + /// The converted . + private static ComplexPolygon ToComplexPolygon(PCPolygon result) + { IPath[] shapes = new IPath[result.Count]; int index = 0; @@ -66,7 +271,7 @@ public static ComplexPolygon GenerateClippedShapes( /// The polygon containing the contour hierarchy. /// The contour index to convert. /// The converted point array. - private static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex) + public static PointF[] CreateContourPoints(PCPolygon polygon, int contourIndex) { Contour contour = polygon[contourIndex]; PointF[] points = new PointF[contour.Count]; diff --git a/src/ImageSharp.Drawing/PolygonGeometry/NormalizedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/NormalizedShapeGenerator.cs new file mode 100644 index 000000000..8cd40d2e6 --- /dev/null +++ b/src/ImageSharp.Drawing/PolygonGeometry/NormalizedShapeGenerator.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.PolygonClipper; +using PCPolygon = SixLabors.PolygonClipper.Polygon; +using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; + +namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; + +/// +/// Generates normalized polygon shapes from paths. +/// +/// +/// Normalization converts the incoming path contours to a positive-winding polygon representation. +/// This gives later boolean operations a stable contour set instead of preserving ambiguous +/// self-intersections, overlaps, or parity-dependent contour relationships from the source path. +/// +internal static class NormalizedShapeGenerator +{ + /// + /// Normalizes the specified path. + /// + /// The path to normalize. + /// + /// A containing the normalized positive-winding contours. + /// + public static ComplexPolygon NormalizeShape(IPath path) + { + IPath closed = path.AsClosedPath(); + PCPolygon rings = []; + + foreach (ISimplePath sp in closed.Flatten()) + { + ReadOnlySpan span = sp.Points.Span; + + if (span.Length < 2) + { + continue; + } + + Contour ring = new(span.Length); + for (int i = 0; i < span.Length; i++) + { + PointF p = span[i]; + ring.Add(new Vertex(p.X, p.Y)); + } + + // PolygonClipper expects closed rings to repeat their start point. + if (sp.IsClosed) + { + ring.Add(ring[0]); + } + + rings.Add(ring); + } + + int count = rings.Count; + if (count == 0) + { + return new([]); + } + + PCPolygon result = PolygonClipperAction.Normalize(rings); + + IPath[] shapes = new IPath[result.Count]; + for (int i = 0; i < result.Count; i++) + { + shapes[i] = new Polygon(ClippedShapeGenerator.CreateContourPoints(result, i)); + } + + return new(shapes); + } +} diff --git a/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs index 454820488..115b0c2ec 100644 --- a/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/PolygonClipperFactory.cs @@ -3,6 +3,7 @@ using SixLabors.PolygonClipper; using PCPolygon = SixLabors.PolygonClipper.Polygon; +using PolygonClipperAction = SixLabors.PolygonClipper.PolygonClipper; namespace SixLabors.ImageSharp.Drawing.PolygonGeometry; @@ -22,17 +23,99 @@ internal static class PolygonClipperFactory /// The paths to convert. /// A containing all flattened paths as contours. public static PCPolygon FromClosedPaths(IEnumerable paths) + => FromClosedPaths(paths, IntersectionRule.NonZero); + + /// + /// Creates a polygon area from multiple paths using the specified fill rule. + /// + /// The paths to convert. + /// The fill rule used to interpret the input paths. + /// A containing the converted contours. + public static PCPolygon FromClosedPaths(IEnumerable paths, IntersectionRule intersectionRule) { PCPolygon polygon = []; foreach (IPath path in paths) { + if (path is IRegionPath regionPath) + { + AddRegionRectangles(regionPath.Rectangles, polygon); + continue; + } + polygon = FromSimpleClosedPaths(path.Flatten(), polygon); } + return ApplyIntersectionRule(polygon, intersectionRule); + } + + /// + /// Creates a polygon area from a path using the specified fill rule. + /// + /// The path to convert. + /// The fill rule used to interpret the path. + /// A containing the converted contours. + public static PCPolygon FromClosedPath(IPath path, IntersectionRule intersectionRule) + { + if (path is IRegionPath regionPath) + { + PCPolygon regionPolygon = []; + AddRegionRectangles(regionPath.Rectangles, regionPolygon); + + return ApplyIntersectionRule(regionPolygon, intersectionRule); + } + + PCPolygon polygon = FromSimpleClosedPaths(path.AsClosedPath().Flatten()); + + return ApplyIntersectionRule(polygon, intersectionRule); + } + + /// + /// Creates a polygon from one axis-aligned integer rectangle. + /// + /// The rectangle to convert. + /// A containing one rectangle contour. + public static PCPolygon FromRectangle(Rectangle rectangle) + { + PCPolygon polygon = []; + AddRectangle(rectangle, polygon); return polygon; } + /// + /// Adds an integer region as rectangle contours without first exporting its boundary path. + /// + /// The normalized rectangles that cover the region. + /// The polygon receiving the rectangle contours. + private static void AddRegionRectangles(IReadOnlyList rectangles, PCPolygon polygon) + { + // Skia clips regions as rectangle sets. Preserve that model at the polygon boundary so + // disjoint dirty-region islands are unioned by coverage instead of being inferred from + // boundary-link geometry. + for (int i = 0; i < rectangles.Count; i++) + { + AddRectangle(rectangles[i], polygon); + } + } + + /// + /// Adds one rectangle contour to a polygon. + /// + /// The rectangle to add. + /// The polygon receiving the rectangle contour. + private static void AddRectangle(Rectangle rectangle, PCPolygon polygon) + { + Contour contour = + [ + new Vertex(rectangle.Left, rectangle.Top), + new Vertex(rectangle.Right, rectangle.Top), + new Vertex(rectangle.Right, rectangle.Bottom), + new Vertex(rectangle.Left, rectangle.Bottom) + ]; + + polygon.Add(contour); + } + /// /// Converts closed simple paths to PolygonClipper contours. /// @@ -70,10 +153,33 @@ public static PCPolygon FromSimpleClosedPaths(IEnumerable paths, PC contour.Add(new Vertex(points[i].X, points[i].Y)); } - // Add the contour - PolygonClipper will determine parent/depth/orientation during sweep polygon.Add(contour); } return polygon; } + + /// + /// Prepares a polygon so the boolean clipper reproduces the requested fill rule. + /// + /// + /// The clipper classifies coverage by even-odd boundary crossings, which is orientation + /// independent: a point is inside when an odd number of the polygon's edges lie below it, + /// regardless of edge direction. Even-odd input already matches that model and is returned + /// unchanged. Non-zero input differs from even-odd only where the outline covers a region more + /// than once, that is where contours self-overlap or self-intersect (winding of magnitude two + /// or more, common in glyph outlines with overlapping components or crossings introduced by + /// curve flattening). Non-zero fills such a region while even-odd punches it out as a hole, + /// which is what produced clipped glyphs with spurious holes. Reorienting the contours cannot + /// fix this, because even-odd ignores direction, so the overlapping regions must actually be + /// merged. performs that self-union and yields an + /// equivalent simple polygon whose even-odd boundary equals the non-zero fill. + /// + /// The polygon to prepare. + /// The fill rule used to interpret the polygon. + /// A containing the prepared contours. + private static PCPolygon ApplyIntersectionRule(PCPolygon polygon, IntersectionRule intersectionRule) + => intersectionRule == IntersectionRule.NonZero + ? PolygonClipperAction.Normalize(polygon) + : polygon; } diff --git a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs index b8e2c0f35..70b8774d5 100644 --- a/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs +++ b/src/ImageSharp.Drawing/PolygonGeometry/StrokedShapeGenerator.cs @@ -24,7 +24,8 @@ internal static class StrokedShapeGenerator /// public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, StrokeOptions options) { - // 1) Stroke the input path as open or closed. + // Convert the flattened contours to clipper rings first; the stroker handles + // open and closed contours differently, so closedness must be preserved. PCPolygon rings = []; foreach (ISimplePath sp in path.Flatten()) @@ -43,6 +44,7 @@ public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, Stro ring.Add(new Vertex(p.X, p.Y)); } + // PolygonClipper expects closed rings to repeat their start point. if (sp.IsClosed) { ring.Add(ring[0]); @@ -63,21 +65,17 @@ public static ComplexPolygon GenerateStrokedShapes(IPath path, float width, Stro int index = 0; for (int i = 0; i < result.Count; i++) { - Contour contour = result[i]; - PointF[] points = new PointF[contour.Count]; - - for (int j = 0; j < contour.Count; j++) - { - Vertex vertex = contour[j]; - points[j] = new PointF((float)vertex.X, (float)vertex.Y); - } - - shapes[index++] = new Polygon(points); + shapes[index++] = new Polygon(ClippedShapeGenerator.CreateContourPoints(result, i)); } return new(shapes); } + /// + /// Maps the ImageSharp to the equivalent PolygonClipper options. + /// + /// The ImageSharp stroke geometry options. + /// The equivalent . private static PolygonClipper.StrokeOptions CreateStrokeOptions(StrokeOptions options) { PolygonClipper.StrokeOptions o = new() diff --git a/src/ImageSharp.Drawing/Processing/BackdropLayerEffect.cs b/src/ImageSharp.Drawing/Processing/BackdropLayerEffect.cs new file mode 100644 index 000000000..ae258ff9c --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/BackdropLayerEffect.cs @@ -0,0 +1,390 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Describes an effect applied to the backdrop of a compositing layer: the pixels already on the +/// canvas beneath the layer's region are filtered when the layer is opened, and the layer's content +/// then renders above the filtered backdrop. This is the CSS backdrop-filter model; the +/// filtered result is clipped to the layer's region. +/// +public abstract class BackdropLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The operation that implements the effect. + protected BackdropLayerEffect(Action operation) + : base(operation) + { + } + + /// + /// Initializes a new instance of the class with the + /// observable behavior of another backdrop effect when this effect is not executed natively. + /// + /// The backdrop effect whose behavior is used as the fallback. + protected BackdropLayerEffect(BackdropLayerEffect fallbackEffect) + : base(fallbackEffect) + { + } + + /// + /// Gets the output reach. Backdrop effects filter the pixels already beneath the layer + /// clipped to the region, so their output never extends past it. + /// + public sealed override int Reach => 0; +} + +/// +/// Blurs the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: blur(). +/// +public sealed class BackdropBlurLayerEffect : BackdropLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels; zero leaves the backdrop unchanged. + public BackdropBlurLayerEffect(float sigma) + : base(context => context.GaussianBlur(sigma)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Sigma = sigma; + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + public override bool IsPassThrough => this.Sigma == 0; +} + +/// +/// Blurs and tints the backdrop beneath a compositing layer, producing a frosted-glass acrylic +/// material. This is the tinted extension of and has no CSS +/// equivalent; CSS composes the tint from the element's own translucent background instead. +/// +public sealed class BackdropAcrylicLayerEffect : BackdropLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels. + /// + /// The tint blended over the blurred backdrop. Its alpha controls the tint strength, so an + /// opaque tint hides the backdrop entirely. + /// + public BackdropAcrylicLayerEffect(float sigma, Color tint) + : base(CreateOperation(sigma, tint)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Sigma = sigma; + this.Tint = tint; + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the tint blended over the blurred backdrop. + /// + public Color Tint { get; } + + /// + /// Creates the CPU acrylic operation for the supplied effect values. + /// + /// The Gaussian blur sigma. + /// The acrylic tint. + /// The configured image-processing operation. + private static Action CreateOperation(float sigma, Color color) + { + // Blending a constant colour over the backdrop is linear, so it is expressed as a colour + // matrix: the backdrop's channels scale by one minus the tint's alpha and the constant row + // carries the pre-scaled tint. The backdrop's own alpha is preserved. + Vector4 vector = color.ToPixel().ToScaledVector4(); + float keep = 1F - vector.W; + ColorMatrix tint = default; + tint.M11 = keep; + tint.M22 = keep; + tint.M33 = keep; + tint.M44 = 1F; + tint.M51 = vector.X * vector.W; + tint.M52 = vector.Y * vector.W; + tint.M53 = vector.Z * vector.W; + + return context => + { + if (sigma > 0) + { + context.GaussianBlur(sigma); + } + + context.Filter(tint); + }; + } +} + +/// +/// Composites a drop shadow of the backdrop's silhouette beneath it. The CSS equivalent is +/// backdrop-filter: drop-shadow(). +/// +public sealed class BackdropDropShadowLayerEffect : BackdropLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The shadow offset, in pixels. + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// The shadow colour. Its alpha scales the backdrop's alpha. + public BackdropDropShadowLayerEffect(Point offset, float sigma, Color color) + : base(CreateOperation(sigma, color)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + public override GraphicsOptions? WriteBackOptions + => new() { AlphaCompositionMode = PixelAlphaCompositionMode.DestOver }; + + /// + public override Point WriteBackOffset => this.Offset; + + /// + /// Creates the CPU backdrop drop-shadow operation for the supplied effect values. + /// + /// The Gaussian blur sigma. + /// The shadow colour. + /// The configured image-processing operation. + private static Action CreateOperation(float sigma, Color color) + { + // The tint replaces the backdrop's colour with the constant shadow colour and scales its + // alpha; the DestOver write-back slots the blurred silhouette beneath the untouched + // backdrop at the offset. + Vector4 vector = color.ToPixel().ToScaledVector4(); + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + return context => + { + context.Filter(tint); + if (sigma > 0) + { + context.GaussianBlur(sigma); + } + }; + } +} + +/// +/// Transforms the colours of the backdrop beneath a compositing layer through a colour matrix. The +/// named backdrop colour effects all specialize this; use it directly for matrices they do not +/// cover. +/// +public class BackdropColorMatrixLayerEffect : BackdropLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The colour matrix applied to the backdrop. + public BackdropColorMatrixLayerEffect(ColorMatrix matrix) + : base(context => context.Filter(matrix)) + => this.Matrix = matrix; + + /// + /// Gets the colour matrix applied to the backdrop. + /// + public ColorMatrix Matrix { get; } + + /// + public override bool IsPassThrough => this.Matrix == ColorMatrix.Identity; +} + +/// +/// Adjusts the brightness of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: brightness(). +/// +public sealed class BackdropBrightnessLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The brightness amount: 0 is black, 1 leaves the backdrop unchanged, and values above 1 brighten. + public BackdropBrightnessLayerEffect(float amount) + : base(KnownFilterMatrices.CreateBrightnessFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the brightness amount. + /// + public float Amount { get; } +} + +/// +/// Adjusts the contrast of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: contrast(). +/// +public sealed class BackdropContrastLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The contrast amount: 0 is fully grey, 1 leaves the backdrop unchanged, and values above 1 increase contrast. + public BackdropContrastLayerEffect(float amount) + : base(KnownFilterMatrices.CreateContrastFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the contrast amount. + /// + public float Amount { get; } +} + +/// +/// Desaturates the backdrop beneath a compositing layer towards grayscale using ITU-R BT.709 +/// luminance coefficients, matching the CSS definition. The CSS equivalent is +/// backdrop-filter: grayscale(). +/// +public sealed class BackdropGrayscaleLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The grayscale amount between 0 and 1: 0 leaves the backdrop unchanged and 1 is fully grayscale. + public BackdropGrayscaleLayerEffect(float amount) + : base(KnownFilterMatrices.CreateGrayscaleBt709Filter(amount)) + => this.Amount = amount; + + /// + /// Gets the grayscale amount. + /// + public float Amount { get; } +} + +/// +/// Rotates the hue of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: hue-rotate(). +/// +public sealed class BackdropHueRotateLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The hue rotation in degrees; 0 leaves the backdrop unchanged. + public BackdropHueRotateLayerEffect(float degrees) + : base(KnownFilterMatrices.CreateHueFilter(degrees)) + => this.Degrees = degrees; + + /// + /// Gets the hue rotation in degrees. + /// + public float Degrees { get; } +} + +/// +/// Inverts the colours of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: invert(). +/// +public sealed class BackdropInvertLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The inversion amount between 0 and 1: 0 leaves the backdrop unchanged and 1 fully inverts. + public BackdropInvertLayerEffect(float amount) + : base(KnownFilterMatrices.CreateInvertFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the inversion amount. + /// + public float Amount { get; } +} + +/// +/// Scales the opacity of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: opacity(). +/// +public sealed class BackdropOpacityLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The opacity amount between 0 and 1: 0 is fully transparent and 1 leaves the backdrop unchanged. + public BackdropOpacityLayerEffect(float amount) + : base(KnownFilterMatrices.CreateOpacityFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the opacity amount. + /// + public float Amount { get; } +} + +/// +/// Shifts the colours of the backdrop beneath a compositing layer towards sepia. The CSS equivalent +/// is backdrop-filter: sepia(). +/// +public sealed class BackdropSepiaLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The sepia amount between 0 and 1: 0 leaves the backdrop unchanged and 1 is fully sepia. + public BackdropSepiaLayerEffect(float amount) + : base(KnownFilterMatrices.CreateSepiaFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the sepia amount. + /// + public float Amount { get; } +} + +/// +/// Adjusts the colour saturation of the backdrop beneath a compositing layer. The CSS equivalent is +/// backdrop-filter: saturate(). +/// +public sealed class BackdropSaturateLayerEffect : BackdropColorMatrixLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The saturation amount: 0 is fully desaturated, 1 leaves the backdrop unchanged, and values above 1 over-saturate. + public BackdropSaturateLayerEffect(float amount) + : base(KnownFilterMatrices.CreateSaturateFilter(amount)) + => this.Amount = amount; + + /// + /// Gets the saturation amount. + /// + public float Amount { get; } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs b/src/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs index e94ecaedc..f0fa8c6f7 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/ApplyBarrier.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Memory; - namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// @@ -15,30 +13,41 @@ internal sealed class ApplyBarrier /// /// The closed path defining the processed region. /// The drawing options captured when the barrier was recorded. - /// The active clip paths captured when the barrier was recorded. /// The canvas-local bounds captured when the barrier was recorded. /// The absolute target bounds captured when the barrier was recorded. /// The absolute destination offset captured when the barrier was recorded. - /// Indicates whether the barrier was recorded inside a layer. + /// The layer that owned this barrier when it was recorded. /// The processor operation to run against the replay-time snapshot. - internal ApplyBarrier( + /// The layer effect represented by the operation, or for a direct Apply operation. + /// + /// The graphics options used to composite the processed pixels back onto the target, or + /// to replace the region outright. + /// + /// The offset at which the processed pixels are written back. + public ApplyBarrier( IPath path, DrawingOptions options, - IReadOnlyList clipPaths, Rectangle canvasBounds, Rectangle targetBounds, Point destinationOffset, - bool isInsideLayer, - Action operation) + DrawingCanvasLayer? ownerLayer, + Action operation, + LayerEffect? effect, + GraphicsOptions? writeBackOptions, + Point writeBackOffset) { this.Path = path; + this.OutputBounds = path.Bounds; + this.Options = options; - this.ClipPaths = clipPaths; this.CanvasBounds = canvasBounds; this.TargetBounds = targetBounds; this.DestinationOffset = destinationOffset; - this.IsInsideLayer = isInsideLayer; + this.OwnerLayer = ownerLayer; this.Operation = operation; + this.Effect = effect; + this.WriteBackOptions = writeBackOptions; + this.WriteBackOffset = writeBackOffset; } /// @@ -47,14 +56,14 @@ internal ApplyBarrier( public IPath Path { get; } /// - /// Gets the drawing options captured when the barrier was recorded. + /// Gets the local bounds within which the processed output is written. /// - public DrawingOptions Options { get; } + public RectangleF OutputBounds { get; } /// - /// Gets the active clip paths captured when the barrier was recorded. + /// Gets the drawing options captured when the barrier was recorded. /// - public IReadOnlyList ClipPaths { get; } + public DrawingOptions Options { get; } /// /// Gets the canvas-local bounds captured when the barrier was recorded. @@ -74,7 +83,12 @@ internal ApplyBarrier( /// /// Gets a value indicating whether the barrier was recorded inside a layer. /// - public bool IsInsideLayer { get; } + public bool IsInsideLayer => this.OwnerLayer is not null; + + /// + /// Gets the layer that owned this barrier when it was recorded. + /// + public DrawingCanvasLayer? OwnerLayer { get; } /// /// Gets the processor operation to run against the replay-time snapshot. @@ -82,91 +96,19 @@ internal ApplyBarrier( public Action Operation { get; } /// - /// Creates the transient image-brush draw command that writes this barrier's processed snapshot back to the target. + /// Gets the layer effect represented by the operation, or for a direct Apply operation. /// - /// The pixel format. - /// The active processing configuration. - /// The backend used to read the replay-time target pixels. - /// The target frame. - /// The image resource that must stay alive while the returned command batch is rendered. - /// The transient write-back command batch, or when the barrier has no target coverage. - public DrawingCommandBatch? CreateWriteBackBatch( - Configuration configuration, - IDrawingBackend backend, - ICanvasFrame target, - out IDisposable? ownedResource) - where TPixel : unmanaged, IPixel - { - RectangleF rawBounds = RectangleF.Transform(this.Path.Bounds, this.Options.Transform); - Rectangle sourceRect = ToConservativeBounds(rawBounds); - sourceRect = Rectangle.Intersect(this.CanvasBounds, sourceRect); - - if (sourceRect.Width <= 0 || sourceRect.Height <= 0) - { - ownedResource = null; - return null; - } - - Image sourceImage = new(configuration, sourceRect.Width, sourceRect.Height); - try - { - backend.ReadRegion( - configuration, - target, - sourceRect, - sourceImage.Frames.RootFrame.PixelBuffer.GetRegion()); + public LayerEffect? Effect { get; } - sourceImage.Mutate(this.Operation); - - Point brushOffset = new( - sourceRect.X - (int)MathF.Floor(rawBounds.Left), - sourceRect.Y - (int)MathF.Floor(rawBounds.Top)); - - ImageBrush brush = new(sourceImage, sourceImage.Bounds, brushOffset); - GraphicsOptions graphicsOptions = this.Options.GraphicsOptions; - RasterizationMode rasterizationMode = graphicsOptions.Antialias - ? RasterizationMode.Antialiased - : RasterizationMode.Aliased; - - RectangleF pathBounds = this.Path.Bounds; - Rectangle interest = Rectangle.FromLTRB( - (int)MathF.Floor(pathBounds.Left), - (int)MathF.Floor(pathBounds.Top), - (int)MathF.Ceiling(pathBounds.Right), - (int)MathF.Ceiling(pathBounds.Bottom)); - - RasterizerOptions rasterizerOptions = new( - interest, - this.Options.ShapeOptions.IntersectionRule, - rasterizationMode, - RasterizerSamplingOrigin.PixelBoundary, - graphicsOptions.AntialiasThreshold); - - CompositionCommand command = CompositionCommand.Create( - this.Path, - brush, - this.Options, - in rasterizerOptions, - this.TargetBounds, - this.DestinationOffset, - this.ClipPaths, - this.IsInsideLayer); - - ownedResource = sourceImage; - CompositionSceneCommand[] commands = [new PathCompositionSceneCommand(command)]; - return new DrawingCommandBatch(commands, hasLayers: false); - } - catch - { - sourceImage.Dispose(); - throw; - } - } + /// + /// Gets the graphics options used to composite the processed pixels back onto the target. + /// When the processed pixels replace the region outright. + /// + public GraphicsOptions? WriteBackOptions { get; } - private static Rectangle ToConservativeBounds(RectangleF bounds) - => Rectangle.FromLTRB( - (int)MathF.Floor(bounds.Left), - (int)MathF.Floor(bounds.Top), - (int)MathF.Ceiling(bounds.Right), - (int)MathF.Ceiling(bounds.Bottom)); + /// + /// Gets the offset, in device pixels, at which the processed pixels are written back relative + /// to the region they were read from. + /// + public Point WriteBackOffset { get; } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs index e25c27a27..5f99446d9 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CanvasRegionFrame{TPixel}.cs @@ -52,6 +52,10 @@ public bool TryGetCpuRegion(out Buffer2DRegion region) } /// + /// + /// Native surfaces cannot be sub-sliced, so the parent surface is returned as-is; + /// consumers address the subregion through the absolute instead. + /// public bool TryGetNativeSurface([NotNullWhen(true)] out NativeSurface? surface) => this.parent.TryGetNativeSurface(out surface); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs index e159be8cb..f0d314180 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionCommand.cs @@ -23,7 +23,22 @@ public enum CompositionCommandKind : byte /// /// Ends the most recently opened layer. /// - EndLayer = 2 + EndLayer = 2, + + /// + /// Applies an image processor to the current target before later commands are rendered. + /// + Apply = 3, + + /// + /// Starts a clip scope. + /// + BeginClip = 4, + + /// + /// Ends the most recently opened clip scope. + /// + EndClip = 5 } /// @@ -37,33 +52,54 @@ public readonly struct CompositionCommand private readonly IPath? sourcePath; private readonly Brush? brush; private readonly DrawingOptions? drawingOptions; - private readonly GraphicsOptions? layerGraphicsOptions; - private readonly IReadOnlyList? clipPaths; + private readonly DrawingCanvasLayer? layer; + private readonly DrawingClipDescriptor? clipDescriptor; + private readonly ApplyBarrier? applyBarrier; + /// + /// Initializes a new instance of the struct. + /// + /// The command kind. + /// The source path, or for commands without geometry. + /// The composition brush, or for commands without a brush. + /// The drawing options, or for commands without them. + /// The shared layer state, or for non-layer commands. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute layer bounds for begin/end-layer commands. + /// The absolute destination offset for composited coverage. + /// The clip descriptor, or for non begin-clip commands. + /// True if the command was recorded inside a layer. + /// The apply barrier, or for non-apply commands. + /// The fractional translation composed into . private CompositionCommand( CompositionCommandKind kind, IPath? sourcePath, Brush? brush, DrawingOptions? drawingOptions, - GraphicsOptions? layerGraphicsOptions, + DrawingCanvasLayer? layer, in RasterizerOptions rasterizerOptions, Rectangle targetBounds, Rectangle layerBounds, Point destinationOffset, - IReadOnlyList? clipPaths, - bool isInsideLayer) + DrawingClipDescriptor? clipDescriptor, + bool isInsideLayer, + ApplyBarrier? applyBarrier, + Vector2 subPixelOffset) { this.Kind = kind; this.sourcePath = sourcePath; this.brush = brush; this.drawingOptions = drawingOptions; - this.layerGraphicsOptions = layerGraphicsOptions; + this.layer = layer; this.RasterizerOptions = rasterizerOptions; this.TargetBounds = targetBounds; this.LayerBounds = layerBounds; this.DestinationOffset = destinationOffset; - this.clipPaths = clipPaths; + this.clipDescriptor = clipDescriptor; this.IsInsideLayer = isInsideLayer; + this.applyBarrier = applyBarrier; + this.SubPixelOffset = subPixelOffset; } /// @@ -96,9 +132,12 @@ private CompositionCommand( public DrawingOptions DrawingOptions => this.drawingOptions ?? throw new InvalidOperationException("Layer commands do not carry drawing options."); /// - /// Gets graphics options used for composition or layer compositing. + /// Gets graphics options used by layer compositing commands. /// - public GraphicsOptions GraphicsOptions => this.drawingOptions?.GraphicsOptions ?? this.layerGraphicsOptions!; + /// + /// Only valid for commands that carry layer state; accessing it on other commands throws. + /// + public GraphicsOptions LayerOptions => this.Layer.Options; /// /// Gets rasterizer options used to generate coverage. @@ -116,25 +155,93 @@ private CompositionCommand( public IPath SourcePath => this.sourcePath ?? throw new InvalidOperationException("Layer commands do not carry path geometry."); /// - /// Gets the command transform. + /// Gets the fractional translation applied after the drawing options transform. Glyph + /// geometry rides an integer destination offset; the sub-pixel remainder travels here so + /// the queued command does not need per-operation drawing options to carry it. /// - public Matrix4x4 Transform => this.drawingOptions?.Transform ?? Matrix4x4.Identity; + public Vector2 SubPixelOffset { get; } /// - /// Gets the clip paths carried by the command. + /// Gets the command transform: the drawing options transform followed by the sub-pixel + /// translation. /// - public IReadOnlyList? ClipPaths => this.clipPaths; + public Matrix4x4 Transform + { + get + { + Matrix4x4 transform = this.drawingOptions?.Transform ?? Matrix4x4.Identity; + if (this.SubPixelOffset == Vector2.Zero) + { + return transform; + } + + return transform.IsIdentity + ? Matrix4x4.CreateTranslation(this.SubPixelOffset.X, this.SubPixelOffset.Y, 0F) + : transform * Matrix4x4.CreateTranslation(this.SubPixelOffset.X, this.SubPixelOffset.Y, 0F); + } + } /// - /// Gets the shape options carried by the command. + /// Gets the clip descriptor opened by a command. /// - public ShapeOptions ShapeOptions => this.drawingOptions?.ShapeOptions ?? throw new InvalidOperationException("Layer commands do not carry shape options."); + /// + /// The ordered begin/end-clip command stream is the single source of truth for clipping. + /// Draw commands do not carry clip state; backends resolve the active clip stack from the + /// stream commands surrounding each draw. + /// + public DrawingClipDescriptor ClipDescriptor => this.clipDescriptor ?? throw new InvalidOperationException("Only begin-clip commands carry a clip descriptor."); /// /// Gets a value indicating whether the command was recorded inside a layer. /// public bool IsInsideLayer { get; } + /// + /// Gets a value indicating whether this layer command must be rendered as an isolated target for Apply. + /// + public bool RequiresScopedApply => this.layer?.RequiresScopedApply ?? false; + + /// + /// Gets the canvas bounds available to an Apply command. + /// + public Rectangle ApplyCanvasBounds => this.applyBarrier?.CanvasBounds ?? throw new InvalidOperationException("Only apply commands carry apply canvas bounds."); + + /// + /// Gets the local bounds within which an Apply command writes its processed output. + /// + public RectangleF ApplyOutputBounds => this.ApplyBarrier.OutputBounds; + + /// + /// Gets the image processor carried by an Apply command. + /// + public Action ApplyOperation => this.applyBarrier?.Operation ?? throw new InvalidOperationException("Only apply commands carry an apply operation."); + + /// + /// Gets the layer effect carried by an Apply command, or when the command represents a direct Apply operation. + /// + public LayerEffect? ApplyEffect => this.ApplyBarrier.Effect; + + /// + /// Gets the offset subtracted from an Apply command's write rectangle when reading the source + /// pixels, so a write-back recorded at an offset still reads the pre-offset region. + /// + public Point ApplyWriteBackOffset => this.applyBarrier?.WriteBackOffset ?? throw new InvalidOperationException("Only apply commands carry an apply write-back offset."); + + /// + /// Gets the apply barrier carried by an command. + /// + internal ApplyBarrier ApplyBarrier => this.applyBarrier ?? throw new InvalidOperationException("Only apply commands carry an apply barrier."); + + /// + /// Gets the layer state carried by a layer command. + /// + internal DrawingCanvasLayer Layer => this.layer ?? throw new InvalidOperationException("Only layer commands carry layer state."); + + /// + /// Gets the layer state for the layer that owned this command when it was recorded. + /// + internal DrawingCanvasLayer? OwnerLayer => this.layer; + /// /// Creates a fill-path composition command. /// @@ -144,17 +251,15 @@ private CompositionCommand( /// Rasterizer options used to generate coverage. /// The absolute bounds of the logical target for this command. /// Absolute destination offset where coverage is composited. - /// Optional clip paths supplied with the command. /// True if the command was recorded inside a layer. /// The composition command. - public static CompositionCommand Create( + internal static CompositionCommand Create( IPath path, Brush brush, DrawingOptions drawingOptions, in RasterizerOptions rasterizerOptions, Rectangle targetBounds, Point destinationOffset, - IReadOnlyList? clipPaths, bool isInsideLayer) => new( CompositionCommandKind.FillLayer, @@ -166,48 +271,256 @@ public static CompositionCommand Create( targetBounds, default, destinationOffset, - clipPaths, - isInsideLayer); + null, + isInsideLayer, + null, + Vector2.Zero); /// - /// Creates a begin-layer composition command. is false on the - /// BeginLayer marker itself; the flag is only meaningful for fills/strokes that follow it. + /// Creates a fill-path composition command with the owning layer state recorded by the canvas. + /// + /// Path in target-local coordinates. + /// Brush used during composition. + /// Drawing options (graphics, shape, transform) used during composition. + /// Rasterizer options used to generate coverage. + /// The absolute bounds of the logical target for this command. + /// Absolute destination offset where coverage is composited. + /// The layer that owned this command when it was recorded. + /// The composition command. + internal static CompositionCommand Create( + IPath path, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + DrawingCanvasLayer? layer) + => Create( + path, + brush, + drawingOptions, + in rasterizerOptions, + targetBounds, + destinationOffset, + layer, + Vector2.Zero); + + /// + /// Creates a fill-path composition command carrying a sub-pixel translation alongside the + /// owning layer state recorded by the canvas. + /// + /// Path in target-local coordinates. + /// Brush used during composition. + /// Drawing options (graphics, shape, transform) used during composition. + /// Rasterizer options used to generate coverage. + /// The absolute bounds of the logical target for this command. + /// Absolute destination offset where coverage is composited. + /// The layer that owned this command when it was recorded. + /// The fractional translation composed into . + /// The composition command. + internal static CompositionCommand Create( + IPath path, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + DrawingCanvasLayer? layer, + Vector2 subPixelOffset) + => new( + CompositionCommandKind.FillLayer, + path, + brush, + drawingOptions, + layer, + in rasterizerOptions, + targetBounds, + default, + destinationOffset, + null, + layer is not null, + null, + subPixelOffset); + + /// + /// Creates a begin-layer composition command with shared layer state. /// /// The absolute bounds of the layer. - /// The compositing options used when the layer closes. + /// The layer state shared by the begin and end commands. /// The begin-layer command. - public static CompositionCommand CreateBeginLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions) + internal static CompositionCommand CreateBeginLayer( + Rectangle layerBounds, + DrawingCanvasLayer layer) => new( CompositionCommandKind.BeginLayer, null, null, null, - graphicsOptions, + layer, default, layerBounds, layerBounds, default, null, - false); + false, + null, + Vector2.Zero); /// - /// Creates an end-layer composition command. is false on the - /// EndLayer marker itself; the flag is only meaningful for fills/strokes that preceded it. + /// Creates an end-layer composition command with shared layer state. /// /// The absolute bounds of the layer being closed. - /// The compositing options used by the layer. + /// The layer state shared by the begin and end commands. /// The end-layer command. - public static CompositionCommand CreateEndLayer(Rectangle layerBounds, GraphicsOptions graphicsOptions) + internal static CompositionCommand CreateEndLayer(Rectangle layerBounds, DrawingCanvasLayer layer) => new( CompositionCommandKind.EndLayer, null, null, null, - graphicsOptions, + layer, default, layerBounds, layerBounds, default, null, - false); + false, + null, + Vector2.Zero); + + /// + /// Creates a begin-clip composition command. + /// + /// The clip descriptor opened by the command. + /// Absolute destination offset used to place clip geometry. + /// The begin-clip command. + internal static CompositionCommand CreateBeginClip(DrawingClipDescriptor descriptor, Point destinationOffset) + => new( + CompositionCommandKind.BeginClip, + null, + null, + null, + null, + default, + default, + default, + destinationOffset, + descriptor, + false, + null, + Vector2.Zero); + + /// + /// Creates an end-clip composition command. + /// + /// The end-clip command. + internal static CompositionCommand CreateEndClip() + => new( + CompositionCommandKind.EndClip, + null, + null, + null, + null, + default, + default, + default, + default, + null, + false, + null, + Vector2.Zero); + + /// + /// Creates an apply composition command. + /// + /// The apply barrier to execute. + /// The apply command. + internal static CompositionCommand CreateApply(ApplyBarrier barrier) + { + RasterizerOptions rasterizerOptions = CreateApplyRasterizerOptions(barrier.Path, barrier.Options); + + return CreateApply(barrier, barrier.Path, barrier.Options, in rasterizerOptions); + } + + /// + /// Creates an apply composition command from precomputed rasterizer options. + /// + /// The apply barrier to execute. + /// The closed path defining the processed region. + /// The drawing options captured when the barrier was recorded. + /// The rasterizer options used to generate coverage. + /// The apply command. + private static CompositionCommand CreateApply( + ApplyBarrier barrier, + IPath path, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions) + { + // By default Apply replaces the processed region rather than blending over it, so the + // command carries options forced to Src alpha composition at full blend percentage. A + // barrier recorded with explicit write-back options composites the processed pixels back + // through those options instead, against the still-untouched region content. + DrawingOptions applyOptions = barrier.WriteBackOptions is null + ? drawingOptions.CloneForClearOperation() + : new DrawingOptions(barrier.WriteBackOptions.DeepClone(), drawingOptions.IntersectionRule, drawingOptions.Transform, drawingOptions.TextContrast); + + // The write-back offset translates in device space, after the recorded transform, so the + // processed pixels land offset from where they were read; the read side subtracts the same + // offset when snapshotting the source region. + if (barrier.WriteBackOffset != default) + { + Matrix4x4 translated = applyOptions.Transform * Matrix4x4.CreateTranslation(barrier.WriteBackOffset.X, barrier.WriteBackOffset.Y, 0F); + applyOptions = new DrawingOptions(applyOptions.GraphicsOptions, applyOptions.IntersectionRule, translated, applyOptions.TextContrast); + } + + return new CompositionCommand( + CompositionCommandKind.Apply, + path, + null, + applyOptions, + barrier.OwnerLayer, + in rasterizerOptions, + barrier.TargetBounds, + default, + barrier.DestinationOffset, + null, + barrier.IsInsideLayer, + barrier, + Vector2.Zero); + } + + /// + /// Creates the rasterizer options used to generate coverage for an apply command region. + /// + /// The closed path defining the processed region. + /// The drawing options captured when the barrier was recorded. + /// The rasterizer options. + private static RasterizerOptions CreateApplyRasterizerOptions( + IPath path, + DrawingOptions options) + { + GraphicsOptions graphicsOptions = options.GraphicsOptions; + RasterizationMode rasterizationMode = graphicsOptions.Antialias + ? RasterizationMode.Antialiased + : RasterizationMode.Aliased; + + // Interest rectangles are transform-baked and destination-offset-relative, matching the + // fill and stroke producers. Snap outward to whole pixels so fractional path bounds + // never crop coverage. + RectangleF pathBounds = options.Transform == Matrix4x4.Identity + ? path.Bounds + : RectangleF.Transform(path.Bounds, options.Transform); + + Rectangle interest = Rectangle.FromLTRB( + (int)MathF.Floor(pathBounds.Left), + (int)MathF.Floor(pathBounds.Top), + (int)MathF.Ceiling(pathBounds.Right), + (int)MathF.Ceiling(pathBounds.Bottom)); + + return new RasterizerOptions( + interest, + options.IntersectionRule, + rasterizationMode, + graphicsOptions.AntialiasThreshold); + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs b/src/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs index d99829ed2..d859048f2 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/CompositionSceneCommandBase.cs @@ -62,6 +62,10 @@ public PathCompositionSceneCommand(in CompositionCommand command) /// /// Gets the wrapped composition command. /// + /// + /// The internal setter lets the batcher rewrite the command in place during flush + /// preparation (for example brush normalization) without reallocating the scene node. + /// public CompositionCommand Command { get; internal set; } /// @@ -83,6 +87,10 @@ public StrokePathCompositionSceneCommand(in StrokePathCommand command) /// /// Gets the wrapped stroke path command. /// + /// + /// The internal setter lets the batcher rewrite the command in place during flush + /// preparation (for example dash or transform expansion) without reallocating the scene node. + /// public StrokePathCommand Command { get; internal set; } /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md index 8352a19b3..6cd202f00 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md +++ b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_DRAWING_BACKEND.md @@ -1,6 +1,6 @@ # DefaultDrawingBackend -`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable scratch, and writes the result into a CPU destination buffer. +`DefaultDrawingBackend` is the CPU execution backend for ImageSharp.Drawing. It creates retained CPU scenes from prepared drawing command batches, executes those scenes with reusable worker-local scratch, and writes the result into a CPU destination buffer. This document explains the backend as a system rather than as a list of methods. The goal is to help a newcomer understand: @@ -8,7 +8,7 @@ This document explains the backend as a system rather than as a list of methods. - what problem the CPU backend is solving - why the backend is organized around a retained row-oriented execution plan - what `FlushScene` means in this architecture -- how rasterization, brush application, and layer composition fit together +- how rasterization, clipping, brush application, and layer composition fit together ## Where The CPU Backend Fits @@ -31,9 +31,10 @@ The backend still has to solve a hard scheduling problem. It needs to answer questions such as: -- which destination rows each command touches +- which destination row bands each command touches - how to preserve draw order while running work in parallel - how to avoid re-deriving geometry information in the hot loop +- how clips recorded as ordered stream commands become per-item clip state without serializing scene creation - where temporary memory should live and when it should be reused If the CPU backend executed commands directly from the incoming scene, each worker would repeatedly rediscover which rows matter, which parts of the geometry matter in those rows, and how much scratch is needed. That would push expensive planning work into the hottest part of the pipeline. @@ -50,11 +51,11 @@ The CPU backend is a flush executor, not a command-at-a-time painter. Its central idea is: -> convert a command batch into row-local raster work once, then execute rows directly with reusable worker-local scratch +> convert a command batch into row-local raster work once, then execute row bands in parallel with reusable worker-local scratch That is why the backend is built around `FlushScene`. -`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it can live with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume. +`FlushScene` is a retained execution plan. In non-retained rendering it is short-lived and disposed after one replay entry; in retained rendering it lives with the returned `DefaultDrawingBackendScene`. Its job is to take a prepared command stream and reorganize it into a form that is cheap for the row executor to consume. Execution itself lives in `DefaultDrawingBackend`, which walks the retained plan. If that idea is clear, most of the important types fall into place. @@ -65,9 +66,10 @@ If that idea is clear, most of the important types fall into place. `DefaultDrawingBackend` is the top-level CPU executor. It owns backend policy and orchestration: - acquiring a writable CPU destination -- creating the retained execution plan -- executing that plan +- creating the retained execution plan (`CreateScene`) +- executing that plan (`RenderScene`) - handling CPU layer composition +- pixel copy and readback services (`CopyPixels`, `ReadRegion`, `ComposeLayer`) It does not own every detail of geometry planning or scan conversion. @@ -75,9 +77,9 @@ It also does not own backend selection. By the time `CreateScene(...)` or `Rende ### Scene -In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands and explicit layer boundaries for one contiguous command range. +In the canvas architecture, the backend receives a `DrawingCommandBatch`. That batch already contains prepared commands plus explicit layer boundaries, clip scopes, and apply barriers for one contiguous command range, along with the `HasLayers`/`HasApply`/`HasClipControls` facts about the range. -For the CPU backend, that incoming batch is the starting point, not the final execution form. +For the CPU backend, that incoming batch is the starting point, not the final execution form. `CreateScene(...)` wraps the resulting `FlushScene` in a `DefaultDrawingBackendScene` so retained scenes can be re-rendered later. ### Flush Scene @@ -89,11 +91,10 @@ In this codebase, `FlushScene` means: It owns the retained information needed to make execution cheap: -- the visible prepared commands -- retained rasterizable geometry -- row membership -- row-local execution items -- scratch size requirements for the flush +- the retained fill and stroke scene items (`FillSceneItem`, `StrokeSceneItem`), each carrying its brush, graphics options, retained raster geometry, captured clip state, and destination offset +- retained layer state and, for scenes with apply barriers, control items +- the retained row lists (`SceneRow`), one per touched destination row band +- ordered target-wide segments (`SceneSegment`) when apply barriers or scoped layers split the scene ### Rasterizer @@ -101,7 +102,7 @@ It owns the retained information needed to make execution cheap: It is responsible for: -- fixed-point scan conversion +- fixed-point scan conversion of fills and strokes - fill-rule handling - coverage accumulation - emitting row coverage spans @@ -127,17 +128,20 @@ The important separation is: - the brush renderer decides color - the backend executor binds the two together +Renderers are memoized per scene item: `FillSceneItem.GetRenderer(...)` and `StrokeSceneItem.GetRenderer(...)` create the renderer on first use and cache it, and `RenderScene` warms every renderer before the parallel row pass so the hot loop never constructs one. + ### Worker State `WorkerState` is the reusable per-worker execution state. It owns worker-local scratch such as: -- raster scratch -- brush workspace -- the coverage row handler state +- raster scratch (`DefaultRasterizer.WorkerScratch`) +- a second scratch reserved for path clip rasterization +- a reusable path clip coverage buffer +- the brush workspace -This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass. +This is how the backend avoids allocating fresh buffers for every row item during the hot parallel pass. Each `Parallel.For` worker gets one instance through `localInit` and disposes it in `localFinally`. ## The Big Picture Flow @@ -145,23 +149,25 @@ The easiest way to understand the backend is to follow one command batch from sc ```mermaid flowchart TD - A[DrawingCanvas disposal replay] --> B[DefaultDrawingBackend.CreateScene] + A[DrawingCanvas replay] --> B[DefaultDrawingBackend.CreateScene] B --> C[FlushScene.Create] - C --> D[Prepare visible items] - D --> E[Build row-local execution plan] - E --> F[DefaultDrawingBackend.RenderScene] - F --> G[Acquire CPU destination] - G --> H[Execute rows in parallel] - H --> I[DefaultRasterizer emits coverage] - I --> J[BrushRenderer shades pixels] - J --> K[Destination frame updated] + C --> D[Partition commands and resolve clip seeds] + D --> E[Retain raster geometry and row plan per partition] + E --> F[Merge partitions into scene rows and segments] + F --> G[DefaultDrawingBackend.RenderScene] + G --> H[Acquire CPU destination and warm renderers] + H --> I[Execute row bands in parallel] + I --> J[DefaultRasterizer emits coverage] + J --> K[Clip stack narrows coverage] + K --> L[BrushRenderer shades pixels] + L --> M[Destination frame updated] ``` There are three major stages in that flow: 1. build the retained execution plan 2. establish the destination frame -3. execute rows using that plan +3. execute row bands using that plan ## What `DefaultDrawingBackend` Owns @@ -169,11 +175,11 @@ There are three major stages in that flow: Its responsibilities are: -- create a `FlushScene` +- create a `FlushScene` and wrap it in a `DefaultDrawingBackendScene` - acquire a writable CPU region from the target frame - execute that scene - provide CPU layer composition services -- manage frame usage for CPU-backed targets +- provide pixel copy and readback for CPU-backed targets The expensive work is delegated: @@ -191,44 +197,45 @@ The canvas layer above that split is also important: ## Building The Flush Scene -`FlushScene.Create(...)` turns the prepared command stream into an execution plan in several phases. Each phase changes the data into a form that is cheaper for the next phase to consume. +`FlushScene.Create(...)` turns the prepared command stream into an execution plan. Scene creation itself is parallel: the command range is split into contiguous partitions and prepared by a `Parallel.For`. ```mermaid flowchart LR - A[Prepared commands] --> B[Filter and compact visible work] - B --> C[Create retained raster geometry] - C --> D[Build row membership] - D --> E[Build row-local execution items] - E --> F[FlushScene] + A[Prepared commands] --> B[Sequential clip prescan per partition boundary] + B --> C[Parallel partitions retain raster geometry] + C --> D[Partition row builders record row membership] + D --> E[Merge partitions in order] + E --> F[Finalize rows and segments] + F --> G[FlushScene] ``` -### 1. Filter and compact visible work +### 1. Resolve clip seeds at partition boundaries -The scene builder begins from the incoming command stream and keeps only the work that is visible and relevant to the flush. The later phases should not pay repeatedly for invisible commands through sparse scans or conditional branching. +The ordered begin/end-clip command stream is the single source of truth for clipping. Because partitions process disjoint command ranges in parallel, a single sequential prescan (`CreatePartitionClipSeeds`) walks the stream once and records, for each partition's first command, the clip scopes opened by earlier partitions. Each partition then seeds a `ClipStreamTracker` from that snapshot and tracks the stream independently, so preparation stays parallel without losing clip correctness. -### 2. Create retained raster geometry +### 2. Retain raster geometry per partition -For each visible item, the builder decomposes the command's drawing matrix into an X/Y scale and the rotation-shear-translation-perspective residual, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands both the geometry and the residual to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair — cached on the `IPath` — and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten. +Each partition (`ProcessPartition`) walks its command range in order. Clip commands only mutate the tracker; every draw command that follows captures the composed `DrawingClipState` and clip anchor from the tracker. For each visible fill or stroke, the builder decomposes the command's drawing matrix with `MatrixUtilities.GetScale` and `MatrixUtilities.GetResidual`, asks the path for its scale-baked `LinearGeometry` via `ToLinearGeometry(Vector2 scale)`, and hands the geometry plus the residual matrix to `DefaultRasterizer` to create the retained rasterizable payload. Curve subdivision therefore happens once per (path, scale) pair, cached on the `IPath`, and any per-frame rotation or translation rides into the rasterizer as the residual without forcing the path to re-flatten. This step matters because it moves expensive geometry preparation out of the hot row loop and out of every frame of workloads like text or panning that drift only in their residual. ### 3. Build row membership -Once retained geometry exists, the scene builder determines which scene rows each item touches. That produces row-local membership information while preserving original submission order within every row. +As each partition retains geometry, it appends row operations into partition-local row builders, one slot per destination row band. Partitions cover contiguous ascending command ranges, so merging their row builders in ascending partition order preserves painter's order within every row. -That detail is critical. Parallel execution is allowed, but draw order must remain deterministic within each row. +That detail is critical. Parallel scene creation is allowed, but draw order must remain deterministic within each row band. -### 4. Build row-local execution items +### 4. Finalize rows and segments -The scene then materializes the payload that the row executor will visit. Each row item points into flush-owned retained storage and carries just enough metadata to reconstruct a cheap `RasterizableBand` view when execution reaches that row. +The merged builders become `SceneRow` values, each pointing at a block chain of `SceneOperation` entries that reference flush-owned retained storage. When the batch contains apply barriers, the rows are additionally reorganized into ordered `SceneSegment` values so barriers execute at the right point; scoped layers (layers containing an apply) become `ScopedLayerSceneItem` segments of their own. At that point the scene is execution-ready. ## Why The Backend Is Row-First -The CPU backend executes rows, not commands. +The CPU backend executes row bands, not commands. -This is one of the most important architectural choices in the whole path. +A scene row is one horizontal band of the target, `DefaultRasterizer.DefaultTileHeight` (16) pixels tall, aligned to an absolute tile grid. Each `SceneRow` owns one disjoint band, so rows can execute in parallel with no synchronization: no two workers ever write the same destination pixel. Why it helps: @@ -241,26 +248,33 @@ A row-first executor fits the actual shape of CPU rendering much better than a c ## The Execution Pass -When `FlushScene.Execute(...)` runs, the backend prepares brush renderers and then executes scene rows in parallel. +`RenderScene(...)` validates the target, acquires the CPU region, warms the memoized brush renderers, and then executes the plan. + +If the scene has segments (apply barriers or scoped layers), `ExecuteSceneSegments` runs them strictly in order: the rows preceding a barrier must be complete before the barrier runs, because apply items read the pixels those rows produced and layer composition must observe everything drawn beneath the layer. Within each segment, rows still execute in parallel. + +Otherwise `ExecuteSceneRows` runs one `Parallel.For` over the scene rows. ```mermaid sequenceDiagram - participant Exec as FlushScene.Execute + participant Render as DefaultDrawingBackend.RenderScene participant Worker as WorkerState participant Raster as DefaultRasterizer participant Brush as BrushRenderer - Exec->>Brush: create one renderer per visible item - Exec->>Worker: start parallel row pass - Worker->>Exec: enumerate row items in order - Worker->>Raster: ExecuteRasterizableBand(...) - Raster-->>Exec: coverage rows - Exec->>Brush: Apply(...) + Render->>Brush: warm memoized renderers per scene item + Render->>Worker: Parallel.For over scene rows (localInit) + Worker->>Worker: replay row operations via SceneOperationCursor + Worker->>Raster: ExecuteRasterizableItem / ExecuteStrokeRasterizableItem + Raster-->>Worker: coverage rows + Worker->>Brush: Apply(...) after clip narrowing + Render->>Worker: Dispose (localFinally) ``` +Each row replays its operation stream through a `SceneOperationCursor`. The operations are `FillItem`, `StrokeItem`, `BeginLayer`, and `EndLayer`. Layer execution is recursive rather than stack-backed: `BeginLayer` allocates a clean temporary `BandTarget`, the nested call renders into it from the same cursor, and the band is blended back with the layer's graphics options when the scope ends. + There are two important ownership patterns in that pass: -- renderers are created once per visible item before the hot row loop +- renderers are created once per scene item (memoized) and warmed before the hot row loop - scratch and workspace are reused per worker during the row loop That is one of the backend's main performance properties. @@ -276,36 +290,54 @@ The rasterizer and the backend solve different problems. - which items execute - when they execute - where their coverage belongs in the destination +- which clips narrow that coverage - which brush renderer should consume that coverage That separation is intentional. It lets the rasterizer stay geometry-focused while the backend handles composition and destination layout. -## Coverage Routing +## Coverage Routing And Clipping The rasterizer does not write destination pixels directly. Instead it emits row coverage through a handler supplied by the backend. -The backend-side row handler: +The backend-side row handler, `FillCoverageRowHandler`: -- receives emitted coverage -- maps band-local coordinates back into destination coordinates -- slices the correct destination row -- invokes the correct `BrushRenderer` +- receives emitted coverage in absolute coordinates +- clips the span to the active band target and slices the correct destination row +- applies the retained clip stack captured with the scene item, recursively narrowing or scaling the span +- invokes the correct `BrushRenderer` with the surviving coverage ```mermaid flowchart LR A[Rasterizer coverage row] --> B[Row handler] - B --> C[Map to destination slice] - C --> D[BrushRenderer.Apply] - D --> E[Pixels updated] + B --> C[Clip to band target] + C --> D[Apply clip descriptors] + D --> E[BrushRenderer.Apply] + E --> F[Pixels updated] ``` +Clip descriptors are applied by kind: rectangle clips narrow the span analytically, integer-region and region clips test rectangle membership, and path clips multiply the span by coverage rasterized from `PreparedPathClipState`, retained clip raster data built once per scene item at scene-creation time. Descriptors are recorded in canvas space and anchored through the item's destination offset so clips track the drawn geometry. + This is why the brush renderer can stay target-unbound. It receives the destination row slice and coverage data at execution time rather than owning the destination frame itself. +## Apply Barriers + +An apply item runs a caller-supplied image processor over a path region as part of scene execution. + +`ExecuteApplyItem`: + +1. copies the covered target rectangle into a temporary `Image` +2. runs the recorded operation through `Mutate` +3. writes the processed pixels back through an `ImageBrush` driven by the apply item's retained coverage, in parallel over the item's row bands + +Write-back uses Src semantics: the item's graphics options were cloned via `CloneForClearOperation` (Src alpha composition at full blend), so the processed pixels replace the covered region outright, including transparency, instead of blending over it. Rows preceding the barrier are guaranteed complete because segments execute in order. + ## Layer Composition CPU layer composition is a separate concern from path rasterization. -`ComposeLayer()` composites one CPU frame into another using `PixelBlender`. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage. +Inline layers composite band-by-band during row execution (`CompositeLayerBand`), and scoped layers composite as a whole after their segments finish (`CompositeLayerTarget`), both using `PixelBlender` with the layer's graphics options. Scoped composition partitions the overlap on the same absolute tile grid used for rendering so the parallel blend never shares a destination row between workers. + +`ComposeLayer()` additionally exposes frame-to-frame composition using `PixelBlender` for callers outside scene execution. That path exists because compositing an already-rasterized layer is a different problem from scanning geometry into coverage. Keeping those paths separate makes the backend easier to reason about. @@ -313,33 +345,33 @@ Keeping those paths separate makes the backend easier to reason about. The backend aligns ownership with the actual execution lifetime. -### Flush-owned +### Scene-owned -Owned by `FlushScene`: +Owned by `FlushScene` (and therefore by the retained `DefaultDrawingBackendScene`): -- visible item arrays -- row structures -- retained raster data -- start-cover storage +- retained fill and stroke scene items, including their memoized renderers +- retained raster geometry and start-cover storage +- retained path clip raster data +- row and segment structures -Disposed when the flush ends. +Disposed when the scene is disposed; short-lived scenes are disposed after one replay entry. ### Worker-owned Owned by `WorkerState` during execution: -- raster scratch +- raster scratch and path-clip scratch +- path-clip coverage buffer - brush workspace -Disposed when the worker completes. - -### Item-owned +Disposed when the worker completes (`localFinally`). -Created once per visible item during execution: +### Execution-scoped -- `BrushRenderer` +Created during execution and released with it: -Retained for the duration of the row pass and then released with the flush-owned scene item state. +- temporary layer `BandTarget` buffers +- the temporary apply source image That ownership model keeps allocation and disposal aligned with real work lifetime. @@ -362,7 +394,7 @@ canvas and backend selection -> backend orchestration -> retained row planning - ## The Mental Model To Keep -The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work with reusable scratch. +The easiest way to keep this backend straight is to remember that it is not a command-at-a-time painter. It is a flush executor that converts visible commands into row-local retained raster work and then executes that work in parallel with reusable scratch. If that model is clear, the major types fall into place: diff --git a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md index 7682a841d..673899dcf 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md +++ b/src/ImageSharp.Drawing/Processing/Backends/DEFAULT_RASTERIZER.md @@ -1,6 +1,6 @@ # DefaultRasterizer -`DefaultRasterizer` is the CPU polygon scanner used by the retained fill path in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels. +`DefaultRasterizer` is the CPU polygon scanner used by the retained fill and stroke paths in ImageSharp.Drawing. Its job is narrow but central: take already-prepared geometry, convert that geometry into fixed-point edge contributions, and emit coverage rows that the CPU backend can turn into pixels. This rasterizer is based on ideas and implementation techniques from the Blaze project: @@ -62,7 +62,7 @@ If that distinction is clear, the code becomes much easier to follow. ### Rasterizer -`DefaultRasterizer` is the geometry-to-coverage engine. +`DefaultRasterizer` is the geometry-to-coverage engine. It is a static class: all mutable state lives in worker-owned scratch, so the static members are safe to call from parallel workers without synchronization. It is responsible for: @@ -87,32 +87,32 @@ In this codebase, retained geometry means: "the fixed-point, band-local line data and start-cover seeds needed to rasterize one prepared shape later without revisiting its original contour data" -That retained form is stored in `RasterizableGeometry`. +That retained form is stored in `RasterizableGeometry` for fills and `StrokeRasterizableGeometry` for strokes. ### Band -A band is one small vertical slice of a shape's retained geometry. +A band is one small vertical slice of a shape's retained geometry, `DefaultTileHeight` (16) pixels tall and aligned to an absolute tile grid. -The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded. +The rasterizer does not keep one giant scene-wide edge table. It stores data in row bands so execution can stay local and bounded, and so the backend can execute bands in parallel. ### Rasterizable Geometry -`RasterizableGeometry` is the retained representation of one prepared shape. +`RasterizableGeometry` is the retained representation of one prepared fill shape. It stores: -- clipped local bounds -- band-local metadata -- retained line arrays -- optional start-cover seeds for bands that need carry-in winding +- the first row-band index and band count covered by the clipped shape +- per-band metadata (`RasterizableBandInfo`) +- retained line block chains for each band +- optional start-cover arrays for bands that need carry-in winding This is the retained object that the CPU backend keeps in `FlushScene`. -### Rasterizable Band +### Rasterizable Item -A `RasterizableBand` is the execution-time view over one retained band of one retained shape. +A `RasterizableItem` is the execution-time view over one retained band of one retained shape: the retained geometry plus a local row index. `StrokeRasterizableItem` is the stroke equivalent. -It is the immediate input to `ExecuteRasterizableBand(...)`. +Together with the band's `RasterizableBandInfo`, it is the immediate input to `ExecuteRasterizableItem(...)` and `ExecuteStrokeRasterizableItem(...)`. ### Context @@ -124,7 +124,7 @@ It is a `ref struct` because it is tied directly to worker-owned scratch spans a Coverage is the rasterizer's output. -The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage to a `BrushRenderer`, which decides how the destination pixels should be shaded. +The rasterizer does not decide final pixel colors. It decides how much geometric coverage each pixel receives. The backend later passes that coverage, after clip narrowing, to a `BrushRenderer`, which decides how the destination pixels should be shaded. ## Pipeline Placement @@ -132,21 +132,21 @@ The rasterizer sits in the middle of the CPU backend pipeline. Upstream: -- `CompositionCommand` preparation produces prepared geometry +- command preparation produces prepared geometry - the typed canvas implementation and `DrawingCanvasBatcher` have already selected and called the CPU backend - `FlushScene` decides which items are visible and when they execute Downstream: - the rasterizer emits row coverage -- `DefaultDrawingBackend` routes that coverage into `BrushRenderer.Apply(...)` +- `DefaultDrawingBackend` routes that coverage through the clip stack into `BrushRenderer.Apply(...)` ```mermaid flowchart TD A[Prepared geometry] --> B[DefaultRasterizer.CreateRasterizableGeometry] B --> C[RasterizableGeometry] - C --> D[Build RasterizableBand view] - D --> E[ExecuteRasterizableBand] + C --> D[Build RasterizableItem per band] + D --> E[ExecuteRasterizableItem] E --> F[Coverage rows] F --> G[Brush renderer] ``` @@ -172,29 +172,35 @@ This phase: - records visible line pieces into retained line storage - records left-of-band winding influence into start-cover tables -The output is `RasterizableGeometry`. +The output is `RasterizableGeometry`. The stroke builders (`CreatePathStrokeRasterizableGeometry`, `CreateLineSegmentStrokeRasterizableGeometry`) produce `StrokeRasterizableGeometry` the same way. ### Phase 2: band execution -`ExecuteRasterizableBand(...)` is the hot execution entry point. +`ExecuteRasterizableItem(...)` is the hot execution entry point for fills; `ExecuteStrokeRasterizableItem(...)` is the stroke equivalent. -It does not revisit the original contour data. It receives a `RasterizableBand` view over retained data and performs the minimum work needed to emit coverage rows for that band. +It does not revisit the original contour data. It receives a `RasterizableItem` over retained data plus the band's `RasterizableBandInfo` and performs the minimum work needed to emit coverage rows for that band. ```mermaid sequenceDiagram - participant Exec as ExecuteRasterizableBand + participant Exec as ExecuteRasterizableItem participant Ctx as Context participant Emit as Coverage Row Handler Exec->>Ctx: Reconfigure(...) Exec->>Ctx: SeedStartCovers(...) - Exec->>Ctx: Rasterize retained lines + Exec->>Ctx: Iterate retained line blocks Exec->>Ctx: EmitCoverageRows(...) Ctx-->>Emit: coverage rows Exec->>Ctx: ResetTouchedRows() ``` -That separation is one of the key reasons the retained fill path performs well. Expensive geometry work happens once; execution consumes compact band-local data. +That separation is one of the key reasons the retained path performs well. Expensive geometry work happens once; execution consumes compact band-local data. + +## Parallel Execution Model + +Execution is parallel across row bands. The drawing backend runs a `Parallel.For` over scene rows (or over a single geometry's row bands for apply items), and each worker replays the retained items that touch its band sequentially into a worker-local `Context`. + +All static members of the rasterizer are stateless; every piece of mutable state lives in a `WorkerScratch` owned by exactly one worker. The scratch is created through `CreateWorkerScratch(...)`, sized for the widest item the worker will execute, and reused across bands by reconfiguration rather than reallocation. No synchronization is required because each scene row owns a disjoint destination band. ## Fixed-Point Precision @@ -223,7 +229,7 @@ This gives the backend several important properties: - execution only touches the band it is currently composing - left-of-band winding can be precomputed - scratch requirements stay bounded -- row-oriented execution consumes compact band-local payloads +- row-oriented parallel execution consumes compact band-local payloads ```mermaid flowchart TD @@ -240,21 +246,18 @@ flowchart TD That includes: -- the local bounds of the prepared shape -- band count and band-local metadata -- retained line arrays for each band +- the clipped band placement of the prepared shape (first row-band index, band count, width) +- per-band metadata in `RasterizableBandInfo` (line counts, destination placement, fill rule, rasterization mode) +- retained line block chains for each band - optional start-cover arrays for bands that need carry-in winding -The retained line arrays use specialized storage formats such as: - -- `LineArrayX16Y16` -- `LineArrayX32Y16` +The retained line data uses specialized storage formats: `LineArrayX16Y16` and `LineArrayX32Y16` collect lines during building, and their retained block chains (`LineArrayX16Y16Block`, `LineArrayX32Y16Block`) are what execution iterates. Narrow geometries (band width below 128 pixels) pack both X endpoints into one 32-bit word, halving retained line memory. These are storage-oriented types. They exist to retain compact fixed-point line segments so execution does not need to revisit contour data. ## The Linearizer -The linearizer is the retained-geometry builder. It is generic over line-array storage, but the conceptual work is the same across variants. +The linearizer is the retained-geometry builder. `Linearizer` is generic over line-array storage with concrete `LinearizerX16Y16` and `LinearizerX32Y16` variants, but the conceptual work is the same across them. Its responsibilities are: @@ -270,9 +273,9 @@ For a newcomer, the most important thing to understand is that the linearizer is ### Residual transform application -The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points — the effective X/Y scale of the drawing matrix has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4 residual`, which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` / `ProcessUncontained` for fills, and at bounds / closure / contour-segment construction sites in the stroke linearizer. +The prepared `LinearGeometry` passed to `CreateRasterizableGeometry(...)` carries scale-baked points: the effective X/Y scale of the drawing matrix (extracted with `MatrixUtilities.GetScale`) has already been absorbed into the flattened contour, so curve subdivision happens at device-scale precision. The remaining rotation, shear, translation, and perspective is handed to the rasterizer as a separate `Matrix4x4` residual (`MatrixUtilities.GetResidual`), which the linearizer applies per-point where the contour is read: at segment emission time in `ProcessContained` and `ProcessUncontained` for fills, and at the point-reading sites of the stroke linearizer. -This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves. +This split keeps the scale-baked geometry cacheable across frames (text and panning workloads reuse the same bake at a fixed zoom, cached on the `IPath` per scale) while letting per-frame rotation or translation ride through the rasterizer without re-subdividing curves. ### Contained lines @@ -291,6 +294,17 @@ This is one of the most important ideas in the retained design: - visible geometry becomes retained lines - invisible left-of-band winding becomes retained start-cover seeds +## Stroke Rasterization + +Strokes reach the rasterizer as centerline geometry plus a pen; they are not pre-expanded to fill paths by the canvas. + +`StrokeRasterizableGeometry` wraps a retained stroke payload (`StrokeRasterData`) that rasterizes one band at execution time. There are two payload strategies: + +- `RetainedStrokeRasterData` holds a fill-style `RasterizableGeometry` produced by the stroke linearizer, which expands the centerline into an outline (joins, miters, arcs, caps) once during building. Band execution then replays that outline exactly like a fill. +- `LineSegmentStrokeRasterData` retains just the two endpoints of a solid line segment and rasterizes the band directly through `DirectLineSegmentBandRasterizer`, which supersamples four vertical positions per pixel row instead of building an edge table. + +The stroke width is scaled separately by the transform's isotropic width scale so expansion runs in device-space pixels. `ExecuteStrokeRasterizableItem(...)` reconfigures the shared `Context` and delegates to the payload's `ExecuteBand(...)`. + ## The Execution Context `DefaultRasterizer.Context` is the mutable fixed-point scanning state used during band execution. @@ -333,7 +347,7 @@ Rows also track sparse touched-column information through bit vectors, so the em ```mermaid flowchart TD A[Rasterize fixed-point line] --> B[Decompose into touched cells] - B --> C["AddCell(row, column, deltaCover, deltaArea)"] + B --> C["AddCell(row, column, delta, area)"] C --> D[Update coverArea] C --> E[Mark bitVectors] C --> F[Track touched rows and bounds] @@ -352,12 +366,12 @@ For each touched row, the emitter: 1. starts from the seeded `startCover` 2. walks the row's touched columns using the bit vectors -3. updates the running cover from `deltaCover` -4. combines running cover and `deltaArea` into signed area +3. updates the running cover from the accumulated cover deltas +4. combines running cover and accumulated area into signed area 5. converts signed area into normalized coverage using the selected fill rule -6. coalesces equal-coverage spans -7. writes only non-zero spans into the reusable scanline buffer -8. invokes the row callback +6. coalesces equal-coverage spans and buffers contiguous non-zero runs +7. writes only non-zero coverage into the reusable scanline buffer +8. invokes the row callback once per contiguous non-zero run ```mermaid flowchart LR @@ -420,6 +434,7 @@ The backend decides: - which scene items execute - which retained band is being scanned - which destination slice receives the coverage +- which clips narrow the coverage - which brush renderer consumes the emitted spans That separation is one of the main architectural advantages of the current CPU path. @@ -435,9 +450,9 @@ If you are new to this part of the library, read the rasterizer in this order: 5. `FlushScene.cs` 6. `CreateRasterizableGeometry(...)` in `DefaultRasterizer.cs` 7. `Linearizer` and the concrete linearizers in `DefaultRasterizer.Linearizer.cs` -8. retained line types in `DefaultRasterizer.RetainedTypes.cs` -9. `ExecuteRasterizableBand(...)` in `DefaultRasterizer.cs` -10. `Context` in `DefaultRasterizer.cs` +8. retained line and item types in `DefaultRasterizer.RetainedTypes.cs` and `DefaultRasterizer.RasterizableGeometry.cs` +9. `ExecuteRasterizableItem(...)` and `Context` in `DefaultRasterizer.cs` +10. stroke payloads in `DefaultRasterizer.Stroke.cs` and `DefaultRasterizer.StrokeLinearizer.cs` That order mirrors the data lifecycle: diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs index d653ed5ef..5b2577bac 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.Helpers.cs @@ -1,6 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; +using System.Numerics; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -19,22 +22,38 @@ public sealed partial class DefaultDrawingBackend { private readonly BrushRenderer renderer; private readonly BandTarget target; + private readonly DrawingClipState clipState; + private readonly PreparedPathClipState? pathClipState; + private readonly Point destinationOffset; private readonly BrushWorkspace brushWorkspace; + private readonly WorkerState workerState; /// /// Initializes a new instance of the struct. /// /// The brush renderer that will consume emitted coverage spans. /// The active band target being rendered. + /// The exact clip state captured with the retained scene item. + /// The retained path clip raster data captured with the retained scene item. + /// The destination offset used to place clip descriptors. /// The worker-local brush workspace. + /// The worker-local execution state. public FillCoverageRowHandler( BrushRenderer renderer, BandTarget target, - BrushWorkspace brushWorkspace) + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, + BrushWorkspace brushWorkspace, + WorkerState workerState) { this.renderer = renderer; this.target = target; + this.clipState = clipState; + this.pathClipState = pathClipState; + this.destinationOffset = destinationOffset; this.brushWorkspace = brushWorkspace; + this.workerState = workerState; } /// @@ -65,7 +84,772 @@ public void Handle(int y, int startX, Span coverage) Span destinationRow = this.target.Region .DangerousGetRowSpan(localY) .Slice(clipStartX - this.target.AbsoluteLeft, clippedLength); - this.renderer.Apply(destinationRow, coverage.Slice(coverageOffset, clippedLength), clipStartX, y, this.brushWorkspace); + + Span clippedCoverage = coverage.Slice(coverageOffset, clippedLength); + this.ApplyClipDescriptors(0, y, clipStartX, destinationRow, clippedCoverage); + } + + /// + /// Applies the retained clip stack to a coverage span, then forwards surviving coverage to the brush renderer. + /// + /// The current clip descriptor index. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyClipDescriptors( + int clipIndex, + int y, + int startX, + Span destination, + Span coverage) + { + if (coverage.Length == 0) + { + return; + } + + if (clipIndex == this.clipState.Count) + { + // Clip descriptors are applied recursively by narrowing or scaling the active span. + // Once the stack is exhausted, the brush sees only pixels that survived every clip. + this.renderer.Apply(destination, coverage, startX, y, this.brushWorkspace); + return; + } + + DrawingClipDescriptor descriptor = this.clipState.GetDescriptor(clipIndex); + switch (descriptor.Kind) + { + case DrawingClipKind.Rectangle: + this.ApplyRectangleClip(clipIndex, descriptor, y, startX, destination, coverage); + break; + + case DrawingClipKind.IntegerRegion: + this.ApplyIntegerRegionClip(clipIndex, descriptor, y, startX, destination, coverage); + break; + + case DrawingClipKind.Region: + this.ApplyRegionClip(clipIndex, descriptor, y, startX, destination, coverage); + break; + + default: + this.ApplyPathClip(clipIndex, descriptor, y, startX, destination, coverage); + break; + } + } + + /// + /// Applies one rectangle clip descriptor to a coverage span. + /// + /// The current clip descriptor index. + /// The rectangle clip descriptor. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRectangleClip( + int clipIndex, + DrawingClipDescriptor descriptor, + int y, + int startX, + Span destination, + Span coverage) + { + // Clip descriptors are recorded in canvas space; the item's destination offset + // anchors them into the space the rasterizer emits so clips track the drawn geometry. + RectangleF rectangle = descriptor.Rectangle; + rectangle.Offset(this.destinationOffset.X, this.destinationOffset.Y); + + if (descriptor.Operation == ClipOperation.Difference) + { + this.ApplyRectangleDifference(clipIndex, descriptor, rectangle, y, startX, destination, coverage); + return; + } + + this.ApplyRectangleIntersection(clipIndex, descriptor, rectangle, y, startX, destination, coverage); + } + + /// + /// Applies an intersecting rectangle clip to a coverage span. + /// + /// The current clip descriptor index. + /// The rectangle clip descriptor. + /// The destination-space rectangle. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRectangleIntersection( + int clipIndex, + DrawingClipDescriptor descriptor, + RectangleF rectangle, + int y, + int startX, + Span destination, + Span coverage) + { + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + if (yCoverage <= 0F) + { + return; + } + + int clipStart = Math.Max(startX, (int)MathF.Floor(rectangle.Left)); + int clipEnd = Math.Min(startX + coverage.Length, (int)MathF.Ceiling(rectangle.Right)); + if (clipEnd <= clipStart) + { + return; + } + + this.ApplyRectangleInterior(clipIndex, descriptor, rectangle, yCoverage, y, startX, clipStart, clipEnd, destination, coverage); + } + + /// + /// Applies a subtracting rectangle clip to a coverage span. + /// + /// The current clip descriptor index. + /// The rectangle clip descriptor. + /// The destination-space rectangle. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRectangleDifference( + int clipIndex, + DrawingClipDescriptor descriptor, + RectangleF rectangle, + int y, + int startX, + Span destination, + Span coverage) + { + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + if (yCoverage <= 0F) + { + this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination, coverage); + return; + } + + int spanEnd = startX + coverage.Length; + int clipStart = Math.Max(startX, (int)MathF.Floor(rectangle.Left)); + int clipEnd = Math.Min(spanEnd, (int)MathF.Ceiling(rectangle.Right)); + if (clipEnd <= clipStart) + { + this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination, coverage); + return; + } + + // Difference clips preserve the span outside the rectangle and invert only the + // overlapping interior, so the outside ranges can advance to the next clip unchanged. + this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination[..(clipStart - startX)], coverage[..(clipStart - startX)]); + this.ApplyRectangleDifferenceInterior(clipIndex, descriptor, rectangle, yCoverage, y, startX, clipStart, clipEnd, destination, coverage); + this.ApplyClipDescriptors(clipIndex + 1, y, clipEnd, destination[(clipEnd - startX)..], coverage[(clipEnd - startX)..]); + } + + /// + /// Applies the interior of an intersecting rectangle clip to a coverage span. + /// + /// The current clip descriptor index. + /// The rectangle clip descriptor. + /// The destination-space rectangle. + /// The vertical coverage contribution for the current row. + /// The absolute destination row. + /// The absolute start column of and . + /// The first absolute column overlapped by the rectangle. + /// The exclusive absolute column where rectangle overlap ends. + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRectangleInterior( + int clipIndex, + DrawingClipDescriptor descriptor, + RectangleF rectangle, + float yCoverage, + int y, + int spanStart, + int clipStart, + int clipEnd, + Span destination, + Span coverage) + { + int x = clipStart; + while (x < clipEnd) + { + float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold); + int runStart = x++; + float multiplier = xCoverage * yCoverage; + + // Rectangle edge coverage is constant across interior pixels and only changes + // at the left/right edge pixels, so collapse adjacent equal-coverage columns. + while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold) == xCoverage) + { + x++; + } + + this.ApplyCoverageRun(clipIndex + 1, y, runStart, x, spanStart, multiplier, destination, coverage); + } + } + + /// + /// Applies the interior of a subtracting rectangle clip to a coverage span. + /// + /// The current clip descriptor index. + /// The rectangle clip descriptor. + /// The destination-space rectangle. + /// The vertical coverage contribution for the current row. + /// The absolute destination row. + /// The absolute start column of and . + /// The first absolute column overlapped by the rectangle. + /// The exclusive absolute column where rectangle overlap ends. + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRectangleDifferenceInterior( + int clipIndex, + DrawingClipDescriptor descriptor, + RectangleF rectangle, + float yCoverage, + int y, + int spanStart, + int clipStart, + int clipEnd, + Span destination, + Span coverage) + { + int x = clipStart; + while (x < clipEnd) + { + float xCoverage = GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold); + int runStart = x++; + float multiplier = 1F - (xCoverage * yCoverage); + + // Subtraction uses the inverse coverage of the rectangle, but the same run + // coalescing applies: edge pixels differ, interior pixels share one value. + while (x < clipEnd && GetAxisCoverage(x, rectangle.Left, rectangle.Right, descriptor.EdgeMode, descriptor.AntialiasThreshold) == xCoverage) + { + x++; + } + + this.ApplyCoverageRun(clipIndex + 1, y, runStart, x, spanStart, multiplier, destination, coverage); + } + } + + /// + /// Applies an integer-region clip descriptor to a coverage span. + /// + /// The current clip descriptor index. + /// The integer-region clip descriptor. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyIntegerRegionClip( + int clipIndex, + DrawingClipDescriptor descriptor, + int y, + int startX, + Span destination, + Span coverage) + { + IReadOnlyList rectangles = descriptor.IntegerRectangles; + if (descriptor.Operation == ClipOperation.Difference) + { + int cursor = startX; + int endX = startX + coverage.Length; + + // Integer regions are hard-edged, so difference clips can split the span into + // retained gaps without allocating or rasterizing a mask. + for (int i = 0; i < rectangles.Count && cursor < endX; i++) + { + Rectangle rectangle = rectangles[i]; + rectangle.Offset(this.destinationOffset); + if ((uint)(y - rectangle.Top) >= (uint)rectangle.Height || rectangle.Right <= cursor) + { + continue; + } + + if (rectangle.Left > cursor) + { + int runEnd = Math.Min(rectangle.Left, endX); + this.ApplyIntegerRun(clipIndex + 1, y, cursor, startX, runEnd, destination, coverage); + } + + cursor = Math.Max(cursor, rectangle.Right); + } + + this.ApplyIntegerRun(clipIndex + 1, y, cursor, startX, endX, destination, coverage); + return; + } + + for (int i = 0; i < rectangles.Count; i++) + { + Rectangle rectangle = rectangles[i]; + rectangle.Offset(this.destinationOffset); + if ((uint)(y - rectangle.Top) >= (uint)rectangle.Height) + { + continue; + } + + int runStart = Math.Max(startX, rectangle.Left); + int runEnd = Math.Min(startX + coverage.Length, rectangle.Right); + this.ApplyIntegerRun(clipIndex + 1, y, runStart, startX, runEnd, destination, coverage); + } + } + + /// + /// Applies a floating-point region clip descriptor to a coverage span. + /// + /// The current clip descriptor index. + /// The region clip descriptor. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyRegionClip( + int clipIndex, + DrawingClipDescriptor descriptor, + int y, + int startX, + Span destination, + Span coverage) + { + IReadOnlyList rectangles = descriptor.Rectangles; + if (descriptor.Operation == ClipOperation.Difference) + { + this.ApplyFloatingRegionDifference(clipIndex, descriptor, rectangles, y, startX, destination, coverage); + return; + } + + for (int i = 0; i < rectangles.Count; i++) + { + RectangleF rectangle = rectangles[i]; + rectangle.Offset(this.destinationOffset.X, this.destinationOffset.Y); + this.ApplyRectangleIntersection(clipIndex, descriptor, rectangle, y, startX, destination, coverage); + } + } + + /// + /// Applies a subtracting floating-point region clip to a coverage span. + /// + /// The current clip descriptor index. + /// The region clip descriptor. + /// The region rectangles in descriptor order. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyFloatingRegionDifference( + int clipIndex, + DrawingClipDescriptor descriptor, + IReadOnlyList rectangles, + int y, + int startX, + Span destination, + Span coverage) + { + int cursor = startX; + int endX = startX + coverage.Length; + + // The cursor marks the prefix already emitted to the next clip. This avoids + // revisiting or re-emitting spans when a region contributes multiple rectangles. + for (int i = 0; i < rectangles.Count && cursor < endX; i++) + { + RectangleF rectangle = rectangles[i]; + rectangle.Offset(this.destinationOffset.X, this.destinationOffset.Y); + + float yCoverage = GetAxisCoverage(y, rectangle.Top, rectangle.Bottom, descriptor.EdgeMode, descriptor.AntialiasThreshold); + if (yCoverage <= 0F) + { + continue; + } + + int clipStart = Math.Max(startX, (int)MathF.Floor(rectangle.Left)); + int clipEnd = Math.Min(endX, (int)MathF.Ceiling(rectangle.Right)); + if (clipEnd <= cursor) + { + continue; + } + + if (clipStart > cursor) + { + int runEnd = Math.Min(clipStart, endX); + this.ApplyIntegerRun(clipIndex + 1, y, cursor, startX, runEnd, destination, coverage); + } + + this.ApplyRectangleDifferenceInterior(clipIndex, descriptor, rectangle, yCoverage, y, startX, Math.Max(cursor, clipStart), clipEnd, destination, coverage); + cursor = Math.Max(cursor, clipEnd); + } + + this.ApplyIntegerRun(clipIndex + 1, y, cursor, startX, endX, destination, coverage); + } + + /// + /// Applies a retained path clip descriptor to a coverage span. + /// + /// The current clip descriptor index. + /// The path clip descriptor. + /// The absolute destination row. + /// The absolute start column of and . + /// The destination pixels covered by the span. + /// The subject coverage values for . + private void ApplyPathClip( + int clipIndex, + DrawingClipDescriptor descriptor, + int y, + int startX, + Span destination, + Span coverage) + { + // A missing or empty clip raster means the clip path contributes no coverage here: + // a Difference clip removes nothing so the subject span passes through unchanged, + // while an intersecting clip keeps nothing so the span is dropped entirely. + DefaultRasterizer.RasterizableGeometry? rasterizable = this.pathClipState?.GetRasterizable(clipIndex); + if (rasterizable is null) + { + if (descriptor.Operation == ClipOperation.Difference) + { + this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination, coverage); + } + + return; + } + + int rowBandIndex = y / DefaultRasterizer.DefaultTileHeight; + int localRowIndex = rowBandIndex - rasterizable.FirstRowBandIndex; + if ((uint)localRowIndex >= (uint)rasterizable.RowBandCount || !rasterizable.HasCoverage(localRowIndex)) + { + if (descriptor.Operation == ClipOperation.Difference) + { + this.ApplyClipDescriptors(clipIndex + 1, y, startX, destination, coverage); + } + + return; + } + + Span clipCoverage = this.workerState.GetOrCreatePathClipCoverage(coverage.Length); + clipCoverage = clipCoverage[..coverage.Length]; + clipCoverage.Fill(descriptor.Operation == ClipOperation.Difference ? 1F : 0F); + + DefaultRasterizer.RasterizableItem item = new(rasterizable, localRowIndex); + DefaultRasterizer.RasterizableBandInfo bandInfo = rasterizable.GetBandInfo(localRowIndex); + DefaultRasterizer.WorkerScratch scratch = this.workerState.GetOrCreatePathClipScratch(rasterizable.Width); + DefaultRasterizer.Context context = scratch.CreateContext( + bandInfo.IntersectionRule, + bandInfo.RasterizationMode, + bandInfo.AntialiasThreshold); + + PathClipCoverageRowHandler rowHandler = new( + this.workerState, + descriptor.Operation, + y, + startX, + coverage.Length); + + // Path clips use the same retained rasterizer as fills. The clip row is materialized + // into worker-local coverage only for the subject span currently being painted, so + // rectangle and region clips stay on their cheaper span-slicing paths. + DefaultRasterizer.ExecuteRasterizableItem( + ref context, + in item, + in bandInfo, + scratch.Scanline, + ref rowHandler); + + int spanEnd = startX + coverage.Length; + int runStart = startX; + float runCoverage = clipCoverage[0]; + for (int x = startX + 1; x < spanEnd; x++) + { + float pixelCoverage = clipCoverage[x - startX]; + if (pixelCoverage == runCoverage) + { + continue; + } + + // Coalesce equal clip coverage into runs so deeper clips and brush rendering + // are invoked per coverage run instead of once per pixel. + this.ApplyCoverageRun(clipIndex + 1, y, runStart, x, startX, runCoverage, destination, coverage); + + runStart = x; + runCoverage = pixelCoverage; + } + + this.ApplyCoverageRun(clipIndex + 1, y, runStart, spanEnd, startX, runCoverage, destination, coverage); + } + + /// + /// Advances one hard-edged integer run to the next clip descriptor. + /// + /// The next clip descriptor index. + /// The absolute destination row. + /// The absolute first column in the run. + /// The absolute start column of and . + /// The exclusive absolute end column in the run. + /// The destination pixels covered by the parent span. + /// The subject coverage values for . + private void ApplyIntegerRun( + int clipIndex, + int y, + int runStart, + int spanStart, + int runEnd, + Span destination, + Span coverage) + { + if (runEnd <= runStart) + { + return; + } + + int offset = runStart - spanStart; + this.ApplyClipDescriptors(clipIndex, y, runStart, destination.Slice(offset, runEnd - runStart), coverage.Slice(offset, runEnd - runStart)); + } + + /// + /// Applies a coverage multiplier to one run before advancing it to the next clip descriptor. + /// + /// The next clip descriptor index. + /// The absolute destination row. + /// The absolute first column in the run. + /// The exclusive absolute end column in the run. + /// The absolute start column of and . + /// The clip coverage multiplier applied to the run. + /// The destination pixels covered by the parent span. + /// The subject coverage values for . + private void ApplyCoverageRun( + int clipIndex, + int y, + int runStart, + int runEnd, + int spanStart, + float multiplier, + Span destination, + Span coverage) + { + if (multiplier <= 0F) + { + return; + } + + int offset = runStart - spanStart; + int length = runEnd - runStart; + + Span runCoverage = coverage.Slice(offset, length); + if (multiplier != 1F) + { + int i = 0; + if (Vector.IsHardwareAccelerated) + { + int vectorCount = Vector.Count; + int vectorLength = runCoverage.Length - (runCoverage.Length % vectorCount); + Vector multiplierVector = new(multiplier); + Span> vectors = MemoryMarshal.Cast>(runCoverage[..vectorLength]); + + // The coverage span is borrowed from the rasterizer. Scale it in place so + // downstream clipping and blending consume the combined coverage without + // allocating a temporary span for every clipped run. + for (int j = 0; j < vectors.Length; j++) + { + vectors[j] *= multiplierVector; + } + + i = vectorLength; + } + + for (; i < runCoverage.Length; i++) + { + runCoverage[i] *= multiplier; + } + } + + this.ApplyClipDescriptors(clipIndex, y, runStart, destination.Slice(offset, length), runCoverage); + + if (multiplier != 1F) + { + int i = 0; + if (Vector.IsHardwareAccelerated) + { + int vectorCount = Vector.Count; + int vectorLength = runCoverage.Length - (runCoverage.Length % vectorCount); + Vector multiplierVector = new(multiplier); + Span> vectors = MemoryMarshal.Cast>(runCoverage[..vectorLength]); + + // Restore the borrowed coverage before returning; sibling runs and outer + // clip descriptors still observe the original rasterizer coverage buffer. + for (int j = 0; j < vectors.Length; j++) + { + vectors[j] /= multiplierVector; + } + + i = vectorLength; + } + + for (; i < runCoverage.Length; i++) + { + runCoverage[i] /= multiplier; + } + } + } + + /// + /// Computes one-dimensional pixel coverage against a clip interval. + /// + /// The integer pixel coordinate. + /// The inclusive clip interval start. + /// The exclusive clip interval end. + /// The edge mode used to quantize coverage. + /// The hard-edge threshold used when is hard. + /// The coverage contribution for the pixel. + private static float GetAxisCoverage( + int pixel, + float clipStart, + float clipEnd, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + { + float coverage = MathF.Min(pixel + 1F, clipEnd) - MathF.Max(pixel, clipStart); + coverage = Math.Clamp(coverage, 0F, 1F); + + return edgeMode == DrawingClipEdgeMode.Hard + ? coverage > antialiasThreshold ? 1F : 0F + : coverage; + } + } + + /// + /// Writes retained path clip coverage into a worker-local span for one subject row. + /// + /// The pixel format. + private readonly struct PathClipCoverageRowHandler : IRasterizerCoverageRowHandler + where TPixel : unmanaged, IPixel + { + private readonly WorkerState workerState; + private readonly ClipOperation operation; + private readonly int targetY; + private readonly int targetStartX; + private readonly int targetLength; + + /// + /// Initializes a new instance of the struct. + /// + /// The worker-local execution state that owns the clip coverage span. + /// The clip operation represented by the path descriptor. + /// The subject row being clipped. + /// The subject span start. + /// The subject span length. + public PathClipCoverageRowHandler( + WorkerState workerState, + ClipOperation operation, + int targetY, + int targetStartX, + int targetLength) + { + this.workerState = workerState; + this.operation = operation; + this.targetY = targetY; + this.targetStartX = targetStartX; + this.targetLength = targetLength; + } + + /// + /// Records clip coverage emitted by the rasterizer for the target subject row. + /// + /// The absolute row emitted by the clip rasterizer. + /// The absolute start column emitted by the clip rasterizer. + /// The emitted clip coverage values. + public void Handle(int y, int startX, Span coverage) + { + if (y != this.targetY) + { + return; + } + + int targetEndX = this.targetStartX + this.targetLength; + int overlapStart = Math.Max(startX, this.targetStartX); + int overlapEnd = Math.Min(startX + coverage.Length, targetEndX); + if (overlapEnd <= overlapStart) + { + return; + } + + Span clipCoverage = this.workerState.PathClipCoverage; + int sourceOffset = overlapStart - startX; + int targetOffset = overlapStart - this.targetStartX; + int length = overlapEnd - overlapStart; + Span source = coverage.Slice(sourceOffset, length); + Span target = clipCoverage.Slice(targetOffset, length); + + if (this.operation == ClipOperation.Difference) + { + int i = 0; + if (Vector.IsHardwareAccelerated) + { + int vectorCount = Vector.Count; + int vectorLength = length - (length % vectorCount); + Vector one = Vector.One; + ReadOnlySpan> sourceVectors = MemoryMarshal.Cast>(source[..vectorLength]); + Span> targetVectors = MemoryMarshal.Cast>(target[..vectorLength]); + + // Difference clips invert the clip coverage into the reusable target span. + // Casting the vector-width prefix avoids per-lane loads and stores when SIMD is available. + for (int j = 0; j < sourceVectors.Length; j++) + { + targetVectors[j] = one - sourceVectors[j]; + } + + i = vectorLength; + } + + for (; i < length; i++) + { + target[i] = 1F - source[i]; + } + + return; + } + + source.CopyTo(target); + } + } + + /// + /// Walks retained row-operation blocks without flattening them into another list. + /// + private struct SceneOperationCursor + { + private FlushScene.SceneOperationBlock? block; + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The first retained row-operation block. + public SceneOperationCursor(FlushScene.SceneOperationBlock? block) + { + this.block = block; + this.index = 0; + } + + /// + /// Reads the next retained row operation. + /// + /// The next operation. + /// True when an operation was read; otherwise false. + public bool TryRead(out FlushScene.SceneOperation operation) + { + while (this.block is not null) + { + Span items = this.block.Items; + if (this.index < items.Length) + { + operation = items[this.index++]; + return true; + } + + this.block = this.block.Next; + this.index = 0; + } + + operation = default; + return false; } } @@ -73,13 +857,13 @@ public void Handle(int y, int startX, Span coverage) /// Represents one active composition target for a retained row. /// /// The pixel format. - private sealed class BandTarget : IDisposable + private readonly struct BandTarget : IDisposable where TPixel : unmanaged, IPixel { private readonly Buffer2D? owner; /// - /// Initializes a new instance of the class over an existing region. + /// Initializes a new instance of the struct over an existing region. /// /// The destination region. /// The absolute X origin of the region. @@ -94,7 +878,7 @@ public BandTarget(Buffer2DRegion region, int absoluteLeft, int absoluteT } /// - /// Initializes a new instance of the class over an owned temporary buffer. + /// Initializes a new instance of the struct over an owned temporary buffer. /// /// The owned buffer backing the target. /// The absolute bounds represented by the target. @@ -113,6 +897,11 @@ public BandTarget(Buffer2D owner, Rectangle bounds, GraphicsOptions? gra /// public Buffer2DRegion Region { get; } + /// + /// Gets the absolute bounds represented by . + /// + public Rectangle Bounds => new(this.AbsoluteLeft, this.AbsoluteTop, this.Region.Width, this.Region.Height); + /// /// Gets the absolute X origin of . /// @@ -141,34 +930,155 @@ public BandTarget(Buffer2D owner, Rectangle bounds, GraphicsOptions? gra private sealed class WorkerState : IDisposable where TPixel : unmanaged, IPixel { + // Worker states hold only allocator-owned buffers (brush workspace, raster scratch, + // clip coverage), never scene references, so instances can be pooled across flushes + // exactly like the WebGPU backend's scheduling arenas. One Parallel.For's worth of + // states covers steady-state rendering; anything beyond the cap is released. + // + // The pool trims itself on Gen2 collections, mirroring the memory allocator's own + // trimming: when a Gen2 GC arrives and nothing rented since the previous one, the + // renderer is idle and every pooled state is disposed, returning its buffers to the + // allocator. Active rendering keeps the pool untouched. This bounds idle retention + // without an explicit lifetime or shutdown hook. + private static readonly Stack> Pool = new(); + private static readonly object PoolSync = new(); + private static readonly int MaxPooledStates = Environment.ProcessorCount; + + // 1 when Rent ran since the last Gen2 trim check; the sentinel resets it each Gen2. + private static int rentedSinceLastGen2; + private readonly MemoryAllocator allocator; private DefaultRasterizer.WorkerScratch? scratch; + private DefaultRasterizer.WorkerScratch? pathClipScratch; + private IMemoryOwner? pathClipCoverageOwner; + private int pathClipCoverageLength; + + /// + /// Initializes static members of the class, + /// arming the Gen2 trim sentinel. The instance is intentionally dropped: it lives + /// on the finalizer queue and resurrects itself after every collection. + /// + static WorkerState() => _ = new Gen2TrimSentinel(); /// /// Initializes a new instance of the class. /// /// The memory allocator used for scratch growth. /// The destination width used to size the brush workspace. - /// The maximum retained layer depth required by the scene. public WorkerState( MemoryAllocator allocator, - int destinationWidth, - int layerDepth) + int destinationWidth) { this.allocator = allocator; + this.DestinationWidth = destinationWidth; this.BrushWorkspace = new BrushWorkspace(allocator, destinationWidth); - this.TargetStack = new BandTarget[layerDepth]; } + /// + /// Gets the destination width the brush workspace was sized for. A wider state can + /// service any narrower target, so pooling reuses states whose width is sufficient. + /// + public int DestinationWidth { get; } + /// /// Gets the reusable brush workspace for the worker. /// public BrushWorkspace BrushWorkspace { get; } /// - /// Gets the reusable composition target stack for the worker. + /// Gets the worker-local path clip coverage span last requested by . + /// + public Span PathClipCoverage + { + get + { + IMemoryOwner? owner = this.pathClipCoverageOwner; + return owner is null ? [] : owner.Memory.Span[..this.pathClipCoverageLength]; + } + } + + /// + /// Rents a pooled worker state sized for at least the requested width, or creates one. + /// A popped state with a different allocator or an insufficient width is released and + /// replaced, so the pool converges to full-width states for the active allocator. + /// + /// The memory allocator that must own the state's buffers. + /// The minimum destination width the state must service. + /// A worker state ready for one worker's row execution. + public static WorkerState Rent(MemoryAllocator allocator, int destinationWidth) + { + Volatile.Write(ref rentedSinceLastGen2, 1); + + WorkerState? pooled = null; + lock (PoolSync) + { + if (Pool.Count > 0) + { + pooled = Pool.Pop(); + } + } + + if (pooled is not null) + { + if (ReferenceEquals(pooled.allocator, allocator) && pooled.DestinationWidth >= destinationWidth) + { + return pooled; + } + + pooled.Dispose(); + } + + return new WorkerState(allocator, destinationWidth); + } + + /// + /// Returns a worker state to the pool for a later flush, or releases it when the pool + /// is already holding one full set of worker states. /// - public BandTarget[] TargetStack { get; } + /// The state to recycle. + public static void Return(WorkerState state) + { + lock (PoolSync) + { + if (Pool.Count < MaxPooledStates) + { + Pool.Push(state); + return; + } + } + + state.Dispose(); + } + + /// + /// Disposes every pooled state when no rent occurred since the previous Gen2 + /// collection, returning all held buffers to the memory allocator while the + /// renderer is idle. Invoked from the trim sentinel's finalizer. + /// + private static void TrimPoolIfIdle() + { + if (Interlocked.Exchange(ref rentedSinceLastGen2, 0) == 1) + { + return; + } + + WorkerState[] trimmed; + lock (PoolSync) + { + if (Pool.Count == 0) + { + return; + } + + trimmed = Pool.ToArray(); + Pool.Clear(); + } + + foreach (WorkerState state in trimmed) + { + state.Dispose(); + } + } /// /// Returns a reusable raster scratch instance sized for the requested width. @@ -183,18 +1093,93 @@ public DefaultRasterizer.WorkerScratch GetOrCreateScratch(int requiredWidth) return current; } + // Null before allocating: an allocation throw must not leave the field pointing at + // the disposed scratch, or later rows read released memory and Dispose re-releases it. current?.Dispose(); + this.scratch = null; this.scratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth); return this.scratch; } /// - /// Releases the worker-local scratch and brush workspace. + /// Returns a reusable raster scratch instance reserved for path clip rasterization. + /// + /// The minimum scanline width required by the current clip row. + /// A scratch instance that can execute the clip row. + public DefaultRasterizer.WorkerScratch GetOrCreatePathClipScratch(int requiredWidth) + { + DefaultRasterizer.WorkerScratch? current = this.pathClipScratch; + if (current is not null && current.CanReuse(requiredWidth)) + { + return current; + } + + // Null before allocating; see GetOrCreateScratch. + current?.Dispose(); + this.pathClipScratch = null; + this.pathClipScratch = DefaultRasterizer.CreateWorkerScratch(this.allocator, requiredWidth); + return this.pathClipScratch; + } + + /// + /// Returns a reusable coverage span used to combine one path clip with one subject span. + /// + /// The minimum coverage length required by the current subject span. + /// A worker-local coverage span. + public Span GetOrCreatePathClipCoverage(int requiredLength) + { + IMemoryOwner? current = this.pathClipCoverageOwner; + if (current is null || current.Memory.Length < requiredLength) + { + // Null before allocating; see GetOrCreateScratch. + current?.Dispose(); + this.pathClipCoverageOwner = null; + this.pathClipCoverageOwner = this.allocator.Allocate(requiredLength); + } + + this.pathClipCoverageLength = requiredLength; + return this.PathClipCoverage; + } + + /// + /// Releases the worker-local scratch and brush workspace. The scratch fields are nulled + /// so a disposed state can never hand out spans over returned pool memory. /// public void Dispose() { this.scratch?.Dispose(); + this.scratch = null; + this.pathClipScratch?.Dispose(); + this.pathClipScratch = null; + this.pathClipCoverageOwner?.Dispose(); + this.pathClipCoverageOwner = null; this.BrushWorkspace.Dispose(); } + + /// + /// A finalization-resurrected sentinel that runs one pool trim check per garbage + /// collection of its generation. After the first resurrection the instance is + /// tenured, so in steady state the check runs once per Gen2 collection, matching + /// the trimming cadence of the pooled memory allocator itself. + /// + private sealed class Gen2TrimSentinel + { + /// + /// Finalizes an instance of the class. + /// Runs the idle trim and resurrects the sentinel for the next collection. + /// + ~Gen2TrimSentinel() + { + // Never resurrect during teardown: the runtime is finalizing for exit and + // the pool's buffers are reclaimed with the process. + if (Environment.HasShutdownStarted || AppDomain.CurrentDomain.IsFinalizingForUnload()) + { + return; + } + + TrimPoolIfIdle(); + GC.ReRegisterForFinalize(this); + } + } } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs index 5b4b72826..88283627b 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackend.cs @@ -41,7 +41,7 @@ public void RenderScene( { if (scene is not DefaultDrawingBackendScene cpuScene) { - throw new InvalidOperationException("The retained scene is not a CPU drawing backend scene."); + throw new InvalidOperationException("The scene is not compatible with the CPU drawing backend."); } if (!target.TryGetCpuRegion(out Buffer2DRegion destinationFrame)) @@ -51,12 +51,12 @@ public void RenderScene( if (target.Bounds != cpuScene.Bounds) { - throw new InvalidOperationException("The target bounds do not match the retained CPU scene bounds."); + throw new InvalidOperationException("The target bounds do not match the CPU drawing backend scene bounds."); } - if (cpuScene.Scene is FlushScene flushScene && flushScene.RowCount != 0) + if (cpuScene.Scene.RowCount != 0 || cpuScene.Scene.HasApply) { - ExecuteScene(configuration, destinationFrame, flushScene); + ExecuteScene(configuration, target.Bounds, destinationFrame, cpuScene.Scene); } } @@ -65,10 +65,12 @@ public void RenderScene( /// /// The pixel format. /// The active processing configuration. + /// The logical target bounds represented by . /// The destination CPU region. /// The retained scene to execute. private static void ExecuteScene( Configuration configuration, + Rectangle targetBounds, Buffer2DRegion destinationFrame, FlushScene scene) where TPixel : unmanaged, IPixel @@ -97,24 +99,126 @@ private static void ExecuteScene( } } + // The root target carries null graphics options: options are only needed when a target + // is composited back as a layer, and the root frame is never composited onto anything. + BandTarget target = new(destinationFrame, targetBounds.X, targetBounds.Y, null); + + // Segments exist only when apply or scoped-layer barriers split the scene; each barrier + // requires all preceding rows to be fully rendered before it may read or composite pixels. + if (scene.Segments.Length != 0) + { + ExecuteSceneSegments(configuration, destinationFrame.Width, scene, scene.Segments, target); + return; + } + + if (scene.RowCount != 0) + { + ExecuteSceneRows(configuration, destinationFrame.Width, scene, target); + } + } + + /// + /// Executes retained target-wide segments against the supplied target. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The retained flush scene. + /// The retained segments to execute in order. + /// The active target receiving segment output. + private static void ExecuteSceneSegments( + Configuration configuration, + int canvasWidth, + FlushScene scene, + FlushScene.SceneSegment[] segments, + BandTarget target) + where TPixel : unmanaged, IPixel + { + // Segments execute strictly in order: the rows preceding a barrier must be complete + // before the barrier runs, because apply items read the pixels those rows produced and + // layer composition must observe everything drawn beneath the layer. + for (int i = 0; i < segments.Length; i++) + { + FlushScene.SceneSegment segment = segments[i]; + if (segment.Rows.Length != 0) + { + ExecuteSceneRows(configuration, canvasWidth, scene, segment.Rows, target); + } + + if (segment.ApplyItem is FlushScene.ApplySceneItem applyItem) + { + ExecuteApplyItem(configuration, canvasWidth, applyItem, target); + } + else if (segment.LayerItem is FlushScene.ScopedLayerSceneItem layerItem) + { + // Layers render into a clean temporary buffer so their contents blend back as a + // single unit with the layer's own graphics options, preserving back-to-front order. + using BandTarget layerTarget = new( + configuration.MemoryAllocator.Allocate2D(layerItem.Bounds.Width, layerItem.Bounds.Height, AllocationOptions.Clean), + layerItem.Bounds, + layerItem.Layer.Options); + + ExecuteSceneSegments(configuration, canvasWidth, scene, layerItem.Segments, layerTarget); + CompositeLayerTarget(configuration, layerTarget, target); + } + } + } + + /// + /// Executes the retained row stream without command filtering. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The retained flush scene supplying the rows. + /// The active target receiving row output. + private static void ExecuteSceneRows( + Configuration configuration, + int canvasWidth, + FlushScene scene, + BandTarget target) + where TPixel : unmanaged, IPixel + => ExecuteSceneRows(configuration, canvasWidth, scene, scene.Rows, target); + + /// + /// Executes the supplied retained row stream without command filtering. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The retained flush scene. + /// The retained scene rows to execute. + /// The active target receiving row output. + private static void ExecuteSceneRows( + Configuration configuration, + int canvasWidth, + FlushScene scene, + FlushScene.SceneRow[] rows, + BandTarget target) + where TPixel : unmanaged, IPixel + { int requestedParallelism = configuration.MaxDegreeOfParallelism; + + // Each SceneRow owns one disjoint horizontal band of the target, so rows can execute in + // parallel with no synchronization: no two workers ever write the same destination pixel. _ = Parallel.For( fromInclusive: 0, - toExclusive: scene.RowCount, - parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, scene.RowCount), - localInit: () => new WorkerState(configuration.MemoryAllocator, destinationFrame.Width, scene.MaxLayerDepth + 1), + toExclusive: rows.Length, + parallelOptions: ParallelExecutionHelper.CreateParallelOptions(requestedParallelism, rows.Length), + localInit: () => WorkerState.Rent(configuration.MemoryAllocator, target.Region.Width), body: (rowIndex, _, state) => { ExecuteSceneRow( configuration, - destinationFrame, + canvasWidth, scene, - scene.Rows[rowIndex], + rows[rowIndex], + target, state); return state; }, - localFinally: static state => state.Dispose()); + localFinally: static state => WorkerState.Return(state)); } /// @@ -122,95 +226,129 @@ private static void ExecuteScene( /// /// The pixel format. /// The active processing configuration. - /// The destination CPU region. + /// The destination canvas width. /// The retained flush scene. /// The retained scene row to execute. + /// The active target receiving row output. /// The worker-local scratch and compositing state. private static void ExecuteSceneRow( Configuration configuration, - Buffer2DRegion destinationFrame, + int canvasWidth, FlushScene scene, in FlushScene.SceneRow row, + BandTarget target, WorkerState state) where TPixel : unmanaged, IPixel { + Rectangle targetBounds = target.Bounds; + + // Row band indices are absolute canvas-space multiples of the tile height, but the active + // target may be a layer with arbitrary bounds, so the band must be clipped to the target + // and translated into local buffer coordinates before rendering. int bandTop = row.RowBandIndex * DefaultRasterizer.DefaultTileHeight; - int localBandTop = bandTop - destinationFrame.Bounds.Y; - int bandHeight = Math.Min(DefaultRasterizer.DefaultTileHeight, destinationFrame.Height - localBandTop); + int bandBottom = bandTop + DefaultRasterizer.DefaultTileHeight; + int clippedBandTop = Math.Max(bandTop, targetBounds.Y); + int clippedBandBottom = Math.Min(bandBottom, targetBounds.Bottom); + int bandHeight = clippedBandBottom - clippedBandTop; if (bandHeight <= 0) { return; } - Buffer2DRegion destinationBand = destinationFrame.GetSubRegion(0, localBandTop, destinationFrame.Width, bandHeight); - BandTarget[] targetStack = state.TargetStack; - int targetCount = 1; - targetStack[0] = new BandTarget(destinationBand, destinationFrame.Bounds.X, bandTop, null); - int scratchWidth = GetRowScratchWidth(scene, row, destinationFrame.Width); + int localBandTop = clippedBandTop - targetBounds.Y; + Buffer2DRegion destinationBand = target.Region.GetSubRegion(0, localBandTop, target.Region.Width, bandHeight); + BandTarget rowTarget = new(destinationBand, targetBounds.X, clippedBandTop, target.GraphicsOptions); + int scratchWidth = GetRowScratchWidth(scene, row, target.Region.Width); DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(scratchWidth); + SceneOperationCursor cursor = new(row.FirstBlock); + + ExecuteSceneRowOperations( + ref cursor, + configuration, + canvasWidth, + scene, + rowTarget, + scratch, + state); + } - try + /// + /// Executes retained row operations against the supplied target until the current layer scope ends. + /// + /// The pixel format. + /// The cursor over the retained row-operation stream; shared across recursion so nested layers consume from the same stream. + /// The active processing configuration. + /// The destination canvas width. + /// The retained flush scene. + /// The active target receiving operation output. + /// The worker-local raster scratch. + /// The worker-local execution state. + private static void ExecuteSceneRowOperations( + ref SceneOperationCursor cursor, + Configuration configuration, + int canvasWidth, + FlushScene scene, + BandTarget target, + DefaultRasterizer.WorkerScratch scratch, + WorkerState state) + where TPixel : unmanaged, IPixel + { + while (cursor.TryRead(out FlushScene.SceneOperation operation)) { - for (FlushScene.SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next) + // Layer execution is recursive rather than stack-backed: the caller's target remains + // the parent target while the nested call renders into the temporary layer target. + switch (operation.Kind) { - foreach (FlushScene.SceneOperation operation in block.Items) - { - // Each retained row contains a compact mix of layer control operations and - // draw operations in original command order, so the executor can replay the - // row without re-walking the full scene description. - switch (operation.Kind) + case FlushScene.SceneOperationKind.BeginLayer: + GraphicsOptions? layerOptions = scene.Layers[operation.ItemIndex]?.Options; + using (BandTarget layerTarget = new( + configuration.MemoryAllocator.Allocate2D(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean), + operation.LayerBounds, + layerOptions)) { - case FlushScene.SceneOperationKind.BeginLayer: - GraphicsOptions? layerOptions = scene.LayerOptions[operation.ItemIndex]; - - targetStack[targetCount++] = - new BandTarget( - configuration.MemoryAllocator.Allocate2D(operation.LayerBounds.Width, operation.LayerBounds.Height, AllocationOptions.Clean), - operation.LayerBounds, - layerOptions); - break; - - case FlushScene.SceneOperationKind.EndLayer: - BandTarget source = targetStack[--targetCount]; - BandTarget destination = targetStack[targetCount - 1]; - CompositeLayerBand(configuration, source, destination, state.BrushWorkspace); - source.Dispose(); - break; - - case FlushScene.SceneOperationKind.FillItem: - BandTarget target = targetStack[targetCount - 1]; - FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!; - ExecuteFillOperation( - sceneItem.GetRenderer(configuration, destinationFrame.Width), - new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex), - target, - scratch, - state); - break; - - case FlushScene.SceneOperationKind.StrokeItem: - BandTarget strokeTarget = targetStack[targetCount - 1]; - FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!; - ExecuteStrokeOperation( - strokeSceneItem.GetRenderer(configuration, destinationFrame.Width), - new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex), - strokeTarget, - scratch, - state); - break; + ExecuteSceneRowOperations( + ref cursor, + configuration, + canvasWidth, + scene, + layerTarget, + scratch, + state); + + CompositeLayerBand(configuration, layerTarget, target, state.BrushWorkspace); } - } - } - } - finally - { - for (int i = 1; i < targetCount; i++) - { - targetStack[i].Dispose(); - targetStack[i] = null!; - } - targetStack[0] = null!; + break; + + case FlushScene.SceneOperationKind.EndLayer: + return; + + case FlushScene.SceneOperationKind.FillItem: + FlushScene.FillSceneItem sceneItem = scene.FillItems[operation.ItemIndex]!; + ExecuteFillOperation( + sceneItem.GetRenderer(configuration, canvasWidth), + new DefaultRasterizer.RasterizableItem(sceneItem.Rasterizable, operation.LocalRowIndex), + target, + scratch, + sceneItem.ClipState, + sceneItem.PathClipState, + sceneItem.DestinationOffset, + state); + break; + + case FlushScene.SceneOperationKind.StrokeItem: + FlushScene.StrokeSceneItem strokeSceneItem = scene.StrokeItems[operation.ItemIndex]!; + ExecuteStrokeOperation( + strokeSceneItem.GetRenderer(configuration, canvasWidth), + new DefaultRasterizer.StrokeRasterizableItem(strokeSceneItem.Rasterizable, operation.LocalRowIndex), + target, + scratch, + strokeSceneItem.ClipState, + strokeSceneItem.PathClipState, + strokeSceneItem.DestinationOffset, + state); + break; + } } } @@ -249,6 +387,129 @@ private static int GetRowScratchWidth( return width; } + /// + /// Executes every retained row band for one apply item against the supplied target. + /// + /// The pixel format. + /// The active processing configuration. + /// The image brush renderer that writes the processed pixels back. + /// The retained apply operation. + /// The active target receiving the processed pixels. + private static void ExecuteApplyItemParallel( + Configuration configuration, + BrushRenderer renderer, + FlushScene.ApplySceneItem item, + BandTarget target) + where TPixel : unmanaged, IPixel + { + int rowBandCount = item.Rasterizable.RowBandCount; + + _ = Parallel.For( + fromInclusive: 0, + toExclusive: rowBandCount, + parallelOptions: ParallelExecutionHelper.CreateParallelOptions(configuration.MaxDegreeOfParallelism, rowBandCount), + localInit: () => WorkerState.Rent(configuration.MemoryAllocator, target.Region.Width), + body: (localRowIndex, _, state) => + { + if (item.Rasterizable.HasCoverage(localRowIndex)) + { + DefaultRasterizer.WorkerScratch scratch = state.GetOrCreateScratch(Math.Max(target.Region.Width, item.Rasterizable.Width)); + ExecuteFillOperation( + renderer, + new DefaultRasterizer.RasterizableItem(item.Rasterizable, localRowIndex), + target, + scratch, + item.ClipState, + item.PathClipState, + item.DestinationOffset, + state); + } + + return state; + }, + localFinally: static state => WorkerState.Return(state)); + } + + /// + /// Applies one retained processor operation to the current target. + /// + /// The pixel format. + /// The active processing configuration. + /// The destination canvas width. + /// The retained apply operation. + /// The active target the operation reads from and writes back to. + private static void ExecuteApplyItem( + Configuration configuration, + int canvasWidth, + FlushScene.ApplySceneItem item, + BandTarget target) + where TPixel : unmanaged, IPixel + { + // The source pixels are read from the pre-offset region so write-back offsets do not alter + // which caller-supplied pixels participate in the operation. + Rectangle readRect = item.InputRect; + readRect.Offset(-item.ReadOffset.X, -item.ReadOffset.Y); + + // Layer targets store their pixels at a layer-local origin, so the absolute source + // rectangle must be translated before reading; the root target already reads absolute. + if (item.OwnerLayer is not null) + { + readRect = new Rectangle( + readRect.X - target.AbsoluteLeft, + readRect.Y - target.AbsoluteTop, + readRect.Width, + readRect.Height); + } + + using Image sourceImage = new(configuration, item.InputRect.Width, item.InputRect.Height); + CopyTargetToImage(target, readRect, sourceImage.Frames.RootFrame.PixelBuffer.GetRegion()); + sourceImage.Mutate(item.Operation); + + // Write-back uses Src semantics: item.GraphicsOptions was cloned via + // CloneForClearOperation (AlphaCompositionMode.Src, full blend), so the processed pixels + // replace the covered region outright, including transparency, instead of blending over it. + Brush brush = new ImageBrush(sourceImage, sourceImage.Bounds, item.BrushOffset); + BrushRenderer renderer = brush.CreateRenderer( + configuration, + item.GraphicsOptions, + canvasWidth, + item.BrushBounds); + + ExecuteApplyItemParallel(configuration, renderer, item, target); + } + + /// + /// Copies the requested target rectangle into an image-sized destination region. + /// + /// The pixel format. + /// The target to read pixels from. + /// The target-local rectangle to copy. + /// The destination region sized for the full source rectangle. + private static void CopyTargetToImage( + BandTarget target, + Rectangle readRect, + Buffer2DRegion destination) + where TPixel : unmanaged, IPixel + { + Rectangle clipped = Rectangle.Intersect(new Rectangle(0, 0, target.Region.Width, target.Region.Height), readRect); + if (clipped.Width <= 0 || clipped.Height <= 0) + { + return; + } + + // When the read rectangle extends past the target, only the overlap is copied and it is + // placed at the matching offset inside the destination; the uncovered remainder keeps the + // destination image's transparent pixels so the processor sees correctly aligned input. + int destinationX = clipped.X - readRect.X; + int destinationY = clipped.Y - readRect.Y; + for (int y = 0; y < clipped.Height; y++) + { + target.Region.DangerousGetRowSpan(clipped.Y + y) + .Slice(clipped.X, clipped.Width) + .CopyTo(destination.DangerousGetRowSpan(destinationY + y).Slice(destinationX, clipped.Width)); + } + } + /// /// Executes one retained fill operation through the rasterizer and brush renderer. /// @@ -257,12 +518,18 @@ private static int GetRowScratchWidth( /// The retained rasterizable row item to execute. /// The active composition target for the row. /// The worker-local raster scratch. + /// The exact clip state captured with the retained scene item. + /// The retained path clip raster data captured with the retained scene item. + /// The destination offset used to place clip descriptors. /// The worker-local execution state. private static void ExecuteFillOperation( BrushRenderer renderer, DefaultRasterizer.RasterizableItem item, BandTarget target, DefaultRasterizer.WorkerScratch scratch, + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, WorkerState state) where TPixel : unmanaged, IPixel { @@ -271,7 +538,16 @@ private static void ExecuteFillOperation( bandInfo.IntersectionRule, bandInfo.RasterizationMode, bandInfo.AntialiasThreshold); - FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); + + FillCoverageRowHandler rowHandler = new( + renderer, + target, + clipState, + pathClipState, + destinationOffset, + state.BrushWorkspace, + state); + DefaultRasterizer.ExecuteRasterizableItem( ref context, in item, @@ -288,12 +564,18 @@ private static void ExecuteFillOperation( /// The retained stroke rasterizable row item to execute. /// The active composition target for the row. /// The worker-local raster scratch. + /// The exact clip state captured with the retained scene item. + /// The retained path clip raster data captured with the retained scene item. + /// The destination offset used to place clip descriptors. /// The worker-local execution state. private static void ExecuteStrokeOperation( BrushRenderer renderer, DefaultRasterizer.StrokeRasterizableItem item, BandTarget target, DefaultRasterizer.WorkerScratch scratch, + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, WorkerState state) where TPixel : unmanaged, IPixel { @@ -302,7 +584,16 @@ private static void ExecuteStrokeOperation( bandInfo.IntersectionRule, bandInfo.RasterizationMode, bandInfo.AntialiasThreshold); - FillCoverageRowHandler rowHandler = new(renderer, target, state.BrushWorkspace); + + FillCoverageRowHandler rowHandler = new( + renderer, + target, + clipState, + pathClipState, + destinationOffset, + state.BrushWorkspace, + state); + Span strokeBandCoverage = item.Rasterizable.RequiresBandCoverage ? scratch.StrokeBandCoverage : []; DefaultRasterizer.ExecuteStrokeRasterizableItem( ref context, @@ -313,6 +604,66 @@ private static void ExecuteStrokeOperation( ref rowHandler); } + /// + /// Composites one full temporary layer target back into its destination target. + /// + /// The pixel format. + /// The active processing configuration. + /// The temporary layer target carrying the layer's graphics options. + /// The destination target to blend into. + private static void CompositeLayerTarget( + Configuration configuration, + BandTarget source, + BandTarget destination) + where TPixel : unmanaged, IPixel + { + // A null source options means the target was not created as a layer; only layer targets + // carry the options that define how their contents blend back into the destination. + Rectangle overlap = Rectangle.Intersect(source.Bounds, destination.Bounds); + if (overlap.Width <= 0 || overlap.Height <= 0 || source.GraphicsOptions is null) + { + return; + } + + // Composite band-by-band on the same absolute tile grid used for rendering so the + // parallel blend partitions rows without two workers sharing a destination row. + int firstRowBandIndex = overlap.Top / DefaultRasterizer.DefaultTileHeight; + int lastRowBandIndex = (overlap.Bottom - 1) / DefaultRasterizer.DefaultTileHeight; + int rowBandCount = (lastRowBandIndex - firstRowBandIndex) + 1; + + _ = Parallel.For( + fromInclusive: 0, + toExclusive: rowBandCount, + parallelOptions: ParallelExecutionHelper.CreateParallelOptions(configuration.MaxDegreeOfParallelism, rowBandCount), + localInit: () => WorkerState.Rent(configuration.MemoryAllocator, overlap.Width), + body: (rowSlot, _, state) => + { + int bandTop = (firstRowBandIndex + rowSlot) * DefaultRasterizer.DefaultTileHeight; + Rectangle rowBand = new(overlap.X, bandTop, overlap.Width, DefaultRasterizer.DefaultTileHeight); + Rectangle band = Rectangle.Intersect(overlap, rowBand); + + if (band.Width > 0 && band.Height > 0) + { + Buffer2DRegion sourceBand = source.Region.GetSubRegion( + band.X - source.AbsoluteLeft, + band.Y - source.AbsoluteTop, + band.Width, + band.Height); + Buffer2DRegion destinationBand = destination.Region.GetSubRegion( + band.X - destination.AbsoluteLeft, + band.Y - destination.AbsoluteTop, + band.Width, + band.Height); + BandTarget sourceTarget = new(sourceBand, band.X, band.Y, source.GraphicsOptions); + BandTarget destinationTarget = new(destinationBand, band.X, band.Y, destination.GraphicsOptions); + CompositeLayerBand(configuration, sourceTarget, destinationTarget, state.BrushWorkspace); + } + + return state; + }, + localFinally: static state => WorkerState.Return(state)); + } + /// /// Composites one temporary layer band back into its destination band. /// @@ -439,6 +790,63 @@ public static void ComposeLayer( } } + /// + public void CopyPixels( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame target, + Rectangle sourceRectangle, + Point targetPoint) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(configuration, nameof(configuration)); + + if (!source.TryGetCpuRegion(out Buffer2DRegion sourceRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible source frames."); + } + + if (!target.TryGetCpuRegion(out Buffer2DRegion targetRegion)) + { + throw new NotSupportedException($"{nameof(DefaultDrawingBackend)} requires CPU-accessible target frames."); + } + + Rectangle sourceBounds = new(0, 0, sourceRegion.Width, sourceRegion.Height); + Rectangle clippedSource = Rectangle.Intersect(sourceBounds, sourceRectangle); + if (clippedSource.Width <= 0 || clippedSource.Height <= 0) + { + return; + } + + Rectangle targetBounds = new(0, 0, targetRegion.Width, targetRegion.Height); + Rectangle targetRectangle = new( + targetPoint.X + clippedSource.X - sourceRectangle.X, + targetPoint.Y + clippedSource.Y - sourceRectangle.Y, + clippedSource.Width, + clippedSource.Height); + + Rectangle clippedTarget = Rectangle.Intersect(targetBounds, targetRectangle); + + if (clippedTarget.Width <= 0 || clippedTarget.Height <= 0) + { + return; + } + + int sourceX = clippedSource.X + clippedTarget.X - targetRectangle.X; + int sourceY = clippedSource.Y + clippedTarget.Y - targetRectangle.Y; + int targetX = clippedTarget.X; + int targetY = clippedTarget.Y; + int width = clippedTarget.Width; + int height = clippedTarget.Height; + + for (int y = 0; y < height; y++) + { + sourceRegion.DangerousGetRowSpan(sourceY + y) + .Slice(sourceX, width) + .CopyTo(targetRegion.DangerousGetRowSpan(targetY + y).Slice(targetX, width)); + } + } + /// public void ReadRegion( Configuration configuration, diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs index 967025bce..01db2003c 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultDrawingBackendScene.cs @@ -22,11 +22,11 @@ internal DefaultDrawingBackendScene( => this.Scene = scene; /// - /// Gets the retained CPU flush scene when this is a leaf scene. + /// Gets the retained CPU flush scene executed by . /// - internal FlushScene? Scene { get; } + internal FlushScene Scene { get; } /// protected override void DisposeCore() - => this.Scene?.Dispose(); + => this.Scene.Dispose(); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs index aeb8da517..99e07eb76 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.Outputs.cs @@ -134,6 +134,8 @@ public LinearizedRasterData( /// The mutable scan-conversion context. public void Iterate(int rowIndex, ref Context context) { + // The chain is newest-first: only the front block may be partially filled, so its + // valid count is tracked separately; every block after it is full. int count = this.FirstBlockLineCounts[rowIndex]; TLineBlock? lineBlock = this.Lines[rowIndex]; while (lineBlock is not null) diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs index df110883a..b07d75a80 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Linearizer.cs @@ -18,6 +18,20 @@ private abstract class Linearizer { private bool hasAnyCoverage; + /// + /// Initializes a new instance of the class. + /// + /// The source geometry to lower. + /// The residual transform applied to each source point during emission. + /// The whole-pixel X translation applied to the geometry. + /// The whole-pixel Y translation applied to the geometry. + /// The minimum destination X bound of the clipped interest region. + /// The minimum destination Y bound of the clipped interest region. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index touched by the geometry. + /// The number of retained row bands owned by the geometry. + /// The allocator used for retained start-cover storage. protected Linearizer( LinearGeometry geometry, Matrix4x4 residual, @@ -29,8 +43,6 @@ protected Linearizer( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) { this.Geometry = geometry; @@ -44,8 +56,6 @@ protected Linearizer( this.Height = height; this.FirstBandIndex = firstBandIndex; this.RowBandCount = rowBandCount; - this.SamplingOffsetX = samplingOffsetX; - this.SamplingOffsetY = samplingOffsetY; this.Allocator = allocator; this.BandTopStart = (firstBandIndex * PreferredRowHeight) - minY; this.FirstBlockLineCounts = new int[rowBandCount]; @@ -109,16 +119,6 @@ protected Linearizer( /// protected int RowBandCount { get; } - /// - /// Gets the horizontal sampling offset applied before fixed-point conversion. - /// - protected float SamplingOffsetX { get; } - - /// - /// Gets the vertical sampling offset applied before fixed-point conversion. - /// - protected float SamplingOffsetY { get; } - /// /// Gets the allocator used for retained start-cover storage. /// @@ -163,7 +163,7 @@ protected virtual bool ProcessCore() RectangleF translatedBounds = this.HasResidual ? RectangleF.Transform(this.Geometry.Info.Bounds, this.Residual) : this.Geometry.Info.Bounds; - translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); + translatedBounds.Offset(this.TranslateX - this.MinX, this.TranslateY - this.MinY); bool contains = translatedBounds.Left >= 0F && @@ -212,10 +212,10 @@ protected void ProcessContained() } this.AddContainedLineF24Dot8( - FloatToFixed24Dot8(((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), - FloatToFixed24Dot8(((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), - FloatToFixed24Dot8(((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), - FloatToFixed24Dot8(((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); + FloatToFixed24Dot8((p0.X + this.TranslateX) - this.MinX), + FloatToFixed24Dot8((p0.Y + this.TranslateY) - this.MinY), + FloatToFixed24Dot8((p1.X + this.TranslateX) - this.MinX), + FloatToFixed24Dot8((p1.Y + this.TranslateY) - this.MinY)); } } @@ -239,22 +239,28 @@ protected void ProcessUncontained() } this.AddUncontainedLine( - ((p0.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, - ((p0.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, - ((p1.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, - ((p1.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); + (p0.X + this.TranslateX) - this.MinX, + (p0.Y + this.TranslateY) - this.MinY, + (p1.X + this.TranslateX) - this.MinX, + (p1.Y + this.TranslateY) - this.MinY); } } /// /// Clips one geometry line against the destination interest and adds the retained result. /// + /// + /// The interest region is the local rectangle [0, ] x [0, ]. + /// Segments above, below, or right of the interest are discarded outright, but segments left of + /// it must be retained as start covers because winding accumulates left to right across a scanline. + /// /// The starting X coordinate in translated float space. /// The starting Y coordinate in translated float space. /// The ending X coordinate in translated float space. /// The ending Y coordinate in translated float space. protected void AddUncontainedLine(float x0, float y0, float x1, float y1) { + // Horizontal segments never change scanline winding. if (y0 == y1) { return; @@ -270,6 +276,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) return; } + // Fully right of the interest cannot affect any visible pixel; fully left would, + // via winding, so that case is handled further down rather than rejected here. if (x0 >= this.Width && x1 >= this.Width) { return; @@ -296,6 +304,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) return; } + // Vertical clipping first: intersections with y == 0 and y == Height are computed + // parametrically in double precision so the clipped endpoints stay on the original line. double deltayV = Math.Abs(y1 - y0); double deltaxV = x1 - x0; double rx0 = x0; @@ -336,11 +346,13 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) } } + // Vertical clipping can reveal that the surviving portion lies fully right of the interest. if (rx0 >= this.Width && rx1 >= this.Width) { return; } + // Fully inside horizontally: emit directly without edge splitting. if (rx0 > 0D && rx1 > 0D && rx0 < this.Width && rx1 < this.Width) { this.AddContainedLineF24Dot8( @@ -364,6 +376,8 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) double deltayH = ry1 - ry0; double deltaxH = Math.Abs(rx1 - rx0); + // Horizontal clipping, split by travel direction so the right-edge clip is always + // applied to the far endpoint and the left-edge clip to the near one. if (rx1 > rx0) { double bx1 = rx1; @@ -445,6 +459,7 @@ protected void AddUncontainedLine(float x0, float y0, float x1, float y1) /// The ending Y coordinate. protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) { + // Horizontal segments never change scanline winding. if (y0 == y1) { return; @@ -452,29 +467,30 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) if (x0 == x1) { - if (y0 < y1) - { - this.VerticalDown(x0, y0, y1); - } - else - { - this.VerticalUp(x0, y0, y1); - } - + // Winding direction is carried by the y endpoint order itself, so both the + // downward and upward cases hand the endpoints to the band splitter as-is. + this.SplitAcrossBands(x0, y0, x0, y1); return; } - int dx = Math.Abs(x1 - x0); - int dy = Math.Abs(y1 - y0); + long dx = Math.Abs((long)x1 - x0); + long dy = Math.Abs((long)y1 - y0); if (dx > MaximumDelta || dy > MaximumDelta) { - int mx = (x0 + x1) >> 1; - int my = (y0 + y1) >> 1; + // Halve overlong segments recursively so downstream fixed-point + // interpolation always operates on bounded deltas. The midpoint must be + // computed in 64-bit: an int sum overflows for coordinates beyond ~4.2M + // pixels, placing the midpoint outside [x0, x1] so the segment never + // shrinks and the recursion overflows the stack (issue #403). + int mx = (int)(((long)x0 + x1) >> 1); + int my = (int)(((long)y0 + y1) >> 1); this.AddContainedLineF24Dot8(x0, y0, mx, my); this.AddContainedLineF24Dot8(mx, my, x1, y1); return; } + // Band indices treat the larger Y endpoint as exclusive (hence the -1) so a + // segment ending exactly on a band boundary does not spill into the next band. int rowIndex0; int rowIndex1; int bandTopStart = this.BandTopStart * FixedOne; @@ -490,6 +506,8 @@ protected void AddContainedLineF24Dot8(int x0, int y0, int x1, int y1) rowIndex1 = (y1 - bandTopStart) / bandHeight; } + // Bounds guard: never write outside the retained band range. The contained path + // trusts the caller's bounds, so float-to-fixed rounding may land a hair outside. if ((uint)rowIndex0 >= (uint)this.RowBandCount || (uint)rowIndex1 >= (uint)this.RowBandCount) { return; @@ -546,22 +564,6 @@ protected TL GetOrCreateLineArray(int rowIndex) return lineArray; } - /// - /// Adds a downward vertical segment by delegating to the shared band-splitting path. - /// - /// The fixed-point X coordinate. - /// The starting fixed-point Y coordinate. - /// The ending fixed-point Y coordinate. - private void VerticalDown(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); - - /// - /// Adds an upward vertical segment by delegating to the shared band-splitting path. - /// - /// The fixed-point X coordinate. - /// The starting fixed-point Y coordinate. - /// The ending fixed-point Y coordinate. - private void VerticalUp(int x, int y0, int y1) => this.SplitAcrossBands(x, y0, x, y1); - /// /// Splits a contained line segment at row-band boundaries and appends each retained piece. /// @@ -584,6 +586,8 @@ private void SplitAcrossBands(int x0, int y0, int x1, int y1) while (currentBand != endBand) { + // Walk to the band boundary in the direction of travel, interpolating X in + // 64-bit so the dx * deltaY product cannot overflow 32-bit fixed point. int bandBoundaryY = dy > 0 ? bandTopStart + ((currentBand + 1) * bandHeight) : bandTopStart + (currentBand * bandHeight); int deltaY = bandBoundaryY - currentY; int nextX = currentX + (int)(((long)dx * deltaY) / dy); @@ -597,6 +601,7 @@ private void SplitAcrossBands(int x0, int y0, int x1, int y1) currentY = bandBoundaryY; currentBand += step; + // Bounds guard: stop rather than write outside the retained band range. if ((uint)currentBand >= (uint)this.RowBandCount) { return; @@ -612,6 +617,10 @@ private void SplitAcrossBands(int x0, int y0, int x1, int y1) /// /// Updates retained start-cover rows for a line that has been clipped against the visible band. /// + /// + /// Travel direction encodes winding sign: downward segments subtract cover and upward + /// segments add it, matching the accumulation performed by the scanline rasterizer. + /// /// The clipped starting Y coordinate. /// The clipped ending Y coordinate. private void UpdateStartCoversClipped(int y0, int y1) @@ -781,6 +790,17 @@ private sealed class LinearizerX32Y16 : Linearizer /// /// Initializes a new instance of the class. /// + /// The source geometry to lower. + /// The residual transform applied to each source point during emission. + /// The whole-pixel X translation applied to the geometry. + /// The whole-pixel Y translation applied to the geometry. + /// The minimum destination X bound of the clipped interest region. + /// The minimum destination Y bound of the clipped interest region. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index touched by the geometry. + /// The number of retained row bands owned by the geometry. + /// The allocator used for retained start-cover storage. public LinearizerX32Y16( LinearGeometry geometry, Matrix4x4 residual, @@ -792,10 +812,8 @@ public LinearizerX32Y16( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; /// @@ -826,7 +844,7 @@ protected override void FinalizeLines() /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . - internal bool TryProcess(out LinearizedRasterData result) + public bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { @@ -853,6 +871,17 @@ private sealed class LinearizerX16Y16 : Linearizer /// /// Initializes a new instance of the class. /// + /// The source geometry to lower. + /// The residual transform applied to each source point during emission. + /// The whole-pixel X translation applied to the geometry. + /// The whole-pixel Y translation applied to the geometry. + /// The minimum destination X bound of the clipped interest region. + /// The minimum destination Y bound of the clipped interest region. + /// The visible destination width in pixels. + /// The visible destination height in pixels. + /// The first retained row-band index touched by the geometry. + /// The number of retained row bands owned by the geometry. + /// The allocator used for retained start-cover storage. public LinearizerX16Y16( LinearGeometry geometry, Matrix4x4 residual, @@ -864,10 +893,8 @@ public LinearizerX16Y16( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; /// @@ -898,7 +925,7 @@ protected override void FinalizeLines() /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . - internal bool TryProcess(out LinearizedRasterData result) + public bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs index b798f82e5..8a6cdf62f 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RasterizableGeometry.cs @@ -106,6 +106,9 @@ public RasterizableGeometry( /// /// Gets the retained narrow line block chain for one local row. /// + /// + /// Valid only when is ; the narrow table is not allocated otherwise. + /// /// The local row band index. /// The retained narrow line chain for the row. public LineArrayX16Y16Block? GetLinesX16ForRow(int localRowIndex) => this.linesX16![localRowIndex]; @@ -113,6 +116,9 @@ public RasterizableGeometry( /// /// Gets the retained wide line block chain for one local row. /// + /// + /// Valid only when is ; the wide table is not allocated otherwise. + /// /// The local row band index. /// The retained wide line chain for the row. public LineArrayX32Y16Block? GetLinesX32ForRow(int localRowIndex) => this.linesX32![localRowIndex]; @@ -154,6 +160,8 @@ public ReadOnlySpan GetCoversForRow(int localRowIndex) /// public void Dispose() { + // Line blocks are plain managed objects; clearing the tables drops the references so + // the chains become collectable even while this instance itself remains reachable. if (this.linesX16 is not null) { Array.Clear(this.linesX16); @@ -164,9 +172,12 @@ public void Dispose() Array.Clear(this.linesX32); } + // Nulling each entry makes GetCoversForRow return empty after disposal instead of a + // span over returned pool memory, and makes a second disposal pass a no-op. for (int i = 0; i < this.startCoverTable.Length; i++) { this.startCoverTable[i]?.Dispose(); + this.startCoverTable[i] = null; } } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs index afd154ed0..8dcb57fa4 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.RetainedTypes.cs @@ -103,6 +103,7 @@ internal readonly struct RasterizableBandInfo /// The fill rule used when resolving accumulated winding. /// The rasterization mode used by the band. /// The aliased threshold used when the band runs in aliased mode. + /// The perceptual coverage boost used when the band runs in antialiased mode. /// Indicates whether the band has non-zero start-cover seeds. public RasterizableBandInfo( int lineCount, @@ -115,6 +116,7 @@ public RasterizableBandInfo( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, float antialiasThreshold, + float coverageBoost, bool hasStartCovers) { this.LineCount = lineCount; @@ -127,6 +129,7 @@ public RasterizableBandInfo( this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; this.AntialiasThreshold = antialiasThreshold; + this.CoverageBoost = coverageBoost; this.HasStartCovers = hasStartCovers; } @@ -171,7 +174,7 @@ public RasterizableBandInfo( public IntersectionRule IntersectionRule { get; } /// - /// Gets the coverage mode used by the band. + /// Gets the rasterization mode used by the band. /// public RasterizationMode RasterizationMode { get; } @@ -180,6 +183,13 @@ public RasterizableBandInfo( /// public float AntialiasThreshold { get; } + /// + /// Gets the perceptual coverage boost used when the band runs in antialiased mode. + /// Partial coverage is remapped by the S-curve a + boost * a * (1 - a) * (2a - 1); + /// zero disables it. + /// + public float CoverageBoost { get; } + /// /// Gets a value indicating whether the band has non-zero start-cover seeds. /// @@ -197,6 +207,9 @@ public RasterizableBandInfo( internal sealed class LineArrayX32Y16 { private LineArrayX32Y16Block? current; + + // Starting at full capacity forces the first AppendLine to allocate the front block, + // so empty rows never pay for a block allocation. private int count = LineArrayX32Y16Block.LineCount; /// @@ -220,6 +233,7 @@ internal sealed class LineArrayX32Y16 /// The ending Y coordinate in 24.8 fixed-point. public void AppendLine(int x0, int y0, int x1, int y1) { + // Horizontal segments never change scanline winding, so they carry no raster payload. if (y0 == y1) { return; @@ -338,17 +352,17 @@ public void Iterate(int firstBlockLineCount, ref Context context) private struct PackedLineX32Y16 { /// - /// Gets or sets the packed Y endpoints. + /// The packed signed 16-bit Y endpoints. /// public int PackedY0Y1; /// - /// Gets or sets the starting X coordinate. + /// The starting X coordinate in 24.8 fixed-point. /// public int X0; /// - /// Gets or sets the ending X coordinate. + /// The ending X coordinate in 24.8 fixed-point. /// public int X1; } @@ -369,6 +383,9 @@ private struct PackedLineX32Y16Buffer internal sealed class LineArrayX16Y16 { private LineArrayX16Y16Block? current; + + // Starting at full capacity forces the first AppendLine to allocate the front block, + // so empty rows never pay for a block allocation. private int count = LineArrayX16Y16Block.LineCount; /// @@ -392,6 +409,7 @@ internal sealed class LineArrayX16Y16 /// The ending Y coordinate in 24.8 fixed-point. public void AppendLine(int x0, int y0, int x1, int y1) { + // Horizontal segments never change scanline winding, so they carry no raster payload. if (y0 == y1) { return; @@ -513,12 +531,12 @@ public void Iterate(int firstBlockLineCount, ref Context context) private struct PackedLineX16Y16 { /// - /// Gets or sets the packed Y endpoints. + /// The packed signed 16-bit Y endpoints. /// public int PackedY0Y1; /// - /// Gets or sets the packed X endpoints. + /// The packed signed 16-bit X endpoints. /// public int PackedX0X1; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs index 87bf1f2a6..3c7ac166a 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.Stroke.cs @@ -10,8 +10,21 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; internal static partial class DefaultRasterizer { + /// + /// The length threshold below which a segment direction, arc radius, or edge extent is treated as degenerate. + /// Mirrored on the GPU as TANGENT_THRESH in flatten.wgsl; the two must stay in lockstep. + /// private const float StrokeDirectionEpsilon = 1e-6F; + + /// + /// The cross-product magnitude below which two offset support lines are treated as parallel + /// and their intersection is rejected. + /// private const float StrokeParallelEpsilon = 1e-5F; + + /// + /// The number of vertical supersamples taken per pixel row by . + /// private const int DirectStrokeVerticalSampleCount = 4; /// @@ -26,7 +39,7 @@ internal static partial class DefaultRasterizer /// The isotropic scale factor applied to the stroke width so expansion runs in device-space pixels. /// The allocator used for retained raster storage. /// The retained rasterizable geometry for the stroke, or when the stroke produces no coverage. - internal static StrokeRasterizableGeometry? CreatePathStrokeRasterizableGeometry( + public static StrokeRasterizableGeometry? CreatePathStrokeRasterizableGeometry( LinearGeometry geometry, Matrix4x4 residual, Pen pen, @@ -63,7 +76,7 @@ internal static partial class DefaultRasterizer /// The isotropic scale factor applied to the stroke width so expansion runs in device-space pixels. /// The allocator used for retained raster storage. /// The retained rasterizable geometry for the stroke, or when the stroke produces no coverage. - internal static StrokeRasterizableGeometry? CreateLineSegmentStrokeRasterizableGeometry( + public static StrokeRasterizableGeometry? CreateLineSegmentStrokeRasterizableGeometry( PointF start, PointF end, Pen pen, @@ -78,9 +91,6 @@ internal static partial class DefaultRasterizer return null; } - float samplingOffsetX = 0.5F; - float samplingOffsetY = 0.5F; - StrokeStyle strokeStyle = new(pen, widthScale); RectangleF bounds = RectangleF.FromLTRB( @@ -91,7 +101,7 @@ internal static partial class DefaultRasterizer RectangleF translatedBounds = InflateStrokeBounds(bounds, strokeStyle); - translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + translatedBounds.Offset(translateX, translateY); Rectangle geometryBounds = Rectangle.FromLTRB( (int)MathF.Floor(translatedBounds.Left), @@ -133,6 +143,7 @@ internal static partial class DefaultRasterizer options.IntersectionRule, options.RasterizationMode, options.AntialiasThreshold, + coverageBoost: 0F, hasStartCovers: false); } @@ -151,9 +162,7 @@ internal static partial class DefaultRasterizer translateX, translateY, firstRowBandIndex, - rowBandCount, - samplingOffsetX, - samplingOffsetY)); + rowBandCount)); } /// @@ -181,12 +190,9 @@ internal static partial class DefaultRasterizer return null; } - float samplingOffsetX = 0.5F; - float samplingOffsetY = 0.5F; - RectangleF sourceBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); RectangleF translatedBounds = InflateStrokeBounds(sourceBounds, stroke); - translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + translatedBounds.Offset(translateX, translateY); Rectangle geometryBounds = Rectangle.FromLTRB( (int)MathF.Floor(translatedBounds.Left), @@ -227,8 +233,6 @@ internal static partial class DefaultRasterizer height, firstRowBandIndex, rowBandCount, - samplingOffsetX, - samplingOffsetY, allocator); if (!linearizer.TryProcess(out LinearizedRasterData result)) @@ -259,8 +263,6 @@ internal static partial class DefaultRasterizer height, firstRowBandIndex, rowBandCount, - samplingOffsetX, - samplingOffsetY, allocator); if (!wideLinearizer.TryProcess(out LinearizedRasterData wideResult)) @@ -282,6 +284,15 @@ internal static partial class DefaultRasterizer /// /// Wraps finalized retained stroke line storage in the normal stroke rasterizable payload. /// + /// The first absolute row-band index touched by the stroke. + /// The number of retained local row bands owned by the stroke. + /// The stroke-local visible band width in pixels. + /// The bit-vector width in machine words required by the stroke. + /// The scanner cover/area stride required by the stroke. + /// The destination-space band left edge. + /// The rasterizer options used for the retained bands. + /// The finalized retained line storage in the packed 16-bit-X encoding. + /// The retained stroke rasterizable geometry. private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeometry( int firstRowBandIndex, int rowBandCount, @@ -308,6 +319,7 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet options.IntersectionRule, options.RasterizationMode, options.AntialiasThreshold, + coverageBoost: 0F, hasStartCovers); } @@ -340,6 +352,15 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet /// /// Wraps finalized retained wide stroke line storage in the normal stroke rasterizable payload. /// + /// The first absolute row-band index touched by the stroke. + /// The number of retained local row bands owned by the stroke. + /// The stroke-local visible band width in pixels. + /// The bit-vector width in machine words required by the stroke. + /// The scanner cover/area stride required by the stroke. + /// The destination-space band left edge. + /// The rasterizer options used for the retained bands. + /// The finalized retained line storage in the 32-bit-X encoding. + /// The retained stroke rasterizable geometry. private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeometry( int firstRowBandIndex, int rowBandCount, @@ -366,6 +387,7 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet options.IntersectionRule, options.RasterizationMode, options.AntialiasThreshold, + coverageBoost: 0F, hasStartCovers); } @@ -403,8 +425,7 @@ private static StrokeRasterizableGeometry CreateRetainedStrokeRasterizableGeomet /// The estimated retained line count for the stroke. private static int EstimateStrokeBandLineCount(PointF start, PointF end) { - float samplingOffset = 0.5F; - int segmentCount = (int)MathF.Floor(start.Y + samplingOffset) != (int)MathF.Floor(end.Y + samplingOffset) ? 1 : 0; + int segmentCount = (int)MathF.Floor(start.Y) != (int)MathF.Floor(end.Y) ? 1 : 0; return Math.Max(segmentCount * 4, 1); } @@ -416,6 +437,8 @@ private static int EstimateStrokeBandLineCount(PointF start, PointF end) /// The inflated stroke bounds. private static RectangleF InflateStrokeBounds(RectangleF bounds, in StrokeStyle stroke) { + // Miter joins can protrude up to halfWidth * miterLimit past the corner; every other + // join stays within halfWidth of the centerline. float joinInflate = stroke.LineJoin switch { LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound @@ -423,6 +446,8 @@ LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound _ => stroke.HalfWidth }; + // A square cap's corner sits halfWidth along the tangent and halfWidth along the + // normal, so its farthest point is halfWidth * sqrt(2) from the endpoint. float capInflate = stroke.LineCap == LineCap.Square ? stroke.HalfWidth * MathF.Sqrt(2F) : stroke.HalfWidth; @@ -434,7 +459,7 @@ LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound } /// - /// Initializes a new instance of the class. + /// Base retained stroke source payload that rasterizes one row band at execution time. /// internal abstract class StrokeRasterData { @@ -446,26 +471,23 @@ internal abstract class StrokeRasterData /// The destination-space Y translation applied at composition time. /// The first retained row-band index touched by the stroke. /// The number of retained row bands touched by the stroke. - /// The horizontal sampling offset. - /// The vertical sampling offset. protected StrokeRasterData( StrokeStyle stroke, int translateX, int translateY, int firstBandIndex, - int rowBandCount, - float samplingOffsetX, - float samplingOffsetY) + int rowBandCount) { this.Stroke = stroke; this.TranslateX = translateX; this.TranslateY = translateY; this.FirstBandIndex = firstBandIndex; this.RowBandCount = rowBandCount; - this.SamplingOffsetX = samplingOffsetX; - this.SamplingOffsetY = samplingOffsetY; } + /// + /// Gets the stroke style consumed during band execution. + /// public StrokeStyle Stroke { get; } /// @@ -489,15 +511,10 @@ protected StrokeRasterData( public int RowBandCount { get; } /// - /// Gets the horizontal sampling offset applied during rasterization. - /// - public float SamplingOffsetX { get; } - - /// - /// Gets the vertical sampling offset applied during rasterization. + /// Gets a value indicating whether band execution needs the shared per-band stroke coverage + /// scratch buffer. Derived payloads that accumulate whole-band coverage opt in; the caller + /// passes an empty span otherwise. /// - public float SamplingOffsetY { get; } - public virtual bool RequiresBandCoverage => false; /// @@ -533,8 +550,6 @@ internal sealed class LineSegmentStrokeRasterData : StrokeRasterData /// The destination-space Y translation applied at composition time. /// The first retained row-band index touched by the stroke. /// The number of retained row bands touched by the stroke. - /// The horizontal sampling offset. - /// The vertical sampling offset. public LineSegmentStrokeRasterData( PointF start, PointF end, @@ -542,10 +557,8 @@ public LineSegmentStrokeRasterData( int translateX, int translateY, int firstBandIndex, - int rowBandCount, - float samplingOffsetX, - float samplingOffsetY) - : base(stroke, translateX, translateY, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY) + int rowBandCount) + : base(stroke, translateX, translateY, firstBandIndex, rowBandCount) { this.Start = start; this.End = end; @@ -580,8 +593,6 @@ public override void ExecuteBand( this.Stroke, this.TranslateX, this.TranslateY, - this.SamplingOffsetX, - this.SamplingOffsetY, in bandInfo, scanline, ref rowHandler); @@ -597,7 +608,7 @@ internal sealed class RetainedStrokeRasterData : StrokeRasterData /// /// The retained fill-style raster payload replayed for the stroke. public RetainedStrokeRasterData(RasterizableGeometry outline) - : base(default, 0, 0, outline.FirstRowBandIndex, outline.RowBandCount, 0F, 0F) + : base(default, 0, 0, outline.FirstRowBandIndex, outline.RowBandCount) => this.Outline = outline; /// @@ -711,6 +722,9 @@ public StrokeRasterizableGeometry( /// public int BandHeight { get; } + /// + /// Gets a value indicating whether band execution needs the shared per-band stroke coverage scratch buffer. + /// public bool RequiresBandCoverage => this.strokeData.RequiresBandCoverage; /// @@ -775,8 +789,6 @@ private readonly struct DirectLineSegmentBandRasterizer /// The stroke style. /// The destination-space X translation applied at composition time. /// The destination-space Y translation applied at composition time. - /// The horizontal sampling offset. - /// The vertical sampling offset. /// The retained band metadata. private DirectLineSegmentBandRasterizer( PointF start, @@ -784,13 +796,11 @@ private DirectLineSegmentBandRasterizer( StrokeStyle stroke, int translateX, int translateY, - float samplingOffsetX, - float samplingOffsetY, in RasterizableBandInfo bandInfo) { this.translation = new( - (translateX - bandInfo.DestinationLeft) + samplingOffsetX, - (translateY - bandInfo.DestinationTop) + samplingOffsetY); + translateX - bandInfo.DestinationLeft, + translateY - bandInfo.DestinationTop); this.start = start; this.end = end; @@ -812,8 +822,6 @@ private DirectLineSegmentBandRasterizer( /// The stroke style. /// The destination-space X translation applied at composition time. /// The destination-space Y translation applied at composition time. - /// The horizontal sampling offset. - /// The vertical sampling offset. /// The retained band metadata. /// The reusable scanline scratch buffer. /// The coverage row handler that receives emitted runs. @@ -823,8 +831,6 @@ public static void Rasterize( StrokeStyle stroke, int translateX, int translateY, - float samplingOffsetX, - float samplingOffsetY, in RasterizableBandInfo bandInfo, Span scanline, ref TRowHandler rowHandler) @@ -835,8 +841,6 @@ public static void Rasterize( stroke, translateX, translateY, - samplingOffsetX, - samplingOffsetY, in bandInfo).Rasterize(scanline, ref rowHandler); /// @@ -861,6 +865,9 @@ private void Rasterize(Span scanline, ref TRowHandler rowHan return; } + // The stroke body is the offset rectangle p0..p3. Square caps are folded into the + // rectangle by extending it halfWidth along the tangent; round caps are sampled + // separately as endpoint circles during row emission. float halfWidth = this.stroke.HalfWidth; Vector2 normal = GetStrokeOffsetNormal(tangent) * halfWidth; Vector2 extension = this.stroke.LineCap == LineCap.Square ? tangent * halfWidth : Vector2.Zero; @@ -1219,6 +1226,8 @@ private static void AccumulateIntervalCoverage( return; } + // Boundary pixels receive their fractional horizontal overlap; interior pixels are + // fully covered by the interval so they take the whole sample weight. if (endPixel == startPixel + 1) { rowCoverage[startPixel - baseColumn] += (clampedRight - clampedLeft) * sampleWeight; @@ -1415,6 +1424,8 @@ private static void AppendEdgeInterval( /// /// Returns the tessellation segment count used for one round join or cap arc. + /// Ported to the GPU as stroke_arc_subdivision_count in flatten.wgsl; + /// changes here must be mirrored there. /// /// The arc radius. /// The arc sweep angle in radians. @@ -1424,6 +1435,10 @@ private static int GetArcSubdivisionCount(float radius, double angle, double arc { double safeRadius = Math.Max(radius, StrokeDirectionEpsilon); double safeScale = Math.Max(arcDetailScale, 0.01D); + + // AGG's chordal-error step: theta is the largest angular step whose chord midpoint + // deviates from the arc by at most 0.125 / scale pixels, so tessellation density + // adapts to both radius and requested detail. double ratio = safeRadius / (safeRadius + (0.125D / safeScale)); ratio = Math.Clamp(ratio, -1D, 1D); double theta = Math.Acos(ratio) * 2D; @@ -1504,7 +1519,8 @@ private static bool TryIntersectOffsetLines( private static float Cross(Vector2 left, Vector2 right) => (left.X * right.Y) - (left.Y * right.X); /// - /// Normalizes an angle into the inclusive-exclusive range [0, 2Ï€). + /// Normalizes an angle into the inclusive-exclusive range [0, 2 * PI). + /// Ported to the GPU as stroke_normalize_positive_angle in flatten.wgsl. /// /// The angle to normalize. /// The normalized angle. diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs index 9e95aca33..4603d2fed 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.StrokeLinearizer.cs @@ -13,10 +13,22 @@ internal static partial class DefaultRasterizer /// /// Base retained stroke linearizer that expands stroked centerlines once into row-local line storage. /// + /// + /// The stroke expansion below is a port of AGG's PolygonStroker and is the reference + /// implementation for the GPU stroker: flatten.wgsl's stroke_side_join, + /// stroke_calc_miter, stroke_calc_arc, and stroke_chain_point are direct + /// ports of the corresponding members here, and WebGPUSceneEncoder.EncodeStrokePath + /// ports the contour preprocessing. Any behavioral change here must be mirrored there. + /// /// The mutable per-row retained line collector type. private abstract class StrokeLinearizer : Linearizer where TL : class { + /// + /// The 1/64 px length below which a contour segment is collapsed forward onto the last + /// kept point so every surviving segment carries a well conditioned tangent. Mirrored by + /// the GPU encoder's WebGPUSceneEncoder.StrokeMicroSegmentEpsilon. + /// private const float StrokeMicroSegmentEpsilon = 1F / 64F; private readonly StrokeStyle stroke; @@ -35,8 +47,6 @@ private abstract class StrokeLinearizer : Linearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. - /// The horizontal sampling offset. - /// The vertical sampling offset. /// The allocator used for retained start-cover storage. protected StrokeLinearizer( LinearGeometry geometry, @@ -50,16 +60,28 @@ protected StrokeLinearizer( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) - : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + : base(geometry, residual, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) => this.stroke = stroke; + /// + /// Classifies one stroked contour's inflated bounds against the clipped interest bounds. + /// private enum ContourInterest { + /// + /// The stroked contour cannot touch the interest bounds and is skipped entirely. + /// Outside, + + /// + /// The stroked contour straddles the interest bounds; emitted lines require clipping. + /// Clipped, + + /// + /// The stroked contour lies fully inside the interest bounds; lines are emitted unclipped. + /// Contained } @@ -104,7 +126,7 @@ protected override bool ProcessCore() private ContourInterest GetContourInterest(ReadOnlySpan contourPoints) { RectangleF translatedBounds = InflateStrokeBounds(this.GetPointBounds(contourPoints), this.stroke); - translatedBounds.Offset(this.TranslateX + this.SamplingOffsetX - this.MinX, this.TranslateY + this.SamplingOffsetY - this.MinY); + translatedBounds.Offset(this.TranslateX - this.MinX, this.TranslateY - this.MinY); if (translatedBounds.Right <= 0F || translatedBounds.Bottom <= 0F || @@ -127,12 +149,16 @@ private ContourInterest GetContourInterest(ReadOnlySpan contourPoints) /// /// Returns whether a contour should be treated as closed when emitting stroke geometry. + /// A contour is closed when it is declared closed, its first and last points coincide, or + /// the endpoint gap is at most max(strokeWidth, 1e-3). Mirrored at encode time by + /// WebGPUSceneEncoder.EncodeStrokePath; both must stay in lockstep. /// /// The contour points. /// Indicates whether the contour is explicitly closed. /// when the contour should be stroked as closed; otherwise . private bool IsContourClosedForEmission(ReadOnlySpan contourPoints, bool isDeclaredClosed) { + // Fewer than three points cannot enclose area, so such contours always stroke open. if (contourPoints.Length < 3) { return false; @@ -146,11 +172,18 @@ private bool IsContourClosedForEmission(ReadOnlySpan contourPoints, bool return true; } + // Near-closure: a gap no wider than the stroke itself is visually sealed by the + // stroke body, so emit a wrap join instead of two caps facing across a slit. Vector2 delta = first - last; float closeThreshold = MathF.Max(this.stroke.Width, 1E-3F); return delta.LengthSquared() <= closeThreshold * closeThreshold; } + /// + /// Applies the residual transform to one source point when one is present. + /// + /// The source point. + /// The transformed point. [MethodImpl(MethodImplOptions.AggressiveInlining)] private PointF TransformPoint(PointF point) => this.HasResidual ? PointF.Transform(point, this.Residual) : point; @@ -197,6 +230,12 @@ private void ProcessContour(ReadOnlySpan contourPoints, bool isClosed, b /// /// Builds one contour-local stroke segment array while collapsing immediate duplicate points. /// + /// + /// Exact duplicates are skipped outright and sub-1/64 px micro-segments are collapsed + /// forward onto the last kept point so every surviving segment carries a well conditioned + /// tangent for join math. Ported to encode time on the GPU path by + /// WebGPUSceneEncoder.EncodeStrokePath; both must stay in lockstep. + /// /// The contiguous contour points. /// Indicates whether the contour is closed. /// The destination segment buffer. @@ -233,6 +272,9 @@ private int BuildContourSegments( continue; } + // When the candidate segment is rejected as a micro-segment, previousPoint stays + // anchored on the last kept point so the run collapses forward; pointLike still + // tracks the newest source point so a fully degenerate contour falls back to it. if (TryCreateStrokeContourSegment(previousPoint, point, out StrokeContourSegment segment)) { distinctPointCount++; @@ -243,6 +285,8 @@ private int BuildContourSegments( pointLike = point; } + // A closed contour that repeats its first point as its last would otherwise count + // the shared vertex twice. if (isClosed && distinctPointCount > 1 && previousPoint == firstPoint) @@ -250,6 +294,9 @@ private int BuildContourSegments( distinctPointCount--; } + // Bridge back to the first point when a closed contour does not already end on it. + // A sub-1/64 px gap fails TryCreateStrokeContourSegment and is bridged by the wrap + // join instead, matching the GPU encoder. if (isClosed && segmentCount > 1 && previousPoint != firstPoint && @@ -270,6 +317,8 @@ private int BuildContourSegments( /// when a non-degenerate segment exists. private static bool TryCreateStrokeContourSegment(PointF start, PointF end, out StrokeContourSegment segment) { + // Segments shorter than 1/64 px cannot produce a numerically stable tangent, so + // they are rejected here and collapsed forward by the caller. if (Vector2.DistanceSquared(start, end) <= StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon) { segment = default; @@ -363,6 +412,13 @@ private void EmitOpenSegmentStrokeContour(PointF start, PointF end, bool contain /// /// Emits one stroked open multi-segment contour from precomputed contour-local segments. /// + /// + /// Two-pass outline: the forward pass walks the segments emitting one offset side and its + /// joins followed by the end cap, then the reverse pass walks back emitting the opposite + /// side followed by the start cap, producing a single closed outline. Join overshoots + /// retrace shared offset lines in opposite directions, so they cancel in the signed + /// winding sum under the non-zero fill rule. + /// /// The precomputed contour-local segments. /// Indicates whether the contour is fully contained within the interest. private void EmitOpenStrokeContour(ReadOnlySpan segments, bool contained) @@ -436,6 +492,11 @@ private void EmitOpenStrokeContour(ReadOnlySpan segments, /// /// Emits the two stroked contours for a closed contour from precomputed contour-local segments. /// + /// + /// A closed centerline strokes to a ring: the forward pass emits one boundary and the + /// reversed pass emits the other with opposite winding, so the ring interior fills and + /// the centerline hole cancels in the winding sum. + /// /// The precomputed contour-local segments. /// Indicates whether the contour is fully contained within the interest. private void EmitClosedStrokeContour(ReadOnlySpan segments, bool contained) @@ -488,12 +549,15 @@ private void EmitPointStrokeContour(PointF point, bool contained) float halfWidth = this.stroke.HalfWidth; if (this.stroke.LineCap == LineCap.Round) { + // A round-capped point is a full circle emitted as two half arcs. Vector2 startOffset = new(halfWidth, 0F); this.EmitDirectedArcContour(center, startOffset, -startOffset, contained); this.EmitDirectedArcContour(center, -startOffset, startOffset, contained); return; } + // Butt and square caps have no tangent to orient by, so the footprint degenerates + // to an axis-aligned halfWidth square around the point. PointF p0 = center + new Vector2(-halfWidth, -halfWidth); PointF p1 = center + new Vector2(halfWidth, -halfWidth); PointF p2 = center + new Vector2(halfWidth, halfWidth); @@ -549,7 +613,8 @@ private void EmitDirectedArcContour( } /// - /// Appends a contour arc directly to the active stroke contour. + /// Appends a contour arc directly to the active stroke contour. Ported to the GPU as + /// stroke_chain_arc in flatten.wgsl; changes here must be mirrored there. /// /// The active contour state. /// The arc center. @@ -601,7 +666,8 @@ private void AppendDirectedArcContour( /// Direct port of PolygonStroker.CalcJoin so the rasterizer emits the same /// join geometry as the reference CPU stroker. Each side of the outline calls this /// once per source vertex; the reverse side reverses the vertex order exactly - /// like PolygonStroker's Outline2 state. + /// like PolygonStroker's Outline2 state. Ported to the GPU as stroke_side_join + /// in flatten.wgsl; changes here must be mirrored there. /// /// The active contour state. /// Previous source vertex in the emission's traversal order. @@ -658,6 +724,10 @@ private void AppendSideJoinContour( if (MathF.Abs(cp) > float.Epsilon && cp > 0F) { + // Inner corner: always miter to the offset-line intersection, reverting past a + // limit derived from the local segment lengths. The reverted points overshoot + // along the shared offset lines and are retraced by the opposite pass, so the + // excess cancels in the signed winding sum under the non-zero fill rule. float limit = MathF.Min(len1, len2) / widthAbs; if (limit < 1.01F) { @@ -672,6 +742,9 @@ private void AppendSideJoinContour( Vector2 averageOffset = new Vector2(dx1 + dx2, dy1 + dy2) * 0.5F; float bevelDistance = averageOffset.Length(); + // Nearly straight corner: when the round/bevel join would deviate from a straight + // offset edge by less than halfWidth / 1024 (scaled by the arc detail), a single + // offset-line intersection point is indistinguishable from the full join. float widthEps = widthAbs / 1024F; if ((this.stroke.LineJoin is LineJoin.Round or LineJoin.Bevel) && ((float)this.stroke.ArcDetailScale * (widthAbs - bevelDistance)) < widthEps) @@ -711,8 +784,21 @@ private void AppendSideJoinContour( /// /// Direct port of PolygonStroker.CalcMiter. Emits the miter apex (or the - /// configured overflow fallback) at the join vertex. + /// configured overflow fallback) at the join vertex. Ported to the GPU as + /// stroke_calc_miter in flatten.wgsl; changes here must be mirrored there. /// + /// The active contour state. + /// Previous source vertex in the emission's traversal order. + /// Current source vertex (the corner). + /// Next source vertex in the emission's traversal order. + /// The X component of the scaled offset for segment v0-v1; the offset vector is (dx1, -dy1). + /// The negated Y component of the scaled offset for segment v0-v1. + /// The X component of the scaled offset for segment v1-v2; the offset vector is (dx2, -dy2). + /// The negated Y component of the scaled offset for segment v1-v2. + /// The overflow behavior applied when the miter limit is exceeded. + /// The miter limit expressed in half-width units. + /// The distance from the corner to the bevel midpoint, used to interpolate the clipped miter. + /// Indicates whether the join is fully contained within the interest. private void CalcMiter( ref ContourState contour, Vector2 v0, @@ -810,8 +896,17 @@ private void CalcMiter( /// /// Direct port of PolygonStroker.CalcArc. Emits intermediate arc vertices - /// around a join center between two offset vectors. + /// around a join center between two offset vectors. Ported to the GPU as + /// stroke_calc_arc in flatten.wgsl; changes here must be mirrored there. /// + /// The active contour state. + /// The join center X coordinate. + /// The join center Y coordinate. + /// The X component of the arc start offset from the center. + /// The Y component of the arc start offset from the center. + /// The X component of the arc end offset from the center. + /// The Y component of the arc end offset from the center. + /// Indicates whether the arc is fully contained within the interest. private void CalcArc( ref ContourState contour, float x, @@ -826,15 +921,20 @@ private void CalcArc( double a1 = Math.Atan2(dy1, dx1); double a2 = Math.Atan2(dy2, dx2); + // AGG's chordal-error step: da is the largest angular step whose chord stays within + // 0.125 / arc-detail-scale pixels of the true arc. double widthAbs = strokeWidth; double da = Math.Acos(widthAbs / (widthAbs + (0.125D / this.stroke.ArcDetailScale))) * 2D; this.AppendContourPoint(ref contour, new Vector2(x + dx1, y + dy1), contained); + // Wrap the end angle forward so the sweep is always positive. if (a1 > a2) { a2 += Math.PI * 2D; } + // Distribute the sweep evenly over n interior points so the last step lands exactly + // on the end offset. int n = (int)((a2 - a1) / da); da = (a2 - a1) / (n + 1); a1 += da; @@ -851,16 +951,30 @@ private void CalcArc( } /// - /// Signed area of triangle (a, b, point), matching PolygonStroker.CrossProduct. + /// Signed area of triangle (a, b, point), matching PolygonStroker.CrossProduct; + /// used as a side-of-line test for against the line through + /// and . Ported to the GPU as + /// stroke_cross_product in flatten.wgsl. /// + /// The first line point. + /// The second line point. + /// The point to test. + /// The signed triangle area scalar. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static float CrossProduct(Vector2 a, Vector2 b, Vector2 point) => ((point.X - b.X) * (b.Y - a.Y)) - ((point.Y - b.Y) * (b.X - a.X)); /// /// Intersects two infinite lines defined by point pairs (a, b) and (c, d), - /// matching PolygonStroker.TryCalcIntersection. + /// matching PolygonStroker.TryCalcIntersection. Ported to the GPU as + /// stroke_try_intersect in flatten.wgsl. /// + /// The first point on the first line. + /// The second point on the first line. + /// The first point on the second line. + /// The second point on the second line. + /// Receives the intersection when the lines are not near-parallel. + /// when the lines intersect; otherwise . private static bool TryCalcIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 d, out Vector2 intersection) { const float eps = 1e-7F; @@ -879,7 +993,9 @@ private static bool TryCalcIntersection(Vector2 a, Vector2 b, Vector2 c, Vector2 } /// - /// Appends one point to the active contour. + /// Appends one point to the active contour, emitting a line from the previously appended + /// point. Ported to the GPU as stroke_chain_point in flatten.wgsl; changes + /// here must be mirrored there. /// /// The active contour state. /// The point to append. @@ -894,6 +1010,7 @@ private void AppendContourPoint(ref ContourState state, PointF point, bool conta return; } + // Coincident points are skipped so the chain never emits zero-length lines. if (state.PreviousPoint == point) { return; @@ -930,18 +1047,18 @@ private void EmitLine(PointF start, PointF end, bool contained) if (contained) { this.AddContainedLineF24Dot8( - FloatToFixed24Dot8(((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), - FloatToFixed24Dot8(((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY), - FloatToFixed24Dot8(((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX), - FloatToFixed24Dot8(((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY)); + FloatToFixed24Dot8((start.X + this.TranslateX) - this.MinX), + FloatToFixed24Dot8((start.Y + this.TranslateY) - this.MinY), + FloatToFixed24Dot8((end.X + this.TranslateX) - this.MinX), + FloatToFixed24Dot8((end.Y + this.TranslateY) - this.MinY)); return; } this.AddUncontainedLine( - ((start.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, - ((start.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY, - ((end.X + this.TranslateX) - this.MinX) + this.SamplingOffsetX, - ((end.Y + this.TranslateY) - this.MinY) + this.SamplingOffsetY); + (start.X + this.TranslateX) - this.MinX, + (start.Y + this.TranslateY) - this.MinY, + (end.X + this.TranslateX) - this.MinX, + (end.Y + this.TranslateY) - this.MinY); } /// @@ -951,8 +1068,18 @@ private void EmitLine(PointF start, PointF end, bool contained) /// The stroke-side offset normal. private static Vector2 GetStrokeOffsetNormal(Vector2 tangent) => new(tangent.Y, -tangent.X); + /// + /// One preprocessed centerline segment with its cached tangent, offset normal, and length. + /// private readonly struct StrokeContourSegment { + /// + /// Initializes a new instance of the struct. + /// + /// The segment start point. + /// The segment end point. + /// The normalized segment tangent. + /// The segment length. public StrokeContourSegment(PointF start, PointF end, Vector2 tangent, float length) { this.Start = start; @@ -962,21 +1089,50 @@ public StrokeContourSegment(PointF start, PointF end, Vector2 tangent, float len this.Length = length; } + /// + /// Gets the segment start point. + /// public PointF Start { get; } + /// + /// Gets the segment end point. + /// public PointF End { get; } + /// + /// Gets the normalized segment tangent. + /// public Vector2 Tangent { get; } + /// + /// Gets the unit offset normal following PolygonStroker's (tangent.Y, -tangent.X) convention. + /// public Vector2 Normal { get; } + /// + /// Gets the segment length. + /// public float Length { get; } } + /// + /// Mutable chain state for one emitted stroke outline contour. + /// private struct ContourState { + /// + /// Indicates whether any point has been appended to the contour yet. + /// public bool HasPoint; + + /// + /// The first appended point, used to close the contour. + /// public PointF FirstPoint; + + /// + /// The most recently appended point; the start of the next emitted line. + /// public PointF PreviousPoint; } } @@ -1000,8 +1156,6 @@ private sealed class StrokeLinearizerX32Y16 : StrokeLinearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. - /// The horizontal sampling offset. - /// The vertical sampling offset. /// The allocator used for retained start-cover storage. public StrokeLinearizerX32Y16( LinearGeometry geometry, @@ -1015,10 +1169,8 @@ public StrokeLinearizerX32Y16( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) - : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) => this.FinalLines = new LineArrayX32Y16Block?[rowBandCount]; /// @@ -1049,7 +1201,7 @@ protected override void FinalizeLines() /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . - internal bool TryProcess(out LinearizedRasterData result) + public bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { @@ -1087,8 +1239,6 @@ private sealed class StrokeLinearizerX16Y16 : StrokeLinearizer /// The visible destination height in pixels. /// The first retained row-band index. /// The retained row-band count. - /// The horizontal sampling offset. - /// The vertical sampling offset. /// The allocator used for retained start-cover storage. public StrokeLinearizerX16Y16( LinearGeometry geometry, @@ -1102,10 +1252,8 @@ public StrokeLinearizerX16Y16( int height, int firstBandIndex, int rowBandCount, - float samplingOffsetX, - float samplingOffsetY, MemoryAllocator allocator) - : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, samplingOffsetX, samplingOffsetY, allocator) + : base(geometry, residual, stroke, translateX, translateY, minX, minY, width, height, firstBandIndex, rowBandCount, allocator) => this.FinalLines = new LineArrayX16Y16Block?[rowBandCount]; /// @@ -1136,7 +1284,7 @@ protected override void FinalizeLines() /// /// The finalized retained raster data. /// when retained coverage was produced; otherwise . - internal bool TryProcess(out LinearizedRasterData result) + public bool TryProcess(out LinearizedRasterData result) { if (!this.ProcessCore()) { diff --git a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs index 0a087a077..7f241e5d6 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DefaultRasterizer.cs @@ -12,34 +12,88 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// Fixed-point rasterizer that converts retained fill geometry into per-row coverage. /// /// -/// The rasterizer works in scene-aligned row bands. Each retained band stores compact line blocks -/// plus optional start-cover seeds, and execution replays that retained payload directly against -/// worker-local scratch without rebuilding geometry on every row. +/// +/// The rasterizer works in scene-aligned row bands of pixels. Each +/// retained band stores compact line blocks plus optional start-cover seeds, and execution replays +/// that retained payload directly against worker-local scratch without rebuilding geometry on every row. +/// +/// +/// Execution is parallel across row bands: the drawing backend runs a Parallel.For over scene rows +/// (or over a single geometry's row bands), and each worker replays the retained items that touch +/// its band sequentially into a worker-local . All static members here are +/// stateless; every piece of mutable state lives in a owned by exactly +/// one worker, so no synchronization is required. +/// /// internal static partial class DefaultRasterizer { - // Tile height used by the parallel row-tiling pipeline. - internal const int DefaultTileHeight = 16; + /// + /// Tile height, in pixels, of one row band in the parallel row-tiling pipeline. + /// + public const int DefaultTileHeight = 16; + /// + /// Number of fractional bits in the 24.8 fixed-point coordinate format. + /// private const int FixedShift = 8; + + /// + /// The value 1.0 in 24.8 fixed-point, i.e. one pixel cell. + /// private const int FixedOne = 1 << FixedShift; + + /// + /// Maximum per-segment coordinate delta (2048 pixels in 24.8 fixed-point) accepted by the + /// linearizer before it splits a segment, keeping DDA intermediate products inside 32 bits. + /// private const int MaximumDelta = 2048 << FixedShift; + + /// + /// Number of bits in one native machine word; bitset rows are sized in these units. + /// private static readonly int WordBitCount = nint.Size * 8; + + /// + /// Right-shift that converts an accumulated doubled cell area (max 2 * 256 * 256) down to the + /// 0..256 coverage step domain used by . + /// private const int AreaToCoverageShift = 9; + + /// + /// Number of discrete coverage steps per fully covered pixel (one 24.8 unit of winding). + /// private const int CoverageStepCount = 256; + + /// + /// Bitmask implementing modulo 2 * for even-odd wrapping. + /// private const int EvenOddMask = (CoverageStepCount * 2) - 1; + + /// + /// Length of one even-odd winding period; values past the midpoint mirror back down. + /// private const int EvenOddPeriod = CoverageStepCount * 2; + + /// + /// Multiplier converting integer coverage steps to normalized [0, 1] coverage. + /// private const float CoverageScale = 1F / CoverageStepCount; /// /// Gets the preferred scene row height used by the CPU rasterizer. /// - internal static int PreferredRowHeight => DefaultTileHeight; + public static int PreferredRowHeight => DefaultTileHeight; /// /// Executes one retained rasterizable row item against a reusable scanner context. /// - internal static void ExecuteRasterizableItem( + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// The worker-local scanner context to replay the item into. + /// The retained fill row item to execute. + /// The retained metadata describing the destination band. + /// Reusable scanline scratch used to materialize emitted coverage spans. + /// The coverage callback invoked for each emitted non-zero span. + public static void ExecuteRasterizableItem( ref Context context, in RasterizableItem item, in RasterizableBandInfo bandInfo, @@ -54,7 +108,8 @@ internal static void ExecuteRasterizableItem( bandInfo.BandHeight, bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold); + bandInfo.AntialiasThreshold, + bandInfo.CoverageBoost); context.SeedStartCovers(item.GetActualCovers()); if (item.Rasterizable.IsX16) @@ -75,7 +130,14 @@ internal static void ExecuteRasterizableItem( /// /// Executes one retained stroke row item against a reusable scanner context. /// - internal static void ExecuteStrokeRasterizableItem( + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// The worker-local scanner context to replay the item into. + /// The retained stroke row item to execute. + /// The retained metadata describing the destination band. + /// Reusable scanline scratch used to materialize emitted coverage spans. + /// Reusable per-band stroke coverage scratch used by the direct stroke path. + /// The coverage callback invoked for each emitted non-zero span. + public static void ExecuteStrokeRasterizableItem( ref Context context, in StrokeRasterizableItem item, in RasterizableBandInfo bandInfo, @@ -91,7 +153,8 @@ internal static void ExecuteStrokeRasterizableItem( bandInfo.BandHeight, bandInfo.IntersectionRule, bandInfo.RasterizationMode, - bandInfo.AntialiasThreshold); + bandInfo.AntialiasThreshold, + bandInfo.CoverageBoost); item.Rasterizable.ExecuteBand(ref context, in bandInfo, scanline, strokeBandCoverage, ref rowHandler); } @@ -99,16 +162,26 @@ internal static void ExecuteStrokeRasterizableItem( /// /// Converts bit count to the number of machine words needed to hold the bitset row. /// + /// The maximum number of bits the row must represent. + /// The number of machine words required, rounded up. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int BitVectorsForMaxBitCount(int maxBitCount) => (maxBitCount + WordBitCount - 1) / WordBitCount; + /// + /// Allocates worker-local scratch sized for the default band configuration at the given width. + /// + /// The memory allocator that owns the scratch buffers. + /// The maximum band width, in pixels, the scratch must support. + /// A new instance. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static WorkerScratch CreateWorkerScratch(MemoryAllocator allocator, int width) + public static WorkerScratch CreateWorkerScratch(MemoryAllocator allocator, int width) => WorkerScratch.Create(allocator, BitVectorsForMaxBitCount(width), checked(width << 1), width, PreferredRowHeight); /// /// Converts a float coordinate to signed 24.8 fixed-point. /// + /// The floating-point coordinate to convert. + /// The rounded 24.8 fixed-point value. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FloatToFixed24Dot8(float value) => (int)MathF.Round(value * FixedOne); @@ -116,9 +189,14 @@ internal static WorkerScratch CreateWorkerScratch(MemoryAllocator allocator, int /// Returns one when a fixed-point value lies exactly on a cell boundary at or below zero. /// This is used to keep edge ownership consistent for vertical lines. /// + /// The 24.8 fixed-point coordinate to test. + /// One when the value is a non-positive exact cell boundary; otherwise zero. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int FindAdjustment(int value) { + // Branchless: sign-extend (value - 1) so lte0 is 1 for value <= 0, and test the low + // fractional bits for an exact multiple of FixedOne the same way. The product of the two + // flags nudges boundary-sitting vertical edges into the cell to their left. int lte0 = ~((value - 1) >> 31) & 1; int divisibleBy256 = (((value & (FixedOne - 1)) - 1) >> 31) & 1; return lte0 & divisibleBy256; @@ -127,6 +205,8 @@ private static int FindAdjustment(int value) /// /// Machine-word trailing zero count used for sparse bitset iteration. /// + /// The word to scan. + /// The number of trailing zero bits in . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int TrailingZeroCount(nuint value) => nint.Size == sizeof(ulong) @@ -136,6 +216,7 @@ private static int TrailingZeroCount(nuint value) /// /// Throws when the requested raster interest exceeds the scanner's indexing limits. /// + /// Always thrown. [MethodImpl(MethodImplOptions.NoInlining)] private static void ThrowInterestBoundsTooLarge() => throw new ImageProcessingException("The rasterizer interest bounds are too large for DefaultRasterizer buffers."); @@ -143,7 +224,14 @@ private static void ThrowInterestBoundsTooLarge() /// /// Creates retained row-local raster payload for one lowered geometry. /// - internal static RasterizableGeometry? CreateRasterizableGeometry( + /// The lowered linear geometry to linearize into retained line blocks. + /// The residual transform to apply to the geometry before rasterization. + /// The integer X translation applied after the residual transform. + /// The integer Y translation applied after the residual transform. + /// The rasterizer options carrying interest bounds, fill rule, and antialias settings. + /// The memory allocator used for retained start-cover storage. + /// The retained geometry payload, or when nothing is visible. + public static RasterizableGeometry? CreateRasterizableGeometry( LinearGeometry geometry, Matrix4x4 residual, int translateX, @@ -151,11 +239,8 @@ private static void ThrowInterestBoundsTooLarge() in RasterizerOptions options, MemoryAllocator allocator) { - float samplingOffsetX = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - float samplingOffsetY = options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter ? 0.5F : 0F; - RectangleF translatedBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); - translatedBounds.Offset(translateX + samplingOffsetX, translateY + samplingOffsetY); + translatedBounds.Offset(translateX, translateY); // The retained clipper ignores segments at the maximum X edge, // so extend the right bound by one pixel to keep closing vertical edges available. @@ -184,6 +269,8 @@ private static void ThrowInterestBoundsTooLarge() ThrowInterestBoundsTooLarge(); } + // Narrow geometries pack both X endpoints into one 32-bit word: band-local 24.8 X values + // stay below 128 * 256 = 32768, so they fit signed 16 bits and halve retained line memory. if (width < 128) { LinearizerX16Y16 linearizer = new( @@ -197,8 +284,6 @@ private static void ThrowInterestBoundsTooLarge() height, firstRowBandIndex, rowBandCount, - samplingOffsetX, - samplingOffsetY, allocator); if (!linearizer.TryProcess(out LinearizedRasterData result)) @@ -222,6 +307,7 @@ private static void ThrowInterestBoundsTooLarge() options.IntersectionRule, options.RasterizationMode, options.AntialiasThreshold, + options.CoverageBoost, hasStartCovers); } @@ -252,8 +338,6 @@ private static void ThrowInterestBoundsTooLarge() height, firstRowBandIndex, rowBandCount, - samplingOffsetX, - samplingOffsetY, allocator); if (!linearizer.TryProcess(out LinearizedRasterData result)) @@ -277,6 +361,7 @@ private static void ThrowInterestBoundsTooLarge() options.IntersectionRule, options.RasterizationMode, options.AntialiasThreshold, + options.CoverageBoost, hasStartCovers); } @@ -296,6 +381,13 @@ private static void ThrowInterestBoundsTooLarge() } } + /// + /// Counts the total retained lines in a block chain. + /// + /// The retained line block type. + /// The front block of the chain, or when the chain is empty. + /// The number of valid lines in the front block. + /// The total line count across the chain. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int CountLines(TLineBlock? firstLineBlock, int firstBlockLineCount) where TLineBlock : class, ILineBlock @@ -305,6 +397,8 @@ private static int CountLines(TLineBlock? firstLineBlock, int firstB return 0; } + // Only the front block is partially filled; every block behind it is full by construction + // because the collectors allocate a new front block only after the previous one overflows. int count = firstBlockLineCount; TLineBlock? block = firstLineBlock.Next; while (block is not null) @@ -339,6 +433,7 @@ internal ref struct Context private IntersectionRule intersectionRule; private RasterizationMode rasterizationMode; private float antialiasThreshold; + private float coverageBoost; private int touchedRowCount; /// @@ -383,6 +478,7 @@ public Context( this.intersectionRule = intersectionRule; this.rasterizationMode = rasterizationMode; this.antialiasThreshold = antialiasThreshold; + this.coverageBoost = 0F; this.touchedRowCount = 0; } @@ -396,6 +492,7 @@ public Context( /// The fill rule used when converting accumulated winding/coverage into final alpha. /// The rasterization mode that controls how antialiasing thresholds are interpreted. /// The threshold used when antialiasing is conditionally reduced or disabled. + /// The perceptual coverage boost applied to antialiased coverage; zero disables it. public void Reconfigure( int width, int wordsPerRow, @@ -403,7 +500,8 @@ public void Reconfigure( int height, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - float antialiasThreshold) + float antialiasThreshold, + float coverageBoost) { this.width = width; this.height = height; @@ -412,6 +510,7 @@ public void Reconfigure( this.intersectionRule = intersectionRule; this.rasterizationMode = rasterizationMode; this.antialiasThreshold = antialiasThreshold; + this.coverageBoost = coverageBoost; } /// @@ -446,6 +545,9 @@ public void AddClippedStartCover(int y0, int y1) return; } + // Sign convention mirrors CellVertical's delta = y0 - y1: downward intervals subtract + // winding and upward intervals add it, so left-of-band edges stay consistent with + // edges rasterized through the cell tables. if (y0 < y1) { int rowIndex0 = y0 >> FixedShift; @@ -502,6 +604,7 @@ public void RasterizeLineSegment(int x0, int y0, int x1, int y1) /// /// Converts accumulated cover/area tables into non-zero coverage span callbacks. /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. /// Absolute destination Y corresponding to row zero in this context. /// Absolute destination X corresponding to column zero in this context. /// Reusable scanline scratch buffer used to materialize emitted spans. @@ -588,6 +691,7 @@ public void ResetTouchedRows() /// /// Emits one row by iterating touched columns and coalescing equal-coverage spans. /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. /// Bitset words indicating touched columns in this row. /// Row index inside the context. /// Initial carry cover value from x less than zero contributions. @@ -742,6 +846,11 @@ private readonly void EmitRowCoverage( /// /// Converts accumulated signed area to normalized coverage under the selected fill rule. /// + /// + /// The accumulated doubled signed area in fixed-point cell units; a fully covered pixel + /// corresponds to 2 * 256 * 256, which maps to . + /// + /// The normalized coverage value in [0, 1]. [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly float AreaToCoverage(int area) { @@ -780,12 +889,27 @@ private readonly float AreaToCoverage(int area) return coverage >= this.antialiasThreshold ? 1F : 0F; } + if (this.coverageBoost != 0F) + { + // Perceptual contrast boost for text: an S-curve that darkens coverage above + // one half and lightens it below, so stems solidify while nearly-open counters + // stay bright. 0, 0.5, and 1 are fixed points; at full strength the remap is + // exactly smoothstep (3a^2 - 2a^3), so the boost blends identity to smoothstep. + coverage += this.coverageBoost * coverage * (1F - coverage) * ((2F * coverage) - 1F); + } + return coverage; } /// /// Buffers one non-zero span into the current contiguous row run. /// + /// The scratch scanline that stores per-pixel coverage for the buffered run. + /// The inclusive start column of the span. + /// The exclusive end column of the span. + /// The constant coverage value for the span. + /// The inclusive start column of the buffered run; negative when no run is open. + /// The exclusive end column of the buffered run. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void BufferSpan( Span scanline, @@ -819,6 +943,13 @@ private static void BufferSpan( /// /// Emits the currently buffered contiguous run, if any. /// + /// The struct coverage handler type; constrained to a value type so calls devirtualize. + /// The coverage callback receiving the run. + /// Absolute destination Y for the emitted row. + /// Absolute destination X corresponding to column zero in this context. + /// The scratch scanline containing the buffered per-pixel coverage. + /// The inclusive start column of the buffered run; reset to -1 after flushing. + /// The exclusive end column of the buffered run; reset to -1 after flushing. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void FlushBufferedRun( ref TRowHandler rowHandler, @@ -842,6 +973,10 @@ private static void FlushBufferedRun( /// /// Sets a row/column bit and reports whether it was newly set. /// + /// The band-local row index. + /// The band-local column index. + /// Receives whether the row already had any bit-vector backed cell data. + /// when the bit was newly set; otherwise . [MethodImpl(MethodImplOptions.AggressiveInlining)] private readonly bool ConditionalSetBit(int row, int column, out bool rowHadBits) { @@ -866,6 +1001,10 @@ private readonly bool ConditionalSetBit(int row, int column, out bool rowHadBits /// /// Adds one cell contribution into cover/area accumulators. /// + /// The band-local row index; out-of-range rows are ignored. + /// The band-local column index; negative columns fold into the row carry. + /// The signed winding delta contributed by the edge crossing this cell. + /// The signed doubled area contributed inside this cell. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddCell(int row, int column, int delta, int area) { @@ -924,6 +1063,8 @@ private void AddCell(int row, int column, int delta, int area) /// /// Adds one start-cover delta for a touched row. /// + /// The band-local row index; out-of-range rows are ignored. + /// The signed winding delta carried into the row from left of the band. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void AddStartCoverCell(int row, int delta) { @@ -939,6 +1080,7 @@ private void AddStartCoverCell(int row, int delta) /// /// Marks a row as touched once so sparse reset can clear it later. /// + /// The band-local row index to mark. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void MarkRowTouched(int row) { @@ -954,9 +1096,17 @@ private void MarkRowTouched(int row) /// /// Emits one vertical cell contribution. /// + /// The band-local column index of the cell. + /// The band-local row index of the cell. + /// The cell-local X coordinate of the vertical edge (0 to ). + /// The cell-local starting Y coordinate. + /// The cell-local ending Y coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CellVertical(int px, int py, int x, int y0, int y1) { + // Signed winding is y0 - y1; area is twice the trapezoid between the edge and the + // cell's right boundary, keeping the math integral by deferring the divide by two + // until AreaToCoverage shifts it out. int delta = y0 - y1; int area = delta * ((FixedOne * 2) - x - x); this.AddCell(py, px, delta, area); @@ -965,9 +1115,17 @@ private void CellVertical(int px, int py, int x, int y0, int y1) /// /// Emits one general cell contribution. /// + /// The band-local row index of the cell. + /// The band-local column index of the cell. + /// The cell-local starting X coordinate (0 to ). + /// The cell-local starting Y coordinate. + /// The cell-local ending X coordinate (0 to ). + /// The cell-local ending Y coordinate. [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Cell(int row, int px, int x0, int y0, int x1, int y1) { + // Same doubled trapezoid formulation as CellVertical, using the average of the two + // X endpoints as the edge position within the cell. int delta = y0 - y1; int area = delta * ((FixedOne * 2) - x0 - x1); this.AddCell(row, px, delta, area); @@ -976,6 +1134,10 @@ private void Cell(int row, int px, int x0, int y0, int x1, int y1) /// /// Rasterizes a downward vertical edge segment. /// + /// The band-local column index owning the edge. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. + /// The X coordinate of the edge in 24.8 fixed-point band-local space. private void VerticalDown(int columnIndex, int y0, int y1, int x) { int rowIndex0 = y0 >> FixedShift; @@ -1005,6 +1167,10 @@ private void VerticalDown(int columnIndex, int y0, int y1, int x) /// /// Rasterizes an upward vertical edge segment. /// + /// The band-local column index owning the edge. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. + /// The X coordinate of the edge in 24.8 fixed-point band-local space. private void VerticalUp(int columnIndex, int y0, int y1, int x) { int rowIndex0 = (y0 - 1) >> FixedShift; @@ -1037,6 +1203,11 @@ private void VerticalUp(int columnIndex, int y0, int y1, int x) /// /// Rasterizes a downward, left-to-right segment within a single row. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = p0x >> FixedShift; @@ -1050,6 +1221,8 @@ private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) return; } + // pp/mod/lift/rem implement an integer DDA that advances y at column boundaries + // without accumulating rounding error; the remainder carries the exact fraction. int dx = p1x - p0x; int dy = p1y - p0y; int pp = (FixedOne - fx0) * dy; @@ -1088,6 +1261,11 @@ private void RowDownR(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// RowDownR variant that handles perfectly vertical edge ownership consistently. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x < p1x) @@ -1105,6 +1283,11 @@ private void RowDownR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// Rasterizes an upward, left-to-right segment within a single row. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = p0x >> FixedShift; @@ -1156,6 +1339,11 @@ private void RowUpR(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// RowUpR variant that handles perfectly vertical edge ownership consistently. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x < p1x) @@ -1173,6 +1361,11 @@ private void RowUpR_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// Rasterizes a downward, right-to-left segment within a single row. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = (p0x - 1) >> FixedShift; @@ -1224,6 +1417,11 @@ private void RowDownL(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// RowDownL variant that handles perfectly vertical edge ownership consistently. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x > p1x) @@ -1241,6 +1439,11 @@ private void RowDownL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// Rasterizes an upward, right-to-left segment within a single row. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) { int columnIndex0 = (p0x - 1) >> FixedShift; @@ -1292,6 +1495,11 @@ private void RowUpL(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// RowUpL variant that handles perfectly vertical edge ownership consistently. /// + /// The band-local row index containing the segment. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point row-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point row-local space. private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) { if (p0x > p1x) @@ -1309,6 +1517,12 @@ private void RowUpL_V(int rowIndex, int p0x, int p0y, int p1x, int p1y) /// /// Rasterizes a downward, left-to-right segment spanning multiple rows. /// + /// The band-local index of the first touched row. + /// The band-local index of the last touched row. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x1 - x0; @@ -1355,6 +1569,12 @@ private void LineDownR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int /// /// Rasterizes an upward, left-to-right segment spanning multiple rows. /// + /// The band-local index of the first touched row. + /// The band-local index of the last touched row. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x1 - x0; @@ -1399,6 +1619,12 @@ private void LineUpR(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y /// /// Rasterizes a downward, right-to-left segment spanning multiple rows. /// + /// The band-local index of the first touched row. + /// The band-local index of the last touched row. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x0 - x1; @@ -1443,6 +1669,12 @@ private void LineDownL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int /// /// Rasterizes an upward, right-to-left segment spanning multiple rows. /// + /// The band-local index of the first touched row. + /// The band-local index of the last touched row. + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y1) { int dx = x0 - x1; @@ -1487,6 +1719,10 @@ private void LineUpL(int rowIndex0, int rowIndex1, int x0, int y0, int x1, int y /// /// Dispatches a clipped edge to the correct directional fixed-point walker. /// + /// The starting X coordinate in 24.8 fixed-point band-local space. + /// The starting Y coordinate in 24.8 fixed-point band-local space. + /// The ending X coordinate in 24.8 fixed-point band-local space. + /// The ending Y coordinate in 24.8 fixed-point band-local space. private void RasterizeLine(int x0, int y0, int x1, int y1) { if (x0 == x1) @@ -1566,48 +1802,6 @@ private void RasterizeLine(int x0, int y0, int x1, int y1) } } - /// - /// Immutable scanner-local edge record (16 bytes). - /// - /// - /// All coordinates are stored as signed 24.8 fixed-point integers for predictable hot-path - /// access without per-read unpacking. Row bounds are computed inline from Y coordinates - /// where needed. - /// - internal readonly struct EdgeData - { - /// - /// Gets edge start X in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int X0; - - /// - /// Gets edge start Y in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int Y0; - - /// - /// Gets edge end X in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int X1; - - /// - /// Gets edge end Y in scanner-local coordinates (24.8 fixed-point). - /// - public readonly int Y1; - - /// - /// Initializes a new instance of the struct. - /// - public EdgeData(int x0, int y0, int x1, int y1) - { - this.X0 = x0; - this.Y0 = y0; - this.X1 = x1; - this.Y1 = y1; - } - } - /// /// Reusable per-worker scratch buffers used by raster band execution. /// @@ -1629,6 +1823,23 @@ internal sealed class WorkerScratch : IDisposable private readonly IMemoryOwner scanlineOwner; private IMemoryOwner? strokeBandCoverageOwner; + /// + /// Initializes a new instance of the class taking ownership of the supplied buffers. + /// + /// The allocator used for lazily created stroke coverage scratch. + /// The bit-vector row width, in machine words, this scratch supports. + /// The cover/area stride, in cells, this scratch supports. + /// The maximum band width, in pixels, this scratch supports. + /// The maximum band height, in rows, this scratch supports. + /// The owned bit-vector storage. + /// The owned cover/area cell storage. + /// The owned per-row start-cover storage. + /// The owned per-row minimum touched column storage. + /// The owned per-row maximum touched column storage. + /// The owned per-row bit-data flag storage. + /// The owned per-row touched flag storage. + /// The owned touched-row index list storage. + /// The owned scanline scratch storage. private WorkerScratch( MemoryAllocator allocator, int wordsPerRow, @@ -1678,7 +1889,12 @@ public Span StrokeBandCoverage /// Returns when this scratch has compatible dimensions and sufficient /// capacity for the requested parameters, making it safe to reuse without reallocation. /// - internal bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int requiredWidth, int minCapacity) + /// The bit-vector row width, in machine words, the caller needs. + /// The cover/area stride, in cells, the caller needs. + /// The band width, in pixels, the caller needs. + /// The minimum band height, in rows, the caller needs. + /// when reuse is safe; otherwise . + private bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int requiredWidth, int minCapacity) => this.wordsPerRow >= requiredWordsPerRow && this.coverStride >= requiredCoverStride && this.width >= requiredWidth @@ -1688,46 +1904,84 @@ internal bool CanReuse(int requiredWordsPerRow, int requiredCoverStride, int req /// Returns when this scratch can be reused for the default band configuration /// at the requested width. /// - internal bool CanReuse(int requiredWidth) + /// The band width, in pixels, the caller needs. + /// when reuse is safe; otherwise . + public bool CanReuse(int requiredWidth) => this.CanReuse(BitVectorsForMaxBitCount(requiredWidth), checked(requiredWidth << 1), requiredWidth, PreferredRowHeight); /// /// Allocates worker-local scratch sized for the configured tile/band capacity. /// + /// The memory allocator that owns the scratch buffers. + /// The bit-vector row width in machine words. + /// The cover/area stride in cells (two cells per pixel). + /// The maximum band width in pixels. + /// The maximum band height in rows. + /// A new instance. public static WorkerScratch Create(MemoryAllocator allocator, int wordsPerRow, int coverStride, int width, int tileCapacity) { int bitVectorCapacity = checked(wordsPerRow * tileCapacity); int coverAreaCapacity = checked(coverStride * tileCapacity); - IMemoryOwner bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); - IMemoryOwner coverAreaOwner = allocator.Allocate(coverAreaCapacity); - IMemoryOwner startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner rowMinTouchedColumnOwner = allocator.Allocate(tileCapacity); - IMemoryOwner rowMaxTouchedColumnOwner = allocator.Allocate(tileCapacity); - IMemoryOwner rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); - IMemoryOwner touchedRowsOwner = allocator.Allocate(tileCapacity); - IMemoryOwner scanlineOwner = allocator.Allocate(width); - - return new WorkerScratch( - allocator, - wordsPerRow, - coverStride, - width, - tileCapacity, - bitVectorsOwner, - coverAreaOwner, - startCoverOwner, - rowMinTouchedColumnOwner, - rowMaxTouchedColumnOwner, - rowHasBitsOwner, - rowTouchedOwner, - touchedRowsOwner, - scanlineOwner); + IMemoryOwner? bitVectorsOwner = null; + IMemoryOwner? coverAreaOwner = null; + IMemoryOwner? startCoverOwner = null; + IMemoryOwner? rowMinTouchedColumnOwner = null; + IMemoryOwner? rowMaxTouchedColumnOwner = null; + IMemoryOwner? rowHasBitsOwner = null; + IMemoryOwner? rowTouchedOwner = null; + IMemoryOwner? touchedRowsOwner = null; + + try + { + bitVectorsOwner = allocator.Allocate(bitVectorCapacity, AllocationOptions.Clean); + coverAreaOwner = allocator.Allocate(coverAreaCapacity); + startCoverOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + rowMinTouchedColumnOwner = allocator.Allocate(tileCapacity); + rowMaxTouchedColumnOwner = allocator.Allocate(tileCapacity); + rowHasBitsOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + rowTouchedOwner = allocator.Allocate(tileCapacity, AllocationOptions.Clean); + touchedRowsOwner = allocator.Allocate(tileCapacity); + IMemoryOwner scanlineOwner = allocator.Allocate(width); + + return new WorkerScratch( + allocator, + wordsPerRow, + coverStride, + width, + tileCapacity, + bitVectorsOwner, + coverAreaOwner, + startCoverOwner, + rowMinTouchedColumnOwner, + rowMaxTouchedColumnOwner, + rowHasBitsOwner, + rowTouchedOwner, + touchedRowsOwner, + scanlineOwner); + } + catch + { + // A later allocation throwing strands the earlier rentals: the scratch never + // materialized, so nothing else can return them. + bitVectorsOwner?.Dispose(); + coverAreaOwner?.Dispose(); + startCoverOwner?.Dispose(); + rowMinTouchedColumnOwner?.Dispose(); + rowMaxTouchedColumnOwner?.Dispose(); + rowHasBitsOwner?.Dispose(); + rowTouchedOwner?.Dispose(); + touchedRowsOwner?.Dispose(); + throw; + } } /// /// Creates a context view over a compatible prefix of this scratch for the requested geometry width. /// + /// The fill rule used when converting accumulated winding/coverage into final alpha. + /// The rasterization mode that controls how antialiasing thresholds are interpreted. + /// The threshold used when antialiasing is conditionally reduced or disabled. + /// A backed by this scratch; the caller must not use two contexts over the same scratch concurrently. public Context CreateContext( IntersectionRule intersectionRule, RasterizationMode rasterizationMode, diff --git a/src/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs b/src/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs index f9f00c354..2b4e35bce 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DrawingBackendScene.cs @@ -4,8 +4,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Base type for retained drawing backend scenes. +/// Base type for a retained backend scene: the lowered, reusable form of a batch of drawing +/// commands produced by and later rendered into a +/// target through . /// +/// +/// A scene captures everything a backend needs to replay the commands, including any resources +/// (such as image-brush sources) that must remain alive until the scene is disposed. Disposing +/// the scene releases backend-specific state through and then the +/// retained owned resources. +/// public abstract class DrawingBackendScene : IDisposable { private readonly IReadOnlyList? ownedResources; @@ -37,9 +45,19 @@ public void Dispose() return; } - this.DisposeCore(); - this.DisposeOwnedResources(); + // The flag is set before the releases so a DisposeCore failure can never leave the + // scene re-disposable: a second Dispose after a throw would release every owned + // resource twice. this.isDisposed = true; + try + { + this.DisposeCore(); + } + finally + { + this.DisposeOwnedResources(); + } + GC.SuppressFinalize(this); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs b/src/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs index 4ec65e7e8..f50fcd108 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/DrawingCommandBatch.cs @@ -11,6 +11,10 @@ public readonly struct DrawingCommandBatch /// /// Initializes a new instance of the struct. /// + /// + /// Batches created through this constructor never contain apply barriers or clip controls; + /// those commands are only produced by the internal canvas batcher. + /// /// The draw-order scene commands. /// Indicates whether the command stream contains layer boundaries. public DrawingCommandBatch( @@ -19,6 +23,8 @@ public DrawingCommandBatch( { this.Commands = commands; this.HasLayers = hasLayers; + this.HasApply = false; + this.HasClipControls = false; } /// @@ -27,12 +33,19 @@ public DrawingCommandBatch( /// The backing command buffer. /// The number of commands in the prepared batch. /// Indicates whether the command stream contains layer boundaries. + /// Indicates whether the command stream contains apply barriers. + /// Indicates whether the command stream contains clip controls. internal DrawingCommandBatch( CompositionSceneCommand[] commands, int commandCount, - bool hasLayers) - : this(new ArraySegment(commands, 0, commandCount), hasLayers) + bool hasLayers, + bool hasApply, + bool hasClipControls) { + this.Commands = new ArraySegment(commands, 0, commandCount); + this.HasLayers = hasLayers; + this.HasApply = hasApply; + this.HasClipControls = hasClipControls; } /// @@ -42,13 +55,20 @@ internal DrawingCommandBatch( /// The first command index. /// The number of commands in the prepared batch. /// Indicates whether the command stream contains layer boundaries. + /// Indicates whether the command stream contains apply barriers. + /// Indicates whether the command stream contains clip controls. internal DrawingCommandBatch( CompositionSceneCommand[] commands, int startIndex, int commandCount, - bool hasLayers) - : this(new ArraySegment(commands, startIndex, commandCount), hasLayers) + bool hasLayers, + bool hasApply, + bool hasClipControls) { + this.Commands = new ArraySegment(commands, startIndex, commandCount); + this.HasLayers = hasLayers; + this.HasApply = hasApply; + this.HasClipControls = hasClipControls; } /// @@ -65,4 +85,14 @@ internal DrawingCommandBatch( /// Gets a value indicating whether this scene contains inline layer commands. /// public bool HasLayers { get; } + + /// + /// Gets a value indicating whether this scene contains apply barriers. + /// + public bool HasApply { get; } + + /// + /// Gets a value indicating whether this scene contains clip control commands. + /// + public bool HasClipControls { get; } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs index b7e567221..e0104f18f 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.RetainedTypes.cs @@ -34,12 +34,17 @@ internal enum SceneOperationKind : byte /// /// Ends the most recently opened layer. /// - EndLayer = 3 + EndLayer = 3, + + /// + /// Applies an image processor to the current target. + /// + Apply = 4, } /// - /// Holds one retained row operation. - /// + /// Holds one retained row operation. + /// internal readonly struct SceneOperation { /// @@ -61,7 +66,7 @@ public SceneOperation(SceneOperationKind kind, int itemIndex, int localRowIndex) /// /// The layer operation kind. /// The retained row-local layer bounds. - /// The retained layer-options index for begin-layer operations. + /// The layer command's own index; begin-layer operations use it to look up shared layer state. public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int itemIndex) { this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer; @@ -76,12 +81,14 @@ public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int it public SceneOperationKind Kind { get; } /// - /// Gets the retained scene item index for fill operations. + /// Gets the retained fill or stroke item index for draw operations. For layer operations + /// this is the layer command's own index; begin-layer operations use it to look up the + /// shared layer state. In all cases this equals the original command index. /// public int ItemIndex { get; } /// - /// Gets the retained rasterizable row index for fill operations. + /// Gets the retained rasterizable row index for draw operations, or -1 for layer operations. /// public int LocalRowIndex { get; } @@ -91,6 +98,61 @@ public SceneOperation(CompositionCommandKind kind, Rectangle layerBounds, int it public Rectangle LayerBounds { get; } } + /// + /// Holds one retained command-indexed layer or apply operation. + /// + internal readonly struct SceneControlItem + { + private readonly ApplySceneItem? applyItem; + private readonly DrawingCanvasLayer? layer; + + /// + /// Initializes a new instance of the struct for a layer control operation. + /// + /// The layer operation kind. + /// The retained layer bounds. + /// The retained layer state. + public SceneControlItem(CompositionCommandKind kind, Rectangle layerBounds, DrawingCanvasLayer layer) + { + this.Kind = kind == CompositionCommandKind.BeginLayer ? SceneOperationKind.BeginLayer : SceneOperationKind.EndLayer; + this.LayerBounds = layerBounds; + this.layer = layer; + this.applyItem = null; + } + + /// + /// Initializes a new instance of the struct for an apply operation. + /// + /// The retained apply operation. + public SceneControlItem(ApplySceneItem applyItem) + { + this.Kind = SceneOperationKind.Apply; + this.LayerBounds = default; + this.layer = null; + this.applyItem = applyItem; + } + + /// + /// Gets the operation kind. + /// + public SceneOperationKind Kind { get; } + + /// + /// Gets the retained layer bounds for layer operations. + /// + public Rectangle LayerBounds { get; } + + /// + /// Gets the layer state for layer operations. + /// + public DrawingCanvasLayer Layer => this.layer ?? throw new InvalidOperationException("Only layer steps carry layer state."); + + /// + /// Gets the retained apply operation. + /// + public ApplySceneItem ApplyItem => this.applyItem ?? throw new InvalidOperationException("Only apply steps carry apply state."); + } + /// /// Holds one retained scene row. /// @@ -241,6 +303,9 @@ public static void AppendBuilder(ref RowBuilder destination, ref RowBuilder sour source.firstBlock.Previous = destination.lastBlock; destination.lastBlock = source.lastBlock; destination.count += source.count; + + // Block ownership transfers to the destination; resetting the source prevents its + // Dispose from freeing blocks that are now linked into the destination chain. source = default; } @@ -321,6 +386,242 @@ public SceneOperationBlock(MemoryAllocator allocator) public void Dispose() => this.owner.Dispose(); } + /// + /// Holds retained row work followed by an optional target-wide control operation. + /// + internal sealed class SceneSegment : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The retained rows in this segment. + /// The number of retained row operations in this segment. + /// The apply operation executed after the rows. + /// The scoped layer executed after the rows. + public SceneSegment( + SceneRow[] rows, + int rowItemCount, + ApplySceneItem? applyItem, + ScopedLayerSceneItem? layerItem) + { + this.Rows = rows; + this.RowItemCount = rowItemCount; + this.ApplyItem = applyItem; + this.LayerItem = layerItem; + } + + /// + /// Gets the retained rows in this segment. + /// + public SceneRow[] Rows { get; } + + /// + /// Gets the number of retained row operations in this segment. + /// + public int RowItemCount { get; } + + /// + /// Gets the apply operation executed after the rows. + /// + public ApplySceneItem? ApplyItem { get; } + + /// + /// Gets the scoped layer executed after the rows. + /// + public ScopedLayerSceneItem? LayerItem { get; } + + /// + public void Dispose() + { + for (int i = 0; i < this.Rows.Length; i++) + { + this.Rows[i].Dispose(); + } + + this.ApplyItem?.Dispose(); + this.LayerItem?.Dispose(); + } + } + + /// + /// Holds a scoped layer whose contents must be rendered into one full target before compositing. + /// + internal sealed class ScopedLayerSceneItem : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The retained layer state. + /// The absolute layer target bounds. + /// The retained layer contents. + /// The retained row count inside the layer. + /// The retained row operation count inside the layer. + public ScopedLayerSceneItem( + DrawingCanvasLayer layer, + Rectangle bounds, + SceneSegment[] segments, + int rowCount, + int rowItemCount) + { + this.Layer = layer; + this.Bounds = bounds; + this.Segments = segments; + this.RowCount = rowCount; + this.RowItemCount = rowItemCount; + } + + /// + /// Gets the retained layer state. + /// + public DrawingCanvasLayer Layer { get; } + + /// + /// Gets the absolute layer target bounds. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the retained layer contents. + /// + public SceneSegment[] Segments { get; } + + /// + /// Gets the retained row count inside the layer. + /// + public int RowCount { get; } + + /// + /// Gets the retained row operation count inside the layer. + /// + public int RowItemCount { get; } + + /// + public void Dispose() + { + for (int i = 0; i < this.Segments.Length; i++) + { + this.Segments[i].Dispose(); + } + } + } + + /// + /// Holds one retained apply scene operation. + /// + internal sealed class ApplySceneItem : IDisposable + { + /// + /// Initializes a new instance of the class. + /// + /// The image processing operation. + /// The canvas-local rectangle containing the source pixels supplied to the operation. + /// The canvas-local bounds within which the processed pixels are written. + /// The offset subtracted from when reading the source pixels. + /// The offset used by the runtime image brush. + /// The graphics options used by the apply item. + /// The brush bounds used for applicator creation. + /// The retained rasterizable geometry. + /// The exact clip state captured with the command. + /// The retained path clip raster data captured with the command. + /// The destination offset used to place clip descriptors. + /// The layer that owned this item when it was recorded. + public ApplySceneItem( + Action operation, + Rectangle inputRect, + Rectangle outputRect, + Point readOffset, + Point brushOffset, + GraphicsOptions graphicsOptions, + Rectangle brushBounds, + DefaultRasterizer.RasterizableGeometry rasterizable, + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, + DrawingCanvasLayer? ownerLayer) + { + this.Operation = operation; + this.InputRect = inputRect; + this.OutputRect = outputRect; + this.ReadOffset = readOffset; + this.BrushOffset = brushOffset; + this.GraphicsOptions = graphicsOptions; + this.BrushBounds = brushBounds; + this.Rasterizable = rasterizable; + this.ClipState = clipState; + this.PathClipState = pathClipState; + this.DestinationOffset = destinationOffset; + this.OwnerLayer = ownerLayer; + } + + /// + /// Gets the image processing operation. + /// + public Action Operation { get; } + + /// + /// Gets the canvas-local rectangle containing the source pixels supplied to the operation. + /// + public Rectangle InputRect { get; } + + /// + /// Gets the canvas-local bounds within which the processed pixels are written. + /// + public Rectangle OutputRect { get; } + + /// + /// Gets the offset subtracted from when reading the source pixels, + /// so a write-back recorded at an offset still reads the pre-offset region. + /// + public Point ReadOffset { get; } + + /// + /// Gets the offset used by the runtime image brush. + /// + public Point BrushOffset { get; } + + /// + /// Gets the graphics options used by the apply item. + /// + public GraphicsOptions GraphicsOptions { get; } + + /// + /// Gets the brush bounds used for applicator creation. + /// + public Rectangle BrushBounds { get; } + + /// + /// Gets the retained rasterizable geometry. + /// + public DefaultRasterizer.RasterizableGeometry Rasterizable { get; } + + /// + /// Gets the exact clip state captured with the command. + /// + public DrawingClipState ClipState { get; } + + /// + /// Gets the retained path clip raster data captured with the command. + /// + public PreparedPathClipState? PathClipState { get; } + + /// + /// Gets the destination offset used to place clip descriptors. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the layer that owned this item when it was recorded. + /// + public DrawingCanvasLayer? OwnerLayer { get; } + + /// + public void Dispose() + { + this.Rasterizable.Dispose(); + this.PathClipState?.Dispose(); + } + } + /// /// Holds one retained fill scene item. /// @@ -335,16 +636,28 @@ internal sealed class FillSceneItem : IDisposable /// The graphics options used by the fill item. /// The brush bounds used for applicator creation. /// The retained rasterizable geometry. + /// The exact clip state captured with the command. + /// The retained path clip raster data captured with the command. + /// The destination offset used to place clip descriptors. + /// The layer that owned this item when it was recorded. public FillSceneItem( Brush brush, GraphicsOptions graphicsOptions, Rectangle brushBounds, - DefaultRasterizer.RasterizableGeometry rasterizable) + DefaultRasterizer.RasterizableGeometry rasterizable, + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, + DrawingCanvasLayer? ownerLayer) { this.Brush = brush; this.GraphicsOptions = graphicsOptions; this.BrushBounds = brushBounds; this.Rasterizable = rasterizable; + this.ClipState = clipState; + this.PathClipState = pathClipState; + this.DestinationOffset = destinationOffset; + this.OwnerLayer = ownerLayer; } /// @@ -367,6 +680,26 @@ public FillSceneItem( /// public DefaultRasterizer.RasterizableGeometry Rasterizable { get; } + /// + /// Gets the exact clip state captured with the command. + /// + public DrawingClipState ClipState { get; } + + /// + /// Gets the retained path clip raster data captured with the command. + /// + public PreparedPathClipState? PathClipState { get; } + + /// + /// Gets the destination offset used to place clip descriptors. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the layer that owned this item when it was recorded. + /// + public DrawingCanvasLayer? OwnerLayer { get; } + /// /// Gets the memoized renderer for this scene item, creating it on first use. /// @@ -393,7 +726,11 @@ public BrushRenderer GetRenderer(Configuration configuration, in } /// - public void Dispose() => this.Rasterizable.Dispose(); + public void Dispose() + { + this.Rasterizable.Dispose(); + this.PathClipState?.Dispose(); + } } /// @@ -410,16 +747,28 @@ internal sealed class StrokeSceneItem : IDisposable /// The graphics options for the stroke item. /// The prepared brush bounds. /// The retained stroke rasterizable geometry. + /// The exact clip state captured with the command. + /// The retained path clip raster data captured with the command. + /// The destination offset used to place clip descriptors. + /// The layer that owned this item when it was recorded. public StrokeSceneItem( Brush brush, GraphicsOptions graphicsOptions, Rectangle brushBounds, - DefaultRasterizer.StrokeRasterizableGeometry rasterizable) + DefaultRasterizer.StrokeRasterizableGeometry rasterizable, + DrawingClipState clipState, + PreparedPathClipState? pathClipState, + Point destinationOffset, + DrawingCanvasLayer? ownerLayer) { this.Brush = brush; this.GraphicsOptions = graphicsOptions; this.BrushBounds = brushBounds; this.Rasterizable = rasterizable; + this.ClipState = clipState; + this.PathClipState = pathClipState; + this.DestinationOffset = destinationOffset; + this.OwnerLayer = ownerLayer; } /// @@ -442,6 +791,26 @@ public StrokeSceneItem( /// public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; } + /// + /// Gets the exact clip state captured with the command. + /// + public DrawingClipState ClipState { get; } + + /// + /// Gets the retained path clip raster data captured with the command. + /// + public PreparedPathClipState? PathClipState { get; } + + /// + /// Gets the destination offset used to place clip descriptors. + /// + public Point DestinationOffset { get; } + + /// + /// Gets the layer that owned this item when it was recorded. + /// + public DrawingCanvasLayer? OwnerLayer { get; } + /// /// Gets the memoized renderer for this scene item, creating it on first use. /// @@ -468,6 +837,10 @@ public BrushRenderer GetRenderer(Configuration configuration, in } /// - public void Dispose() => this.Rasterizable.Dispose(); + public void Dispose() + { + this.Rasterizable.Dispose(); + this.PathClipState?.Dispose(); + } } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs index a28df90ee..a0ac19f38 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/FlushScene.cs @@ -1,8 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using System.Numerics; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing.Backends; @@ -12,6 +14,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// internal sealed partial class FlushScene : IDisposable { + private bool isDisposed; + private static readonly FlushScene EmptyScene = new( fillItemCount: 0, strokeItemCount: 0, @@ -23,12 +27,30 @@ internal sealed partial class FlushScene : IDisposable maxLayerDepth: 0, fillItems: [], strokeItems: [], - layerOptions: [], - rows: []); + layers: [], + controlItems: [], + hasApply: false, + rows: [], + segments: []); /// /// Initializes a new instance of the class. /// + /// The number of visible fill items retained by the scene. + /// The number of visible stroke items retained by the scene. + /// The number of scene rows containing executable work. + /// The total number of row items retained by the scene. + /// The total number of encoded raster edges retained by the scene. + /// The number of items that occupy a single row band. + /// The number of items whose retained edge count is small. + /// The maximum retained layer nesting depth. + /// The retained fill items indexed by original command index. + /// The retained stroke items indexed by original command index. + /// The retained layer state indexed by begin-layer command index. + /// The layer and apply control operations indexed by original command index. + /// Whether the scene contains apply barriers. + /// The retained row lists. + /// The retained target-wide execution segments for scenes containing apply barriers. private FlushScene( int fillItemCount, int strokeItemCount, @@ -40,8 +62,11 @@ private FlushScene( int maxLayerDepth, FillSceneItem?[] fillItems, StrokeSceneItem?[] strokeItems, - GraphicsOptions?[] layerOptions, - SceneRow[] rows) + DrawingCanvasLayer?[] layers, + SceneControlItem?[] controlItems, + bool hasApply, + SceneRow[] rows, + SceneSegment[] segments) { this.FillItemCount = fillItemCount; this.StrokeItemCount = strokeItemCount; @@ -53,8 +78,11 @@ private FlushScene( this.MaxLayerDepth = maxLayerDepth; this.FillItems = fillItems; this.StrokeItems = strokeItems; - this.LayerOptions = layerOptions; + this.Layers = layers; + this.ControlItems = controlItems; + this.HasApply = hasApply; this.Rows = rows; + this.Segments = segments; } /// @@ -75,17 +103,27 @@ private FlushScene( /// /// Gets the retained visible scene items. /// - internal FillSceneItem?[] FillItems { get; } + public FillSceneItem?[] FillItems { get; } /// /// Gets the retained visible stroke scene items. /// - internal StrokeSceneItem?[] StrokeItems { get; } + public StrokeSceneItem?[] StrokeItems { get; } + + /// + /// Gets the retained layer state indexed by begin-layer command index. + /// + public DrawingCanvasLayer?[] Layers { get; } + + /// + /// Gets layer and apply control operations indexed by original command index. + /// + private SceneControlItem?[] ControlItems { get; } /// - /// Gets the retained layer options indexed by begin-layer command index. + /// Gets a value indicating whether the scene contains apply barriers. /// - internal GraphicsOptions?[] LayerOptions { get; } + public bool HasApply { get; } /// /// Gets the number of scene rows containing executable work. @@ -95,7 +133,12 @@ private FlushScene( /// /// Gets the retained row lists. /// - internal SceneRow[] Rows { get; } + public SceneRow[] Rows { get; } + + /// + /// Gets retained target-wide execution segments for scenes containing apply barriers. + /// + public SceneSegment[] Segments { get; } /// /// Gets the total number of row items retained by the scene. @@ -140,13 +183,11 @@ public static FlushScene Create( int maxDegreeOfParallelism) { int commandCount = scene.CommandCount; - if (commandCount == 0) { return Empty(); } - IReadOnlyList commands = scene.Commands; int firstTargetRowBandIndex = targetBounds.Top / DefaultRasterizer.DefaultTileHeight; int lastTargetRowBandIndex = (targetBounds.Bottom - 1) / DefaultRasterizer.DefaultTileHeight; int targetRowCount = (lastTargetRowBandIndex - firstTargetRowBandIndex) + 1; @@ -159,124 +200,253 @@ public static FlushScene Create( FillSceneItem?[] fillItems = new FillSceneItem?[commandCount]; StrokeSceneItem?[] strokeItems = new StrokeSceneItem?[commandCount]; - GraphicsOptions?[] layerOptions = new GraphicsOptions?[commandCount]; + DrawingCanvasLayer?[] layers = new DrawingCanvasLayer?[commandCount]; + SceneControlItem?[] controlItems = scene.HasApply ? new SceneControlItem?[commandCount] : []; int partitionCount = ParallelExecutionHelper.GetPartitionCount(maxDegreeOfParallelism, commandCount, targetRowCount); PartitionState[] partitions = new PartitionState[partitionCount]; - _ = Parallel.For( - 0, - partitionCount, - ParallelExecutionHelper.CreateParallelOptions(maxDegreeOfParallelism, partitionCount), - partitionIndex => - { - // Integer division splits the commands into contiguous half-open ranges, - // keeping the partitions balanced while assigning each command exactly once. - int commandStart = (partitionIndex * commandCount) / partitionCount; - int commandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; - - partitions[partitionIndex] = ProcessPartition( - commands, - commandStart, - commandEnd, - targetRectangle, - firstTargetRowBandIndex, - targetRowCount, - allocator, - fillItems, - strokeItems, - layerOptions); - }); + // The ordered begin/end-clip command stream is the single source of truth for clipping. + // A single sequential prescan resolves the clip scopes open at each partition boundary so + // partitions can track the stream independently while processing commands in parallel. + (DrawingClipDescriptor Descriptor, Point Anchor)[][]? partitionClipSeeds = scene.HasClipControls + ? CreatePartitionClipSeeds(scene.Commands, commandCount, partitionCount) + : null; RowBuilder[] rowBuilders = new RowBuilder[targetRowCount]; - int fillItemCount = 0; - int strokeItemCount = 0; - long totalEdgeCount = 0; - int singleBandItemCount = 0; - int smallEdgeItemCount = 0; - int currentLayerDepth = 0; - int maxLayerDepth = 0; + SceneRow[] sceneRows = []; + SceneSegment[] segments = []; + bool rowsFinalized = false; - for (int i = 0; i < partitionCount; i++) + try { - PartitionState partition = partitions[i]; - fillItemCount += partition.FillItemCount; - strokeItemCount += partition.StrokeItemCount; - totalEdgeCount += partition.TotalEdgeCount; - singleBandItemCount += partition.SingleBandItemCount; - smallEdgeItemCount += partition.SmallEdgeItemCount; - maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth + partition.MaxLayerDepth); - currentLayerDepth += partition.LayerDepthDelta; + _ = Parallel.For( + 0, + partitionCount, + ParallelExecutionHelper.CreateParallelOptions(maxDegreeOfParallelism, partitionCount), + partitionIndex => + { + // Integer division splits the commands into contiguous half-open ranges, + // keeping the partitions balanced while assigning each command exactly once. + int partitionCommandStart = (partitionIndex * commandCount) / partitionCount; + int partitionCommandEnd = ((partitionIndex + 1) * commandCount) / partitionCount; + + partitions[partitionIndex] = ProcessPartition( + scene.Commands, + partitionCommandStart, + partitionCommandEnd, + partitionClipSeeds?[partitionIndex], + targetRectangle, + firstTargetRowBandIndex, + targetRowCount, + allocator, + fillItems, + strokeItems, + layers, + controlItems); + }); + + int fillItemCount = 0; + int strokeItemCount = 0; + long totalEdgeCount = 0; + int singleBandItemCount = 0; + int smallEdgeItemCount = 0; + int currentLayerDepth = 0; + int maxLayerDepth = 0; + + // Merge partitions in ascending index order. Partitions cover contiguous ascending command + // ranges, so appending their row builders in this order preserves painter's order per row. + for (int i = 0; i < partitionCount; i++) + { + PartitionState partition = partitions[i]; + fillItemCount += partition.FillItemCount; + strokeItemCount += partition.StrokeItemCount; + totalEdgeCount += partition.TotalEdgeCount; + singleBandItemCount += partition.SingleBandItemCount; + smallEdgeItemCount += partition.SmallEdgeItemCount; + + // Partition layer depths are relative to their own command start; offsetting by the + // depth accumulated across earlier partitions recovers the absolute nesting depth. + maxLayerDepth = Math.Max(maxLayerDepth, currentLayerDepth + partition.MaxLayerDepth); + currentLayerDepth += partition.LayerDepthDelta; + + for (int rowSlot = 0; rowSlot < targetRowCount; rowSlot++) + { + RowBuilder.AppendBuilder(ref rowBuilders[rowSlot], ref partition.RowBuilders[rowSlot]); + } + } - for (int rowSlot = 0; rowSlot < targetRowCount; rowSlot++) + int rowCount = 0; + int rowItemCount = 0; + for (int i = 0; i < rowBuilders.Length; i++) { - RowBuilder.AppendBuilder(ref rowBuilders[rowSlot], ref partition.RowBuilders[rowSlot]); + if (!rowBuilders[i].IsInitialized) + { + continue; + } + + rowCount++; + rowItemCount += rowBuilders[i].Count; } - } - int rowCount = 0; - int rowItemCount = 0; - for (int i = 0; i < rowBuilders.Length; i++) - { - if (!rowBuilders[i].IsInitialized) + // Apply barriers execute even when no draw work survived clipping, so a scene that + // contains them can never collapse to the empty singleton. + if (((fillItemCount + strokeItemCount) == 0 || rowItemCount == 0) && !scene.HasApply) { - continue; + // Retained items whose row operations were all clipped away still own rasterized + // geometry and clip state; the empty singleton carries nothing, so release them here. + DisposeRetainedItems(fillItems, strokeItems, controlItems); + DisposeRows(rowBuilders); + return Empty(); } - rowCount++; - rowItemCount += rowBuilders[i].Count; - } + sceneRows = rowCount == 0 + ? [] + : FinalizeRows(rowBuilders, firstTargetRowBandIndex, rowCount); + rowsFinalized = true; - if ((fillItemCount + strokeItemCount) == 0 || rowItemCount == 0) - { - DisposeRows(rowBuilders); - return Empty(); + if (scene.HasApply) + { + segments = CreateApplySegments( + commandCount, + sceneRows, + controlItems, + allocator, + firstTargetRowBandIndex, + targetRowCount, + out rowCount, + out rowItemCount); + + // Segment building copied every row operation into per-segment storage, so the merged + // rows are redundant. Apply item ownership also moved into the segments; clearing the + // control items prevents Dispose from double-disposing those apply items. + for (int i = 0; i < sceneRows.Length; i++) + { + sceneRows[i].Dispose(); + } + + sceneRows = []; + controlItems = []; + } + + return new FlushScene( + fillItemCount, + strokeItemCount, + rowCount, + rowItemCount, + totalEdgeCount, + singleBandItemCount, + smallEdgeItemCount, + maxLayerDepth, + fillItems, + strokeItems, + layers, + controlItems, + scene.HasApply, + sceneRows, + segments); } + catch + { + // A throw anywhere above strands every retained item already written into the shared + // arrays. Partition builders emptied by the merge dispose as no-ops, finalized rows + // own the block chains the builders shared, and apply items are reachable through the + // control items until segment creation succeeds, at which point the segments own them. + for (int i = 0; i < partitions.Length; i++) + { + if (partitions[i].RowBuilders is not null) + { + DisposeRows(partitions[i].RowBuilders); + } + } - SceneRow[] sceneRows = FinalizeRows(rowBuilders, firstTargetRowBandIndex, rowCount); - return new FlushScene( - fillItemCount, - strokeItemCount, - rowCount, - rowItemCount, - totalEdgeCount, - singleBandItemCount, - smallEdgeItemCount, - maxLayerDepth, - fillItems, - strokeItems, - layerOptions, - sceneRows); + if (!rowsFinalized) + { + DisposeRows(rowBuilders); + } + + for (int i = 0; i < sceneRows.Length; i++) + { + sceneRows[i].Dispose(); + } + + for (int i = 0; i < segments.Length; i++) + { + segments[i].Dispose(); + } + + DisposeRetainedItems(fillItems, strokeItems, segments.Length == 0 ? controlItems : []); + throw; + } } /// - /// Releases retained scene storage. + /// Releases retained scene storage. The shared empty singleton is returned for every empty + /// batch and must survive any caller's disposal. /// public void Dispose() { + if (this.isDisposed || ReferenceEquals(this, EmptyScene)) + { + return; + } + + this.isDisposed = true; for (int i = 0; i < this.Rows.Length; i++) { this.Rows[i].Dispose(); } - for (int i = 0; i < this.FillItems.Length; i++) + for (int i = 0; i < this.Segments.Length; i++) { - this.FillItems[i]?.Dispose(); + this.Segments[i].Dispose(); } - for (int i = 0; i < this.StrokeItems.Length; i++) + DisposeRetainedItems(this.FillItems, this.StrokeItems, this.ControlItems); + } + + /// + /// Releases retained draw items and apply control items that never transferred into a + /// constructed scene, or that a constructed scene still owns at disposal. + /// + /// The retained fill items indexed by original command index. + /// The retained stroke items indexed by original command index. + /// The layer and apply control operations indexed by original command index. + private static void DisposeRetainedItems( + FillSceneItem?[] fillItems, + StrokeSceneItem?[] strokeItems, + SceneControlItem?[] controlItems) + { + for (int i = 0; i < fillItems.Length; i++) { - this.StrokeItems[i]?.Dispose(); + fillItems[i]?.Dispose(); + } + + for (int i = 0; i < strokeItems.Length; i++) + { + strokeItems[i]?.Dispose(); + } + + for (int i = 0; i < controlItems.Length; i++) + { + if (controlItems[i] is SceneControlItem controlItem && + controlItem.Kind == SceneOperationKind.Apply) + { + controlItem.ApplyItem.Dispose(); + } } } /// - /// Creates an empty scene instance. + /// Gets the shared empty scene instance. /// + /// The cached empty scene singleton. private static FlushScene Empty() => EmptyScene; /// /// Identifies whether a path-backed command contributes executable retained raster work to the scene. /// + /// The command to inspect. + /// when the command retains raster work; otherwise . [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsSceneDrawable(in CompositionCommand command) => command.Kind == CompositionCommandKind.FillLayer; @@ -284,6 +454,10 @@ private static bool IsSceneDrawable(in CompositionCommand command) /// /// Accumulates retained fill statistics used for scene heuristics. /// + /// The retained rasterizable fill geometry to measure. + /// The running total of encoded raster edges. + /// The running count of covered bands with a small edge count. + /// The running count of items that occupy a single row band. private static void AccumulateFillItemStats( DefaultRasterizer.RasterizableGeometry rasterizable, ref long totalEdgeCount, @@ -314,6 +488,10 @@ private static void AccumulateFillItemStats( /// /// Accumulates retained stroke statistics used for scene heuristics. /// + /// The retained rasterizable stroke geometry to measure. + /// The running total of encoded raster edges. + /// The running count of covered bands with a small edge count. + /// The running count of items that occupy a single row band. private static void AccumulateStrokeItemStats( DefaultRasterizer.StrokeRasterizableGeometry rasterizable, ref long totalEdgeCount, @@ -344,6 +522,13 @@ private static void AccumulateStrokeItemStats( /// /// Appends retained fill row operations for one item into the row builders owned by the current partition. /// + /// The partition-owned row builders, one slot per target row band. + /// The first row slot the item may write to. + /// The exclusive end row slot the item may write to. + /// The first row-band index covered by the target. + /// The retained scene item index; equals the original command index. + /// The retained rasterizable fill geometry. + /// The allocator used for row-block storage. private static void AppendFillRowOperations( RowBuilder[] rowBuilders, int rowStart, @@ -376,6 +561,13 @@ private static void AppendFillRowOperations( /// /// Appends retained stroke row operations for one item into the row builders owned by the current partition. /// + /// The partition-owned row builders, one slot per target row band. + /// The first row slot the item may write to. + /// The exclusive end row slot the item may write to. + /// The first row-band index covered by the target. + /// The retained scene item index; equals the original command index. + /// The retained rasterizable stroke geometry. + /// The allocator used for row-block storage. private static void AppendStrokeRowOperations( RowBuilder[] rowBuilders, int rowStart, @@ -414,8 +606,8 @@ private static void AppendStrokeRowOperations( /// region's bounds would change long-standing rendering behaviour for region-only paths. /// /// The command's absolute target bounds. - /// The first row-band index covered by the partition. - /// The total number of row slots owned by the partition. + /// The first row-band index covered by the target. + /// The total number of target row slots. /// True if the command was recorded inside a SaveLayer scope. /// The first row slot the command may write to. /// The exclusive end row slot the command may write to. @@ -443,6 +635,14 @@ private static void GetEffectiveRowSlotRange( /// /// Identifies whether a command contributes retained per-row layer control operations. /// + /// The command to inspect. + /// The destination bounds of the flush. + /// The first row-band index covered by the target. + /// The resolved begin or end layer operation kind. + /// The layer bounds intersected with the target bounds. + /// The first row slot touched by the layer. + /// The last row slot touched by the layer, inclusive. + /// when the command is a layer operation visible within the target; otherwise . private static bool TryGetLayerOperation( in CompositionCommand command, in Rectangle targetBounds, @@ -488,6 +688,10 @@ private static bool TryGetLayerOperation( /// /// Finalizes row-owned append builders into immutable scene rows. /// + /// The row builders to finalize, one slot per target row band. + /// The first row-band index covered by the target. + /// The number of initialized builders; sizes the resulting array. + /// The finalized scene rows in ascending row-band order. private static SceneRow[] FinalizeRows(RowBuilder[] builders, int firstTargetRowBandIndex, int rowCount) { SceneRow[] rows = new SceneRow[rowCount]; @@ -508,6 +712,7 @@ private static SceneRow[] FinalizeRows(RowBuilder[] builders, int firstTargetRow /// /// Disposes partially created row builders. /// + /// The row builders to dispose. private static void DisposeRows(RowBuilder[] builders) { for (int i = 0; i < builders.Length; i++) @@ -516,17 +721,179 @@ private static void DisposeRows(RowBuilder[] builders) } } + /// + /// Builds target-wide execution segments for a scene containing apply barriers. + /// + /// The number of commands in the source batch. + /// The merged scene rows whose operations are redistributed into segments. + /// The layer and apply control operations indexed by original command index. + /// The allocator used for retained row storage. + /// The first row-band index covered by the target. + /// The number of row bands covered by the target. + /// The total retained row count across all segments. + /// The total retained row operation count across all segments. + /// The finalized target-wide execution segments. + private static SceneSegment[] CreateApplySegments( + int commandCount, + SceneRow[] rows, + SceneControlItem?[] controlItems, + MemoryAllocator allocator, + int firstTargetRowBandIndex, + int targetRowCount, + out int rowCount, + out int rowItemCount) + { + SceneSequenceBuilder root = new(allocator, firstTargetRowBandIndex, targetRowCount); + SceneSegmentBuilder?[] commandSegments = new SceneSegmentBuilder?[commandCount]; + List scopedLayers = []; + SceneSequenceBuilder current = root; + + for (int commandIndex = 0; commandIndex < commandCount; commandIndex++) + { + if (controlItems[commandIndex] is not SceneControlItem controlItem) + { + commandSegments[commandIndex] = current.CurrentSegment; + continue; + } + + switch (controlItem.Kind) + { + case SceneOperationKind.BeginLayer: + if (controlItem.Layer.RequiresScopedApply) + { + SceneSequenceBuilder child = new(allocator, firstTargetRowBandIndex, targetRowCount); + scopedLayers.Add(new ScopedLayerBuildFrame(current, controlItem.Layer, controlItem.LayerBounds, child)); + current = child; + } + else + { + commandSegments[commandIndex] = current.CurrentSegment; + } + + break; + + case SceneOperationKind.EndLayer: + if (scopedLayers.Count != 0 && + ReferenceEquals(scopedLayers[^1].Layer, controlItem.Layer)) + { + ScopedLayerBuildFrame frame = scopedLayers[^1]; + scopedLayers.RemoveAt(scopedLayers.Count - 1); + current = frame.Parent; + current.AddLayer(new ScopedLayerSceneBuilder(frame.Layer, frame.Bounds, frame.Content)); + } + else + { + commandSegments[commandIndex] = current.CurrentSegment; + } + + break; + + case SceneOperationKind.Apply: + current.AddApply(controlItem.ApplyItem); + break; + } + } + + // Apply barriers are whole-target operations. Split retained rows once here so render + // can replay each segment directly instead of filtering every row by command index. + for (int rowIndex = 0; rowIndex < rows.Length; rowIndex++) + { + SceneRow row = rows[rowIndex]; + int rowSlot = row.RowBandIndex - firstTargetRowBandIndex; + + for (SceneOperationBlock? block = row.FirstBlock; block is not null; block = block.Next) + { + foreach (SceneOperation operation in block.Items) + { + // ItemIndex always equals the original command index, so it selects the segment + // that was current when the command was recorded. Scoped-layer begin/end markers + // have no segment and are dropped; the scoped layer item composites its content + // as a whole instead of replaying inline layer markers. + commandSegments[operation.ItemIndex]?.Append(rowSlot, operation); + } + } + } + + return root.FinalizeSegments(out rowCount, out rowItemCount); + } + + /// + /// Computes, for each partition's command start, the clip scopes opened by earlier partitions, + /// outermost first, resolved from the ordered begin/end-clip command stream. + /// + /// The ordered retained command stream. + /// The number of commands in the stream. + /// The number of partitions the commands are split into. + /// Per-partition arrays of the clip scopes open at each partition boundary, outermost first. + private static (DrawingClipDescriptor Descriptor, Point Anchor)[][] CreatePartitionClipSeeds( + IReadOnlyList commands, + int commandCount, + int partitionCount) + { + (DrawingClipDescriptor Descriptor, Point Anchor)[][] seeds = new (DrawingClipDescriptor, Point)[partitionCount][]; + List<(DrawingClipDescriptor Descriptor, Point Anchor)> openClips = []; + int cursor = 0; + + for (int partitionIndex = 0; partitionIndex < partitionCount; partitionIndex++) + { + // Partition boundaries are non-decreasing, so a single forward cursor visits each + // command exactly once even though every partition boundary is snapshotted. + int boundary = (partitionIndex * commandCount) / partitionCount; + for (; cursor < boundary; cursor++) + { + if (commands[cursor] is not PathCompositionSceneCommand pathCommand) + { + continue; + } + + CompositionCommand composition = pathCommand.Command; + if (composition.Kind == CompositionCommandKind.BeginClip) + { + openClips.Add((composition.ClipDescriptor, composition.DestinationOffset)); + } + else if (composition.Kind == CompositionCommandKind.EndClip && openClips.Count > 0) + { + openClips.RemoveAt(openClips.Count - 1); + } + } + + seeds[partitionIndex] = openClips.Count == 0 ? [] : [.. openClips]; + } + + return seeds; + } + + /// + /// Prepares one contiguous command range into retained scene items and partition-local row builders. + /// Partitions may run in parallel; each writes only to its own command indices in the shared item + /// arrays and to its own row builders, so no synchronization is required. + /// + /// The ordered retained command stream. + /// The first command index owned by this partition. + /// The exclusive end command index owned by this partition. + /// The clip scopes opened by earlier partitions, outermost first, or when the batch has no clip controls. + /// The destination bounds of the flush. + /// The first row-band index covered by the target. + /// The number of row bands covered by the target. + /// The allocator used for retained row storage. + /// The shared retained fill items indexed by original command index. + /// The shared retained stroke items indexed by original command index. + /// The shared retained layer state indexed by begin-layer command index. + /// The shared layer and apply control operations indexed by original command index; empty when the batch has no apply barriers. + /// The partition-local counts and row builders to merge after all partitions complete. private static PartitionState ProcessPartition( IReadOnlyList commands, int commandStart, int commandEnd, + (DrawingClipDescriptor Descriptor, Point Anchor)[]? clipSeed, in Rectangle targetBounds, int firstTargetRowBandIndex, int targetRowCount, MemoryAllocator allocator, FillSceneItem?[] fillItems, StrokeSceneItem?[] strokeItems, - GraphicsOptions?[] layerOptions) + DrawingCanvasLayer?[] layers, + SceneControlItem?[] controlItems) { RowBuilder[] rowBuilders = new RowBuilder[targetRowCount]; int fillItemCount = 0; @@ -536,22 +903,43 @@ private static PartitionState ProcessPartition( int smallEdgeItemCount = 0; int currentLayerDepth = 0; int maxLayerDepth = 0; + ClipStreamTracker clipTracker = ClipStreamTracker.CreateFromSeed(clipSeed); for (int commandIndex = commandStart; commandIndex < commandEnd; commandIndex++) { CompositionSceneCommand command = commands[commandIndex]; if (command is PathCompositionSceneCommand pathCommand) { + CompositionCommand composition = pathCommand.Command; + + // Clip commands only mutate the tracker; they retain no scene items. Every draw + // command that follows captures the composed clip state and anchor from the tracker, + // keeping the ordered clip stream the single source of truth for clipping. + if (composition.Kind == CompositionCommandKind.BeginClip) + { + clipTracker.Push(composition.ClipDescriptor, composition.DestinationOffset); + continue; + } + + if (composition.Kind == CompositionCommandKind.EndClip) + { + clipTracker.Pop(); + continue; + } + ProcessPathCommand( - pathCommand.Command, + composition, commandIndex, + clipTracker.Current, + clipTracker.Anchor, targetBounds, firstTargetRowBandIndex, rowBuilders, allocator, fillItems, strokeItems, - layerOptions, + layers, + controlItems, ref fillItemCount, ref strokeItemCount, ref totalEdgeCount, @@ -565,6 +953,8 @@ private static PartitionState ProcessPartition( ProcessStrokePathCommand( strokePathCommand.Command, commandIndex, + clipTracker.Current, + clipTracker.Anchor, targetRowCount, firstTargetRowBandIndex, rowBuilders, @@ -580,6 +970,8 @@ private static PartitionState ProcessPartition( ProcessLineSegmentCommand( lineSegmentCommand.Command, commandIndex, + clipTracker.Current, + clipTracker.Anchor, targetRowCount, firstTargetRowBandIndex, rowBuilders, @@ -590,11 +982,13 @@ private static PartitionState ProcessPartition( ref singleBandItemCount, ref smallEdgeItemCount); } - else + else if (command is PolylineCompositionSceneCommand polylineCommand) { ProcessPolylineCommand( - ((PolylineCompositionSceneCommand)command).Command, + polylineCommand.Command, commandIndex, + clipTracker.Current, + clipTracker.Anchor, targetRowCount, firstTargetRowBandIndex, rowBuilders, @@ -618,16 +1012,42 @@ private static PartitionState ProcessPartition( rowBuilders); } + /// + /// Prepares one path-backed composition command, retaining an apply, layer, or fill scene item + /// and distributing its row operations into the partition-local row builders. + /// + /// The composition command to prepare. + /// The original command index; the retained item is stored at this index. + /// The composed clip state resolved from the clip stream at this command. + /// The destination offset the open clip scopes are anchored at. + /// The destination bounds of the flush. + /// The first row-band index covered by the target. + /// The partition-owned row builders, one slot per target row band. + /// The allocator used for retained row storage. + /// The shared retained fill items indexed by original command index. + /// The shared retained stroke items indexed by original command index. + /// The shared retained layer state indexed by begin-layer command index. + /// The shared layer and apply control operations; empty when the batch has no apply barriers. + /// The running partition fill item count. + /// The running partition stroke item count. + /// The running total of encoded raster edges. + /// The running count of items that occupy a single row band. + /// The running count of covered bands with a small edge count. + /// The running layer depth relative to the partition start. + /// The maximum layer depth observed relative to the partition start. private static void ProcessPathCommand( in CompositionCommand command, int commandIndex, + DrawingClipState clipState, + Point clipAnchor, in Rectangle targetBounds, int firstTargetRowBandIndex, RowBuilder[] rowBuilders, MemoryAllocator allocator, FillSceneItem?[] fillItems, StrokeSceneItem?[] strokeItems, - GraphicsOptions?[] layerOptions, + DrawingCanvasLayer?[] layers, + SceneControlItem?[] controlItems, ref int fillItemCount, ref int strokeItemCount, ref long totalEdgeCount, @@ -636,6 +1056,61 @@ private static void ProcessPathCommand( ref int currentLayerDepth, ref int maxLayerDepth) { + if (command.Kind == CompositionCommandKind.Apply) + { + RectangleF rawOutputBounds = RectangleF.Transform(command.ApplyOutputBounds, command.DrawingOptions.Transform); + Rectangle outputRect = Rectangle.Intersect(command.ApplyCanvasBounds, ToConservativeBounds(rawOutputBounds)); + if (outputRect.Width <= 0 || outputRect.Height <= 0) + { + return; + } + + if (!TryPrepareFillPath( + command.SourcePath, + Brushes.Solid(Color.White), + command.DrawingOptions, + command.RasterizerOptions, + command.TargetBounds, + command.DestinationOffset, + command.SubPixelOffset, + allocator, + out PreparedFillItem preparedApply)) + { + return; + } + + if (!PreparedPathClipState.TryCreate( + clipState, + command.TargetBounds, + clipAnchor, + allocator, + out PreparedPathClipState? preparedApplyPathClips)) + { + preparedApply.Rasterizable.Dispose(); + return; + } + + Point brushOffset = new( + outputRect.X - (int)MathF.Floor(rawOutputBounds.Left), + outputRect.Y - (int)MathF.Floor(rawOutputBounds.Top)); + + controlItems[commandIndex] = new SceneControlItem( + new ApplySceneItem( + command.ApplyBarrier.Operation, + outputRect, + outputRect, + command.ApplyBarrier.WriteBackOffset, + brushOffset, + preparedApply.GraphicsOptions, + preparedApply.BrushBounds, + preparedApply.Rasterizable, + clipState, + preparedApplyPathClips, + clipAnchor, + command.OwnerLayer)); + return; + } + if (TryGetLayerOperation( command, targetBounds, @@ -655,13 +1130,17 @@ private static void ProcessPathCommand( currentLayerDepth--; } - int layerOptionsIndex = -1; + int layerOptionsIndex = commandIndex; if (operationKind == CompositionCommandKind.BeginLayer) { - // BeginLayer carries the compositing options used later by the matching EndLayer. - // Store them at the command index so row operations can keep a compact integer reference. - layerOptions[commandIndex] = command.GraphicsOptions; - layerOptionsIndex = commandIndex; + // BeginLayer carries the shared layer state used later by the matching EndLayer. + // Store it at the command index so row operations can keep a compact integer reference. + layers[commandIndex] = command.Layer; + } + + if (controlItems.Length != 0) + { + controlItems[commandIndex] = new SceneControlItem(operationKind, layerBounds, command.Layer); } AppendLayerOperations(rowBuilders, firstRowSlot, lastRowSlot, layerBounds, operationKind, layerOptionsIndex, targetBounds, allocator); @@ -679,16 +1158,54 @@ private static void ProcessPathCommand( return; } - fillItems[commandIndex] = new FillSceneItem(preparedFill.Brush, preparedFill.GraphicsOptions, preparedFill.BrushBounds, preparedFill.Rasterizable); + if (!PreparedPathClipState.TryCreate( + clipState, + command.TargetBounds, + clipAnchor, + allocator, + out PreparedPathClipState? preparedPathClips)) + { + preparedFill.Rasterizable.Dispose(); + return; + } + + fillItems[commandIndex] = new FillSceneItem( + preparedFill.Brush, + preparedFill.GraphicsOptions, + preparedFill.BrushBounds, + preparedFill.Rasterizable, + clipState, + preparedPathClips, + clipAnchor, + command.OwnerLayer); fillItemCount++; AccumulateFillItemStats(preparedFill.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, rowBuilders.Length, command.IsInsideLayer, out int rowStart, out int rowEnd); AppendFillRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedFill.Rasterizable, allocator); } + /// + /// Prepares one stroked path command, retaining a stroke scene item and distributing its row + /// operations into the partition-local row builders. + /// + /// The stroke path command to prepare. + /// The original command index; the retained item is stored at this index. + /// The composed clip state resolved from the clip stream at this command. + /// The destination offset the open clip scopes are anchored at. + /// The number of row bands covered by the target. + /// The first row-band index covered by the target. + /// The partition-owned row builders, one slot per target row band. + /// The allocator used for retained row storage. + /// The shared retained stroke items indexed by original command index. + /// The running partition stroke item count. + /// The running total of encoded raster edges. + /// The running count of items that occupy a single row band. + /// The running count of covered bands with a small edge count. private static void ProcessStrokePathCommand( in StrokePathCommand command, int commandIndex, + DrawingClipState clipState, + Point clipAnchor, int targetRowCount, int firstTargetRowBandIndex, RowBuilder[] rowBuilders, @@ -705,16 +1222,54 @@ private static void ProcessStrokePathCommand( return; } - strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + if (!PreparedPathClipState.TryCreate( + clipState, + command.TargetBounds, + clipAnchor, + allocator, + out PreparedPathClipState? preparedPathClips)) + { + preparedStroke.Rasterizable.Dispose(); + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem( + preparedStroke.Brush, + preparedStroke.GraphicsOptions, + preparedStroke.BrushBounds, + preparedStroke.Rasterizable, + clipState, + preparedPathClips, + clipAnchor, + command.OwnerLayer); strokeItemCount++; AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); } + /// + /// Prepares one stroked line segment command, retaining a stroke scene item and distributing its + /// row operations into the partition-local row builders. + /// + /// The line segment stroke command to prepare. + /// The original command index; the retained item is stored at this index. + /// The composed clip state resolved from the clip stream at this command. + /// The destination offset the open clip scopes are anchored at. + /// The number of row bands covered by the target. + /// The first row-band index covered by the target. + /// The partition-owned row builders, one slot per target row band. + /// The allocator used for retained row storage. + /// The shared retained stroke items indexed by original command index. + /// The running partition stroke item count. + /// The running total of encoded raster edges. + /// The running count of items that occupy a single row band. + /// The running count of covered bands with a small edge count. private static void ProcessLineSegmentCommand( in StrokeLineSegmentCommand command, int commandIndex, + DrawingClipState clipState, + Point clipAnchor, int targetRowCount, int firstTargetRowBandIndex, RowBuilder[] rowBuilders, @@ -731,16 +1286,54 @@ private static void ProcessLineSegmentCommand( return; } - strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + if (!PreparedPathClipState.TryCreate( + clipState, + command.TargetBounds, + clipAnchor, + allocator, + out PreparedPathClipState? preparedPathClips)) + { + preparedStroke.Rasterizable.Dispose(); + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem( + preparedStroke.Brush, + preparedStroke.GraphicsOptions, + preparedStroke.BrushBounds, + preparedStroke.Rasterizable, + clipState, + preparedPathClips, + clipAnchor, + command.OwnerLayer); strokeItemCount++; AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); } + /// + /// Prepares one stroked polyline command, retaining a stroke scene item and distributing its row + /// operations into the partition-local row builders. + /// + /// The polyline stroke command to prepare. + /// The original command index; the retained item is stored at this index. + /// The composed clip state resolved from the clip stream at this command. + /// The destination offset the open clip scopes are anchored at. + /// The number of row bands covered by the target. + /// The first row-band index covered by the target. + /// The partition-owned row builders, one slot per target row band. + /// The allocator used for retained row storage. + /// The shared retained stroke items indexed by original command index. + /// The running partition stroke item count. + /// The running total of encoded raster edges. + /// The running count of items that occupy a single row band. + /// The running count of covered bands with a small edge count. private static void ProcessPolylineCommand( in StrokePolylineCommand command, int commandIndex, + DrawingClipState clipState, + Point clipAnchor, int targetRowCount, int firstTargetRowBandIndex, RowBuilder[] rowBuilders, @@ -757,13 +1350,44 @@ private static void ProcessPolylineCommand( return; } - strokeItems[commandIndex] = new StrokeSceneItem(preparedStroke.Brush, preparedStroke.GraphicsOptions, preparedStroke.BrushBounds, preparedStroke.Rasterizable); + if (!PreparedPathClipState.TryCreate( + clipState, + command.TargetBounds, + clipAnchor, + allocator, + out PreparedPathClipState? preparedPathClips)) + { + preparedStroke.Rasterizable.Dispose(); + return; + } + + strokeItems[commandIndex] = new StrokeSceneItem( + preparedStroke.Brush, + preparedStroke.GraphicsOptions, + preparedStroke.BrushBounds, + preparedStroke.Rasterizable, + clipState, + preparedPathClips, + clipAnchor, + command.OwnerLayer); strokeItemCount++; AccumulateStrokeItemStats(preparedStroke.Rasterizable, ref totalEdgeCount, ref smallEdgeItemCount, ref singleBandItemCount); GetEffectiveRowSlotRange(command.TargetBounds, firstTargetRowBandIndex, targetRowCount, command.IsInsideLayer, out int rowStart, out int rowEnd); AppendStrokeRowOperations(rowBuilders, rowStart, rowEnd, firstTargetRowBandIndex, commandIndex, preparedStroke.Rasterizable, allocator); } + /// + /// Appends one begin or end layer operation into every row builder the layer touches so that + /// per-row rendering can open and close the layer at the correct point in row order. + /// + /// The partition-owned row builders, one slot per target row band. + /// The first row slot touched by the layer. + /// The last row slot touched by the layer, inclusive. + /// The layer bounds intersected with the target bounds. + /// The begin or end layer operation kind. + /// The command index where the shared layer state is stored. + /// The destination bounds of the flush. + /// The allocator used for row-block storage. private static void AppendLayerOperations( RowBuilder[] rowBuilders, int firstRowSlot, @@ -789,26 +1413,76 @@ private static void AppendLayerOperations( } } + /// + /// Prepares the retained fill payload for one fill-layer composition command. + /// + /// The command supplying the path, brush, and options. + /// The allocator used for retained raster storage. + /// The prepared fill payload when successful. + /// when the fill intersects the target and rasterizable geometry was created; otherwise . private static bool TryPrepareFillPath( in CompositionCommand command, MemoryAllocator allocator, out PreparedFillItem prepared) + => TryPrepareFillPath( + command.SourcePath, + command.Brush, + command.DrawingOptions, + command.RasterizerOptions, + command.TargetBounds, + command.DestinationOffset, + command.SubPixelOffset, + allocator, + out prepared); + + /// + /// Prepares the retained fill payload for a path. The transform is split into an axis-aligned + /// scale, applied while flattening so curve tolerance tracks the device resolution, and a + /// residual matrix applied during rasterization. + /// + /// The source path to fill. + /// The brush recorded with the command. + /// The drawing options supplying the transform and graphics options. + /// The rasterizer options recorded with the command. + /// The destination bounds of the flush. + /// The offset from path-local to destination coordinates. + /// The fractional translation applied after the drawing options transform. + /// The allocator used for retained raster storage. + /// The prepared fill payload when successful. + /// when the fill intersects the target and rasterizable geometry was created; otherwise . + internal static bool TryPrepareFillPath( + IPath path, + Brush sourceBrush, + DrawingOptions drawingOptions, + in RasterizerOptions sourceRasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Vector2 subPixelOffset, + MemoryAllocator allocator, + out PreparedFillItem prepared) { - IPath path = command.SourcePath; - Matrix4x4 transform = command.Transform; - bool hasTransform = !transform.IsIdentity; - Vector2 scale = ExtractScale(transform); - Matrix4x4 residual = ComputeResidual(scale, transform); + Matrix4x4 transform = drawingOptions.Transform; + if (subPixelOffset != Vector2.Zero) + { + // The sub-pixel remainder is a device-space nudge applied after the recorded + // transform, mirroring CompositionCommand.Transform for callers that receive the + // options and offset separately. + transform = transform.IsIdentity + ? Matrix4x4.CreateTranslation(subPixelOffset.X, subPixelOffset.Y, 0F) + : transform * Matrix4x4.CreateTranslation(subPixelOffset.X, subPixelOffset.Y, 0F); + } + + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); LinearGeometry geometry = path.ToLinearGeometry(scale); - Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); if (!TryResolveRasterization( sourceBrush, geometryBounds, - command.RasterizerOptions, - command.DestinationOffset, - command.TargetBounds, + sourceRasterizerOptions, + destinationOffset, + targetBounds, out Brush brush, out RasterizerOptions rasterizerOptions, out Rectangle brushBounds)) @@ -820,8 +1494,8 @@ private static bool TryPrepareFillPath( DefaultRasterizer.RasterizableGeometry? rasterizable = DefaultRasterizer.CreateRasterizableGeometry( geometry, residual, - command.DestinationOffset.X, - command.DestinationOffset.Y, + destinationOffset.X, + destinationOffset.Y, rasterizerOptions, allocator); @@ -831,10 +1505,19 @@ private static bool TryPrepareFillPath( return false; } - prepared = new PreparedFillItem(brush, command.GraphicsOptions, brushBounds, rasterizable); + prepared = new PreparedFillItem(brush, drawingOptions.GraphicsOptions, brushBounds, rasterizable); return true; } + /// + /// Prepares the retained stroke payload for a stroked path. The transform is split into an + /// axis-aligned scale, applied while flattening, and a residual matrix applied during + /// rasterization; the stroke width is scaled separately by the transform's determinant scale. + /// + /// The command supplying the path, pen, brush, and options. + /// The allocator used for retained raster storage. + /// The prepared stroke payload when successful. + /// when the stroke intersects the target and rasterizable geometry was created; otherwise . private static bool TryPrepareStrokePath( in StrokePathCommand command, MemoryAllocator allocator, @@ -842,14 +1525,13 @@ private static bool TryPrepareStrokePath( { IPath path = command.SourcePath; Matrix4x4 transform = command.Transform; - bool hasTransform = !transform.IsIdentity; - Vector2 scale = ExtractScale(transform); - Matrix4x4 residual = ComputeResidual(scale, transform); + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); LinearGeometry geometry = path.ToLinearGeometry(scale); float widthScale = GetTransformWidthScale(transform); RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); - Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + Brush sourceBrush = command.Brush; if (!TryResolveRasterization( sourceBrush, @@ -884,6 +1566,14 @@ private static bool TryPrepareStrokePath( return true; } + /// + /// Prepares the retained stroke payload for a single line segment. The endpoints are fully + /// transformed up front, so no residual matrix is needed at rasterization time. + /// + /// The command supplying the endpoints, pen, brush, and options. + /// The allocator used for retained raster storage. + /// The prepared stroke payload when successful. + /// when the stroke intersects the target and rasterizable geometry was created; otherwise . private static bool TryPrepareLineSegmentStroke( in StrokeLineSegmentCommand command, MemoryAllocator allocator, @@ -900,7 +1590,7 @@ private static bool TryPrepareLineSegmentStroke( MathF.Max(start.X, end.X), MathF.Max(start.Y, end.Y)); RectangleF bounds = GetStrokeBounds(segmentBounds, command.Pen, widthScale); - Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + Brush sourceBrush = command.Brush; if (!TryResolveRasterization( sourceBrush, @@ -936,20 +1626,27 @@ private static bool TryPrepareLineSegmentStroke( return true; } + /// + /// Prepares the retained stroke payload for an open polyline using the same scale and residual + /// transform split as stroked paths. + /// + /// The command supplying the points, pen, brush, and options. + /// The allocator used for retained raster storage. + /// The prepared stroke payload when successful. + /// when the stroke intersects the target and rasterizable geometry was created; otherwise . private static bool TryPreparePolylineStroke( in StrokePolylineCommand command, MemoryAllocator allocator, out PreparedStrokeItem prepared) { Matrix4x4 transform = command.Transform; - bool hasTransform = !transform.IsIdentity; - Vector2 scale = ExtractScale(transform); - Matrix4x4 residual = ComputeResidual(scale, transform); + Vector2 scale = MatrixUtilities.GetScale(transform); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, transform); LinearGeometry geometry = LinearGeometry.CreateOpenPolyline(command.SourcePoints, scale); float widthScale = GetTransformWidthScale(transform); RectangleF geometryBounds = residual.IsIdentity ? geometry.Info.Bounds : RectangleF.Transform(geometry.Info.Bounds, residual); RectangleF strokeBounds = GetStrokeBounds(geometryBounds, command.Pen, widthScale); - Brush sourceBrush = hasTransform ? command.Brush.Transform(transform) : command.Brush; + Brush sourceBrush = command.Brush; if (!TryResolveRasterization( sourceBrush, @@ -985,6 +1682,19 @@ private static bool TryPreparePolylineStroke( return true; } + /// + /// Resolves the destination-clipped rasterizer options and brush bounds for a draw operation, + /// rejecting operations whose interest area does not intersect the target. + /// + /// The source brush. + /// The path-local geometry bounds after stroke and residual expansion. + /// The rasterizer options recorded with the command. + /// The offset from path-local to destination coordinates. + /// The destination bounds of the flush. + /// The brush to use for rendering. + /// The rasterizer options clipped to the visible destination. + /// The unclipped absolute interest area used for applicator creation. + /// when the operation is visible within the target; otherwise . private static bool TryResolveRasterization( Brush brush, RectangleF bounds, @@ -997,11 +1707,6 @@ private static bool TryResolveRasterization( { resolvedBrush = brush; - if (options.SamplingOrigin == RasterizerSamplingOrigin.PixelCenter) - { - bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); - } - Rectangle localInterest = Rectangle.FromLTRB( (int)MathF.Floor(bounds.Left), (int)MathF.Floor(bounds.Top), @@ -1015,6 +1720,7 @@ private static bool TryResolveRasterization( localInterest.Height); Rectangle clippedDestination = Rectangle.Intersect(targetBounds, absoluteInterest); + if (clippedDestination.Width <= 0 || clippedDestination.Height <= 0) { resolvedOptions = default; @@ -1023,16 +1729,35 @@ private static bool TryResolveRasterization( } resolvedOptions = new RasterizerOptions( - absoluteInterest, + clippedDestination, options.IntersectionRule, options.RasterizationMode, - options.SamplingOrigin, - options.AntialiasThreshold); + options.AntialiasThreshold, + options.CoverageBoost); brushBounds = absoluteInterest; return true; } + /// + /// Rounds fractional bounds outward to the smallest enclosing integer rectangle. + /// + /// The fractional bounds. + /// The enclosing integer rectangle. + private static Rectangle ToConservativeBounds(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + + /// + /// Expands path bounds by the stroke joins and caps that can extend past the path geometry. + /// + /// The path geometry bounds before stroke inflation. + /// The pen that defines stroke width, joins, caps, and miter limit. + /// The transform-derived scale applied to the stroke width. + /// The inflated stroke bounds. private static RectangleF GetStrokeBounds(RectangleF bounds, Pen pen, float widthScale) { float halfWidth = pen.StrokeWidth * widthScale * 0.5F; @@ -1055,6 +1780,8 @@ private static RectangleF GetStrokeBounds(RectangleF bounds, Pen pen, float widt /// /// Returns the isotropic scale factor embedded in a drawing transform so stroke widths match device-space pixels. /// + /// The transform to inspect. + /// The scale factor to apply to stroke widths. /// /// Uses the square root of the absolute 2D determinant, the SVG-style fallback for non-uniform /// scale. Reduces to the uniform scale for pure scale/rotate/translate matrices. @@ -1070,16 +1797,18 @@ private static float GetTransformWidthScale(Matrix4x4 transform) return MathF.Sqrt(MathF.Abs(det)); } - private static Vector2 ExtractScale(Matrix4x4 matrix) - => new( - MathF.Sqrt((matrix.M11 * matrix.M11) + (matrix.M12 * matrix.M12)), - MathF.Sqrt((matrix.M21 * matrix.M21) + (matrix.M22 * matrix.M22))); - - private static Matrix4x4 ComputeResidual(Vector2 scale, Matrix4x4 matrix) - => Matrix4x4.CreateScale(1F / scale.X, 1F / scale.Y, 1F) * matrix; - - private readonly struct PreparedFillItem + /// + /// Holds the prepared payload for a fill operation before it is committed to a scene item. + /// + internal readonly struct PreparedFillItem { + /// + /// Initializes a new instance of the struct. + /// + /// The resolved brush. + /// The graphics options for the fill. + /// The absolute brush bounds used for applicator creation. + /// The retained rasterizable geometry. public PreparedFillItem( Brush brush, GraphicsOptions graphicsOptions, @@ -1092,17 +1821,39 @@ public PreparedFillItem( this.Rasterizable = rasterizable; } + /// + /// Gets the resolved brush. + /// public Brush Brush { get; } + /// + /// Gets the graphics options for the fill. + /// public GraphicsOptions GraphicsOptions { get; } + /// + /// Gets the absolute brush bounds used for applicator creation. + /// public Rectangle BrushBounds { get; } + /// + /// Gets the retained rasterizable geometry. + /// public DefaultRasterizer.RasterizableGeometry Rasterizable { get; } } + /// + /// Holds the prepared payload for a stroke operation before it is committed to a scene item. + /// private readonly struct PreparedStrokeItem { + /// + /// Initializes a new instance of the struct. + /// + /// The resolved brush. + /// The graphics options for the stroke. + /// The absolute brush bounds used for applicator creation. + /// The retained stroke rasterizable geometry. public PreparedStrokeItem( Brush brush, GraphicsOptions graphicsOptions, @@ -1115,17 +1866,45 @@ public PreparedStrokeItem( this.Rasterizable = rasterizable; } + /// + /// Gets the resolved brush. + /// public Brush Brush { get; } + /// + /// Gets the graphics options for the stroke. + /// public GraphicsOptions GraphicsOptions { get; } + /// + /// Gets the absolute brush bounds used for applicator creation. + /// public Rectangle BrushBounds { get; } + /// + /// Gets the retained stroke rasterizable geometry. + /// public DefaultRasterizer.StrokeRasterizableGeometry Rasterizable { get; } } + /// + /// Holds the counts and row builders produced by one partition, merged sequentially after all + /// partitions complete. Layer depths are relative to the partition start; the merge composes + /// them using the running depth carried across partitions. + /// private readonly struct PartitionState { + /// + /// Initializes a new instance of the struct. + /// + /// The number of fill items retained by the partition. + /// The number of stroke items retained by the partition. + /// The total number of encoded raster edges retained by the partition. + /// The number of items that occupy a single row band. + /// The number of covered bands with a small edge count. + /// The net begin minus end layer count across the partition. + /// The maximum layer depth reached, relative to the partition start. + /// The partition-owned row builders, one slot per target row band. public PartitionState( int fillItemCount, int strokeItemCount, @@ -1146,20 +1925,375 @@ public PartitionState( this.RowBuilders = rowBuilders; } + /// + /// Gets the number of fill items retained by the partition. + /// public int FillItemCount { get; } + /// + /// Gets the number of stroke items retained by the partition. + /// public int StrokeItemCount { get; } + /// + /// Gets the total number of encoded raster edges retained by the partition. + /// public long TotalEdgeCount { get; } + /// + /// Gets the number of items that occupy a single row band. + /// public int SingleBandItemCount { get; } + /// + /// Gets the number of covered bands with a small edge count. + /// public int SmallEdgeItemCount { get; } + /// + /// Gets the net begin minus end layer count across the partition. + /// public int LayerDepthDelta { get; } + /// + /// Gets the maximum layer depth reached, relative to the partition start. + /// public int MaxLayerDepth { get; } + /// + /// Gets the partition-owned row builders, one slot per target row band. + /// public RowBuilder[] RowBuilders { get; } } + + /// + /// Holds one open scoped layer while command ownership is assigned. + /// + private readonly struct ScopedLayerBuildFrame + { + /// + /// Initializes a new instance of the struct. + /// + /// The parent sequence receiving the finalized layer. + /// The retained layer state. + /// The absolute layer target bounds. + /// The child sequence receiving layer contents. + public ScopedLayerBuildFrame( + SceneSequenceBuilder parent, + DrawingCanvasLayer layer, + Rectangle bounds, + SceneSequenceBuilder content) + { + this.Parent = parent; + this.Layer = layer; + this.Bounds = bounds; + this.Content = content; + } + + /// + /// Gets the parent sequence receiving the finalized layer. + /// + public SceneSequenceBuilder Parent { get; } + + /// + /// Gets the retained layer state. + /// + public DrawingCanvasLayer Layer { get; } + + /// + /// Gets the absolute layer target bounds. + /// + public Rectangle Bounds { get; } + + /// + /// Gets the child sequence receiving layer contents. + /// + public SceneSequenceBuilder Content { get; } + } + + /// + /// Builds an ordered sequence of retained row segments. + /// + private sealed class SceneSequenceBuilder + { + private readonly MemoryAllocator allocator; + private readonly int firstTargetRowBandIndex; + private readonly int targetRowCount; + private readonly List segments = []; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for retained row storage. + /// The first target row-band index. + /// The target row-band count. + public SceneSequenceBuilder( + MemoryAllocator allocator, + int firstTargetRowBandIndex, + int targetRowCount) + { + this.allocator = allocator; + this.firstTargetRowBandIndex = firstTargetRowBandIndex; + this.targetRowCount = targetRowCount; + this.CurrentSegment = this.CreateSegment(); + } + + /// + /// Gets the segment currently receiving retained row operations. + /// + public SceneSegmentBuilder CurrentSegment { get; private set; } + + /// + /// Appends an apply operation after the current row segment. + /// + /// The retained apply operation. + public void AddApply(ApplySceneItem applyItem) + { + this.CurrentSegment.SetApply(applyItem); + this.CurrentSegment = this.CreateSegment(); + } + + /// + /// Appends a scoped layer after the current row segment. + /// + /// The retained scoped layer builder. + public void AddLayer(ScopedLayerSceneBuilder layerBuilder) + { + this.CurrentSegment.SetLayer(layerBuilder); + this.CurrentSegment = this.CreateSegment(); + } + + /// + /// Finalizes all retained segments in this sequence. + /// + /// The total retained row count. + /// The total retained row operation count. + /// The finalized retained segments. + public SceneSegment[] FinalizeSegments(out int rowCount, out int rowItemCount) + { + List finalized = []; + rowCount = 0; + rowItemCount = 0; + + for (int i = 0; i < this.segments.Count; i++) + { + if (this.segments[i].FinalizeSegment( + this.firstTargetRowBandIndex, + out SceneSegment? segment, + out int segmentRowCount, + out int segmentRowItemCount)) + { + finalized.Add(segment); + rowCount += segmentRowCount; + rowItemCount += segmentRowItemCount; + } + } + + return finalized.Count == 0 ? [] : [.. finalized]; + } + + private SceneSegmentBuilder CreateSegment() + { + SceneSegmentBuilder segment = new(this.allocator, this.targetRowCount); + this.segments.Add(segment); + return segment; + } + } + + /// + /// Builds one retained row segment and its optional trailing operation. + /// + private sealed class SceneSegmentBuilder + { + private readonly MemoryAllocator allocator; + private readonly int targetRowCount; + private RowBuilder[]? rowBuilders; + private ApplySceneItem? applyItem; + private ScopedLayerSceneBuilder? layerBuilder; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for retained row storage. + /// The target row-band count. + public SceneSegmentBuilder(MemoryAllocator allocator, int targetRowCount) + { + this.allocator = allocator; + this.targetRowCount = targetRowCount; + } + + /// + /// Appends one retained row operation to this segment. + /// + /// The target row slot. + /// The retained row operation. + public void Append(int rowSlot, SceneOperation operation) + { + this.rowBuilders ??= new RowBuilder[this.targetRowCount]; + ref RowBuilder builder = ref this.rowBuilders[rowSlot]; + if (!builder.IsInitialized) + { + builder = new RowBuilder(this.allocator); + } + + builder.Append(operation); + } + + /// + /// Sets the apply operation executed after this segment's rows. + /// + /// The retained apply operation. + public void SetApply(ApplySceneItem item) => this.applyItem = item; + + /// + /// Sets the scoped layer executed after this segment's rows. + /// + /// The retained scoped layer builder. + public void SetLayer(ScopedLayerSceneBuilder builder) => this.layerBuilder = builder; + + /// + /// Finalizes this builder into retained segment storage. + /// + /// The first target row-band index. + /// The finalized retained segment. + /// The retained row count in the segment and any child layer. + /// The retained row operation count in the segment and any child layer. + /// True when the segment has retained work. + public bool FinalizeSegment( + int firstTargetRowBandIndex, + [NotNullWhen(true)] out SceneSegment? segment, + out int rowCount, + out int rowItemCount) + { + int segmentRowCount = 0; + int segmentRowItemCount = 0; + SceneRow[] rows = []; + + if (this.rowBuilders is not null) + { + for (int i = 0; i < this.rowBuilders.Length; i++) + { + if (!this.rowBuilders[i].IsInitialized) + { + continue; + } + + segmentRowCount++; + segmentRowItemCount += this.rowBuilders[i].Count; + } + + rows = segmentRowCount == 0 + ? [] + : FinalizeRows(this.rowBuilders, firstTargetRowBandIndex, segmentRowCount); + } + + ScopedLayerSceneItem? layerItem = this.layerBuilder?.FinalizeSceneItem(); + rowCount = segmentRowCount + (layerItem?.RowCount ?? 0); + rowItemCount = segmentRowItemCount + (layerItem?.RowItemCount ?? 0); + + if (segmentRowCount == 0 && this.applyItem is null && layerItem is null) + { + segment = null; + return false; + } + + segment = new SceneSegment(rows, segmentRowItemCount, this.applyItem, layerItem); + return true; + } + } + + /// + /// Builds a retained scoped layer from child segments. + /// + private sealed class ScopedLayerSceneBuilder + { + private readonly DrawingCanvasLayer layer; + private readonly Rectangle bounds; + private readonly SceneSequenceBuilder content; + + /// + /// Initializes a new instance of the class. + /// + /// The retained layer state. + /// The absolute layer target bounds. + /// The retained layer content builder. + public ScopedLayerSceneBuilder( + DrawingCanvasLayer layer, + Rectangle bounds, + SceneSequenceBuilder content) + { + this.layer = layer; + this.bounds = bounds; + this.content = content; + } + + /// + /// Finalizes this builder into retained scoped layer storage. + /// + /// The finalized retained scoped layer. + public ScopedLayerSceneItem FinalizeSceneItem() + { + SceneSegment[] segments = this.content.FinalizeSegments(out int rowCount, out int rowItemCount); + return new ScopedLayerSceneItem(this.layer, this.bounds, segments, rowCount, rowItemCount); + } + } + + /// + /// Tracks the active clip stack resolved from the ordered begin/end-clip command stream. + /// + /// + /// Clip descriptors are recorded in canvas-local coordinates; the canvas re-anchors the + /// stream at each consuming state's destination offset, so at every draw command all open + /// descriptors share one anchor offset. + /// + private sealed class ClipStreamTracker + { + private readonly Stack previous = new(); + + /// + /// Gets the clip state composed from the currently open clip scopes. + /// + public DrawingClipState Current { get; private set; } = DrawingClipState.Empty; + + /// + /// Gets the destination offset the open clip scopes are anchored at. + /// + public Point Anchor { get; private set; } + + /// + /// Creates a tracker seeded with the clip scopes opened by earlier partitions. + /// + /// The seed clip scopes, outermost first. + /// The seeded tracker. + public static ClipStreamTracker CreateFromSeed((DrawingClipDescriptor Descriptor, Point Anchor)[]? seed) + { + ClipStreamTracker tracker = new(); + if (seed is not null) + { + for (int i = 0; i < seed.Length; i++) + { + tracker.Push(seed[i].Descriptor, seed[i].Anchor); + } + } + + return tracker; + } + + /// + /// Opens one clip scope. + /// + /// The clip descriptor to open. + /// The destination offset the descriptor is anchored at. + public void Push(DrawingClipDescriptor descriptor, Point anchor) + { + this.previous.Push(this.Current); + this.Current = this.Current.Append(descriptor); + this.Anchor = anchor; + } + + /// + /// Closes the most recently opened clip scope. + /// + public void Pop() => this.Current = this.previous.Count > 0 ? this.previous.Pop() : DrawingClipState.Empty; + } } diff --git a/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs index f3cb2e795..85dc8a76e 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/ICanvasFrame{TPixel}.cs @@ -7,8 +7,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Per-frame destination for . +/// A render destination for . Implementations back the canvas +/// with either a CPU-accessible pixel region or an opaque native surface; a usable frame must +/// expose at least one of the two. /// +/// +/// A backend inspects the frame through and +/// to choose its render path. Exactly one of the two succeeds +/// for a given frame: a CPU frame yields a , a native frame yields +/// a . +/// /// The pixel format. public interface ICanvasFrame where TPixel : unmanaged, IPixel diff --git a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs index 372edc263..ef9694662 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IDrawingBackend.cs @@ -6,18 +6,20 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Defines the contract for creating and rendering retained drawing scenes for canvas targets. +/// Defines the contract a drawing backend implements to turn recorded canvas commands into pixels: +/// creating retained scenes from command batches, rendering those scenes into a canvas frame, and +/// transferring pixels between frames (copy and readback). /// public interface IDrawingBackend { /// - /// Creates a retained backend scene from a prepared command batch. + /// Creates a backend scene from a prepared command batch. /// /// The active processing configuration. /// The target bounds used for target-dependent scene data. /// The scene commands in submission order. /// The resources that must stay alive for the returned scene. - /// A retained backend scene. + /// The created backend scene. public DrawingBackendScene CreateScene( Configuration configuration, Rectangle targetBounds, @@ -25,18 +27,35 @@ public DrawingBackendScene CreateScene( IReadOnlyList? ownedResources = null); /// - /// Renders a retained backend scene into the target. + /// Renders a backend scene into the target. /// /// The pixel format. /// The active processing configuration. /// The target frame. - /// The retained backend scene to render. + /// The backend scene to render. public void RenderScene( Configuration configuration, ICanvasFrame target, DrawingBackendScene scene) where TPixel : unmanaged, IPixel; + /// + /// Copies pixels from a source frame into a target frame. + /// + /// The pixel format. + /// The active processing configuration. + /// The source frame. + /// The target frame. + /// The source rectangle in source-local coordinates. + /// The target point in target-local coordinates. + public void CopyPixels( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame target, + Rectangle sourceRectangle, + Point targetPoint) + where TPixel : unmanaged, IPixel; + /// /// Reads source pixels from the target into the destination region. /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs b/src/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs index faee58b79..cb5473de3 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/IRasterizerCoverageRowHandler.cs @@ -6,6 +6,12 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// Receives one emitted non-zero coverage span from the rasterizer. /// +/// +/// Implementations are value types invoked through a struct generic constraint so calls +/// devirtualize and inline on the hot path. The rasterizer executes row bands in parallel, but a +/// handler instance is only ever used by the single worker that owns it, so implementations do +/// not need to be thread safe. +/// internal interface IRasterizerCoverageRowHandler { /// @@ -13,6 +19,9 @@ internal interface IRasterizerCoverageRowHandler /// /// The destination y coordinate. /// The first x coordinate represented by . - /// Non-zero coverage values starting at . + /// + /// Positive per-pixel coverage values starting at . The span aliases + /// reusable worker scratch and is only valid for the duration of the call. + /// public void Handle(int y, int startX, Span coverage); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs index bba696021..5a75e7e61 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/NativeSurface.cs @@ -4,8 +4,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// -/// Base type for backend-specific native drawing targets. +/// Opaque base type for a backend-specific native (non-CPU) drawing target, such as a +/// GPU surface. /// +/// +/// A canvas frame exposes an instance through . +/// The concrete backend that owns the frame downcasts this to its own surface type; the base +/// type carries no members because its contents are entirely backend-defined. +/// public abstract class NativeSurface { /// diff --git a/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs b/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs new file mode 100644 index 000000000..8f7422f8a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/Backends/PreparedPathClipState.cs @@ -0,0 +1,135 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Drawing.Processing.Backends; + +/// +/// Holds flush-scoped raster data for path clip descriptors. +/// +/// +/// Path descriptors in the clip stack are pre-rasterized once per flush so backends can compose +/// them into coverage masks. All raster data is anchored at the shared destination offset so +/// every clip layer resolves in the same coordinate space as the draw it constrains. +/// +internal sealed class PreparedPathClipState : IDisposable +{ + private readonly DefaultRasterizer.RasterizableGeometry?[] rasterizables; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The retained raster data indexed by clip descriptor position. Non-path descriptors and + /// empty difference clips are represented by entries. + /// + private PreparedPathClipState(DefaultRasterizer.RasterizableGeometry?[] rasterizables) + => this.rasterizables = rasterizables; + + /// + /// Creates retained path clip raster data for the path descriptors in a clip state. + /// + /// The command clip state. + /// The absolute command target bounds. + /// The destination offset used to place clip descriptors. + /// The allocator used for retained raster storage. + /// The prepared path clip state, or when the clip state has no path clips. + /// when the command can still draw after preparing clips; otherwise . + public static bool TryCreate( + DrawingClipState clipState, + Rectangle targetBounds, + Point destinationOffset, + MemoryAllocator allocator, + out PreparedPathClipState? prepared) + { + prepared = null; + int count = clipState.Count; + int pathCount = 0; + for (int i = 0; i < count; i++) + { + if (clipState.GetDescriptor(i).Kind == DrawingClipKind.Path) + { + pathCount++; + } + } + + if (pathCount == 0) + { + return true; + } + + DefaultRasterizer.RasterizableGeometry?[] rasterizables = new DefaultRasterizer.RasterizableGeometry?[count]; + try + { + for (int i = 0; i < count; i++) + { + DrawingClipDescriptor descriptor = clipState.GetDescriptor(i); + if (descriptor.Kind != DrawingClipKind.Path) + { + continue; + } + + LinearGeometry geometry = descriptor.ToLinearGeometry(out Matrix4x4 residual); + RasterizationMode mode = descriptor.EdgeMode == DrawingClipEdgeMode.Hard + ? RasterizationMode.Aliased + : RasterizationMode.Antialiased; + + RasterizerOptions options = new( + targetBounds, + descriptor.IntersectionRule, + mode, + descriptor.AntialiasThreshold); + + DefaultRasterizer.RasterizableGeometry? rasterizable = DefaultRasterizer.CreateRasterizableGeometry( + geometry, + residual, + destinationOffset.X, + destinationOffset.Y, + options, + allocator); + + // An empty intersecting path clip makes the whole command empty. An empty + // difference clip is a no-op, so it remains represented by a null rasterizable. + if (rasterizable is null && descriptor.Operation == ClipOperation.Intersection) + { + return false; + } + + rasterizables[i] = rasterizable; + } + + prepared = new PreparedPathClipState(rasterizables); + + // Ownership has transferred to the prepared instance; clearing the local stops the + // finally block from disposing raster data that is now retained. + rasterizables = []; + return true; + } + finally + { + for (int i = 0; i < rasterizables.Length; i++) + { + rasterizables[i]?.Dispose(); + } + } + } + + /// + /// Gets retained raster data for the path descriptor at the clip index. + /// + /// The clip descriptor index. + /// The retained path raster data, or when the descriptor is an empty difference clip. + public DefaultRasterizer.RasterizableGeometry? GetRasterizable(int clipIndex) + => this.rasterizables[clipIndex]; + + /// + public void Dispose() + { + for (int i = 0; i < this.rasterizables.Length; i++) + { + this.rasterizables[i]?.Dispose(); + } + } +} diff --git a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs index 8200baab5..33667b284 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/RasterizerOptions.cs @@ -19,22 +19,6 @@ public enum RasterizationMode Aliased = 1 } -/// -/// Describes where sample coverage is aligned relative to destination pixels. -/// -public enum RasterizerSamplingOrigin -{ - /// - /// Samples are aligned to pixel boundaries. - /// - PixelBoundary = 0, - - /// - /// Samples are aligned to pixel centers. - /// - PixelCenter = 1 -} - /// /// Immutable options used by rasterizers when scan-converting vector geometry. /// @@ -46,20 +30,20 @@ public readonly struct RasterizerOptions /// Destination bounds to rasterize into. /// Polygon intersection rule. /// Rasterization coverage mode. - /// Sampling origin alignment. /// Coverage threshold for aliased mode (0 to 1). + /// Perceptual coverage boost for antialiased mode (0 disables). public RasterizerOptions( Rectangle interest, IntersectionRule intersectionRule, RasterizationMode rasterizationMode, - RasterizerSamplingOrigin samplingOrigin, - float antialiasThreshold) + float antialiasThreshold, + float coverageBoost = 0F) { this.Interest = interest; this.IntersectionRule = intersectionRule; this.RasterizationMode = rasterizationMode; - this.SamplingOrigin = samplingOrigin; this.AntialiasThreshold = antialiasThreshold; + this.CoverageBoost = coverageBoost; } /// @@ -78,15 +62,20 @@ public RasterizerOptions( public RasterizationMode RasterizationMode { get; } /// - /// Gets the sampling origin alignment. + /// Gets the coverage threshold used when is . + /// Pixels whose coverage is greater than or equal to this value are rendered as fully opaque; pixels below it are discarded. /// - public RasterizerSamplingOrigin SamplingOrigin { get; } + public float AntialiasThreshold { get; } /// - /// Gets the coverage threshold used when is . - /// Pixels with coverage above this value are rendered as fully opaque; pixels below are discarded. + /// Gets the perceptual coverage boost used when is + /// . Partial coverage values are remapped by + /// the S-curve a + boost * a * (1 - a) * (2a - 1), darkening mostly covered pixels + /// and lightening mostly empty ones while 0, 0.5, and 1 stay fixed; at 1 the remap + /// is exactly smoothstep. 0 disables the boost. Text rendering uses this to counter + /// the soft, washed-out appearance of small blended glyphs. /// - public float AntialiasThreshold { get; } + public float CoverageBoost { get; } /// /// Creates a copy of the current options with a different interest rectangle. @@ -94,5 +83,5 @@ public RasterizerOptions( /// The replacement interest rectangle. /// A new value. public RasterizerOptions WithInterest(Rectangle interest) - => new(interest, this.IntersectionRule, this.RasterizationMode, this.SamplingOrigin, this.AntialiasThreshold); + => new(interest, this.IntersectionRule, this.RasterizationMode, this.AntialiasThreshold, this.CoverageBoost); } diff --git a/src/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs index bf8a44586..5323f0160 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/StrokeLineSegmentCommand.cs @@ -8,11 +8,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One explicit stroked two-point line-segment command queued by the canvas batcher. /// +/// +/// Stroke commands carry pen geometry only; they do not carry clip state. Backends resolve +/// the active clip stack from the begin/end-clip commands surrounding each draw in the stream. +/// public readonly struct StrokeLineSegmentCommand { private readonly PointF sourceStart; private readonly PointF sourceEnd; private readonly DrawingOptions drawingOptions; + private readonly DrawingCanvasLayer? ownerLayer; /// /// Initializes a new instance of the struct. @@ -36,10 +41,49 @@ public StrokeLineSegmentCommand( Point destinationOffset, Pen pen, bool isInsideLayer) + : this( + sourceStart, + sourceEnd, + brush, + drawingOptions, + in rasterizerOptions, + targetBounds, + destinationOffset, + pen, + isInsideLayer, + null) + { + } + + /// + /// Initializes a new instance of the struct with the owning layer state recorded by the canvas. + /// + /// The source line start point. + /// The source line end point. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + /// The layer that owned this command when it was recorded. + internal StrokeLineSegmentCommand( + PointF sourceStart, + PointF sourceEnd, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer, + DrawingCanvasLayer? ownerLayer) { this.sourceStart = sourceStart; this.sourceEnd = sourceEnd; this.drawingOptions = drawingOptions; + this.ownerLayer = ownerLayer; this.Brush = brush; this.RasterizerOptions = rasterizerOptions; this.TargetBounds = targetBounds; @@ -103,6 +147,11 @@ public StrokeLineSegmentCommand( /// public bool IsInsideLayer { get; } + /// + /// Gets the layer state for the layer that owned this command when it was recorded. + /// + internal DrawingCanvasLayer? OwnerLayer => this.ownerLayer; + /// /// Computes the conservative stroked bounds of one two-point line segment. /// @@ -120,9 +169,18 @@ public static RectangleF GetConservativeBounds(PointF start, PointF end, Pen pen return InflateBounds(bounds, pen); } + /// + /// Inflates point bounds by the maximum distance the stroke outline can extend past the centerline. + /// + /// The tight bounds of the source points. + /// The stroke metadata. + /// The inflated bounds. private static RectangleF InflateBounds(RectangleF bounds, Pen pen) { float halfWidth = pen.StrokeWidth * 0.5F; + + // Miter joins can extend up to halfWidth * miterLimit beyond the centerline; the limit is + // clamped to at least 1 so the inflation never falls below the plain half stroke width. float inflate = pen.StrokeOptions.LineJoin switch { LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), diff --git a/src/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs index 9fb1d1add..c81533c54 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/StrokePathCommand.cs @@ -8,11 +8,15 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One stroked path command queued by the canvas batcher. /// +/// +/// Stroke commands carry pen geometry only; they do not carry clip state. Backends resolve +/// the active clip stack from the begin/end-clip commands surrounding each draw in the stream. +/// public readonly struct StrokePathCommand { private readonly IPath sourcePath; private readonly DrawingOptions drawingOptions; - private readonly IReadOnlyList? clipPaths; + private readonly DrawingCanvasLayer? ownerLayer; /// /// Initializes a new instance of the struct. @@ -24,9 +28,8 @@ public readonly struct StrokePathCommand /// The absolute bounds of the logical target. /// The absolute destination offset of the command. /// The stroke metadata. - /// Optional clip paths supplied with the command. /// True if the command was recorded inside a layer. - public StrokePathCommand( + internal StrokePathCommand( IPath sourcePath, Brush brush, DrawingOptions drawingOptions, @@ -34,18 +37,92 @@ public StrokePathCommand( Rectangle targetBounds, Point destinationOffset, Pen pen, - IReadOnlyList? clipPaths, bool isInsideLayer) + : this( + sourcePath, + brush, + drawingOptions, + in rasterizerOptions, + targetBounds, + destinationOffset, + pen, + isInsideLayer, + null) + { + } + + /// + /// Initializes a new instance of the struct with the owning layer state recorded by the canvas. + /// + /// The source stroke path. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + /// The layer that owned this command when it was recorded. + internal StrokePathCommand( + IPath sourcePath, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer, + DrawingCanvasLayer? ownerLayer) + : this( + sourcePath, + brush, + drawingOptions, + in rasterizerOptions, + targetBounds, + destinationOffset, + pen, + isInsideLayer, + ownerLayer, + Vector2.Zero) + { + } + + /// + /// Initializes a new instance of the struct carrying a + /// sub-pixel translation alongside the owning layer state recorded by the canvas. + /// + /// The source stroke path. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + /// The layer that owned this command when it was recorded. + /// The fractional translation composed into . + internal StrokePathCommand( + IPath sourcePath, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer, + DrawingCanvasLayer? ownerLayer, + Vector2 subPixelOffset) { this.sourcePath = sourcePath; this.drawingOptions = drawingOptions; - this.clipPaths = clipPaths; + this.ownerLayer = ownerLayer; this.Brush = brush; this.RasterizerOptions = rasterizerOptions; this.TargetBounds = targetBounds; this.DestinationOffset = destinationOffset; this.Pen = pen; this.IsInsideLayer = isInsideLayer; + this.SubPixelOffset = subPixelOffset; } /// @@ -89,22 +166,39 @@ public StrokePathCommand( public IPath SourcePath => this.sourcePath; /// - /// Gets the drawing transform. + /// Gets the fractional translation applied after the drawing options transform. Glyph + /// geometry rides an integer destination offset; the sub-pixel remainder travels here so + /// the queued command does not need per-operation drawing options to carry it. /// - public Matrix4x4 Transform => this.drawingOptions.Transform; + public Vector2 SubPixelOffset { get; } /// - /// Gets the optional clip paths carried by the command. + /// Gets the drawing transform: the drawing options transform followed by the sub-pixel + /// translation. /// - public IReadOnlyList? ClipPaths => this.clipPaths; + public Matrix4x4 Transform + { + get + { + Matrix4x4 transform = this.drawingOptions.Transform; + if (this.SubPixelOffset == Vector2.Zero) + { + return transform; + } + + return transform.IsIdentity + ? Matrix4x4.CreateTranslation(this.SubPixelOffset.X, this.SubPixelOffset.Y, 0F) + : transform * Matrix4x4.CreateTranslation(this.SubPixelOffset.X, this.SubPixelOffset.Y, 0F); + } + } /// - /// Gets the shape options carried by the command. + /// Gets a value indicating whether the command was recorded inside a layer. /// - public ShapeOptions ShapeOptions => this.drawingOptions.ShapeOptions; + public bool IsInsideLayer { get; } /// - /// Gets a value indicating whether the command was recorded inside a layer. + /// Gets the layer state for the layer that owned this command when it was recorded. /// - public bool IsInsideLayer { get; } + internal DrawingCanvasLayer? OwnerLayer => this.ownerLayer; } diff --git a/src/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs b/src/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs index c8679570f..92effdd07 100644 --- a/src/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs +++ b/src/ImageSharp.Drawing/Processing/Backends/StrokePolylineCommand.cs @@ -8,10 +8,15 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends; /// /// One explicit stroked open polyline command queued by the canvas batcher. /// +/// +/// Stroke commands carry pen geometry only; they do not carry clip state. Backends resolve +/// the active clip stack from the begin/end-clip commands surrounding each draw in the stream. +/// public readonly struct StrokePolylineCommand { private readonly PointF[] sourcePoints; private readonly DrawingOptions drawingOptions; + private readonly DrawingCanvasLayer? ownerLayer; /// /// Initializes a new instance of the struct. @@ -33,6 +38,41 @@ public StrokePolylineCommand( Point destinationOffset, Pen pen, bool isInsideLayer) + : this( + sourcePoints, + brush, + drawingOptions, + in rasterizerOptions, + targetBounds, + destinationOffset, + pen, + isInsideLayer, + null) + { + } + + /// + /// Initializes a new instance of the struct with the owning layer state recorded by the canvas. + /// + /// The source polyline points. + /// The brush used to shade the stroke. + /// The drawing options (graphics, shape, transform) used during composition. + /// The rasterizer options used to generate coverage. + /// The absolute bounds of the logical target. + /// The absolute destination offset of the command. + /// The stroke metadata. + /// True if the command was recorded inside a layer. + /// The layer that owned this command when it was recorded. + internal StrokePolylineCommand( + PointF[] sourcePoints, + Brush brush, + DrawingOptions drawingOptions, + in RasterizerOptions rasterizerOptions, + Rectangle targetBounds, + Point destinationOffset, + Pen pen, + bool isInsideLayer, + DrawingCanvasLayer? ownerLayer) { ArgumentNullException.ThrowIfNull(sourcePoints); if (sourcePoints.Length < 2) @@ -42,6 +82,7 @@ public StrokePolylineCommand( this.sourcePoints = sourcePoints; this.drawingOptions = drawingOptions; + this.ownerLayer = ownerLayer; this.Brush = brush; this.RasterizerOptions = rasterizerOptions; this.TargetBounds = targetBounds; @@ -100,6 +141,11 @@ public StrokePolylineCommand( /// public bool IsInsideLayer { get; } + /// + /// Gets the layer state for the layer that owned this command when it was recorded. + /// + internal DrawingCanvasLayer? OwnerLayer => this.ownerLayer; + /// /// Computes the conservative stroked bounds of one open polyline. /// @@ -132,9 +178,18 @@ public static RectangleF GetConservativeBounds(PointF[] points, Pen pen) return InflateBounds(bounds, pen); } + /// + /// Inflates point bounds by the maximum distance the stroke outline can extend past the centerline. + /// + /// The tight bounds of the source points. + /// The stroke metadata. + /// The inflated bounds. private static RectangleF InflateBounds(RectangleF bounds, Pen pen) { float halfWidth = pen.StrokeWidth * 0.5F; + + // Miter joins can extend up to halfWidth * miterLimit beyond the centerline; the limit is + // clamped to at least 1 so the inflation never falls below the plain half stroke width. float inflate = pen.StrokeOptions.LineJoin switch { LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), diff --git a/src/ImageSharp.Drawing/Processing/Brush.cs b/src/ImageSharp.Drawing/Processing/Brush.cs index 4862efe28..9f2f42e77 100644 --- a/src/ImageSharp.Drawing/Processing/Brush.cs +++ b/src/ImageSharp.Drawing/Processing/Brush.cs @@ -40,8 +40,11 @@ public abstract BrushRenderer CreateRenderer( /// Returns a new brush with its defining geometry transformed by the given matrix. /// /// The transformation matrix to apply. + /// The command interest before preparation. + /// The command interest after preparation. /// A transformed brush, or this if the brush has no spatial parameters. - public virtual Brush Transform(Matrix4x4 matrix) => this; + public virtual Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) + => this; /// public abstract bool Equals(Brush? other); diff --git a/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs b/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs index ce80b0110..211eadd62 100644 --- a/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs +++ b/src/ImageSharp.Drawing/Processing/BrushWorkspace.cs @@ -17,13 +17,37 @@ public sealed class BrushWorkspace : IDisposable private readonly IMemoryOwner amountsOwner; private readonly IMemoryOwner overlaysOwner; private readonly IMemoryOwner blendScratchOwner; + private bool isDisposed; + /// + /// Initializes a new instance of the class. + /// + /// The memory allocator used to rent the pooled buffers. + /// The maximum row width, in pixels, the workspace must be able to service. internal BrushWorkspace(MemoryAllocator allocator, int rowWidth) { int capacity = Math.Max(1, rowWidth); - this.amountsOwner = allocator.Allocate(capacity); - this.overlaysOwner = allocator.Allocate(capacity); - this.blendScratchOwner = allocator.Allocate(capacity * 3); + IMemoryOwner? amounts = null; + IMemoryOwner? overlays = null; + try + { + amounts = allocator.Allocate(capacity); + overlays = allocator.Allocate(capacity); + + // Callers request at most three vector rows per pixel (see GetBlendScratch), + // so reserve the worst case once instead of reallocating per request. + this.blendScratchOwner = allocator.Allocate(capacity * 3); + this.amountsOwner = amounts; + this.overlaysOwner = overlays; + } + catch + { + // A later allocation throwing strands the earlier rentals: the instance never + // escapes, so nothing else can return them. + amounts?.Dispose(); + overlays?.Dispose(); + throw; + } } /// @@ -64,6 +88,12 @@ public Span GetBlendScratch(int length, int vectorRows) /// public void Dispose() { + if (this.isDisposed) + { + return; + } + + this.isDisposed = true; this.amountsOwner.Dispose(); this.overlaysOwner.Dispose(); this.blendScratchOwner.Dispose(); diff --git a/src/ImageSharp.Drawing/Processing/Brushes.Hatch.cs b/src/ImageSharp.Drawing/Processing/Brushes.Hatch.cs index 8b4c35fa7..4f546d350 100644 --- a/src/ImageSharp.Drawing/Processing/Brushes.Hatch.cs +++ b/src/ImageSharp.Drawing/Processing/Brushes.Hatch.cs @@ -10,6 +10,10 @@ public static partial class Brushes { // These hatch arrays were derived using the GDI+ pixel extraction technique described at // https://web.archive.org/web/20221228174326/https://www.codeproject.com/Articles/5350583/Recreating-Gdiplus-hatches-with-SkiaSharp. + // Each table is an 8x8 tile indexed [row, column] that PatternBrush repeats across the + // filled area anchored to the canvas origin; true selects the foreground color and + // false the background color. The layouts intentionally match the GDI+ HatchStyle + // bitmaps pixel for pixel, so do not "clean up" apparent irregularities. private static readonly bool[,] HorizontalPattern = { { true, true, true, true, true, true, true, true, }, diff --git a/src/ImageSharp.Drawing/Processing/ColorStop.cs b/src/ImageSharp.Drawing/Processing/ColorStop.cs index 9387c80db..975b1111e 100644 --- a/src/ImageSharp.Drawing/Processing/ColorStop.cs +++ b/src/ImageSharp.Drawing/Processing/ColorStop.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// A struct that defines a single color stop. +/// A struct that defines a single color stop: a color pinned to a position along a gradient. /// [DebuggerDisplay("ColorStop({Ratio} -> {Color}")] public readonly struct ColorStop @@ -14,8 +14,10 @@ public readonly struct ColorStop /// /// Initializes a new instance of the struct. /// - /// Where should it be? 0 is at the start, 1 at the end of the Gradient. - /// What color should be used at that point? + /// + /// The position of the stop along the gradient, where 0 is the start and 1 is the end of the gradient. + /// + /// The color of the gradient at the stop position. public ColorStop(float ratio, in Color color) { this.Ratio = ratio; @@ -23,12 +25,12 @@ public ColorStop(float ratio, in Color color) } /// - /// Gets the point along the defined gradient axis. + /// Gets the position of the stop along the gradient, where 0 is the start and 1 is the end. /// public float Ratio { get; } /// - /// Gets the color to be used. + /// Gets the color of the gradient at the stop position. /// public Color Color { get; } } diff --git a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md index f093378a8..3c8ee3bea 100644 --- a/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md +++ b/src/ImageSharp.Drawing/Processing/DRAWING_CANVAS.md @@ -39,13 +39,13 @@ That one decision explains most of the surrounding design. The canvas is a deferred renderer. -Drawing calls do not rasterize immediately. They create `CompositionCommand` records and queue them into `DrawingCanvasBatcher`. The expensive normalization work happens later, during replay, when the batcher prepares those commands and hands `DrawingCommandBatch` ranges to the backend. +Drawing calls do not rasterize immediately. They create command records and queue them into `DrawingCanvasBatcher`. The expensive normalization work happens later, during replay, when the batcher prepares those commands and hands `DrawingCommandBatch` ranges to the backend. That gives the architecture three important benefits. First, the public API stays backend-agnostic. A fill is a fill, whether the target is CPU memory or a GPU surface. -Second, the expensive shared command work can happen once, in one shared place. Transform application, stroke expansion, clip application, and dash expansion are not reimplemented independently by every backend. +Second, the expensive shared command work can happen once, in one shared place. Dash expansion and brush-coordinate normalization are not reimplemented independently by every backend. Third, the backend receives a much more stable handoff. Instead of reacting to a long stream of public API calls, it receives prepared command batches with consistent semantics. @@ -67,29 +67,78 @@ When callers already have an `ImageFrame` or `ImageFrame`, the public `C readback, and backend execution. Factory methods return `DrawingCanvas` so CPU and GPU entry points expose the same canvas-facing API while still constructing the typed implementation internally. +### Options + +`DrawingOptions` is the per-state option bundle. It carries four things: + +- `GraphicsOptions` for blending, antialiasing, and composition +- `IntersectionRule` selecting non-zero or even-odd filling +- `Transform`, a `Matrix4x4` applied to subject geometry and brushes +- `TextContrast`, the perceptual coverage boost applied only to antialiased text + +There is no separate shape-options type; the fill rule and transform live directly on `DrawingOptions`. Explicit path boolean operations (`BooleanOperation`) exist only on the `IPath` `Clip(...)` geometry extensions, not in the drawing options. + +`TextContrast` counters the soft, washed-out look of small antialiased glyphs: partial coverage is remapped after fill-rule resolution and before compositing. Only the dedicated text command path (`CreateTextCompositionCommand`) forwards it into `RasterizerOptions.CoverageBoost`; plain fills, strokes, and clips always rasterize with a zero boost, and aliased text ignores it. Both backends apply the same curve (the CPU rasterizer in `AreaToCoverage`, the WebGPU fine shader in `fill_path`), so text weight matches across backends. + +#### The TextContrast curve + +With `a` the resolved coverage in `[0, 1]` and `k` the clamped `TextContrast`: + +``` +f(a) = a + k · a · (1 - a) · (2a - 1) +``` + +The perturbation term `a(1-a)(2a-1)` is negative below half coverage and positive above it, so mostly-empty pixels lighten while mostly-covered pixels darken: glyph stems solidify and counters stay bright, which reads as sharpening rather than the uniform darkening of a weight boost. `0`, `1/2`, and `1` are exact fixed points. + +Expanding shows the family is a plain blend between identity and smoothstep, because `a + a(1-a)(2a-1) = 3a² - 2a³`: + +``` +f(a) = (1 - k) · a + k · smoothstep(a) +``` + +Properties that make it safe to apply per pixel: + +- **Range preserving.** `f([0,1]) = [0,1]`; no clamp is needed after the remap. +- **Monotone.** `f′(a) = (1 - k) + 6k·a(1 - a)`, so the slope is smallest at the endpoints (`1 - k`) and largest at the midpoint (`1 + k/2`). For `k ≤ 1` the curve is monotone: coverage ordering is preserved and gradients cannot band or invert. +- **Bounded shift.** The perturbation peaks at `a = 1/2 ± √3/6` with magnitude `√3/18 ≈ 0.0962`, so no pixel's coverage moves by more than `0.0962·k` (about `±0.05` at the default). +- **Bounded erosion.** For `a → 0` the multiplicative factor tends to `(1 - k)` (the endpoint slope), so a faint antialiasing fringe keeps at least `(1 - k)` of its coverage; nothing is ever removed and no value crosses the `1/2` midpoint. At the default `k = 0.5` this gives a simple guarantee: no antialiased sample loses more than half its value. Erosion of sub-half-pixel features (hairline stems in very light faces at very small sizes) grows linearly with `k`; `k = 1` (pure smoothstep) attenuates faint fringes quadratically and is the setting to avoid if hairline preservation matters more than contrast. + +The darkening-only alternative `a + k·a(1-a)` (Skia's `apply_contrast` from `SkMaskGamma.cpp`) adds weight but blurs dense glyphs; Skia's full remap (that contrast term followed by sRGB linear-blend compensation) was tested and rejected because it reproduces Skia's *unhinted* rendering, which is lighter and fuzzier than its familiar hinted output. The S-curve was chosen from side-by-side comparisons against Skia across Latin and CJK samples at 8-14px. + ### Batcher -`DrawingCanvasBatcher` is the deferred command queue. It stores pending `CompositionCommand` values, records the canvas replay timeline, prepares commands during replay, and creates `DrawingCommandBatch` values for command-range entries. +`DrawingCanvasBatcher` is the deferred command queue. It stores pending `CompositionSceneCommand` values, records the canvas replay timeline, prepares commands during replay, and creates `DrawingCommandBatch` values for command-range entries. It is the bridge between the immediate-looking public API and the deferred backend handoff. ### Command -`CompositionCommand` is the recorded unit of drawing intent. In the common case it means "fill this path with this brush under this state". The command stream also carries explicit layer boundaries through `BeginLayer` and `EndLayer`. +`CompositionCommand` is the recorded unit of drawing intent for fills. In the common case it means "fill this path with this brush under these options". The same command stream also carries: + +- explicit layer boundaries through `BeginLayer` and `EndLayer` +- explicit clip scopes through `BeginClip` and `EndClip`, where each begin-clip carries a `DrawingClipDescriptor` +- `Apply` barriers that run an image processor over a path region + +Stroked geometry is recorded through dedicated command types (`StrokePathCommand`, `StrokeLineSegmentCommand`, `StrokePolylineCommand`). The batcher stores all of these behind the shared `CompositionSceneCommand` wrapper so the stream stays ordered. -The command remains relatively close to the original user request. It may hold the original path, pen, brush, transform, and clip paths. +The command remains relatively close to the original user request. It may hold the original path, pen, brush, and drawing options including the transform. ### Preparation Preparation is the normalization step that turns recorded intent into backend-ready commands. -`DrawingCanvasBatcher.PrepareCommands(...)` runs only when needed. It applies command transforms, expands strokes to fill paths, applies clip paths so clipped commands reach the backend as ordinary fills, and expands dashed strokes when a stroke pattern is present. +`DrawingCanvasBatcher.PrepareCommands(...)` runs only when needed. It does two things, in parallel across the command buffer when the buffer is large enough: + +1. expands dashed strokes into dash geometry (`GenerateDashes`), because dashing changes the subject geometry before the backend retains raster payload +2. bakes non-identity command transforms into brush coordinates (`Brush.Transform`), so CPU and WebGPU do not each bake the transform in their own way + +Preparation deliberately does not transform subject geometry, expand strokes to fills, or resolve clips. Geometry keeps `DrawingOptions.Transform` so backends can flatten curves scale-aware; strokes stay stroke commands so each backend can expand them in its own execution model; and the ordered begin/end-clip command stream remains the single source of truth for clipping. Preparation stops at `DrawingCommandBatch`. Backend-specific lowering happens after that, inside `IDrawingBackend.CreateScene(...)`. ### Command Batch -`DrawingCommandBatch` is the prepared command range handed to the backend. It contains the command stream for one contiguous range and scene-level facts such as whether that range contains layer boundaries. +`DrawingCommandBatch` is the prepared command range handed to the backend. It contains the command stream for one contiguous range and scene-level facts about that range: `HasLayers`, `HasApply`, and `HasClipControls`. It is the backend handoff boundary. @@ -153,18 +202,17 @@ The easiest way to understand the system is to follow one normal draw call all t ### Step 1: The canvas records intent -A public method such as `Fill(...)`, `Draw(...)`, or `DrawText(...)` resolves the active state and creates one or more `CompositionCommand` values. +A public method such as `Fill(...)`, `Draw(...)`, or `DrawText(...)` resolves the active state and creates one or more commands. At this point the canvas is mostly recording: - geometry references - brushes or pens -- active transform -- clip paths -- graphics options -- target bounds relevant to this command +- the active drawing options, including the transform +- rasterizer options such as the interest rectangle and fill rule +- target bounds and destination offset relevant to this command -The canvas does not try to fully rasterize anything here. +The canvas does not try to fully rasterize anything here. Clip changes are not attached to draw commands; they were already recorded as `BeginClip`/`EndClip` commands in the same stream when `Clip(...)` was called. ### Step 2: The batcher owns the pending work @@ -175,23 +223,26 @@ The batcher exists so the canvas does not need to talk to the backend for every The replay boundary usually comes from: - explicit `Flush()`, which seals the current command range -- `Apply(...)`, which needs read-modify-write behavior +- `CreateScene()`, which turns the recorded work into a retained backend scene - `RenderScene(...)`, which inserts an existing retained scene into the timeline -- disposal of the owning canvas +- `CopyPixelsFrom(...)`, which materializes both timelines before copying pixels +- disposal of the root canvas + +`Apply(...)` is not a replay boundary. It records an apply barrier command inline in the stream; the backend orders execution around it during scene execution. + +Every seal boundary must leave the clip stream balanced: the canvas closes the currently open clip scopes before sealing and reopens them for subsequent commands, because a clip stream cannot span backend scene boundaries. ### Step 3: The batcher prepares commands -When the root canvas is disposed, or when the caller creates a retained scene, the batcher seals any pending commands and prepares the command buffer. This is where the architecture does the heavy shared work that would otherwise be duplicated across backends. +When the root canvas is disposed, or when the caller creates a retained scene, the batcher seals any pending commands and prepares the command buffer. This is where the shared normalization that would otherwise be duplicated across backends happens. -For a typical path-based command, canvas preparation does the following in concept: +For a typical command, canvas preparation does the following in concept: -1. transform the source path into its final geometry space -2. if a pen is present, expand the stroke to fill geometry -3. apply clip paths -4. transform the brush into the same command space -5. leave backend-specific retained geometry construction to `CreateScene(...)` +1. if a pen with a stroke pattern is present, expand the dashes into the subject geometry +2. if the command transform is not identity, bake that transform into the brush coordinates +3. leave subject geometry transformation, stroke expansion, and clip resolution to the backend -This is the architectural center of gravity. It is the shared normalization stage that makes the backends simpler. +This is the architectural center of gravity: normalization that must be identical across backends happens once here, and everything that benefits from backend-specific execution is deferred. Clipping in particular is never rewritten into the subject geometry; the ordered clip command stream is consumed by each backend directly. Explicit path boolean operations stay on the geometry APIs. ### Step 4: The backend creates and renders scenes @@ -207,22 +258,45 @@ The architecture is successful if both backends can differ dramatically here wit ## Why State Is Snapshotted -Drawing APIs look stateful because they are stateful. The active transform, clips, graphics options, and layer information all affect future commands. +Drawing APIs look stateful because they are stateful. The active options, transform, clips, and layer information all affect future commands. `DrawingCanvasState` exists so that state changes are cheap to reason about and cheap to attach to commands. -The state snapshot contains the active options and target information for subsequent commands, including: +The state snapshot contains the active options and target information for subsequent commands: -- `Options` -- `ClipPaths` -- `IsLayer` -- layer-related graphics options and bounds -- current target bounds +- `Options`, the active `DrawingOptions` reference +- `ClipState`, the normalized clip stack for the state +- `TargetBounds`, the absolute target bounds for commands recorded in this state +- `DestinationOffset`, the absolute offset for paths recorded in local coordinates +- `IsLayer` and `Layer`, marking layer scopes The canvas treats this state as immutable snapshots on a stack. `Save()` pushes a copy. `Restore()` pops one. Drawing calls always read the current top-of-stack state. That makes save and restore semantics predictable and backend-independent. +## How Clipping Works + +Clipping is the part of the model where the recorded command stream, not per-command state, is authoritative. + +`Clip(...)` narrows the active clip. It supports `ClipOperation.Intersection` and `ClipOperation.Difference`, builds one `DrawingClipDescriptor` per clip path, and does two things: + +1. appends `BeginClip` commands to the stream, each carrying its descriptor and the state's destination offset +2. replaces the top-of-stack state with one whose `DrawingClipState` includes the new descriptors, so later `Save`/`Restore` and layer logic can reason about the clip stack + +Descriptors classify the clip so backends can pick fast paths: `Rectangle`, `IntegerRegion`, `Region`, or general `Path` geometry. Rectangle and region metadata is captured before the transform is applied so axis-aligned cases survive translation and scaling. + +The ordered begin/end-clip command stream is the single source of truth. Draw commands do not carry clip state; backends resolve the active clip stack from the stream commands surrounding each draw. `Restore()` closes the scopes the restored state no longer holds by appending matching `EndClip` commands. + +Clip descriptors are recorded in canvas-local coordinates and anchored at the recording state's destination offset. When drawing moves to a differently-offset context, for example a child region canvas inheriting parent clips, the canvas calls `DrawingCanvasBatcher.EnsureClipAnchors(...)` before recording draws: the open clip stack is closed and reopened anchored at the new offset. Per-glyph draw offsets do not participate; anchoring always follows the state's destination offset. + +## How Apply Works + +`Apply(...)` runs a caller-supplied `IImageProcessingContext` operation over a path region as part of the recorded timeline. + +The canvas records an `ApplyBarrier` command inline in the stream. When the command is lowered, its drawing options are cloned through `CloneForClearOperation`, which forces `PixelAlphaCompositionMode.Src` at full blend percentage: the processed pixels replace the covered region outright, including transparency, instead of blending over it. + +Because an apply must observe everything drawn beneath it, backends treat it as an ordering barrier during scene execution. If an apply is recorded inside a layer, the affected layers are marked as requiring scoped rendering so the processor sees the isolated layer content. + ## How Layers Work In This Architecture Layer terminology often causes confusion because different systems use it differently. In this codebase, the most useful mental model is: @@ -231,10 +305,9 @@ Layer terminology often causes confusion because different systems use it differ When `SaveLayer(...)` is called, the canvas: -1. clamps the requested layer bounds to the canvas -2. converts them into absolute target bounds -3. records `BeginLayer` -4. pushes a state snapshot that marks the new layer scope +1. resolves the requested layer bounds against the active transform and target bounds +2. records `BeginLayer` carrying the absolute layer bounds and a shared layer state object +3. pushes a state snapshot that marks the new layer scope The layer bounds are expressed in the active local coordinate system, so the canvas transform in effect at `SaveLayer(...)` time is applied when resolving the layer's @@ -280,6 +353,7 @@ The child: - keeps using the same backend - keeps using the same shared batcher - keeps participating in the same deferred replay model +- inherits the parent's clip state; the shared clip stream is re-anchored at the child's destination offset before the child records draws The child canvas has local coordinates starting at `(0, 0)`, but its frame bounds resolve to the correct absolute position inside the parent target. @@ -299,7 +373,7 @@ The rough flow is: 2. if a canvas transform is active, bake that transform into the image pixels 3. align the transformed bitmap to integer canvas bounds 4. create an `ImageBrush` -5. queue the final fill command using that brush +5. queue the final fill command using that brush, with an identity transform so the canvas transform is not applied twice This design avoids applying the canvas transform twice and keeps the later command model consistent with brush-based filling. @@ -312,7 +386,7 @@ Once a command batch reaches `DefaultDrawingBackend.CreateScene(...)`, the publi The CPU backend does not need to understand every public API call individually. It works with: - prepared commands -- layer boundaries +- layer boundaries, clip scopes, and apply barriers as ordered stream commands - target bounds during `CreateScene(...)` - the destination frame during `RenderScene(...)` @@ -337,7 +411,7 @@ The WebGPU backend receives the same command batch shape, but it splits retained It benefits from the same canvas-level decisions: - commands are already normalized -- layers already exist as explicit boundaries +- layers and clips already exist as explicit ordered boundaries - the frame already describes whether a native surface is available The WebGPU public helpers reach this point in a target-first way: @@ -366,7 +440,7 @@ Preparation exists so backend-agnostic normalization happens once. Frames exist so the same canvas can target memory, native surfaces, or sub-regions. -Layers exist as inline composition scopes in the command stream. +Layers and clips exist as inline ordered scopes in the command stream. Once those ideas are clear, the code stops looking like a random collection of types and starts looking like one system with a clear division of responsibility. @@ -378,7 +452,7 @@ If you want to move from the architecture into the code, this is the best order. 2. `DrawingCanvas{TPixel}.cs` 3. `DrawingCanvasFactoryExtensions.cs` and `DrawingCanvas.Shapes.cs` 4. `DrawingCanvasBatcher{TPixel}.cs` -5. `CompositionCommand.cs` +5. `CompositionCommand.cs` and `DrawingClipDescriptor.cs` 6. `DefaultDrawingBackend.cs` 7. `FlushScene.cs` 8. `WebGPUEnvironment.cs` diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs index 1be510bb5..37f8f617d 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas.Shapes.cs @@ -43,9 +43,10 @@ public void Fill(Brush brush, Rectangle region) => this.Fill(brush, new RectanglePolygon(region)); /// - /// Clears the whole canvas using the given brush and clear-style composition options. + /// Clears the whole canvas with the given brush using replacement composition: the brush output + /// overwrites the covered pixels outright, including their alpha, rather than blending over them. /// - /// Brush used to shade destination pixels during clear. + /// Brush used to shade the destination pixels. public void Clear(Brush brush) { Rectangle bounds = this.Bounds; @@ -54,9 +55,10 @@ public void Clear(Brush brush) } /// - /// Clears a local region using the given brush and clear-style composition options. + /// Clears a local region with the given brush using replacement composition: the brush output + /// overwrites the covered pixels outright, including their alpha, rather than blending over them. /// - /// Brush used to shade destination pixels during clear. + /// Brush used to shade the destination pixels. /// Region to clear in local coordinates. public void Clear(Brush brush, Rectangle region) => this.Clear(brush, new RectanglePolygon(region)); diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas.cs index 3b41f4a77..6be0dfd3f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.Fonts; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Text; @@ -9,8 +10,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Represents a drawing canvas over a frame target. +/// Represents a stateful, retained-mode drawing canvas over a frame target. /// +/// +/// Draw calls are recorded into an ordered command stream rather than rasterized immediately; +/// the root canvas replays the recorded timeline when it is disposed. Drawing state (options, +/// transform, clip and layer scope) is managed through the / +/// state stack. +/// public abstract partial class DrawingCanvas : IDisposable { /// @@ -35,23 +42,19 @@ public abstract partial class DrawingCanvas : IDisposable public abstract int Save(); /// - /// Saves the current drawing state and replaces the active state with the provided options and clip paths. + /// Saves the current drawing state and replaces the active state with the provided options. /// /// /// The provided instance is stored by reference. /// Mutating it after this call mutates the active/restored state behavior. /// /// Drawing options for the new active state. - /// Clip paths for the new active state. /// The save count after the previous state has been pushed. - public abstract int Save(DrawingOptions options, params IPath[] clipPaths); + public abstract int Save(DrawingOptions options); /// - /// Saves the current drawing state and begins an isolated compositing layer - /// bounded to a subregion. Subsequent draw commands are recorded into that isolated - /// logical layer. When closes the layer, it is recorded into the - /// canvas timeline and later composed during using the specified - /// . + /// Saves the current drawing state and begins an isolated compositing layer bounded to a subregion. + /// Subsequent draw commands target that layer until closes it. /// /// /// The layer bounds are expressed in the current local coordinate system and are @@ -60,8 +63,7 @@ public abstract partial class DrawingCanvas : IDisposable /// system used by commands recorded inside the layer. /// /// - /// Graphics options controlling how the closed layer is composited against the parent canvas - /// when the canvas timeline is rendered during . + /// Graphics options controlling how the closed layer is composited against the parent canvas. /// /// /// The local bounds of the layer. Only this region is allocated and composited. @@ -69,13 +71,90 @@ public abstract partial class DrawingCanvas : IDisposable /// The save count after the layer state has been pushed. public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds); + /// + /// Saves the current drawing state and begins an isolated compositing layer + /// using the supplied drawing options for commands recorded into the layer. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas. + /// + /// + /// The local bounds of the layer. Only this region is allocated and composited. + /// + /// Drawing options for the layer contents. + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, DrawingOptions options); + + /// + /// Saves the current drawing state and begins an isolated compositing layer whose content is + /// transformed by an effect when the layer is restored. + /// + /// + /// The layer isolates the content, so the effect operates on exactly what is drawn between + /// this call and the matching , against transparency; a + /// , for example, slots its shadow beneath that content + /// before the layer composites onto the canvas. The bounds are expanded internally by the + /// effect's reach so blurred or offset output is not cut off. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas. + /// + /// The content bounds in local canvas coordinates. + /// The effect applied to the layer content on restore. + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect); + + /// + /// Saves the current drawing state and begins an isolated compositing layer whose content is + /// transformed by an effect when the layer is restored, using the supplied drawing options for + /// commands recorded into the layer. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas. + /// + /// The content bounds in local canvas coordinates. + /// The effect applied to the layer content on restore. + /// Drawing options for the layer contents. + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect, DrawingOptions options); + + /// + /// Saves the current drawing state and begins an isolated compositing layer whose content is + /// transformed by an effect confined to a path region when the layer is restored. + /// + /// + /// The effect processes only pixels covered by the supplied path; its output lands on the path + /// translated by the effect's offset. The layer bounds are derived from the path and expanded + /// by the effect's reach. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas. + /// + /// The path region the effect processes, in local coordinates. + /// The effect applied to the layer content on restore. + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, IPath region, LayerEffect effect); + + /// + /// Saves the current drawing state and begins an isolated compositing layer whose content is + /// transformed by an effect confined to a path region when the layer is restored, using the + /// supplied drawing options for commands recorded into the layer. + /// + /// + /// Graphics options controlling how the closed layer is composited against the parent canvas. + /// + /// The path region the effect processes, in local coordinates. + /// The effect applied to the layer content on restore. + /// Drawing options for the layer contents. + /// The save count after the layer state has been pushed. + public abstract int SaveLayer(GraphicsOptions layerOptions, IPath region, LayerEffect effect, DrawingOptions options); + /// /// Restores the most recently saved state. /// /// /// If the most recently saved state was created by a SaveLayer overload, - /// the layer is closed in the recorded timeline. Actual composition happens during - /// . + /// the layer is closed and becomes part of subsequent rendering. /// public abstract void Restore(); @@ -86,8 +165,7 @@ public abstract partial class DrawingCanvas : IDisposable /// State frames above are discarded, /// and the last discarded frame becomes the current state. /// If any discarded state was created by a SaveLayer overload, - /// those layers are closed in the recorded timeline and composed during - /// . + /// those layers are closed and become part of subsequent rendering. /// /// The save count to restore to. public abstract void RestoreTo(int saveCount); @@ -100,9 +178,10 @@ public abstract partial class DrawingCanvas : IDisposable public abstract DrawingCanvas CreateRegion(Rectangle region); /// - /// Clears a path region using the given brush and clear-style composition options. + /// Fills a path region with the given brush using replacement composition: the brush output + /// overwrites the covered pixels outright, including their alpha, rather than blending over them. /// - /// Brush used to shade destination pixels during clear. + /// Brush used to shade destination pixels during the clear. /// The path region to clear. public abstract void Clear(Brush brush, IPath path); @@ -113,6 +192,28 @@ public abstract partial class DrawingCanvas : IDisposable /// The path to fill. public abstract void Fill(Brush brush, IPath path); + /// + /// Narrows the current clip region by intersecting it with the supplied clip paths. + /// + /// + /// The clip paths are transformed by the active transform at the point this is called, then + /// intersected with the existing clip; clipping only ever narrows. The resulting clip is part of + /// the current saved state and is restored by . Multiple paths combine as a + /// union before intersecting (e.g. a region built from several rectangles). + /// + /// The clip paths to intersect with the current clip, in local coordinates. + public abstract void Clip(params IPath[] clipPaths); + + /// + /// Narrows the current clip region by applying the specified clipping operation with the supplied clip paths. + /// + /// + /// The clip paths are transformed by the active transform at the point this is called. + /// + /// The operation to apply to the current clip. + /// The clip paths to combine with the current clip, in local coordinates. + public abstract void Clip(ClipOperation operation, params IPath[] clipPaths); + /// /// Applies an image-processing operation to a local region. /// @@ -137,6 +238,32 @@ public abstract partial class DrawingCanvas : IDisposable /// The image-processing operation to apply to the region. public abstract void Apply(IPath path, Action operation); + /// + /// Applies an image-processing operation to a local region and composites the processed pixels + /// back with explicit options, optionally offset from where they were read. The target region + /// is untouched while the operation runs, so the processed pixels can be blended against the + /// original content; for example a drop shadow composites the tinted, blurred region back + /// beneath the content with at the shadow offset. + /// + /// The local region to process. + /// The image-processing operation to apply to the region. + /// The graphics options used to composite the processed pixels back. + /// The offset at which the processed pixels are written back. + public abstract void Apply(Rectangle region, Action operation, GraphicsOptions writeBackOptions, Point writeBackOffset); + + /// + /// Applies an image-processing operation to a path region and composites the processed pixels + /// back with explicit options, optionally offset from where they were read. + /// + /// + /// The write-back affects only pixels covered by the supplied path translated by the offset. + /// + /// The path region to process. + /// The image-processing operation to apply to the region. + /// The graphics options used to composite the processed pixels back. + /// The offset at which the processed pixels are written back. + public abstract void Apply(IPath path, Action operation, GraphicsOptions writeBackOptions, Point writeBackOffset); + /// /// Draws a polyline outline using the provided pen and drawing options. /// @@ -235,6 +362,31 @@ public abstract void DrawText( Brush? brush, Pen? pen); + /// + /// Draws a single glyph, identified by its glyph id, onto this canvas. + /// + /// The id of the glyph within the font face referenced by . + /// + /// The glyph rendering options, including the font, origin, grapheme index and optional per-glyph paint. + /// + /// Default brush used to fill the glyph when is not set. + /// Default pen used to outline the glyph when is not set. + public abstract void DrawText( + ushort glyphId, + RichGlyphOptions options, + Brush? brush, + Pen? pen); + + /// + /// Draws positioned glyphs onto this canvas. + /// + /// The glyph identifiers. + /// The absolute glyph origins in pixel units. + /// The glyph rendering options, including the font and optional glyph paint. + /// Default brush used to fill glyphs when is not set. + /// Default pen used to outline glyphs when is not set. + public abstract void DrawText(ReadOnlySpan glyphIds, ReadOnlySpan points, RichGlyphOptions options, Brush? brush, Pen? pen); + /// /// Draws layered glyph geometry. /// @@ -270,22 +422,81 @@ public abstract void DrawImage( IResampler? sampler = null); /// - /// Creates a retained backend scene from the drawing commands currently queued on this canvas. + /// Draws an image source region into a destination rectangle, tiling the painted area by repeating + /// the destination rectangle outwards per the supplied s. /// - /// A retained backend scene. + /// The source image. + /// The source rectangle within . + /// The destination rectangle in local canvas coordinates (defines a single tile cell). + /// The horizontal wrap mode applied when sampling beyond . + /// The vertical wrap mode applied when sampling beyond . + /// + /// Optional resampler used when scaling or transforming the image. Defaults to . + /// + public abstract void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + WrapMode wrapX, + WrapMode wrapY, + IResampler? sampler = null); + + /// + /// Copies pixels from another canvas into this canvas. + /// + /// + /// Any queued commands on both canvases are applied before the copy. + /// + /// The source canvas. + /// The source rectangle in source-local coordinates. + /// The target point in this canvas' local coordinates. + public abstract void CopyPixelsFrom(DrawingCanvas source, Rectangle sourceRectangle, Point targetPoint); + + /// + /// Seals the commands recorded so far into a retained backend scene and clears them from the + /// canvas queue. + /// + /// + /// The active clip range is closed before the scene is built and reopened afterwards, so this + /// acts as an ordering boundary in the recorded timeline. Image resources retained for the + /// sealed commands are transferred to the returned scene, which owns and disposes them. + /// + /// The created backend scene. public abstract DrawingBackendScene CreateScene(); /// - /// Renders a retained backend scene into this canvas target. + /// Records a previously created backend scene into this canvas' command timeline. /// - /// The retained backend scene to render. + /// + /// The active command range is sealed before the scene is inserted so the scene renders in + /// submission order relative to the commands recorded around it. Like other recorded work, + /// the scene is replayed into the target frame when the root canvas is disposed or otherwise + /// flushed, not at the point of this call. + /// + /// The backend scene to render. public abstract void RenderScene(DrawingBackendScene scene); /// - /// Seals queued drawing commands into the canvas timeline. + /// Seals the drawing commands recorded so far into an ordered range so that subsequent + /// commands begin a new range. /// + /// + /// This does not rasterize or composite anything into the target frame. Recorded commands + /// are replayed into the target only when the root canvas is disposed, or when they are + /// lowered explicitly through , or + /// . Use this to establish an ordering boundary in the recorded + /// timeline without ending the canvas. + /// public abstract void Flush(); - /// + /// + /// Releases the canvas. Disposing the root canvas closes any open layers and clips and + /// replays the recorded command timeline into the target frame; child region canvases + /// share the root's command stream and do not flush on disposal. + /// + /// + /// The root canvas also releases image resources retained for deferred draws and, when it + /// owns the text drawing cache, clears that cache. Disposal is idempotent. + /// public abstract void Dispose(); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs index b6f73fa92..7a269db93 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasBatcher{TPixel}.cs @@ -17,6 +17,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// separately and referenced by timeline entry index. During disposal replay, command ranges are /// lowered to short-lived backend scenes at the position where the canvas recorded the range. /// +/// The pixel format. internal sealed class DrawingCanvasBatcher where TPixel : unmanaged, IPixel { @@ -32,31 +33,39 @@ internal sealed class DrawingCanvasBatcher // sealing instead of letting layer state leak across later command ranges. private int layerCommandCount; private int sealedLayerCommandCount; + private int applyCommandCount; + private int sealedApplyCommandCount; + private int clipCommandCount; + private int sealedClipCommandCount; - // Clip and dash flags gate whole-buffer command preparation; prepared commands + // Dash and brush-transform flags gate whole-buffer command preparation; prepared commands // remain in the same command buffer until replay consumes it. - private bool hasClips; private bool hasDashes; + private bool hasBrushTransforms; - // Timeline entries keep compact indexes into the command, barrier, and retained + // Mirrors the physical begin/end-clip command stream. The stream is the single source of + // truth for clipping in both backends; the canvas re-anchors it via EnsureClipAnchors when + // drawing moves to a differently-offset context (for example a child region). + private readonly List<(DrawingClipDescriptor Descriptor, Point AnchorOffset)> openClips = []; + + // Timeline entries keep compact indexes into the command and retained // scene buffers while preserving the order recorded by the canvas. private DrawingCanvasTimelineEntry[] entries; - // Apply barriers carry replay-time target read/process/write operations. - private ApplyBarrier[] applyBarriers; - private int applyBarrierCount; - // These are existing retained scenes recorded through RenderScene, not scenes // produced later from this batcher's own command ranges. private DrawingBackendScene[] insertedScenes; private int insertedSceneCount; - internal DrawingCanvasBatcher(Configuration configuration) + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing parallelism settings for command preparation. + public DrawingCanvasBatcher(Configuration configuration) { this.configuration = configuration; this.commands = []; this.entries = []; - this.applyBarriers = []; this.insertedScenes = []; } @@ -80,15 +89,38 @@ internal DrawingCanvasBatcher(Configuration configuration) /// The command to queue. public void AddComposition(in CompositionCommand composition) { + switch (composition.Kind) + { + case CompositionCommandKind.BeginClip: + this.openClips.Add((composition.ClipDescriptor, composition.DestinationOffset)); + break; + + case CompositionCommandKind.EndClip: + if (this.openClips.Count > 0) + { + this.openClips.RemoveAt(this.openClips.Count - 1); + } + + break; + + default: + break; + } + this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new PathCompositionSceneCommand(composition); - if (composition.Kind is not CompositionCommandKind.FillLayer) + if (composition.Kind is CompositionCommandKind.BeginLayer or CompositionCommandKind.EndLayer) { this.layerCommandCount++; } + else if (composition.Kind is CompositionCommandKind.BeginClip or CompositionCommandKind.EndClip) + { + this.clipCommandCount++; + } - this.hasClips |= composition.ClipPaths is not null; + this.hasBrushTransforms |= composition.Kind is CompositionCommandKind.FillLayer + && composition.Transform != Matrix4x4.Identity; } /// @@ -99,8 +131,8 @@ public void AddStrokePath(in StrokePathCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new StrokePathCompositionSceneCommand(command); - this.hasClips |= command.ClipPaths is not null; this.hasDashes |= command.Pen.StrokePattern.Length >= 2; + this.hasBrushTransforms |= command.Transform != Matrix4x4.Identity; } /// @@ -111,6 +143,7 @@ public void AddStrokeLineSegment(in StrokeLineSegmentCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new LineSegmentCompositionSceneCommand(command); + this.hasBrushTransforms |= command.Transform != Matrix4x4.Identity; } /// @@ -121,6 +154,60 @@ public void AddStrokePolyline(in StrokePolylineCommand command) { this.EnsureCommandCapacity(this.commandCount + 1); this.commands[this.commandCount++] = new PolylineCompositionSceneCommand(command); + this.hasBrushTransforms |= command.Transform != Matrix4x4.Identity; + } + + /// + /// Re-anchors the open ordered clip stream at the consuming state's destination offset. + /// + /// + /// Clip descriptors are recorded in canvas-local coordinates and the begin/end-clip command + /// stream is the single source of truth for clipping in both backends. The canvas calls this + /// before recording draws so that when drawing moves to a differently-offset context (for + /// example a child region inheriting parent clips) the open clip stack is closed and reopened + /// anchored at the new offset. Glyph render locations do not participate: the canvas anchors + /// at its state's destination offset, not at per-glyph draw offsets. + /// + /// The destination offset of the consuming canvas state. + public void EnsureClipAnchors(Point destinationOffset) + { + List<(DrawingClipDescriptor Descriptor, Point AnchorOffset)> clips = this.openClips; + int count = clips.Count; + if (count == 0) + { + return; + } + + bool anchored = true; + for (int i = 0; i < count; i++) + { + if (clips[i].AnchorOffset != destinationOffset) + { + anchored = false; + break; + } + } + + if (anchored) + { + return; + } + + DrawingClipDescriptor[] descriptors = new DrawingClipDescriptor[count]; + for (int i = 0; i < count; i++) + { + descriptors[i] = clips[i].Descriptor; + } + + for (int i = 0; i < count; i++) + { + this.AddComposition(CompositionCommand.CreateEndClip()); + } + + for (int i = 0; i < count; i++) + { + this.AddComposition(CompositionCommand.CreateBeginClip(descriptors[i], destinationOffset)); + } } /// @@ -142,25 +229,25 @@ public void SealCommands() this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateCommandRange( this.sealedCommandCount, count, - this.layerCommandCount != this.sealedLayerCommandCount); + this.layerCommandCount != this.sealedLayerCommandCount, + this.applyCommandCount != this.sealedApplyCommandCount, + this.clipCommandCount != this.sealedClipCommandCount); this.sealedCommandCount = this.commandCount; this.sealedLayerCommandCount = this.layerCommandCount; + this.sealedApplyCommandCount = this.applyCommandCount; + this.sealedClipCommandCount = this.clipCommandCount; } /// - /// Appends an apply barrier to the replay timeline after sealing queued commands. + /// Appends an apply barrier to the command stream. /// /// The apply barrier to append. - internal void AddApplyBarrier(ApplyBarrier barrier) + public void AddApplyBarrier(ApplyBarrier barrier) { - this.SealCommands(); - this.EnsureApplyBarrierCapacity(this.applyBarrierCount + 1); - - int barrierIndex = this.applyBarrierCount; - this.applyBarriers[this.applyBarrierCount++] = barrier; - this.EnsureEntryCapacity(this.TimelineEntryCount + 1); - this.entries[this.TimelineEntryCount++] = DrawingCanvasTimelineEntry.CreateApplyBarrier(barrierIndex); + this.EnsureCommandCapacity(this.commandCount + 1); + this.commands[this.commandCount++] = new PathCompositionSceneCommand(CompositionCommand.CreateApply(barrier)); + this.applyCommandCount++; } /// @@ -201,11 +288,14 @@ public DrawingBackendScene CreateScene( this.SealAndPrepareCommands(); - return backend.CreateScene( - this.configuration, - targetBounds, - new DrawingCommandBatch(this.commands, this.commandCount, this.layerCommandCount > 0), - ownedResources); + DrawingCommandBatch commandBatch = this.CreatePreparedCommandBatch( + 0, + this.commandCount, + this.layerCommandCount > 0, + this.applyCommandCount > 0, + this.clipCommandCount > 0); + + return backend.CreateScene(this.configuration, targetBounds, commandBatch, ownedResources); } /// @@ -224,7 +314,29 @@ public void SealAndPrepareCommands() /// The command-range timeline entry. /// The command batch. public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry) - => new(this.commands, entry.Index, entry.Count, entry.HasLayers); + => this.CreatePreparedCommandBatch( + entry.Index, + entry.Count, + entry.HasLayers, + entry.HasApply, + entry.HasClipControls); + + /// + /// Creates a command batch over a contiguous range of the shared command buffer. + /// + /// The first command index. + /// The command count. + /// Indicates whether the range contains layer boundary commands. + /// Indicates whether the range contains apply barriers. + /// Indicates whether the range contains ordered clip-control commands. + /// The command batch. + private DrawingCommandBatch CreatePreparedCommandBatch( + int startIndex, + int commandCount, + bool hasLayers, + bool hasApply, + bool hasClipControls) + => new(this.commands, startIndex, commandCount, hasLayers, hasApply, hasClipControls); /// /// Gets one recorded timeline entry. @@ -234,14 +346,6 @@ public DrawingCommandBatch CreateCommandBatch(DrawingCanvasTimelineEntry entry) public DrawingCanvasTimelineEntry GetEntry(int index) => this.entries[index]; - /// - /// Gets one recorded apply barrier. - /// - /// The apply-barrier index. - /// The recorded apply barrier. - internal ApplyBarrier GetApplyBarrier(int index) - => this.applyBarriers[index]; - /// /// Gets one retained scene reference recorded through . /// @@ -257,17 +361,23 @@ public void ClearCommandBatch() { Array.Clear(this.commands, 0, this.commandCount); Array.Clear(this.entries, 0, this.TimelineEntryCount); - Array.Clear(this.applyBarriers, 0, this.applyBarrierCount); Array.Clear(this.insertedScenes, 0, this.insertedSceneCount); this.commandCount = 0; this.sealedCommandCount = 0; this.layerCommandCount = 0; this.sealedLayerCommandCount = 0; + this.applyCommandCount = 0; + this.sealedApplyCommandCount = 0; + this.clipCommandCount = 0; + this.sealedClipCommandCount = 0; this.TimelineEntryCount = 0; - this.applyBarrierCount = 0; this.insertedSceneCount = 0; - this.hasClips = false; this.hasDashes = false; + this.hasBrushTransforms = false; + + // The canvas re-opens its active clip state after clearing, so the mirrored + // physical clip stream starts empty alongside the new command buffer. + this.openClips.Clear(); } /// @@ -310,26 +420,6 @@ private void EnsureEntryCapacity(int requiredCapacity) Array.Resize(ref this.entries, nextCapacity); } - /// - /// Ensures that the apply-barrier buffer can store the requested barrier count without reallocating. - /// - /// The required barrier capacity. - private void EnsureApplyBarrierCapacity(int requiredCapacity) - { - if (requiredCapacity <= this.applyBarriers.Length) - { - return; - } - - int nextCapacity = this.applyBarriers.Length == 0 ? 2 : this.applyBarriers.Length * 2; - if (nextCapacity < requiredCapacity) - { - nextCapacity = requiredCapacity; - } - - Array.Resize(ref this.applyBarriers, nextCapacity); - } - /// /// Ensures that the inserted-scene buffer can store the requested scene count without reallocating. /// @@ -350,16 +440,20 @@ private void EnsureInsertedSceneCapacity(int requiredCapacity) Array.Resize(ref this.insertedScenes, nextCapacity); } + /// + /// Expands dashed strokes and normalizes brush transforms across the whole command buffer. + /// private void PrepareCommands() { - if (!this.hasClips && !this.hasDashes) + if (!this.hasDashes && !this.hasBrushTransforms) { return; } - // If clipping is present we need to apply that now before handing the command - // to the backend. This avoids complicating the backend with clipping logic - // and allows us to reuse the same optimized backend code for clipped and unclipped paths. + // Command preparation is the last canvas-owned normalization boundary. Dashed strokes are + // expanded and brush coordinates are normalized here so CPU and WebGPU do not each bake + // the transform in their own way. Clipping is not prepared per command: the ordered + // begin/end-clip command stream is the single source of truth for both backends. int requestedParallelism = this.configuration.MaxDegreeOfParallelism; int partitionCount = ParallelExecutionHelper.GetPartitionCount(requestedParallelism, this.commandCount); @@ -370,6 +464,8 @@ private void PrepareCommands() PrepareCommand(ref this.commands[i]); } + this.hasDashes = false; + this.hasBrushTransforms = false; return; } @@ -389,93 +485,145 @@ private void PrepareCommands() PrepareCommand(ref this.commands[i]); } }); + + this.hasDashes = false; + this.hasBrushTransforms = false; } + /// + /// Prepares one queued command in place: expands dashed stroke geometry and bakes + /// non-identity transforms into brush coordinates. + /// + /// The command slot to prepare. private static void PrepareCommand(ref CompositionSceneCommand command) { if (command is PathCompositionSceneCommand pathCommand) { CompositionCommand composition = pathCommand.Command; - if (composition.ClipPaths is { Count: > 0 }) + if (composition.Kind == CompositionCommandKind.FillLayer && composition.Transform != Matrix4x4.Identity) { - IPath path = composition.SourcePath; - DrawingOptions sourceOptions = composition.DrawingOptions; - - if (sourceOptions.Transform != Matrix4x4.Identity) + // The batcher is the brush normalization boundary. Backends still use + // DrawingOptions.Transform for geometry, but brush coordinates are prepared + // here so CPU and WebGPU do not each bake the transform in their own way. + // Position-independent brushes return themselves untransformed, and every + // glyph command reaches here because its composed transform carries the + // sub-pixel fraction, so the rebuild is skipped when nothing changed. + Brush brush = composition.Brush.Transform( + composition.Transform, + composition.RasterizerOptions.Interest, + composition.RasterizerOptions.Interest); + + if (!ReferenceEquals(brush, composition.Brush)) { - path = path.Transform(sourceOptions.Transform); + pathCommand.Command = CompositionCommand.Create( + composition.SourcePath, + brush, + composition.DrawingOptions, + composition.RasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + composition.OwnerLayer, + composition.SubPixelOffset); } - - path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); - - RasterizerOptions rasterizerOptions = composition.RasterizerOptions; - DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); - - // Update the command with the clipped path. - pathCommand.Command = CompositionCommand.Create( - path, - composition.Brush.Transform(sourceOptions.Transform), - preparedOptions, - in rasterizerOptions, - composition.TargetBounds, - composition.DestinationOffset, - null, - composition.IsInsideLayer); } } else if (command is StrokePathCompositionSceneCommand strokePathCommand) { StrokePathCommand composition = strokePathCommand.Command; - - if (composition.ClipPaths is { Count: > 0 }) + Matrix4x4 transform = composition.Transform; + bool hasTransform = transform != Matrix4x4.Identity; + Brush brush = hasTransform + ? composition.Brush.Transform( + transform, + composition.RasterizerOptions.Interest, + composition.RasterizerOptions.Interest) + : composition.Brush; + + Pen pen = composition.Pen; + IPath sourcePath = composition.SourcePath; + bool hasDashes = pen.StrokePattern.Length >= 2; + if (hasDashes) { - IPath path = composition.Pen.GeneratePath(composition.SourcePath); - DrawingOptions sourceOptions = composition.DrawingOptions; + // Dashing changes the subject geometry, so it must be done before the backend + // retains raster payload for the stroke command. + sourcePath = sourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span, pen.StrokePatternOffset); + } - if (sourceOptions.Transform != Matrix4x4.Identity) + // Position-independent brushes return themselves untransformed, so only dashing + // or a genuinely transformed brush warrants rebuilding the command. + if (hasDashes || !ReferenceEquals(brush, composition.Brush)) + { + strokePathCommand.Command = new StrokePathCommand( + sourcePath, + brush, + composition.DrawingOptions, + composition.RasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + composition.Pen, + composition.IsInsideLayer, + composition.OwnerLayer, + composition.SubPixelOffset); + } + } + else if (command is LineSegmentCompositionSceneCommand lineSegmentCommand) + { + StrokeLineSegmentCommand composition = lineSegmentCommand.Command; + Matrix4x4 transform = composition.Transform; + if (transform != Matrix4x4.Identity) + { + Brush brush = composition.Brush.Transform( + transform, + composition.RasterizerOptions.Interest, + composition.RasterizerOptions.Interest); + + // Position-independent brushes return themselves untransformed; skipping the + // rebuild also skips allocating a replacement scene command wrapper. + if (!ReferenceEquals(brush, composition.Brush)) { - path = path.Transform(sourceOptions.Transform); + command = new LineSegmentCompositionSceneCommand( + new StrokeLineSegmentCommand( + composition.SourceStart, + composition.SourceEnd, + brush, + composition.DrawingOptions, + composition.RasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + composition.Pen, + composition.IsInsideLayer, + composition.OwnerLayer)); } - - path = path.Clip(sourceOptions.ShapeOptions, composition.ClipPaths); - - RasterizerOptions rasterizerOptions = composition.RasterizerOptions; - DrawingOptions preparedOptions = WithIdentityTransform(sourceOptions); - - command = new PathCompositionSceneCommand( - CompositionCommand.Create( - path, - composition.Brush.Transform(sourceOptions.Transform), - preparedOptions, - in rasterizerOptions, - composition.TargetBounds, - composition.DestinationOffset, - null, - composition.IsInsideLayer)); } - else + } + else if (command is PolylineCompositionSceneCommand polylineCommand) + { + StrokePolylineCommand composition = polylineCommand.Command; + Matrix4x4 transform = composition.Transform; + if (transform != Matrix4x4.Identity) { - // We need to dash the path here before sending it to the backend. - Pen pen = composition.Pen; - if (pen.StrokePattern.Length >= 2) + Brush brush = composition.Brush.Transform( + transform, + composition.RasterizerOptions.Interest, + composition.RasterizerOptions.Interest); + + // Position-independent brushes return themselves untransformed; skipping the + // rebuild also skips allocating a replacement scene command wrapper. + if (!ReferenceEquals(brush, composition.Brush)) { - strokePathCommand.Command = new StrokePathCommand( - composition.SourcePath.GenerateDashes(pen.StrokeWidth, pen.StrokePattern.Span), - composition.Brush, - composition.DrawingOptions, - composition.RasterizerOptions, - composition.TargetBounds, - composition.DestinationOffset, - composition.Pen, - null, - composition.IsInsideLayer); + command = new PolylineCompositionSceneCommand( + new StrokePolylineCommand( + composition.SourcePoints, + brush, + composition.DrawingOptions, + composition.RasterizerOptions, + composition.TargetBounds, + composition.DestinationOffset, + composition.Pen, + composition.IsInsideLayer, + composition.OwnerLayer)); } } } } - - private static DrawingOptions WithIdentityTransform(DrawingOptions source) - => source.Transform == Matrix4x4.Identity - ? source - : new DrawingOptions(source.GraphicsOptions, source.ShapeOptions, Matrix4x4.Identity); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs index 874009969..bc8b5b6a5 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasFactoryExtensions.cs @@ -41,6 +41,40 @@ public static DrawingCanvas CreateCanvas( clipPaths); } + /// + /// Creates a drawing canvas over an existing typed image frame. + /// + /// + /// The caller owns the returned canvas and must dispose it to replay recorded work into the frame. + /// + /// The pixel format. + /// The frame backing the canvas. + /// The configuration to use for this canvas instance. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas CreateCanvas( + this ImageFrame frame, + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + params IPath[] clipPaths) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(textCache, nameof(textCache)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + return new DrawingCanvas( + configuration, + options, + textCache, + frame.PixelBuffer.GetRegion(), + clipPaths); + } + /// /// Creates a drawing canvas over an existing image frame. /// @@ -67,22 +101,104 @@ public static DrawingCanvas CreateCanvas( return visitor.Value!; } + /// + /// Creates a drawing canvas over an existing image frame. + /// + /// + /// The caller owns the returned canvas and must dispose it to replay recorded work into the frame. + /// + /// The frame backing the canvas. + /// The configuration to use for this canvas instance. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// Initial clip paths for this canvas instance. + /// A drawing canvas targeting . + public static DrawingCanvas CreateCanvas( + this ImageFrame frame, + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + params IPath[] clipPaths) + { + Guard.NotNull(frame, nameof(frame)); + Guard.NotNull(options, nameof(options)); + Guard.NotNull(textCache, nameof(textCache)); + Guard.NotNull(clipPaths, nameof(clipPaths)); + + CanvasFactoryVisitor visitor = new(configuration, options, textCache, clipPaths); + frame.AcceptVisitor(visitor); + return visitor.Value!; + } + + /// + /// Visits a non-generic to create a canvas over its concrete pixel type. + /// private struct CanvasFactoryVisitor : IImageFrameVisitor { + /// + /// The configuration to use for the created canvas. + /// private readonly Configuration configuration; + + /// + /// Initial drawing options for the created canvas. + /// private readonly DrawingOptions options; + + /// + /// Optional text drawing cache; when the canvas creates and owns its own cache. + /// + private readonly DrawingTextCache? textCache; + + /// + /// Initial clip paths for the created canvas. + /// private readonly IPath[] clipPaths; + /// + /// Initializes a new instance of the struct + /// creating a canvas that owns its own text drawing cache. + /// + /// The configuration to use for the created canvas. + /// Initial drawing options for the created canvas. + /// Initial clip paths for the created canvas. public CanvasFactoryVisitor(Configuration configuration, DrawingOptions options, IPath[] clipPaths) { this.configuration = configuration; this.options = options; + this.textCache = null; + this.clipPaths = clipPaths; + } + + /// + /// Initializes a new instance of the struct + /// creating a canvas that shares the supplied text drawing cache. + /// + /// The configuration to use for the created canvas. + /// Initial drawing options for the created canvas. + /// The text drawing cache used by the created canvas. + /// Initial clip paths for the created canvas. + public CanvasFactoryVisitor( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + IPath[] clipPaths) + { + this.configuration = configuration; + this.options = options; + this.textCache = textCache; this.clipPaths = clipPaths; } + /// + /// Gets the canvas created during the visit, or before the visit runs. + /// public DrawingCanvas? Value { get; private set; } + /// void IImageFrameVisitor.Visit(ImageFrame frame) - => this.Value = frame.CreateCanvas(this.configuration, this.options, this.clipPaths); + => this.Value = this.textCache is null + ? frame.CreateCanvas(this.configuration, this.options, this.clipPaths) + : frame.CreateCanvas(this.configuration, this.options, this.textCache, this.clipPaths); } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs index de7e15206..7eef2edc9 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasState.cs @@ -12,17 +12,17 @@ internal sealed class DrawingCanvasState /// Initializes a new instance of the class. /// /// Drawing options for this state. - /// Clip paths for this state. + /// The normalized clip state. /// Absolute target bounds used for commands recorded in this state. /// Absolute destination offset for paths recorded in local canvas coordinates. public DrawingCanvasState( DrawingOptions options, - IReadOnlyList clipPaths, + DrawingClipState clipState, Rectangle targetBounds, Point destinationOffset) { this.Options = options; - this.ClipPaths = clipPaths; + this.ClipState = clipState; this.TargetBounds = targetBounds; this.DestinationOffset = destinationOffset; } @@ -37,9 +37,9 @@ public DrawingCanvasState( public DrawingOptions Options { get; } /// - /// Gets clip paths associated with this state. + /// Gets the normalized clip state associated with this state. /// - public IReadOnlyList ClipPaths { get; } + public DrawingClipState ClipState { get; } /// /// Gets the absolute target bounds used for commands recorded in this state. @@ -57,7 +57,66 @@ public DrawingCanvasState( public bool IsLayer { get; init; } /// - /// Gets the layer compositing options when this state represents a compositing layer. + /// Gets the layer currently receiving commands for this state. /// - public GraphicsOptions? LayerOptions { get; init; } + public DrawingCanvasLayer? Layer { get; init; } + + /// + /// Gets or sets the pending effect applied to the layer's content when the state is restored, + /// together with the region it processes. + /// + public DrawingCanvasLayerEffect? LayerEffect { get; set; } +} + +/// +/// A pending attached to a layer state, applied to the layer's +/// content when the layer is restored. +/// +internal sealed class DrawingCanvasLayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The region of the layer the effect processes, in local coordinates. + /// The effect to apply. + public DrawingCanvasLayerEffect(IPath region, LayerEffect effect) + { + this.Region = region; + this.Effect = effect; + } + + /// + /// Gets the region of the layer the effect processes, in local coordinates. + /// + public IPath Region { get; } + + /// + /// Gets the effect to apply. + /// + public LayerEffect Effect { get; } +} + +/// +/// Mutable layer state shared by the layer's begin and end commands. +/// +internal sealed class DrawingCanvasLayer +{ + /// + /// Initializes a new instance of the class. + /// + /// The compositing options used when the layer closes. + public DrawingCanvasLayer(GraphicsOptions options) + { + this.Options = options; + } + + /// + /// Gets the compositing options used when the layer closes. + /// + public GraphicsOptions Options { get; } + + /// + /// Gets or sets a value indicating whether this layer contains a target-wide apply barrier. + /// + public bool RequiresScopedApply { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs index cf85e4980..1c53340b0 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvasTimelineEntry.cs @@ -13,11 +13,6 @@ internal enum DrawingCanvasTimelineEntryKind /// CommandRange, - /// - /// An apply barrier. - /// - ApplyBarrier, - /// /// An existing retained scene recorded through . /// @@ -29,21 +24,34 @@ internal enum DrawingCanvasTimelineEntryKind /// /// /// Command ranges reference contiguous draw commands; they are not backend scene objects yet. -/// Apply barriers and retained scene references point into side buffers by index, keeping this -/// type compact while preserving the exact order in which the canvas recorded replay work. +/// Retained scene references point into a side buffer by index, keeping this type compact while +/// preserving the exact order in which the canvas recorded replay work. /// internal readonly struct DrawingCanvasTimelineEntry { + /// + /// Initializes a new instance of the struct. + /// + /// The kind of replay item. + /// The command start index for command ranges, or the retained-scene buffer index for scene entries. + /// The number of commands for command-range entries. + /// Indicates whether the command range contains layer boundary commands. + /// Indicates whether the command range contains apply barriers. + /// Indicates whether the command range contains ordered clip-control commands. private DrawingCanvasTimelineEntry( DrawingCanvasTimelineEntryKind kind, int index, int count, - bool hasLayers) + bool hasLayers, + bool hasApply, + bool hasClipControls) { this.Kind = kind; this.Index = index; this.Count = count; this.HasLayers = hasLayers; + this.HasApply = hasApply; + this.HasClipControls = hasClipControls; } /// @@ -52,7 +60,7 @@ private DrawingCanvasTimelineEntry( public DrawingCanvasTimelineEntryKind Kind { get; } /// - /// Gets the command start index for command ranges, or the side-buffer index for barriers and scenes. + /// Gets the command start index for command ranges, or the retained-scene buffer index for scene entries. /// public int Index { get; } @@ -66,23 +74,32 @@ private DrawingCanvasTimelineEntry( /// public bool HasLayers { get; } + /// + /// Gets a value indicating whether the command range contains apply barriers. + /// + public bool HasApply { get; } + + /// + /// Gets a value indicating whether the command range contains ordered clip-control commands. + /// + public bool HasClipControls { get; } + /// /// Creates a command-range entry. /// /// The first command index. /// The command count. /// Indicates whether the command range contains layer boundary commands. + /// Indicates whether the command range contains apply barriers. + /// Indicates whether the command range contains ordered clip-control commands. /// The command-range entry. - public static DrawingCanvasTimelineEntry CreateCommandRange(int startIndex, int count, bool hasLayers) - => new(DrawingCanvasTimelineEntryKind.CommandRange, startIndex, count, hasLayers); - - /// - /// Creates an apply-barrier entry. - /// - /// The apply-barrier index. - /// The apply-barrier entry. - public static DrawingCanvasTimelineEntry CreateApplyBarrier(int index) - => new(DrawingCanvasTimelineEntryKind.ApplyBarrier, index, 0, false); + public static DrawingCanvasTimelineEntry CreateCommandRange( + int startIndex, + int count, + bool hasLayers, + bool hasApply, + bool hasClipControls) + => new(DrawingCanvasTimelineEntryKind.CommandRange, startIndex, count, hasLayers, hasApply, hasClipControls); /// /// Creates an entry for an existing retained scene recorded through . @@ -90,5 +107,5 @@ public static DrawingCanvasTimelineEntry CreateApplyBarrier(int index) /// The retained-scene reference index. /// The retained-scene entry. public static DrawingCanvasTimelineEntry CreateScene(int index) - => new(DrawingCanvasTimelineEntryKind.Scene, index, 0, false); + => new(DrawingCanvasTimelineEntryKind.Scene, index, 0, false, false, false); } diff --git a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs index c44e3b355..962363f91 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.Fonts; using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Helpers; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; using SixLabors.ImageSharp.Drawing.Text; @@ -41,14 +42,21 @@ public sealed class DrawingCanvas : DrawingCanvas /// /// Temporary image resources that must stay alive until queued commands are flushed. + /// Shared between the root canvas and its regions: only the root renders the timeline, so + /// region-deferred images must survive region disposal and release with the root. /// - private readonly List> pendingImageResources = []; + private readonly List> pendingImageResources; /// /// Indicates whether this canvas owns final disposal of the shared batcher. /// private readonly bool ownsBatcher; + /// + /// Indicates whether this canvas owns clearing the text drawing cache. + /// + private readonly bool ownsTextCache; + /// /// Tracks whether this instance has already been disposed. /// @@ -59,11 +67,23 @@ public sealed class DrawingCanvas : DrawingCanvas /// private readonly Stack savedStates = new(); - // Per-canvas glyph-outline cache: hoists RichTextGlyphRenderer's per-glyph outline cache from - // per-DrawText-call scope up to the whole canvas, so a glyph outline built once is reused by - // every DrawText call on this canvas (across a frame's many text runs) instead of being - // rebuilt for every run on a text-heavy page. - private readonly Dictionary> glyphCache = []; + /// + /// Shared text drawing cache used by glyph rendering operations. + /// + private readonly DrawingTextCache textCache; + + /// + /// Reusable operation list handed to each text renderer. Hosted by the text cache because + /// canvases are per-frame objects; sharing the cache-owned list keeps its capacity across + /// frames instead of regrowing a fresh list of large operation structs per draw. + /// + private readonly List textOperations; + + /// + /// Reusable sort buffer for , hosted by the text cache for + /// the same reason as . + /// + private readonly List<(byte RenderPass, int Sequence, CompositionSceneCommand Command)> textCommandSortBuffer; /// /// Initializes a new instance of the class. @@ -77,7 +97,41 @@ public DrawingCanvas( DrawingOptions options, Buffer2DRegion targetRegion, params IPath[] clipPaths) - : this(configuration, options, new MemoryCanvasFrame(targetRegion), clipPaths) + : this(configuration, options, new DrawingTextCache(), ownsTextCache: true, new MemoryCanvasFrame(targetRegion), clipPaths) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// The destination target region. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + Buffer2DRegion targetRegion, + params IPath[] clipPaths) + : this(configuration, options, textCache, ownsTextCache: false, new MemoryCanvasFrame(targetRegion), clipPaths) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The destination frame. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + ICanvasFrame targetFrame, + params IPath[] clipPaths) + : this(configuration, options, new DrawingTextCache(), ownsTextCache: true, configuration.GetDrawingBackend(), targetFrame, clipPaths) { } @@ -86,14 +140,16 @@ public DrawingCanvas( /// /// The active processing configuration. /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. /// The destination frame. /// Initial clip paths for this canvas instance. public DrawingCanvas( Configuration configuration, DrawingOptions options, + DrawingTextCache textCache, ICanvasFrame targetFrame, params IPath[] clipPaths) - : this(configuration, options, configuration.GetDrawingBackend(), targetFrame, clipPaths) + : this(configuration, options, textCache, ownsTextCache: false, configuration.GetDrawingBackend(), targetFrame, clipPaths) { } @@ -111,13 +167,92 @@ public DrawingCanvas( IDrawingBackend backend, ICanvasFrame targetFrame, params IPath[] clipPaths) + : this(configuration, options, new DrawingTextCache(), true, backend, targetFrame, clipPaths) + { + } + + /// + /// Initializes a new instance of the class with an explicit backend and initial state. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// The drawing backend implementation. + /// The destination frame. + /// Initial clip paths for this canvas instance. + public DrawingCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + IDrawingBackend backend, + ICanvasFrame targetFrame, + params IPath[] clipPaths) + : this(configuration, options, textCache, false, backend, targetFrame, clipPaths) + { + } + + /// + /// Initializes a new instance of the class, + /// resolving the drawing backend from the configuration. + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// Whether this canvas owns clearing the text drawing cache. + /// The destination frame. + /// Initial clip paths for this canvas instance. + private DrawingCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + bool ownsTextCache, + ICanvasFrame targetFrame, + params IPath[] clipPaths) + : this(configuration, options, textCache, ownsTextCache, configuration.GetDrawingBackend(), targetFrame, clipPaths) + { + } + + /// + /// Initializes a new instance of the class as a root canvas, + /// creating a fresh batcher and building the default state from the supplied options and clip paths. + /// + /// + /// The initial clip state combines as an intersection, with the clip + /// edge mode derived from the antialiasing settings in . + /// + /// The active processing configuration. + /// Initial drawing options for this canvas instance. + /// The text drawing cache used by this canvas instance. + /// Whether this canvas owns clearing the text drawing cache. + /// The drawing backend implementation. + /// The destination frame. + /// Initial clip paths for this canvas instance. + private DrawingCanvas( + Configuration configuration, + DrawingOptions options, + DrawingTextCache textCache, + bool ownsTextCache, + IDrawingBackend backend, + ICanvasFrame targetFrame, + params IPath[] clipPaths) : this( configuration, backend, targetFrame, new DrawingCanvasBatcher(configuration), - new DrawingCanvasState(options, clipPaths, targetFrame.Bounds, targetFrame.Bounds.Location), - true) + textCache, + ownsTextCache, + new DrawingCanvasState( + options, + DrawingClipState.FromPaths( + clipPaths, + ClipOperation.Intersection, + options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard, + options.GraphicsOptions.AntialiasThreshold), + targetFrame.Bounds, + targetFrame.Bounds.Location), + true, + []) { } @@ -129,20 +264,27 @@ public DrawingCanvas( /// The drawing backend implementation. /// The destination frame. /// The command batcher used for deferred composition. + /// The text drawing cache used by this canvas instance. + /// Whether this canvas owns clearing the text drawing cache. /// The default state used when no scoped state is active. /// Whether this canvas owns final disposal of the shared batcher. + /// The deferred image-resource list, shared by a root canvas and its regions. private DrawingCanvas( Configuration configuration, IDrawingBackend backend, ICanvasFrame targetFrame, DrawingCanvasBatcher batcher, + DrawingTextCache textCache, + bool ownsTextCache, DrawingCanvasState defaultState, - bool ownsBatcher) + bool ownsBatcher, + List> pendingImageResources) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(backend, nameof(backend)); Guard.NotNull(targetFrame, nameof(targetFrame)); Guard.NotNull(batcher, nameof(batcher)); + Guard.NotNull(textCache, nameof(textCache)); Guard.NotNull(defaultState, nameof(defaultState)); if (!targetFrame.TryGetCpuRegion(out _) && !targetFrame.TryGetNativeSurface(out _)) @@ -154,11 +296,23 @@ private DrawingCanvas( this.backend = backend; this.targetFrame = targetFrame; this.batcher = batcher; + this.textCache = textCache; + this.textOperations = textCache.OperationScratch; + this.textCommandSortBuffer = textCache.CommandSortScratch; this.ownsBatcher = ownsBatcher; + this.ownsTextCache = ownsTextCache; + this.pendingImageResources = pendingImageResources; // Canvas coordinates are local to the current frame; origin stays at (0,0). this.Bounds = new Rectangle(0, 0, targetFrame.Bounds.Width, targetFrame.Bounds.Height); this.savedStates.Push(defaultState); + + if (ownsBatcher) + { + // The root canvas owns the command stream. Child regions inherit the already-open + // parent stack and only narrow TargetBounds, so they must not emit duplicate opens. + this.AppendBeginClips(defaultState.ClipState, defaultState.DestinationOffset); + } } /// @@ -173,25 +327,33 @@ public override int Save() this.EnsureNotDisposed(); DrawingCanvasState current = this.ResolveState(); - // Push a non-layer copy of the current state. + // Push a state that does not close a layer on restore. If the current state is already + // inside a layer, keep recording commands into that same layer. // Only states pushed by SaveLayer() should trigger layer compositing on restore. - this.savedStates.Push(new DrawingCanvasState(current.Options, current.ClipPaths, current.TargetBounds, current.DestinationOffset)); + this.savedStates.Push(new DrawingCanvasState(current.Options, current.ClipState, current.TargetBounds, current.DestinationOffset) + { + Layer = current.Layer + }); + return this.savedStates.Count; } /// - public override int Save(DrawingOptions options, params IPath[] clipPaths) - => this.SaveCore(options, clipPaths); - - private int SaveCore(DrawingOptions options, IReadOnlyList clipPaths) + public override int Save(DrawingOptions options) { this.EnsureNotDisposed(); Guard.NotNull(options, nameof(options)); - Guard.NotNull(clipPaths, nameof(clipPaths)); + // Reuse Save() to push the snapshot frame, then swap the top frame for one carrying + // the caller's options while keeping clip, bounds and layer linkage identical. _ = this.Save(); DrawingCanvasState current = this.ResolveState(); - DrawingCanvasState state = new(options, clipPaths, current.TargetBounds, current.DestinationOffset); + DrawingCanvasState state = new(options, current.ClipState, current.TargetBounds, current.DestinationOffset) + { + IsLayer = current.IsLayer, + Layer = current.Layer, + }; + _ = this.savedStates.Pop(); this.savedStates.Push(state); return this.savedStates.Count; @@ -206,16 +368,149 @@ public override int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds) Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds)); DrawingCanvasState currentState = this.ResolveState(); - Rectangle absoluteLayerBounds = ResolveLayerBounds(currentState, bounds); + return this.SaveLayerCore(layerOptions, bounds, currentState.Options, currentState.ClipState); + } + + /// + public override int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, DrawingOptions options) + { + this.EnsureNotDisposed(); + Guard.NotNull(layerOptions, nameof(layerOptions)); + Guard.NotNull(options, nameof(options)); + Guard.MustBeGreaterThan(bounds.Width, 0, nameof(bounds)); + Guard.MustBeGreaterThan(bounds.Height, 0, nameof(bounds)); + + DrawingCanvasState currentState = this.ResolveState(); + return this.SaveLayerCore(layerOptions, bounds, options, currentState.ClipState); + } + + /// + public override int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect) + => this.SaveEffectLayerCore(layerOptions, bounds, effect, options: null); + + /// + public override int SaveLayer(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect, DrawingOptions options) + { + Guard.NotNull(options, nameof(options)); + return this.SaveEffectLayerCore(layerOptions, bounds, effect, options); + } + + /// + public override int SaveLayer(GraphicsOptions layerOptions, IPath region, LayerEffect effect) + => this.SaveEffectLayerCore(layerOptions, region, effect, options: null); + + /// + public override int SaveLayer(GraphicsOptions layerOptions, IPath region, LayerEffect effect, DrawingOptions options) + { + Guard.NotNull(options, nameof(options)); + return this.SaveEffectLayerCore(layerOptions, region, effect, options); + } + + /// + /// Pushes an effect layer for rectangular content bounds: the effect region is the bounds + /// expanded by the effect's output reach, so blurred and offset output spills naturally around the + /// content instead of being cut at its edge. + /// + /// The compositing options used when the layer closes. + /// The content bounds in local canvas coordinates. + /// The effect applied to the layer content on restore. + /// Drawing options for the layer contents, or to inherit. + /// The save count after the layer state has been pushed. + private int SaveEffectLayerCore(GraphicsOptions layerOptions, Rectangle bounds, LayerEffect effect, DrawingOptions? options) + { + Guard.NotNull(effect, nameof(effect)); + + // Rectangular content bounds grow into the effect region here so blurred and offset + // output spills naturally around the content instead of being cut at its edge. + bounds.Inflate(effect.Reach, effect.Reach); + return this.SaveEffectLayerCore(layerOptions, new RectanglePolygon(bounds), effect, options); + } + + /// + /// Pushes a layer carrying a pending effect. The effect is recorded as an apply barrier over + /// the region when the layer is restored, so it transforms exactly the content drawn into the + /// layer before the layer composites. + /// + /// The compositing options used when the layer closes. + /// The path region the effect processes, in local coordinates. + /// The effect applied to the layer content on restore. + /// Drawing options for the layer contents, or to inherit. + /// The save count after the layer state has been pushed. + private int SaveEffectLayerCore(GraphicsOptions layerOptions, IPath region, LayerEffect effect, DrawingOptions? options) + { + this.EnsureNotDisposed(); + Guard.NotNull(layerOptions, nameof(layerOptions)); + Guard.NotNull(region, nameof(region)); + Guard.NotNull(effect, nameof(effect)); + + // The layer must contain the effect output, including pixels the write-back lands beyond + // the region when the effect carries an offset. + RectangleF regionBounds = region.Bounds; + int reach = Math.Max(effect.Reach, 1); + Rectangle layerBounds = Rectangle.FromLTRB( + (int)MathF.Floor(regionBounds.Left) - reach, + (int)MathF.Floor(regionBounds.Top) - reach, + (int)MathF.Ceiling(regionBounds.Right) + reach, + (int)MathF.Ceiling(regionBounds.Bottom) + reach); + + DrawingCanvasState currentState = this.ResolveState(); - // Keep layer boundaries in the shared command stream so the backend can lower them inline. - this.batcher.AddComposition(CompositionCommand.CreateBeginLayer(absoluteLayerBounds, layerOptions)); + // Backdrop effects filter the pixels already beneath the layer, clipped to the region, + // before the layer opens; the layer content then renders above the filtered backdrop and + // nothing remains pending for the restore. The apply is recorded under the supplied + // drawing options so the region honours the caller's transform. + if (effect is BackdropLayerEffect) + { + if (!effect.IsPassThrough) + { + bool pushed = options is not null; + if (pushed) + { + _ = this.Save(options!); + } + + this.ApplyCore(region, effect.CreateOperation(), effect, effect.WriteBackOptions, effect.WriteBackOffset); + if (pushed) + { + this.Restore(); + } + } + + return this.SaveLayerCore(layerOptions, layerBounds, options ?? currentState.Options, currentState.ClipState); + } + + int saveCount = this.SaveLayerCore(layerOptions, layerBounds, options ?? currentState.Options, currentState.ClipState); + this.ResolveState().LayerEffect = new DrawingCanvasLayerEffect(region, effect); + return saveCount; + } + + /// + /// Pushes a layer state with already-resolved drawing options and clip paths. + /// + /// Graphics options used when compositing the closed layer. + /// Layer bounds in local canvas coordinates. + /// Drawing options for commands recorded into the layer. + /// The normalized clip state to apply during backend rendering. + /// The save count after the layer state has been pushed. + private int SaveLayerCore( + GraphicsOptions layerOptions, + Rectangle bounds, + DrawingOptions options, + DrawingClipState clipState) + { + DrawingCanvasState currentState = this.ResolveState(); + Rectangle absoluteLayerBounds = ResolveLayerBounds(options.Transform, currentState.TargetBounds, currentState.DestinationOffset, bounds); + DrawingCanvasLayer layer = new(layerOptions); + + // Clips and layers are separate ordered controls. The active clip stack is already open + // in the command stream; the layer marker only pushes the isolated layer rectangle. + this.batcher.AddComposition(CompositionCommand.CreateBeginLayer(absoluteLayerBounds, layer)); // A bounded layer clips and allocates the isolated target, but it does not shift the canvas coordinate system. - DrawingCanvasState layerState = new(currentState.Options, currentState.ClipPaths, absoluteLayerBounds, currentState.DestinationOffset) + DrawingCanvasState layerState = new(options, clipState, absoluteLayerBounds, currentState.DestinationOffset) { IsLayer = true, - LayerOptions = layerOptions, + Layer = layer, }; this.savedStates.Push(layerState); @@ -231,10 +526,16 @@ public override void Restore() return; } + this.FlushLayerEffect(); DrawingCanvasState popped = this.savedStates.Pop(); + DrawingCanvasState current = this.ResolveState(); + this.AppendEndClips(popped.ClipState.Count - current.ClipState.Count); + if (popped.IsLayer) { - this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.LayerOptions!)); + // Layer closure happens after nested clips are closed so the encoded stream remains + // parent clips -> layer -> layer content -> nested clip ends -> layer end. + this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.Layer!)); } } @@ -262,12 +563,15 @@ public override DrawingCanvas CreateRegion(Rectangle region) this.backend, childFrame, this.batcher, - new DrawingCanvasState(currentState.Options, currentState.ClipPaths, childFrame.Bounds, childFrame.Bounds.Location) + this.textCache, + false, + new DrawingCanvasState(currentState.Options, currentState.ClipState, childFrame.Bounds, childFrame.Bounds.Location) { IsLayer = currentState.IsLayer, - LayerOptions = currentState.LayerOptions, + Layer = currentState.Layer, }, - false); + false, + this.pendingImageResources); } /// @@ -275,7 +579,7 @@ public override void Clear(Brush brush, IPath path) { DrawingCanvasState state = this.ResolveState(); DrawingOptions options = state.Options.CloneForClearOperation(); - this.ExecuteWithTemporaryState(options, state.ClipPaths, () => this.Fill(brush, path)); + this.ExecuteWithTemporaryState(options, state.ClipState, () => this.Fill(brush, path)); } /// @@ -287,6 +591,67 @@ public override void Fill(Brush brush, IPath path) this.EnqueueFillPath(brush, path); } + /// + public override void Clip(params IPath[] clipPaths) + => this.Clip(ClipOperation.Intersection, clipPaths); + + /// + public override void Clip(ClipOperation operation, params IPath[] clipPaths) + { + this.EnsureNotDisposed(); + if (clipPaths is null || clipPaths.Length == 0) + { + return; + } + + DrawingCanvasState state = this.savedStates.Pop(); + + // Clip paths are supplied in the active coordinate space. Build descriptors before + // applying the transform so rectangle and region metadata can survive translations + // and axis-aligned scales instead of being erased by IPath.Transform. + Matrix4x4 transform = state.Options.Transform; + + Rectangle targetBounds = state.TargetBounds; + + DrawingClipState incomingState = CreateClipState( + clipPaths, + transform, + operation, + state.Options.GraphicsOptions.Antialias ? DrawingClipEdgeMode.Antialiased : DrawingClipEdgeMode.Hard, + state.Options.GraphicsOptions.AntialiasThreshold); + + this.AppendBeginClips(incomingState, state.DestinationOffset); + + if (operation == ClipOperation.Intersection && + incomingState.TryGetConservativeBounds(state.DestinationOffset, out Rectangle incomingBounds)) + { + // TargetBounds is the work scheduler's coarse limit. The exact clip state is still + // retained below so fractional edge coverage and region membership are not lost. + targetBounds = Rectangle.Intersect(targetBounds, incomingBounds); + } + + if (!state.ClipState.HasClips) + { + this.savedStates.Push(new DrawingCanvasState(state.Options, incomingState, targetBounds, state.DestinationOffset) + { + IsLayer = state.IsLayer, + Layer = state.Layer, + }); + + return; + } + + // Clip operations are recorded as ordered state. Backends consume that state as + // clip primitives, so recording never rewrites future subject geometry. + DrawingClipState combinedClipState = state.ClipState.Append(incomingState); + + this.savedStates.Push(new DrawingCanvasState(state.Options, combinedClipState, targetBounds, state.DestinationOffset) + { + IsLayer = state.IsLayer, + Layer = state.Layer, + }); + } + /// public override void Apply(Rectangle region, Action operation) => this.Apply(new RectanglePolygon(region), operation); @@ -300,22 +665,63 @@ public override void Apply(PathBuilder pathBuilder, Action public override void Apply(IPath path, Action operation) + => this.ApplyCore(path, operation, effect: null, writeBackOptions: null, writeBackOffset: default); + + /// + public override void Apply(Rectangle region, Action operation, GraphicsOptions writeBackOptions, Point writeBackOffset) + => this.Apply(new RectanglePolygon(region), operation, writeBackOptions, writeBackOffset); + + /// + public override void Apply(IPath path, Action operation, GraphicsOptions writeBackOptions, Point writeBackOffset) + { + Guard.NotNull(writeBackOptions, nameof(writeBackOptions)); + this.ApplyCore(path, operation, effect: null, writeBackOptions, writeBackOffset); + } + + /// + /// Records an apply barrier for a path region with optional write-back compositing options. + /// + /// The path region to process. + /// The image-processing operation to apply to the region. + /// The layer effect represented by the operation, or for a direct Apply operation. + /// + /// The graphics options used to composite the processed pixels back, or + /// to replace the region outright. + /// + /// The offset at which the processed pixels are written back. + private void ApplyCore(IPath path, Action operation, LayerEffect? effect, GraphicsOptions? writeBackOptions, Point writeBackOffset) { this.EnsureNotDisposed(); Guard.NotNull(path, nameof(path)); Guard.NotNull(operation, nameof(operation)); DrawingCanvasState state = this.ResolveState(); + if (state.Layer is DrawingCanvasLayer layer) + { + layer.RequiresScopedApply = true; + } + + foreach (DrawingCanvasState savedState in this.savedStates) + { + if (savedState.Layer is DrawingCanvasLayer savedLayer) + { + savedLayer.RequiresScopedApply = true; + } + } + ApplyBarrier barrier = new( path.AsClosedPath(), state.Options, - state.ClipPaths, this.Bounds, state.TargetBounds, state.DestinationOffset, - state.IsLayer, - operation); + state.Layer, + operation, + effect, + writeBackOptions, + writeBackOffset); + this.batcher.EnsureClipAnchors(state.DestinationOffset); this.batcher.AddApplyBarrier(barrier); } @@ -333,22 +739,14 @@ public void DrawLine(Pen pen, PointF start, PointF end) DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; - // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. - if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) - { - ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); - shapeOptions.IntersectionRule = IntersectionRule.NonZero; - effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); - } - - if (state.ClipPaths.Count > 0 || !pen.StrokePattern.IsEmpty) + // The dedicated stroke-segment command is a fast path for solid, unclipped strokes only. + // Dash patterns and active clips route through the general stroked-path pipeline. + if (state.ClipState.HasClips || !pen.StrokePattern.IsEmpty) { this.PrepareCompositionCore( new Path([start, end]), pen.StrokeFill, effectiveOptions, - RasterizerSamplingOrigin.PixelCenter, - state.ClipPaths, pen); return; } @@ -373,22 +771,14 @@ public override void DrawLine(Pen pen, params PointF[] points) DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; - // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. - if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) - { - ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); - shapeOptions.IntersectionRule = IntersectionRule.NonZero; - effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); - } - - if (state.ClipPaths.Count > 0 || !pen.StrokePattern.IsEmpty) + // The dedicated polyline command is a fast path for solid, unclipped strokes only. + // Dash patterns and active clips route through the general stroked-path pipeline. + if (state.ClipState.HasClips || !pen.StrokePattern.IsEmpty) { this.PrepareCompositionCore( new Path(points), pen.StrokeFill, effectiveOptions, - RasterizerSamplingOrigin.PixelCenter, - state.ClipPaths, pen); return; } @@ -404,22 +794,11 @@ public override void Draw(Pen pen, IPath path) Guard.NotNull(path, nameof(path)); DrawingCanvasState state = this.ResolveState(); - DrawingOptions effectiveOptions = state.Options; - - // Stroke geometry can self-overlap; non-zero winding preserves stroke semantics. - if (effectiveOptions.ShapeOptions.IntersectionRule != IntersectionRule.NonZero) - { - ShapeOptions shapeOptions = effectiveOptions.ShapeOptions.DeepClone(); - shapeOptions.IntersectionRule = IntersectionRule.NonZero; - effectiveOptions = new DrawingOptions(effectiveOptions.GraphicsOptions, shapeOptions, effectiveOptions.Transform); - } this.PrepareCompositionCore( path, pen.StrokeFill, - effectiveOptions, - RasterizerSamplingOrigin.PixelCenter, - state.ClipPaths, + state.Options, pen); } @@ -443,6 +822,14 @@ public override void DrawText( this.DrawTextCore(textOptions, text, path, brush, pen); } + /// + /// Lays out and renders text, converting the produced glyph operations to queued commands. + /// + /// The text rendering options. + /// The text to draw. + /// Optional path used as the text baseline; for straight-line layout. + /// Optional brush used to fill glyphs. + /// Optional pen used to outline glyphs. private void DrawTextCore( RichTextOptions textOptions, ReadOnlySpan text, @@ -463,11 +850,24 @@ private void DrawTextCore( EnsureTextPaint(brush, pen); RichTextOptions configuredOptions = ConfigureTextOptions(textOptions, path, out IPath? configuredPath); - using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.glyphCache); + + // Path text bends the layout along the path so the straight visible band does not + // apply; caller-supplied bounds always win when present. + if (configuredPath is null && + configuredOptions.VisibleBounds is null && + TryGetVisibleTextBounds(state, effectiveOptions.Transform, out FontRectangle visibleBounds)) + { + configuredOptions = new RichTextOptions(configuredOptions) + { + VisibleBounds = visibleBounds + }; + } + + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, configuredPath, pen, brush, this.textCache, this.textOperations); TextRenderer renderer = new(glyphRenderer); - renderer.RenderText(text, configuredOptions); + renderer.Render(text, configuredOptions); - this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions); } /// @@ -490,13 +890,21 @@ public override void DrawText( // transform, instead of mutating text options or rebuilding the block. DrawingOptions placedOptions = new( effectiveOptions.GraphicsOptions, - effectiveOptions.ShapeOptions, - Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform); + effectiveOptions.IntersectionRule, + Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform, + effectiveOptions.TextContrast); - using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache); - textBlock.RenderTo(glyphRenderer, wrappingLength); + using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache, this.textOperations); + if (TryGetVisibleTextBounds(state, placedOptions.Transform, out FontRectangle visibleBounds)) + { + textBlock.RenderTo(glyphRenderer, wrappingLength, visibleBounds); + } + else + { + textBlock.RenderTo(glyphRenderer, wrappingLength); + } - this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths); + this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions); } /// @@ -515,10 +923,10 @@ public override void DrawText( DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; - using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache); + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache, this.textOperations); textBlock.RenderTo(glyphRenderer, wrappingLength); - this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions); } /// @@ -540,13 +948,14 @@ public override void DrawText( // without changing the prepared text object. DrawingOptions placedOptions = new( effectiveOptions.GraphicsOptions, - effectiveOptions.ShapeOptions, - Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform); + effectiveOptions.IntersectionRule, + Matrix4x4.CreateTranslation(location.X, location.Y, 0) * effectiveOptions.Transform, + effectiveOptions.TextContrast); - using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.glyphCache); + using RichTextGlyphRenderer glyphRenderer = new(placedOptions, path: null, pen, brush, this.textCache, this.textOperations); lineLayout.RenderTo(glyphRenderer); - this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions, state.ClipPaths); + this.DrawTextOperations(glyphRenderer.DrawingOperations, placedOptions); } /// @@ -564,10 +973,55 @@ public override void DrawText( DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; - using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.glyphCache); - lineLayout.RenderTo(glyphRenderer); + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path, pen, brush, this.textCache, this.textOperations); + lineLayout.RenderTo(glyphRenderer); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions); + } + + /// + public override void DrawText( + ushort glyphId, + RichGlyphOptions options, + Brush? brush, + Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache, this.textOperations); + TextRenderer renderer = new(glyphRenderer); + renderer.Render(glyphId, options); + + this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions); + } + + /// + public override void DrawText(ReadOnlySpan glyphIds, ReadOnlySpan points, RichGlyphOptions options, Brush? brush, Pen? pen) + { + this.EnsureNotDisposed(); + Guard.NotNull(options, nameof(options)); + Guard.IsTrue(glyphIds.Length == points.Length, nameof(points), "Glyph id and point counts must match."); + + if (glyphIds.IsEmpty) + { + return; + } + + EnsureTextPaint(brush, pen); + + DrawingCanvasState state = this.ResolveState(); + DrawingOptions effectiveOptions = state.Options; + + using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache, this.textOperations); + TextRenderer renderer = new(glyphRenderer); + renderer.Render(glyphIds, points, options); - this.DrawTextOperations(glyphRenderer.DrawingOperations, effectiveOptions, state.ClipPaths); + this.DrawTextOperations(this.BatchGlyphRunOperations(glyphRenderer.DrawingOperations), effectiveOptions); } /// @@ -583,7 +1037,7 @@ public override void DrawGlyphs( DrawingCanvasState state = this.ResolveState(); DrawingOptions baseOptions = state.Options; - IReadOnlyList clipPaths = state.ClipPaths; + DrawingClipState clipState = state.ClipState; foreach (GlyphPathCollection glyph in glyphs) { @@ -598,7 +1052,22 @@ public override void DrawGlyphs( continue; } - float glyphArea = glyph.Bounds.Width * glyph.Bounds.Height; + // The fill-versus-outline heuristic measures color layers against the glyph cell. + // Decoration lanes ride the same collection but extend beyond the glyph, so the + // cell is the union of the non-decoration layers only. + RectangleF glyphCell = default; + bool hasGlyphCell = false; + for (int layerIndex = 0; layerIndex < glyph.LayerCount; layerIndex++) + { + GlyphLayerInfo layer = glyph.Layers[layerIndex]; + if (layer.Kind != GlyphLayerKind.Decoration) + { + glyphCell = hasGlyphCell ? RectangleF.Union(glyphCell, layer.Bounds) : layer.Bounds; + hasGlyphCell = true; + } + } + + float glyphArea = glyphCell.Width * glyphCell.Height; for (int layerIndex = 0; layerIndex < glyph.LayerCount; layerIndex++) { GlyphLayerInfo layer = glyph.Layers[layerIndex]; @@ -620,11 +1089,14 @@ public override void DrawGlyphs( } else { + // Color layers covering half or more of the glyph cell are treated as the + // dominant painted layer and outlined with the pen; only smaller detail + // layers are filled with the brush. float layerArea = layerPaths.ComputeArea(); shouldFill = layerArea > 0F && glyphArea > 0F && (layerArea / glyphArea) < 0.50F; } - this.ExecuteWithTemporaryState(layerOptions, clipPaths, () => + this.ExecuteWithTemporaryState(layerOptions, clipState, () => { if (shouldFill) { @@ -652,13 +1124,23 @@ public override void DrawImage( Rectangle sourceRect, RectangleF destinationRect, IResampler? sampler) + => this.DrawImage(image, sourceRect, destinationRect, WrapMode.Repeat, WrapMode.Repeat, sampler); + + /// + public override void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + WrapMode wrapX, + WrapMode wrapY, + IResampler? sampler) { this.EnsureNotDisposed(); Guard.NotNull(image, nameof(image)); if (image is Image specificImage) { - this.DrawImageCore(specificImage, sourceRect, destinationRect, sampler, ownsSourceImage: false); + this.DrawImage(specificImage, sourceRect, destinationRect, wrapX, wrapY, sampler); return; } @@ -673,13 +1155,13 @@ public override void DrawImage( if (clippedSourceRect == image.Bounds) { Image convertedImage = image.CloneAs(); - this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, ownsSourceImage: true); + this.DrawImageCore(convertedImage, clippedSourceRect, clippedDestinationRect, sampler, wrapX, wrapY, true); return; } using Image croppedSource = image.Clone(ctx => ctx.Crop(clippedSourceRect)); Image convertedRegion = croppedSource.CloneAs(); - this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, ownsSourceImage: true); + this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, wrapX, wrapY, true); } /// @@ -688,16 +1170,33 @@ public void DrawImage( Rectangle sourceRect, RectangleF destinationRect, IResampler? sampler = null) + => this.DrawImage(image, sourceRect, destinationRect, WrapMode.Repeat, WrapMode.Repeat, sampler); + + /// + public void DrawImage( + Image image, + Rectangle sourceRect, + RectangleF destinationRect, + WrapMode wrapX, + WrapMode wrapY, + IResampler? sampler = null) { this.EnsureNotDisposed(); Guard.NotNull(image, nameof(image)); - this.DrawImageCore(image, sourceRect, destinationRect, sampler, ownsSourceImage: false); + + if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect)) + { + return; + } + + this.DrawImageCore(image, clippedSourceRect, clippedDestinationRect, sampler, wrapX, wrapY, false); } /// public override DrawingBackendScene CreateScene() { this.EnsureNotDisposed(); + this.CloseClipsAndSealActiveCommandRange(); IDisposable[]? ownedResources = this.DetachPendingImageResources(); @@ -713,6 +1212,11 @@ public override DrawingBackendScene CreateScene() finally { this.batcher.ClearCommandBatch(); + + // The sealed range consumed the balanced clip stream; reopen the active clip + // stack so later commands keep recording under the same clip state. + DrawingCanvasState state = this.ResolveState(); + this.AppendBeginClips(state.ClipState, state.DestinationOffset); } } @@ -721,14 +1225,70 @@ public override void RenderScene(DrawingBackendScene scene) { this.EnsureNotDisposed(); Guard.NotNull(scene, nameof(scene)); + + this.CloseClipsAndSealActiveCommandRange(); this.batcher.AddScene(scene); + + DrawingCanvasState state = this.ResolveState(); + this.AppendBeginClips(state.ClipState, state.DestinationOffset); + } + + /// + public override void CopyPixelsFrom(DrawingCanvas source, Rectangle sourceRectangle, Point targetPoint) + { + this.EnsureNotDisposed(); + Guard.NotNull(source, nameof(source)); + + if (source is not DrawingCanvas typedSource) + { + throw new ArgumentException("The source canvas pixel type must match the target canvas pixel type.", nameof(source)); + } + + typedSource.EnsureNotDisposed(); + + // Pixel copy transfers already-rasterized frame contents. Materialize both timelines at + // this ordering boundary so the backend copies pixels instead of replaying commands. + typedSource.CloseClipsAndSealActiveCommandRange(); + typedSource.RenderRecordedTimeline(); + + DrawingCanvasState sourceState = typedSource.ResolveState(); + typedSource.AppendBeginClips(sourceState.ClipState, sourceState.DestinationOffset); + + this.CloseClipsAndSealActiveCommandRange(); + this.RenderRecordedTimeline(); + + DrawingCanvasState targetState = this.ResolveState(); + this.AppendBeginClips(targetState.ClipState, targetState.DestinationOffset); + + this.backend.CopyPixels( + this.configuration, + typedSource.targetFrame, + this.targetFrame, + sourceRectangle, + targetPoint); } + /// + /// Normalizes a pre-clipped image draw into an image-brush fill: source pixels are cropped/scaled, + /// then baked through the canvas transform, and the result is queued as a rectangle fill. + /// + /// The source image in the target pixel format. + /// The source rectangle, already clipped to the bounds of . + /// The destination rectangle matching , in local canvas coordinates. + /// Optional resampler used when scaling or transforming the image. + /// The horizontal wrap mode applied when sampling beyond the destination rectangle. + /// The vertical wrap mode applied when sampling beyond the destination rectangle. + /// + /// Whether this method owns and must dispose it or transfer its + /// lifetime to the deferred command batch. + /// private void DrawImageCore( Image image, - Rectangle sourceRect, - RectangleF destinationRect, + Rectangle clippedSourceRect, + RectangleF clippedDestinationRect, IResampler? sampler, + WrapMode wrapX, + WrapMode wrapY, bool ownsSourceImage) { bool disposeSourceImage = ownsSourceImage; @@ -736,16 +1296,10 @@ private void DrawImageCore( DrawingCanvasState state = this.ResolveState(); DrawingOptions effectiveOptions = state.Options; DrawingOptions commandOptions = effectiveOptions; - IReadOnlyList commandClipPaths = state.ClipPaths; Image? ownedImage = null; try { - if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect)) - { - return; - } - Size scaledSize = new( Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)), Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height))); @@ -791,9 +1345,9 @@ private void DrawImageCore( // so the queued fill must not apply the canvas transform a second time. commandOptions = new DrawingOptions( effectiveOptions.GraphicsOptions, - effectiveOptions.ShapeOptions, - Matrix4x4.Identity); - commandClipPaths = TransformClipPaths(state.ClipPaths, effectiveOptions.Transform); + effectiveOptions.IntersectionRule, + Matrix4x4.Identity, + effectiveOptions.TextContrast); } if (renderDestinationRect.Width <= 0 || renderDestinationRect.Height <= 0) @@ -819,7 +1373,7 @@ private void DrawImageCore( disposeSourceImage = false; } - ImageBrush brush = new(brushImage, brushImageRegion); + ImageBrush brush = new(brushImage, brushImageRegion, wrapX, wrapY); IPath destinationPath = new RectanglePolygon( renderDestinationRect.X, renderDestinationRect.Y, @@ -829,9 +1383,7 @@ private void DrawImageCore( this.PrepareCompositionCore( destinationPath, brush, - commandOptions, - RasterizerSamplingOrigin.PixelBoundary, - commandClipPaths); + commandOptions); } finally { @@ -849,44 +1401,51 @@ private void DrawImageCore( /// Path to fill. /// Brush used for shading. /// Effective drawing options. - /// Rasterizer sampling origin. - /// Optional clip paths to apply during preparation. /// Optional pen for stroke commands. private void PrepareCompositionCore( IPath path, Brush brush, DrawingOptions options, - RasterizerSamplingOrigin samplingOrigin, - IReadOnlyList? clipPaths = null, Pen? pen = null) { brush = this.NormalizeBrush(brush); GraphicsOptions graphicsOptions = options.GraphicsOptions; - ShapeOptions shapeOptions = options.ShapeOptions; RasterizationMode rasterizationMode = graphicsOptions.Antialias ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - RectangleF bounds = path.Bounds; - if (samplingOrigin == RasterizerSamplingOrigin.PixelCenter) + Matrix4x4 transform = options.Transform; + RectangleF bounds = transform == Matrix4x4.Identity ? path.Bounds : RectangleF.Transform(path.Bounds, transform); + if (pen is not null) { - bounds = new RectangleF(bounds.X + 0.5F, bounds.Y + 0.5F, bounds.Width, bounds.Height); - } + float halfWidth = pen.StrokeWidth * GetTransformWidthScale(transform) * 0.5F; + float joinInflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; - Rectangle interest = Rectangle.FromLTRB( - (int)MathF.Floor(bounds.Left), - (int)MathF.Floor(bounds.Top), - (int)MathF.Ceiling(bounds.Right), - (int)MathF.Ceiling(bounds.Bottom)); + float capInflate = pen.StrokeOptions.LineCap == LineCap.Square + ? halfWidth * MathF.Sqrt(2F) + : halfWidth; + float inflate = MathF.Max(joinInflate, capInflate); + + bounds.Inflate(new SizeF(inflate, inflate)); + } + + Rectangle interest = ToRasterizerInterest(bounds); RasterizerOptions rasterizerOptions = new( interest, - shapeOptions.IntersectionRule, + options.IntersectionRule, rasterizationMode, - samplingOrigin, graphicsOptions.AntialiasThreshold); DrawingCanvasState state = this.ResolveState(); + // The ordered begin/end-clip stream is the clip source of truth; anchor it at this + // state's destination offset before recording the draw. + this.batcher.EnsureClipAnchors(state.DestinationOffset); + // Commands carry their absolute target bounds and destination origin explicitly. // Bounded layers can clip the target while preserving the active canvas coordinate origin. if (pen is null) @@ -899,8 +1458,7 @@ private void PrepareCompositionCore( in rasterizerOptions, state.TargetBounds, state.DestinationOffset, - clipPaths, - state.IsLayer)); + state.Layer)); return; } @@ -913,13 +1471,18 @@ private void PrepareCompositionCore( state.TargetBounds, state.DestinationOffset, pen, - clipPaths, - state.IsLayer)); + state.Layer is not null, + state.Layer)); } /// /// Enqueues one explicit two-point stroke line-segment command using the current canvas state. /// + /// Line start point in local coordinates. + /// Line end point in local coordinates. + /// Brush used for shading the stroke. + /// Effective drawing options. + /// Pen defining the stroke geometry. private void PrepareStrokeLineSegmentCompositionCore( PointF start, PointF end, @@ -940,12 +1503,12 @@ private void PrepareStrokeLineSegmentCompositionCore( RasterizerOptions rasterizerOptions = new( interest, - options.ShapeOptions.IntersectionRule, + options.IntersectionRule, rasterizationMode, - RasterizerSamplingOrigin.PixelCenter, graphicsOptions.AntialiasThreshold); DrawingCanvasState state = this.ResolveState(); + this.batcher.EnsureClipAnchors(state.DestinationOffset); this.batcher.AddStrokeLineSegment( new StrokeLineSegmentCommand( start, @@ -956,12 +1519,17 @@ private void PrepareStrokeLineSegmentCompositionCore( state.TargetBounds, state.DestinationOffset, pen, - state.IsLayer)); + state.Layer is not null, + state.Layer)); } /// /// Enqueues one explicit stroked open polyline command using the current canvas state. /// + /// Polyline points in local coordinates. + /// Brush used for shading the stroke. + /// Effective drawing options. + /// Pen defining the stroke geometry. private void PrepareStrokePolylineCompositionCore( PointF[] points, Brush brush, @@ -981,12 +1549,12 @@ private void PrepareStrokePolylineCompositionCore( RasterizerOptions rasterizerOptions = new( interest, - options.ShapeOptions.IntersectionRule, + options.IntersectionRule, rasterizationMode, - RasterizerSamplingOrigin.PixelCenter, graphicsOptions.AntialiasThreshold); DrawingCanvasState state = this.ResolveState(); + this.batcher.EnsureClipAnchors(state.DestinationOffset); this.batcher.AddStrokePolyline( new StrokePolylineCommand( points, @@ -996,7 +1564,8 @@ private void PrepareStrokePolylineCompositionCore( state.TargetBounds, state.DestinationOffset, pen, - state.IsLayer)); + state.Layer is not null, + state.Layer)); } /// @@ -1019,7 +1588,7 @@ private Brush NormalizeBrush(Brush brush) // Normalize the source image once so deferred composition does not repeat per-pixel conversions. Image convertedImage = imageBrush.UntypedImage.CloneAs(); this.pendingImageResources.Add(convertedImage); - return new ImageBrush(convertedImage, imageBrush.SourceRegion, imageBrush.Offset); + return new ImageBrush(convertedImage, imageBrush.SourceRegion, imageBrush.Offset, imageBrush.WrapX, imageBrush.WrapY); } /// @@ -1035,9 +1604,185 @@ private void EnqueueFillPath(Brush brush, IPath path) this.PrepareCompositionCore( closed, brush, - state.Options, - RasterizerSamplingOrigin.PixelBoundary, - state.ClipPaths); + state.Options); + } + + /// + /// Combines a uniform glyph run into one fill operation and one draw operation. + /// + /// + /// The text renderer emits one operation per glyph. Merging the glyphs of a uniform run (same + /// brush, pen, blend and render pass) into a single fill path and a single stroke path replaces + /// N composition commands with one or two, which dominates when the same run is redrawn every + /// frame. The merged path is built and cached in run-local space (relative to the first glyph, + /// whose snapped location and sub-pixel fraction ride the combined operation), so a repeat at any + /// position reuses it without a re-merge or a per-position geometry copy. The run is left as its + /// per-glyph operations when it is not uniform (for example colour layers or per-glyph paint), + /// because those depend on the exact per-operation order and paint. The alternative of always + /// emitting the per-glyph operations would cut the merged-path memory and share glyphs across + /// runs, at the cost of N commands per run; that trade favours this batching for the redraw case. + /// + /// Glyph operations produced by the text renderer. + /// The original operations when they cannot be combined; otherwise the combined operations. + private List BatchGlyphRunOperations(List operations) + { + if (operations.Count < 2) + { + return operations; + } + + DrawingTextCache.RunPathCacheEntry[]? fillEntries = null; + DrawingTextCache.RunPathCacheEntry[]? drawEntries = null; + DrawingOperation fillOperation = default; + DrawingOperation drawOperation = default; + int fillCount = 0; + int drawCount = 0; + + for (int i = 0; i < operations.Count; i++) + { + DrawingOperation operation = operations[i]; + switch (operation.Kind) + { + case DrawingOperationKind.Fill: + if (fillEntries is null) + { + fillOperation = operation; + fillEntries = new DrawingTextCache.RunPathCacheEntry[operations.Count]; + } + else if (!CanBatchGlyphRunOperation(fillOperation, operation)) + { + // Color layers and per-glyph paint can depend on the original operation order. + // If any render semantics differ, keep the renderer's exact operation stream. + return operations; + } + + // Entries are keyed relative to the run origin (the first operation's exact + // location including its fraction) so the cached combined path is position + // independent: the same run content drawn at a different location, even a + // fractionally scrolled one, reuses the cached geometry. The origin's snapped + // location and fraction stay on the combined operation. + Vector2 fillRelativeLocation = new Vector2( + operation.RenderLocation.X - fillOperation.RenderLocation.X, + operation.RenderLocation.Y - fillOperation.RenderLocation.Y) + + operation.SubPixelOffset - fillOperation.SubPixelOffset; + fillEntries[fillCount++] = new DrawingTextCache.RunPathCacheEntry( + operation.Path, + fillRelativeLocation, + operation.GlyphKey, + operation.HasGlyphKey); + break; + + case DrawingOperationKind.Draw: + if (drawEntries is null) + { + drawOperation = operation; + drawEntries = new DrawingTextCache.RunPathCacheEntry[operations.Count]; + } + else if (!CanBatchGlyphRunOperation(drawOperation, operation)) + { + // Color layers and per-glyph paint can depend on the original operation order. + // If any render semantics differ, keep the renderer's exact operation stream. + return operations; + } + + Vector2 drawRelativeLocation = new Vector2( + operation.RenderLocation.X - drawOperation.RenderLocation.X, + operation.RenderLocation.Y - drawOperation.RenderLocation.Y) + + operation.SubPixelOffset - drawOperation.SubPixelOffset; + drawEntries[drawCount++] = new DrawingTextCache.RunPathCacheEntry( + operation.Path, + drawRelativeLocation, + operation.GlyphKey, + operation.HasGlyphKey); + break; + } + } + + int capacity = (fillEntries is null ? 0 : 1) + (drawEntries is null ? 0 : 1); + List batched = new(capacity); + + // The combined path is built in run-local space; the operations keep their original + // (first-glyph) RenderLocation and SubPixelOffset so command creation positions the + // shared geometry via the destination offset plus the sub-pixel transform, without a + // per-position copy of the run geometry. + if (fillEntries is not null) + { + fillOperation.Path = this.GetPositionedGlyphRunPath(fillEntries, fillCount); + batched.Add(fillOperation); + } + + if (drawEntries is not null) + { + drawOperation.Path = this.GetPositionedGlyphRunPath(drawEntries, drawCount); + batched.Add(drawOperation); + } + + return batched; + } + + /// + /// Returns whether two glyph operations can share one composition command. + /// + /// The first operation. + /// The second operation. + /// when the operations have identical drawing semantics. + private static bool CanBatchGlyphRunOperation(DrawingOperation left, DrawingOperation right) + => left.Kind == right.Kind + && left.IntersectionRule == right.IntersectionRule + && left.RenderPass == right.RenderPass + && ReferenceEquals(left.Brush, right.Brush) + && ReferenceEquals(left.Pen, right.Pen) + && left.PixelAlphaCompositionMode == right.PixelAlphaCompositionMode + && left.PixelColorBlendingMode == right.PixelColorBlendingMode; + + /// + /// Gets a stable combined path for a uniform glyph-run operation group. Entries carry + /// run-relative locations, so the returned path is in run-local space and one cache + /// entry serves the same run content at every draw position. + /// + /// The run-relative glyph path entries. + /// The number of entries to include in the path. + /// The combined glyph-run path in run-local space. + private IPath GetPositionedGlyphRunPath(DrawingTextCache.RunPathCacheEntry[] entries, int count) + { + DrawingTextCache.RunPathCacheKey key = new(entries, count); + if (this.textCache.TryGetRunPath(key, out IPath? path)) + { + return path; + } + + IPath positionedPath; + if (count == 1) + { + positionedPath = GetPositionedGlyphPath(entries[0]); + } + else + { + List paths = new(count); + for (int i = 0; i < count; i++) + { + paths.Add(GetPositionedGlyphPath(entries[i])); + } + + positionedPath = new ComplexPolygon(paths); + } + + this.textCache.AddRunPath(key, positionedPath); + return positionedPath; + } + + /// + /// Gets a glyph path translated to the entry's exact run-relative location, including the + /// fractional component so relative sub-pixel spacing inside the run is preserved. + /// + /// The run-relative glyph path entry. + /// The translated path. + private static IPath GetPositionedGlyphPath(DrawingTextCache.RunPathCacheEntry entry) + { + Vector2 relativeLocation = entry.RelativeLocation; + return relativeLocation == Vector2.Zero + ? entry.Path + : entry.Path.Translate(relativeLocation.X, relativeLocation.Y); } /// @@ -1045,20 +1790,28 @@ private void EnqueueFillPath(Brush brush, IPath path) /// /// Text drawing operations produced by glyph layout/rendering. /// Drawing options applied to each operation. - /// Clip paths resolved from effective canvas state. - private void DrawTextOperations( - List operations, - DrawingOptions drawingOptions, - IReadOnlyList clipPaths) + private void DrawTextOperations(List operations, DrawingOptions drawingOptions) { // Build composition commands and enforce render-pass ordering while preserving // original emission order inside each pass. This preserves overlapping color-font // layer compositing semantics (for example emoji mouth/teeth layers). - List<(byte RenderPass, int Sequence, CompositionSceneCommand Command)> entries = new(operations.Count); + // The cache-owned buffer keeps its capacity across draws; draw calls never overlap on + // one canvas, and the batcher retains the commands, not this buffer. + List<(byte RenderPass, int Sequence, CompositionSceneCommand Command)> entries = this.textCommandSortBuffer; + entries.Clear(); + + // Queued glyph commands never carry the canvas transform: glyph geometry arrives with + // it already applied, and the sub-pixel remainder rides the command itself. One shared + // identity-transform options instance therefore serves every operation of this draw; + // per-operation options exist only for glyphs whose blend or composition modes differ. + DrawingOptions sharedTextOptions = drawingOptions.Transform.IsIdentity && drawingOptions.IntersectionRule == IntersectionRule.NonZero + ? drawingOptions + : new DrawingOptions(drawingOptions.GraphicsOptions, IntersectionRule.NonZero, Matrix4x4.Identity, drawingOptions.TextContrast); + for (int i = 0; i < operations.Count; i++) { DrawingOperation operation = operations[i]; - entries.Add((operation.RenderPass, i, this.CreateTextCompositionCommand(operation, drawingOptions, clipPaths))); + entries.Add((operation.RenderPass, i, this.CreateTextCompositionCommand(operation, drawingOptions, sharedTextOptions))); } entries.Sort(static (a, b) => @@ -1078,6 +1831,10 @@ private void DrawTextOperations( this.batcher.AddStrokePath(((StrokePathCompositionSceneCommand)entries[i].Command).Command); } } + + // The buffer outlives the canvas (the text cache hosts it), so release the command + // references now rather than rooting the final draw's geometry until the next draw. + entries.Clear(); } /// @@ -1103,12 +1860,23 @@ private static void EnsureTextPaint(Brush? brush, Pen? pen) /// Executes an action with a temporary scoped state, restoring the previous scoped state afterwards. /// /// Temporary drawing options. - /// Temporary clip paths. + /// The normalized clip state used by the temporary state. /// Action to execute. - private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList clipPaths, Action action) + private void ExecuteWithTemporaryState( + DrawingOptions options, + DrawingClipState clipState, + Action action) { + this.EnsureNotDisposed(); + int saveCount = this.savedStates.Count; - _ = this.SaveCore(options, clipPaths); + DrawingCanvasState current = this.ResolveState(); + this.savedStates.Push(new DrawingCanvasState(options, clipState, current.TargetBounds, current.DestinationOffset) + { + IsLayer = current.IsLayer, + Layer = current.Layer, + }); + try { action(); @@ -1123,7 +1891,9 @@ private void ExecuteWithTemporaryState(DrawingOptions options, IReadOnlyList @@ -1139,8 +1909,10 @@ public override void Dispose() // Dispose should finalize the same drawing state transitions as RestoreTo(1), // otherwise active layers can composite with different options than an explicit restore. this.RestoreToCore(1); + if (this.ownsBatcher) { + this.AppendEndClips(this.ResolveState().ClipState.Count); this.RenderRecordedTimeline(); } } @@ -1151,8 +1923,10 @@ public override void Dispose() this.DisposePendingImageResources(); } - // Release the per-canvas glyph-outline cache. - this.glyphCache.Clear(); + if (this.ownsTextCache) + { + this.textCache.Clear(); + } this.isDisposed = true; } @@ -1165,7 +1939,7 @@ private void EnsureNotDisposed() => ObjectDisposedException.ThrowIf(this.isDisposed, this); /// - /// Renders the recorded timeline owned by the root canvas during disposal. + /// Renders the recorded timeline owned by the root canvas. /// /// /// Command-range entries are lowered to short-lived backend scenes here. Scene entries @@ -1190,10 +1964,6 @@ private void RenderRecordedTimeline() this.RenderCommandBatch(this.batcher.CreateCommandBatch(entry)); break; - case DrawingCanvasTimelineEntryKind.ApplyBarrier: - this.RenderApplyBarrier(this.batcher.GetApplyBarrier(entry.Index)); - break; - case DrawingCanvasTimelineEntryKind.Scene: this.backend.RenderScene( this.configuration, @@ -1225,48 +1995,90 @@ private void RenderCommandBatch(DrawingCommandBatch commandBatch) } /// - /// Executes one apply barrier at its replay position. + /// Restores the saved-state stack to without public guard checks. + /// Layer states are unwound through the normal compositing path so restore and disposal + /// preserve identical layer semantics. /// - /// The apply barrier to execute. - private void RenderApplyBarrier(ApplyBarrier barrier) + /// The target stack depth to restore to. + private void RestoreToCore(int saveCount) { - DrawingCommandBatch? maybeCommandBatch = barrier.CreateWriteBackBatch( - this.configuration, - this.backend, - this.targetFrame, - out IDisposable? ownedResource); - - if (maybeCommandBatch is not DrawingCommandBatch commandBatch) + while (this.savedStates.Count > saveCount) { - return; + this.FlushLayerEffect(); + DrawingCanvasState popped = this.savedStates.Pop(); + DrawingCanvasState current = this.ResolveState(); + this.AppendEndClips(popped.ClipState.Count - current.ClipState.Count); + + if (popped.IsLayer) + { + // Restore and Dispose unwind layers through the same command stream path. + this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.Layer!)); + } } + } - try + /// + /// Records the current state's pending layer effect, if any, as an apply barrier while the + /// layer is still receiving commands, so the effect transforms the layer's content just before + /// the layer is composited. + /// + private void FlushLayerEffect() + { + DrawingCanvasState current = this.ResolveState(); + if (current.LayerEffect is DrawingCanvasLayerEffect pending) { - this.RenderCommandBatch(commandBatch); + current.LayerEffect = null; + if (pending.Effect.IsPassThrough) + { + return; + } + + this.ApplyCore( + pending.Region, + pending.Effect.CreateOperation(), + pending.Effect, + pending.Effect.WriteBackOptions, + pending.Effect.WriteBackOffset); } - finally + } + + /// + /// Seals the current command range with the active clip stack balanced inside that range. + /// + private void CloseClipsAndSealActiveCommandRange() + { + DrawingCanvasState state = this.ResolveState(); + + // Backend scenes are created per sealed command range. A Vello-style clip stream cannot + // span those scene boundaries, so the canvas closes the active suffix before sealing and + // the caller reopens it for later commands. + this.AppendEndClips(state.ClipState.Count); + this.batcher.SealCommands(); + } + + /// + /// Appends begin-clip commands for the supplied clip state. + /// + /// The clip state to open. + /// The destination offset associated with the clip state. + private void AppendBeginClips(DrawingClipState clipState, Point destinationOffset) + { + for (int i = 0; i < clipState.Count; i++) { - ownedResource?.Dispose(); + DrawingClipDescriptor descriptor = clipState.GetDescriptor(i); + this.batcher.AddComposition(CompositionCommand.CreateBeginClip(descriptor, destinationOffset)); } } /// - /// Restores the saved-state stack to without public guard checks. - /// Layer states are unwound through the normal compositing path so restore and disposal - /// preserve identical layer semantics. + /// Appends end-clip commands for a previously opened clip-state suffix. /// - /// The target stack depth to restore to. - private void RestoreToCore(int saveCount) + /// The number of clip scopes to close. + private void AppendEndClips(int count) { - while (this.savedStates.Count > saveCount) + for (int i = 0; i < count; i++) { - DrawingCanvasState popped = this.savedStates.Pop(); - if (popped.IsLayer) - { - // Restore and Dispose unwind layers through the same command stream path. - this.batcher.AddComposition(CompositionCommand.CreateEndLayer(popped.TargetBounds, popped.LayerOptions!)); - } + this.batcher.AddComposition(CompositionCommand.CreateEndClip()); } } @@ -1295,17 +2107,52 @@ private static RichTextOptions ConfigureTextOptions(RichTextOptions options, IPa return options; } + /// + /// Computes the visible text-space bounds for the current canvas state when the effective + /// transform is a pure translation. The state's target bounds already fold the target size + /// and every conservative clip bound together, so translating them into text space yields + /// the band the layout engine culls against. + /// + /// The resolved canvas state. + /// The effective drawing transform for the text. + /// The visible bounds in text space. + /// + /// when the transform is a pure translation and the bounds are + /// usable; otherwise . Culling is a fast path only, so rotation, + /// scale, or skew renders unculled rather than risking incorrect rejection. + /// + private static bool TryGetVisibleTextBounds(DrawingCanvasState state, in Matrix4x4 transform, out FontRectangle visibleBounds) + { + if (!MatrixUtilities.IsTranslationOnly(transform)) + { + visibleBounds = default; + return false; + } + + // Target bounds are absolute while glyph geometry is recorded in local canvas + // coordinates and shifted by the destination offset at command creation, so the + // text-space band is the target rectangle pulled back through both translations. + Rectangle target = state.TargetBounds; + visibleBounds = new FontRectangle( + target.X - state.DestinationOffset.X - transform.M41, + target.Y - state.DestinationOffset.Y - transform.M42, + target.Width, + target.Height); + + return true; + } + /// /// Builds a normalized composition command for a text drawing operation. /// /// The source drawing operation. /// Drawing options applied to the operation. - /// Optional clip paths to apply during preparation. + /// + /// The identity-transform options shared by every operation of the draw whose graphics + /// options match the canvas options. + /// /// A composition scene command ready for batching. - private CompositionSceneCommand CreateTextCompositionCommand( - DrawingOperation operation, - DrawingOptions drawingOptions, - IReadOnlyList? clipPaths = null) + private CompositionSceneCommand CreateTextCompositionCommand(DrawingOperation operation, DrawingOptions drawingOptions, DrawingOptions sharedTextOptions) { Brush compositeBrush = operation.Kind == DrawingOperationKind.Fill ? operation.Brush! @@ -1320,52 +2167,71 @@ private CompositionSceneCommand CreateTextCompositionCommand( ? RasterizationMode.Antialiased : RasterizationMode.Aliased; - ShapeOptions shapeOptions = drawingOptions.ShapeOptions; + // Glyph outlines (fills and strokes) are always non-zero winding; even-odd punches holes where a + // glyph's contours overlap. Force non-zero on both rule carriers - the rasterizer options and the + // drawing options - so neither the per-operation rule nor the canvas's even-odd default applies. + const IntersectionRule intersectionRule = IntersectionRule.NonZero; DrawingCanvasState state = this.ResolveState(); Point destinationOffset = new( state.DestinationOffset.X + operation.RenderLocation.X, state.DestinationOffset.Y + operation.RenderLocation.Y); + // The whole-pixel part of the glyph position rides the integer destination offset; + // the fractional remainder rides the command's dedicated sub-pixel field. The + // composed transform keeps unit scale, so the backends reuse the scale-keyed + // flattened geometry and apply the fraction as a residual, rendering the glyph at + // its exact sub-pixel position with one cached path per glyph. + Vector2 subPixelOffset = operation.SubPixelOffset; + Pen? pen = operation.Kind == DrawingOperationKind.Draw ? operation.Pen : null; - IntersectionRule intersectionRule = pen is not null && operation.IntersectionRule != IntersectionRule.NonZero - ? IntersectionRule.NonZero - : operation.IntersectionRule; + RectangleF bounds = operation.Path.Bounds; + bounds.Offset(subPixelOffset.X, subPixelOffset.Y); + if (pen is not null) + { + float halfWidth = pen.StrokeWidth * 0.5F; + float joinInflate = pen.StrokeOptions.LineJoin switch + { + LineJoin.Miter or LineJoin.MiterRevert or LineJoin.MiterRound => (float)(halfWidth * Math.Max(pen.StrokeOptions.MiterLimit, 1D)), + _ => halfWidth + }; + + float capInflate = pen.StrokeOptions.LineCap == LineCap.Square + ? halfWidth * MathF.Sqrt(2F) + : halfWidth; + + float inflate = MathF.Max(joinInflate, capInflate); + + bounds.Inflate(new SizeF(inflate, inflate)); + } + + Rectangle interest = ToRasterizerInterest(bounds); - RasterizerSamplingOrigin samplingOrigin = pen is not null - ? RasterizerSamplingOrigin.PixelCenter - : RasterizerSamplingOrigin.PixelBoundary; + // Text opts into the perceptual coverage boost here; generic vector fills and strokes + // never carry one. The boost only applies to antialiased coverage. + float coverageBoost = rasterizationMode == RasterizationMode.Antialiased + ? Math.Clamp(drawingOptions.TextContrast, 0F, 1F) + : 0F; RasterizerOptions rasterizerOptions = new( - default, + interest, intersectionRule, rasterizationMode, - samplingOrigin, - graphicsOptions.AntialiasThreshold); + graphicsOptions.AntialiasThreshold, + coverageBoost); - // Glyph paths arrive pre-laid-out, so the queued command must report identity transform - // and the GraphicsOptions clone produced above. Reuse the caller's instance only when both already match. + // Glyph paths arrive pre-laid-out, so the queued command carries identity-transform + // options and reports the fraction through its sub-pixel field. The shared instance + // serves every operation whose graphics options match the canvas; a fresh instance + // exists only for the rare operation whose blend or composition modes forced a clone. DrawingOptions effectiveOptions = ReferenceEquals(graphicsOptions, drawingOptions.GraphicsOptions) - && drawingOptions.Transform == Matrix4x4.Identity - ? drawingOptions - : new DrawingOptions(graphicsOptions, shapeOptions, Matrix4x4.Identity); - - IReadOnlyList? operationClipPaths = clipPaths; - if (clipPaths != null && clipPaths.Count > 0 && (operation.RenderLocation.X != 0 || operation.RenderLocation.Y != 0)) - { - IPath[] translatedClipPaths = new IPath[clipPaths.Count]; - - // Text glyph paths are queued in glyph-local coordinates and placed with RenderLocation, - // so canvas-space clip paths must be moved into that same local space before clipping. - for (int i = 0; i < clipPaths.Count; i++) - { - translatedClipPaths[i] = clipPaths[i].Translate(-operation.RenderLocation); - } - - operationClipPaths = translatedClipPaths; - } + ? sharedTextOptions + : new DrawingOptions(graphicsOptions, intersectionRule, Matrix4x4.Identity, drawingOptions.TextContrast); + // Clipping is resolved from the ordered begin/end-clip stream, which the canvas anchors + // at the state's destination offset. Glyph render locations only move the glyph geometry; + // they never re-anchor the clip stack, so no per-operation clip translation is required. if (pen is null) { return new PathCompositionSceneCommand( @@ -1376,8 +2242,8 @@ private CompositionSceneCommand CreateTextCompositionCommand( in rasterizerOptions, state.TargetBounds, destinationOffset, - operationClipPaths, - state.IsLayer)); + state.Layer, + subPixelOffset)); } return new StrokePathCompositionSceneCommand( @@ -1389,8 +2255,9 @@ private CompositionSceneCommand CreateTextCompositionCommand( state.TargetBounds, destinationOffset, pen, - operationClipPaths, - state.IsLayer)); + state.Layer is not null, + state.Layer, + subPixelOffset)); } /// @@ -1406,15 +2273,44 @@ private static Rectangle ToConservativeBounds(RectangleF bounds) (int)MathF.Ceiling(bounds.Bottom)); /// - /// Resolves local layer bounds to absolute target bounds using the active transform. + /// Converts local geometry bounds to the rasterizer area of interest. + /// + /// The local geometry bounds. + /// The conservative rasterizer interest rectangle. + private static Rectangle ToRasterizerInterest(RectangleF bounds) + => Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right) + 1, + (int)MathF.Ceiling(bounds.Bottom) + 1); + + /// + /// Returns the transform scale used when converting stroke width to raster bounds. /// - /// The current drawing state. + /// The drawing transform. + /// The stroke width scale. + private static float GetTransformWidthScale(Matrix4x4 transform) + { + if (transform.IsIdentity) + { + return 1F; + } + + float det = (transform.M11 * transform.M22) - (transform.M12 * transform.M21); + return MathF.Sqrt(MathF.Abs(det)); + } + + /// + /// Resolves local layer bounds to absolute target bounds using the supplied transform. + /// + /// The transform active for the layer state. + /// The absolute target bounds to clip against. + /// Absolute destination offset for local canvas coordinates. /// The layer bounds in local canvas coordinates. /// The absolute layer bounds clipped to the active target. - private static Rectangle ResolveLayerBounds(DrawingCanvasState state, Rectangle bounds) + private static Rectangle ResolveLayerBounds(Matrix4x4 transform, Rectangle targetBounds, Point destinationOffset, Rectangle bounds) { RectangleF transformedBounds = bounds; - Matrix4x4 transform = state.Options.Transform; if (!transform.IsIdentity) { transformedBounds = RectangleF.Transform(transformedBounds, transform); @@ -1422,12 +2318,12 @@ private static Rectangle ResolveLayerBounds(DrawingCanvasState state, Rectangle Rectangle localLayerBounds = ToConservativeBounds(transformedBounds); Rectangle absoluteLayerBounds = new( - state.DestinationOffset.X + localLayerBounds.X, - state.DestinationOffset.Y + localLayerBounds.Y, + destinationOffset.X + localLayerBounds.X, + destinationOffset.Y + localLayerBounds.Y, localLayerBounds.Width, localLayerBounds.Height); - return Rectangle.Intersect(state.TargetBounds, absoluteLayerBounds); + return Rectangle.Intersect(targetBounds, absoluteLayerBounds); } /// @@ -1488,10 +2384,7 @@ private static Image CreateTransformedDrawImage( // Source space: pixel coordinates in the untransformed source image (0..Width, 0..Height). // Destination space: where that image would land on the canvas without any extra transform. // This matrix maps source -> destination by scaling to destination size then translating to destination origin. - Matrix4x4 sourceToDestination = Matrix4x4.CreateScale( - destinationRect.Width / image.Width, - destinationRect.Height / image.Height, - 1) + Matrix4x4 sourceToDestination = Matrix4x4.CreateScale(destinationRect.Width / image.Width, destinationRect.Height / image.Height, 1) * Matrix4x4.CreateTranslation(destinationRect.X, destinationRect.Y, 0); // Apply the canvas transform after source->destination placement: @@ -1499,9 +2392,7 @@ private static Image CreateTransformedDrawImage( Matrix4x4 sourceToTransformedCanvas = sourceToDestination * transform; // Compute the transformed axis-aligned bounds in canvas space. - RectangleF transformedBounds = RectangleF.Transform( - new RectangleF(0, 0, image.Width, image.Height), - sourceToTransformedCanvas); + RectangleF transformedBounds = RectangleF.Transform(new RectangleF(0, 0, image.Width, image.Height), sourceToTransformedCanvas); // ImageBrush samples against integer pixel locations. Align the baked bitmap to integer // canvas bounds so the bitmap origin and brush sampling origin agree exactly. @@ -1595,25 +2486,30 @@ private static RectangleF MapSourceClipToDestination( } /// - /// Transforms clip paths into the same coordinate space as an eagerly-transformed draw-image command. + /// Creates a normalized clip state in the same coordinate space as recorded commands. /// - /// Clip paths from the current canvas state. - /// Canvas transform already applied to the image content. - /// The transformed clip path list. - private static IReadOnlyList TransformClipPaths(IReadOnlyList clipPaths, Matrix4x4 transform) + /// Clip paths from the active canvas state. + /// Canvas transform to apply to the clip state. + /// The operation used to combine the paths with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The transformed clip state. + private static DrawingClipState CreateClipState( + IPath[] clipPaths, + Matrix4x4 transform, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) { - if (clipPaths.Count == 0 || transform.IsIdentity) - { - return clipPaths; - } - - IPath[] transformed = new IPath[clipPaths.Count]; - for (int i = 0; i < transformed.Length; i++) - { - transformed[i] = clipPaths[i].Transform(transform); - } - - return transformed; + DrawingClipState clipState = DrawingClipState.FromPaths( + clipPaths, + operation, + edgeMode, + antialiasThreshold); + + // Transform after descriptor creation. DrawingClipDescriptor.Transform has the + // rectangle/region-specific logic needed to preserve cheap clip primitives. + return clipState.Transform(transform); } /// diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs b/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs new file mode 100644 index 000000000..acbd76c7a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingClipDescriptor.cs @@ -0,0 +1,614 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents one clip primitive carried by a drawing command. +/// +public sealed class DrawingClipDescriptor +{ + private static readonly Rectangle[] EmptyIntegerRectangles = []; + private static readonly RectangleF[] EmptyRectangles = []; + private static readonly IPath[] EmptyPaths = []; + private readonly IReadOnlyList? integerRectangles; + private readonly IReadOnlyList? rectangles; + private readonly IReadOnlyList? paths; + + // Lazily cached linearization of the path operands at PathScale. Safe to cache because + // every mutation (Translate/Transform) produces a new descriptor instance. + private LinearGeometry? pathGeometry; + + /// + /// Initializes a new instance of the class. + /// + /// The clip primitive kind. + /// The rectangle for rectangle clips. + /// The rectangles for integer region clips. + /// The rectangles for floating-point region clips. + /// The path operands for path clips. + /// The path-space scale seen by curve subdivision. + /// The residual transform mapping scaled path geometry into descriptor coordinates. + /// The operation used to combine this descriptor with the existing clip. + /// The fill rule used for path geometry. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + private DrawingClipDescriptor( + DrawingClipKind kind, + RectangleF rectangle, + IReadOnlyList? integerRectangles, + IReadOnlyList? rectangles, + IReadOnlyList? paths, + Vector2 pathScale, + Matrix4x4 pathTransform, + ClipOperation operation, + IntersectionRule intersectionRule, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + { + this.Kind = kind; + this.Rectangle = rectangle; + this.integerRectangles = integerRectangles; + this.rectangles = rectangles; + this.paths = paths; + this.PathScale = pathScale; + this.PathTransform = pathTransform; + this.Operation = operation; + this.IntersectionRule = intersectionRule; + this.EdgeMode = edgeMode; + this.AntialiasThreshold = antialiasThreshold; + } + + /// + /// Gets the clip primitive kind. + /// + public DrawingClipKind Kind { get; } + + /// + /// Gets the operation used to combine this descriptor with the existing clip. + /// + public ClipOperation Operation { get; } + + /// + /// Gets the fill rule used when this clip is represented as path geometry. + /// + public IntersectionRule IntersectionRule { get; } + + /// + /// Gets the edge mode used by rectangle and region clips. + /// + public DrawingClipEdgeMode EdgeMode { get; } + + /// + /// Gets the coverage threshold used when the clip edge mode is hard. + /// + public float AntialiasThreshold { get; } + + /// + /// Gets the rectangle for a rectangle clip. + /// + public RectangleF Rectangle { get; } + + /// + /// Gets the integer rectangles for an integer region clip. + /// + public IReadOnlyList IntegerRectangles => this.integerRectangles ?? EmptyIntegerRectangles; + + /// + /// Gets the floating-point rectangles for a region clip. + /// + public IReadOnlyList Rectangles => this.rectangles ?? EmptyRectangles; + + /// + /// Gets the path operands for a path clip. + /// + public IReadOnlyList Paths => this.paths ?? EmptyPaths; + + /// + /// Gets the path-space scale represented by this descriptor. + /// + public Vector2 PathScale { get; } + + /// + /// Gets the transform that maps scaled path geometry into descriptor coordinates. + /// + public Matrix4x4 PathTransform { get; } + + /// + /// Creates a rectangle clip descriptor. + /// + /// The rectangle in canvas-local coordinates. + /// The operation used to combine the rectangle with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + public static DrawingClipDescriptor CreateRectangle( + RectangleF rectangle, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + => new( + DrawingClipKind.Rectangle, + rectangle, + null, + null, + null, + Vector2.One, + Matrix4x4.Identity, + operation, + IntersectionRule.NonZero, + edgeMode, + antialiasThreshold); + + /// + /// Creates an integer region clip descriptor. + /// + /// The region rectangles in canvas-local coordinates. + /// The operation used to combine the region with the existing clip. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + public static DrawingClipDescriptor CreateIntegerRegion( + IReadOnlyList rectangles, + ClipOperation operation, + float antialiasThreshold) + => new( + DrawingClipKind.IntegerRegion, + default, + rectangles, + null, + null, + Vector2.One, + Matrix4x4.Identity, + operation, + IntersectionRule.NonZero, + DrawingClipEdgeMode.Hard, + antialiasThreshold); + + /// + /// Creates a floating-point region clip descriptor. + /// + /// The region rectangles in canvas-local coordinates. + /// The operation used to combine the region with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + public static DrawingClipDescriptor CreateRegion( + IReadOnlyList rectangles, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + => new( + DrawingClipKind.Region, + default, + null, + rectangles, + null, + Vector2.One, + Matrix4x4.Identity, + operation, + IntersectionRule.NonZero, + edgeMode, + antialiasThreshold); + + /// + /// Creates a path clip descriptor. + /// + /// The path operands interpreted as one clip region. + /// The operation used to combine the paths with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + public static DrawingClipDescriptor CreatePath( + IReadOnlyList paths, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + => CreatePath(paths, Vector2.One, Matrix4x4.Identity, operation, IntersectionRule.NonZero, edgeMode, antialiasThreshold); + + /// + /// Creates a path descriptor from operands plus the transform split used by path lowering. + /// + /// The path operands interpreted as one clip region. + /// The path-space scale seen by curve subdivision. + /// The residual transform mapping scaled path geometry into descriptor coordinates. + /// The operation used to combine the paths with the existing clip. + /// The fill rule used for the path geometry. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + private static DrawingClipDescriptor CreatePath( + IReadOnlyList paths, + Vector2 scale, + Matrix4x4 transform, + ClipOperation operation, + IntersectionRule intersectionRule, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + => new( + DrawingClipKind.Path, + default, + null, + null, + paths, + scale, + transform, + operation, + intersectionRule, + edgeMode, + antialiasThreshold); + + /// + /// Converts one incoming path to a clip descriptor. + /// + /// The incoming path. + /// The operation used to combine the path with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The descriptor. + public static DrawingClipDescriptor FromPath( + IPath path, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + { + if (path is IRegionPath regionPath) + { + return CreateIntegerRegion(regionPath.Rectangles, operation, antialiasThreshold); + } + + if (TryGetRectangle(path, out RectangleF rectangle)) + { + return CreateRectangle(rectangle, operation, edgeMode, antialiasThreshold); + } + + return CreatePath([path], operation, edgeMode, antialiasThreshold); + } + + /// + /// Translates this descriptor. + /// + /// The translation offset. + /// The translated descriptor. + public DrawingClipDescriptor Translate(Vector2 offset) + { + if (offset == Vector2.Zero) + { + return this; + } + + switch (this.Kind) + { + case DrawingClipKind.Rectangle: + { + RectangleF translated = this.Rectangle; + translated.Offset(offset.X, offset.Y); + return CreateRectangle(translated, this.Operation, this.EdgeMode, this.AntialiasThreshold); + } + + case DrawingClipKind.IntegerRegion: + { + IReadOnlyList source = this.IntegerRectangles; + if (offset.X == MathF.Truncate(offset.X) && offset.Y == MathF.Truncate(offset.Y)) + { + Rectangle[] translated = new Rectangle[source.Count]; + Point pointOffset = new((int)offset.X, (int)offset.Y); + for (int i = 0; i < translated.Length; i++) + { + translated[i] = source[i]; + translated[i].Offset(pointOffset); + } + + return CreateIntegerRegion(translated, this.Operation, this.AntialiasThreshold); + } + + RectangleF[] translatedRegion = new RectangleF[source.Count]; + for (int i = 0; i < translatedRegion.Length; i++) + { + Rectangle rectangle = source[i]; + translatedRegion[i] = new RectangleF(rectangle.X + offset.X, rectangle.Y + offset.Y, rectangle.Width, rectangle.Height); + } + + return CreateRegion(translatedRegion, this.Operation, DrawingClipEdgeMode.Hard, this.AntialiasThreshold); + } + + case DrawingClipKind.Region: + { + IReadOnlyList source = this.Rectangles; + RectangleF[] translated = new RectangleF[source.Count]; + for (int i = 0; i < translated.Length; i++) + { + translated[i] = source[i]; + translated[i].Offset(offset.X, offset.Y); + } + + return CreateRegion(translated, this.Operation, this.EdgeMode, this.AntialiasThreshold); + } + + default: + { + // Keep path operands stable and move translation into the residual transform. + // Rebuilding transformed paths here would discard the scale used for curve + // subdivision and force the backends back into Vector2.One lowering. + Matrix4x4 translated = this.GetPathMatrix() * Matrix4x4.CreateTranslation(offset.X, offset.Y, 0); + Vector2 scale = MatrixUtilities.GetScale(translated); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, translated); + + return CreatePath(this.Paths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode, this.AntialiasThreshold); + } + } + } + + /// + /// Transforms this descriptor. + /// + /// The transform matrix. + /// The transformed descriptor. + public DrawingClipDescriptor Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + if (MatrixUtilities.PreservesAxisAlignedRectangles(matrix)) + { + if (this.Kind == DrawingClipKind.Rectangle) + { + return CreateRectangle(TransformRectangle(this.Rectangle, matrix), this.Operation, this.EdgeMode, this.AntialiasThreshold); + } + + if (this.Kind == DrawingClipKind.IntegerRegion) + { + IReadOnlyList source = this.IntegerRectangles; + RectangleF[] transformed = new RectangleF[source.Count]; + for (int i = 0; i < transformed.Length; i++) + { + Rectangle rectangle = source[i]; + RectangleF regionRectangle = new(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + + transformed[i] = TransformRectangle(regionRectangle, matrix); + } + + SortRegionRectangles(transformed); + return CreateRegion(transformed, this.Operation, DrawingClipEdgeMode.Hard, this.AntialiasThreshold); + } + + if (this.Kind == DrawingClipKind.Region) + { + IReadOnlyList source = this.Rectangles; + RectangleF[] transformed = new RectangleF[source.Count]; + for (int i = 0; i < transformed.Length; i++) + { + transformed[i] = TransformRectangle(source[i], matrix); + } + + SortRegionRectangles(transformed); + return CreateRegion(transformed, this.Operation, this.EdgeMode, this.AntialiasThreshold); + } + } + + // For arbitrary path clips, preserve the same scale/residual split used by fill and + // stroke commands: curve subdivision sees the scale, while backends apply the residual. + Matrix4x4 pathMatrix = this.Kind == DrawingClipKind.Path ? this.GetPathMatrix() * matrix : matrix; + Vector2 scale = MatrixUtilities.GetScale(pathMatrix); + Matrix4x4 residual = MatrixUtilities.GetResidual(scale, pathMatrix); + IReadOnlyList sourcePaths = this.Kind == DrawingClipKind.Path ? this.Paths : [this.ToPath()]; + + return CreatePath(sourcePaths, scale, residual, this.Operation, this.IntersectionRule, this.EdgeMode, this.AntialiasThreshold); + } + + /// + /// Converts a path clip descriptor to linear geometry. + /// + /// The transform that maps the returned geometry into descriptor coordinates. + /// The scaled linear geometry for the path clip. + /// Thrown when this descriptor is not a path clip. + public LinearGeometry ToLinearGeometry(out Matrix4x4 transform) + { + if (this.Kind != DrawingClipKind.Path) + { + throw new InvalidOperationException("Only path clip descriptors can be converted to linear geometry."); + } + + LinearGeometry? cached = this.pathGeometry; + if (cached is not null) + { + transform = this.PathTransform; + return cached; + } + + transform = this.PathTransform; + + IReadOnlyList source = this.Paths; + IPath path = source.Count == 1 ? source[0] : new ComplexPolygon(source); + LinearGeometry geometry = path.ToLinearGeometry(this.PathScale); + this.pathGeometry = geometry; + return geometry; + } + + /// + /// Gets conservative integer bounds for this descriptor in absolute target coordinates. + /// + /// The absolute destination offset for the owning command. + /// The conservative bounds. + public Rectangle GetConservativeBounds(Point destinationOffset) + { + RectangleF bounds = this.Kind switch + { + DrawingClipKind.Rectangle => this.Rectangle, + DrawingClipKind.IntegerRegion => GetIntegerRegionBounds(this.IntegerRectangles), + DrawingClipKind.Region => GetRegionBounds(this.Rectangles), + _ => GetPathBounds(this.Paths, this.PathScale, this.PathTransform) + }; + + bounds.Offset(destinationOffset.X, destinationOffset.Y); + return ImageSharp.Rectangle.FromLTRB( + (int)MathF.Floor(bounds.Left), + (int)MathF.Floor(bounds.Top), + (int)MathF.Ceiling(bounds.Right), + (int)MathF.Ceiling(bounds.Bottom)); + } + + /// + /// Converts this descriptor to path geometry. + /// + /// A path describing the descriptor area. + public IPath ToPath() + { + switch (this.Kind) + { + case DrawingClipKind.Rectangle: + return new RectanglePolygon(this.Rectangle); + + case DrawingClipKind.IntegerRegion: + return new Region(this.IntegerRectangles).ToPath(); + + case DrawingClipKind.Region: + { + IReadOnlyList source = this.Rectangles; + IPath[] rectanglePaths = new IPath[source.Count]; + for (int i = 0; i < rectanglePaths.Length; i++) + { + rectanglePaths[i] = new RectanglePolygon(source[i]); + } + + return new ComplexPolygon(rectanglePaths); + } + + default: + { + IReadOnlyList source = this.Paths; + IPath path = source.Count == 1 ? source[0] : new ComplexPolygon(source); + Matrix4x4 transform = this.GetPathMatrix(); + + return transform.IsIdentity ? path : path.Transform(transform); + } + } + } + + /// + /// Gets the full path transform represented by the stored scale and residual transform. + /// + /// The combined scale and residual transform. + private Matrix4x4 GetPathMatrix() + => Matrix4x4.CreateScale(this.PathScale.X, this.PathScale.Y, 1F) * this.PathTransform; + + /// + /// Attempts to read a path as an axis-aligned rectangle. + /// + /// The path to inspect. + /// The rectangle represented by the path. + /// when the path is a rectangle. + private static bool TryGetRectangle(IPath path, out RectangleF rectangle) + { + if (path is RectanglePolygon rectanglePolygon) + { + rectangle = rectanglePolygon.Bounds; + return true; + } + + rectangle = default; + return false; + } + + /// + /// Transforms a rectangle and returns the axis-aligned bounds of its transformed corners. + /// + /// The rectangle to transform. + /// The transform matrix. + /// The axis-aligned bounds of the transformed rectangle. + private static RectangleF TransformRectangle(RectangleF rectangle, Matrix4x4 matrix) + { + Vector2 p0 = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 p1 = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 p2 = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + Vector2 p3 = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(p0.X, p1.X), MathF.Min(p2.X, p3.X)); + float top = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); + float right = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); + float bottom = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); + + return RectangleF.FromLTRB(left, top, right, bottom); + } + + /// + /// Gets the union bounds of an integer region. + /// + /// The region rectangles. + /// The union bounds. + private static RectangleF GetIntegerRegionBounds(IReadOnlyList rectangles) + { + Rectangle bounds = rectangles[0]; + for (int i = 1; i < rectangles.Count; i++) + { + bounds = ImageSharp.Rectangle.Union(bounds, rectangles[i]); + } + + return bounds; + } + + /// + /// Gets the union bounds of a floating-point region. + /// + /// The region rectangles. + /// The union bounds. + private static RectangleF GetRegionBounds(IReadOnlyList rectangles) + { + RectangleF bounds = rectangles[0]; + for (int i = 1; i < rectangles.Count; i++) + { + bounds = RectangleF.Union(bounds, rectangles[i]); + } + + return bounds; + } + + /// + /// Gets the union bounds of the path operands in path space. + /// + /// The path operands. + /// The union bounds. + private static RectangleF GetPathBounds(IReadOnlyList paths) + { + RectangleF bounds = paths[0].Bounds; + for (int i = 1; i < paths.Count; i++) + { + bounds = RectangleF.Union(bounds, paths[i].Bounds); + } + + return bounds; + } + + /// + /// Gets the union bounds of the path operands mapped through the scale and residual transform. + /// + /// The path operands. + /// The path-space scale. + /// The residual transform mapping scaled geometry into descriptor coordinates. + /// The union bounds in descriptor coordinates. + private static RectangleF GetPathBounds(IReadOnlyList paths, Vector2 scale, Matrix4x4 transform) + { + RectangleF bounds = GetPathBounds(paths); + RectangleF scaled = new(bounds.X * scale.X, bounds.Y * scale.Y, bounds.Width * scale.X, bounds.Height * scale.Y); + + return transform.IsIdentity ? scaled : RectangleF.Transform(scaled, transform); + } + + /// + /// Sorts region rectangles into the top-to-bottom, left-to-right order used by the + /// canonical region model. + /// + /// The rectangles to sort in place. + private static void SortRegionRectangles(RectangleF[] rectangles) + => Array.Sort( + rectangles, + static (left, right) => + { + int top = left.Top.CompareTo(right.Top); + return top != 0 ? top : left.Left.CompareTo(right.Left); + }); +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipEdgeMode.cs b/src/ImageSharp.Drawing/Processing/DrawingClipEdgeMode.cs new file mode 100644 index 000000000..e8ff61f3b --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingClipEdgeMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Identifies how rectangle clip edges contribute coverage. +/// +public enum DrawingClipEdgeMode : byte +{ + /// + /// The clip has hard device-pixel edges. + /// + Hard = 0, + + /// + /// The clip participates in antialias coverage. + /// + Antialiased = 1 +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipKind.cs b/src/ImageSharp.Drawing/Processing/DrawingClipKind.cs new file mode 100644 index 000000000..67fd513a3 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingClipKind.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Identifies the clip primitive represented by a drawing clip descriptor. +/// +public enum DrawingClipKind : byte +{ + /// + /// The clip is an axis-aligned rectangle. + /// + Rectangle, + + /// + /// The clip is a set of integer rectangles. + /// + IntegerRegion, + + /// + /// The clip is a set of floating-point rectangles. + /// + Region, + + /// + /// The clip is path geometry. + /// + Path +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingClipState.cs b/src/ImageSharp.Drawing/Processing/DrawingClipState.cs new file mode 100644 index 000000000..d35d72492 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingClipState.cs @@ -0,0 +1,453 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Represents an ordered set of exact clip primitives carried by a drawing command. +/// +public readonly struct DrawingClipState +{ + // Default struct instances cannot initialize reference fields. Inactive inline slots point + // at this descriptor, while Count remains the invariant that controls which slots are read. + private static readonly DrawingClipDescriptor EmptyDescriptor = DrawingClipDescriptor.CreateRectangle( + default, + ClipOperation.Intersection, + DrawingClipEdgeMode.Hard, + 0F); + + // Most clip stacks in UI redraws are shallow: a dirty region plus one or two control clips. + // Keep that path allocation-free and only spill to an array for unusually deep nesting. + private readonly DrawingClipDescriptor descriptor0; + private readonly DrawingClipDescriptor descriptor1; + private readonly DrawingClipDescriptor descriptor2; + private readonly DrawingClipDescriptor descriptor3; + private readonly DrawingClipDescriptor[]? overflowDescriptors; + + /// + /// Initializes a new instance of the struct with one inline descriptor. + /// + /// The first descriptor in stack order. + private DrawingClipState(DrawingClipDescriptor descriptor0) + { + this.descriptor0 = descriptor0; + this.descriptor1 = EmptyDescriptor; + this.descriptor2 = EmptyDescriptor; + this.descriptor3 = EmptyDescriptor; + this.overflowDescriptors = null; + this.Count = 1; + } + + /// + /// Initializes a new instance of the struct with two inline descriptors. + /// + /// The first descriptor in stack order. + /// The second descriptor in stack order. + private DrawingClipState(DrawingClipDescriptor descriptor0, DrawingClipDescriptor descriptor1) + { + this.descriptor0 = descriptor0; + this.descriptor1 = descriptor1; + this.descriptor2 = EmptyDescriptor; + this.descriptor3 = EmptyDescriptor; + this.overflowDescriptors = null; + this.Count = 2; + } + + /// + /// Initializes a new instance of the struct with three inline descriptors. + /// + /// The first descriptor in stack order. + /// The second descriptor in stack order. + /// The third descriptor in stack order. + private DrawingClipState(DrawingClipDescriptor descriptor0, DrawingClipDescriptor descriptor1, DrawingClipDescriptor descriptor2) + { + this.descriptor0 = descriptor0; + this.descriptor1 = descriptor1; + this.descriptor2 = descriptor2; + this.descriptor3 = EmptyDescriptor; + this.overflowDescriptors = null; + this.Count = 3; + } + + /// + /// Initializes a new instance of the struct with four inline descriptors. + /// + /// The first descriptor in stack order. + /// The second descriptor in stack order. + /// The third descriptor in stack order. + /// The fourth descriptor in stack order. + private DrawingClipState( + DrawingClipDescriptor descriptor0, + DrawingClipDescriptor descriptor1, + DrawingClipDescriptor descriptor2, + DrawingClipDescriptor descriptor3) + { + this.descriptor0 = descriptor0; + this.descriptor1 = descriptor1; + this.descriptor2 = descriptor2; + this.descriptor3 = descriptor3; + this.overflowDescriptors = null; + this.Count = 4; + } + + /// + /// Initializes a new instance of the struct from an overflow array. + /// + /// All descriptors in stack order; the inline slots are unused. + private DrawingClipState(DrawingClipDescriptor[] overflowDescriptors) + { + this.descriptor0 = EmptyDescriptor; + this.descriptor1 = EmptyDescriptor; + this.descriptor2 = EmptyDescriptor; + this.descriptor3 = EmptyDescriptor; + this.overflowDescriptors = overflowDescriptors; + this.Count = overflowDescriptors.Length; + } + + /// + /// Gets an empty clip state. + /// + public static DrawingClipState Empty => default; + + /// + /// Gets the number of clip descriptors in stack order. + /// + public int Count { get; } + + /// + /// Gets a value indicating whether this state carries any clips. + /// + public bool HasClips => this.Count > 0; + + /// + /// Creates a clip state from path operands. + /// + /// The clip paths. + /// The operation used to combine the paths with the existing clip. + /// The clip edge mode. + /// The coverage threshold used for hard clip edges. + /// The normalized clip state. + public static DrawingClipState FromPaths( + IReadOnlyList paths, + ClipOperation operation, + DrawingClipEdgeMode edgeMode, + float antialiasThreshold) + { + if (paths.Count == 0) + { + return Empty; + } + + DrawingClipDescriptor descriptor = paths.Count == 1 + ? DrawingClipDescriptor.FromPath(paths[0], operation, edgeMode, antialiasThreshold) + : DrawingClipDescriptor.CreatePath(paths, operation, edgeMode, antialiasThreshold); + + // Multiple paths supplied in one Clip call are one clip operand. They are not separate + // stack entries because the operation applies to the combined incoming shape. + return new DrawingClipState(descriptor); + } + + /// + /// Gets one descriptor by stack order. + /// + /// The descriptor index. + /// The descriptor at the requested index. + public DrawingClipDescriptor GetDescriptor(int index) + { + if (this.overflowDescriptors is not null) + { + return this.overflowDescriptors[index]; + } + + return index switch + { + 0 => this.descriptor0, + 1 => this.descriptor1, + 2 => this.descriptor2, + _ => this.descriptor3, + }; + } + + /// + /// Appends one clip descriptor. + /// + /// The descriptor to append. + /// The updated clip state. + public DrawingClipState Append(DrawingClipDescriptor descriptor) + { + if (!this.HasClips) + { + return new DrawingClipState(descriptor); + } + + if (this.Count == 1) + { + return new DrawingClipState(this.descriptor0, descriptor); + } + + if (this.Count == 2) + { + return new DrawingClipState(this.descriptor0, this.descriptor1, descriptor); + } + + if (this.Count == 3) + { + return new DrawingClipState(this.descriptor0, this.descriptor1, this.descriptor2, descriptor); + } + + // Four inline descriptors cover the common UI path. Allocating here is reserved for + // genuinely deep clip stacks rather than every push. + DrawingClipDescriptor[] appended = new DrawingClipDescriptor[this.Count + 1]; + for (int i = 0; i < this.Count; i++) + { + appended[i] = this.GetDescriptor(i); + } + + appended[^1] = descriptor; + return new DrawingClipState(appended); + } + + /// + /// Appends all descriptors from another state. + /// + /// The clip state to append. + /// The updated clip state. + public DrawingClipState Append(DrawingClipState state) + { + if (!this.HasClips) + { + return state; + } + + if (!state.HasClips) + { + return this; + } + + int count = this.Count + state.Count; + if (count == 2) + { + return new DrawingClipState(this.GetDescriptor(0), state.GetDescriptor(0)); + } + + if (count == 3) + { + return new DrawingClipState( + this.GetDescriptor(0), + this.Count == 2 ? this.GetDescriptor(1) : state.GetDescriptor(0), + state.GetDescriptor(state.Count - 1)); + } + + if (count == 4) + { + DrawingClipDescriptor d0 = this.GetDescriptor(0); + DrawingClipDescriptor d1 = this.Count > 1 ? this.GetDescriptor(1) : state.GetDescriptor(0); + DrawingClipDescriptor d2 = this.Count > 2 ? this.GetDescriptor(2) : state.GetDescriptor(2 - this.Count); + DrawingClipDescriptor d3 = state.GetDescriptor(state.Count - 1); + + return new DrawingClipState(d0, d1, d2, d3); + } + + // Keep ordering stable while spilling only after the inline descriptor slots are full. + DrawingClipDescriptor[] appended = new DrawingClipDescriptor[count]; + for (int i = 0; i < this.Count; i++) + { + appended[i] = this.GetDescriptor(i); + } + + for (int i = 0; i < state.Count; i++) + { + appended[this.Count + i] = state.GetDescriptor(i); + } + + return new DrawingClipState(appended); + } + + /// + /// Translates this clip state. + /// + /// The translation offset. + /// The translated clip state. + public DrawingClipState Translate(Vector2 offset) + { + if (!this.HasClips || offset == Vector2.Zero) + { + return this; + } + + if (this.Count == 1) + { + return new DrawingClipState(this.descriptor0.Translate(offset)); + } + + if (this.Count == 2) + { + return new DrawingClipState( + this.descriptor0.Translate(offset), + this.descriptor1.Translate(offset)); + } + + if (this.Count == 3) + { + return new DrawingClipState( + this.descriptor0.Translate(offset), + this.descriptor1.Translate(offset), + this.descriptor2.Translate(offset)); + } + + if (this.Count == 4) + { + return new DrawingClipState( + this.descriptor0.Translate(offset), + this.descriptor1.Translate(offset), + this.descriptor2.Translate(offset), + this.descriptor3.Translate(offset)); + } + + DrawingClipDescriptor[] translated = new DrawingClipDescriptor[this.Count]; + for (int i = 0; i < translated.Length; i++) + { + translated[i] = this.GetDescriptor(i).Translate(offset); + } + + return new DrawingClipState(translated); + } + + /// + /// Transforms this clip state. + /// + /// The transform matrix. + /// The transformed clip state. + public DrawingClipState Transform(Matrix4x4 matrix) + { + if (!this.HasClips || matrix.IsIdentity) + { + return this; + } + + if (this.Count == 1) + { + return new DrawingClipState(this.descriptor0.Transform(matrix)); + } + + if (this.Count == 2) + { + return new DrawingClipState( + this.descriptor0.Transform(matrix), + this.descriptor1.Transform(matrix)); + } + + if (this.Count == 3) + { + return new DrawingClipState( + this.descriptor0.Transform(matrix), + this.descriptor1.Transform(matrix), + this.descriptor2.Transform(matrix)); + } + + if (this.Count == 4) + { + return new DrawingClipState( + this.descriptor0.Transform(matrix), + this.descriptor1.Transform(matrix), + this.descriptor2.Transform(matrix), + this.descriptor3.Transform(matrix)); + } + + DrawingClipDescriptor[] transformed = new DrawingClipDescriptor[this.Count]; + for (int i = 0; i < transformed.Length; i++) + { + transformed[i] = this.GetDescriptor(i).Transform(matrix); + } + + return new DrawingClipState(transformed); + } + + /// + /// Gets conservative integer bounds for intersection descriptors in absolute target coordinates. + /// + /// The absolute destination offset for the owning command. + /// The conservative bounds. + /// when at least one intersection descriptor has clip bounds. + public bool TryGetConservativeBounds(Point destinationOffset, out Rectangle bounds) + { + bounds = default; + bool hasBounds = false; + + for (int i = 0; i < this.Count; i++) + { + DrawingClipDescriptor descriptor = this.GetDescriptor(i); + if (descriptor.Operation != ClipOperation.Intersection) + { + continue; + } + + Rectangle descriptorBounds = descriptor.GetConservativeBounds(destinationOffset); + bounds = hasBounds ? Rectangle.Intersect(bounds, descriptorBounds) : descriptorBounds; + hasBounds = true; + } + + return hasBounds; + } + + /// + /// Gets bounds for a single pixel-aligned clip that can be represented entirely by target bounds. + /// + /// The absolute destination offset for the owning canvas state. + /// The exact integer clip bounds. + /// when this state contains only one pixel-aligned rectangle clip. + public bool TryGetTargetBoundsClip(Point destinationOffset, out Rectangle bounds) + { + bounds = default; + if (this.Count != 1) + { + return false; + } + + DrawingClipDescriptor descriptor = this.descriptor0; + if (descriptor.Operation != ClipOperation.Intersection) + { + return false; + } + + if (descriptor.Kind == DrawingClipKind.IntegerRegion) + { + IReadOnlyList rectangles = descriptor.IntegerRectangles; + if (rectangles.Count != 1) + { + return false; + } + + bounds = rectangles[0]; + bounds.Offset(destinationOffset); + return bounds.Width > 0 && bounds.Height > 0; + } + + if (descriptor.Kind != DrawingClipKind.Rectangle) + { + return false; + } + + RectangleF rectangle = descriptor.Rectangle; + float left = rectangle.Left + destinationOffset.X; + float top = rectangle.Top + destinationOffset.Y; + float right = rectangle.Right + destinationOffset.X; + float bottom = rectangle.Bottom + destinationOffset.Y; + int integerLeft = (int)left; + int integerTop = (int)top; + int integerRight = (int)right; + int integerBottom = (int)bottom; + + // Target-bounds clipping is exact pixel rejection. Any fractional edge needs + // coverage-based clipping, so it cannot be folded into target bounds. + if (left != integerLeft || top != integerTop || right != integerRight || bottom != integerBottom) + { + return false; + } + + bounds = Rectangle.FromLTRB(integerLeft, integerTop, integerRight, integerBottom); + return bounds.Width > 0 && bounds.Height > 0; + } +} diff --git a/src/ImageSharp.Drawing/Processing/DrawingHelpers.cs b/src/ImageSharp.Drawing/Processing/DrawingHelpers.cs index 289bbb45f..d058816b1 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingHelpers.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingHelpers.cs @@ -3,6 +3,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing; +/// +/// Provides internal helper methods for drawing operations. +/// internal static class DrawingHelpers { /// @@ -10,6 +13,7 @@ internal static class DrawingHelpers /// /// The type of pixel format. /// The color matrix. + /// A matrix of the same dimensions with each color converted to . public static DenseMatrix ToPixelMatrix(this DenseMatrix colorMatrix) where TPixel : unmanaged, IPixel { diff --git a/src/ImageSharp.Drawing/Processing/DrawingOperation.cs b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs index d98eac94f..6e9c122b4 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOperation.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOperation.cs @@ -1,31 +1,97 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; + namespace SixLabors.ImageSharp.Drawing.Processing; +/// +/// Identifies how a text drawing operation paints its path. +/// internal enum DrawingOperationKind : byte { + /// + /// The path interior is filled using . + /// Fill = 0, + + /// + /// The path is stroked using . + /// Draw = 1 } +/// +/// Represents one paint operation emitted by during text +/// rendering and later converted to composition commands by +/// . +/// internal struct DrawingOperation { + /// + /// Gets or sets a value identifying whether the operation fills or strokes . + /// public DrawingOperationKind Kind { get; set; } + /// + /// Gets or sets the glyph or decoration path in local coordinates relative to . + /// public IPath Path { get; set; } + /// + /// Gets or sets the pixel-clamped target location at which is rendered. + /// public Point RenderLocation { get; set; } + /// + /// Gets or sets the fractional-pixel remainder of the target location, in the range [0, 1). + /// Cached glyph paths are anchored at their exact outline origin so one cache entry serves + /// every position; the backends apply this remainder as a residual translation, which reuses + /// the scale-keyed flattened geometry while rendering the glyph at its exact sub-pixel + /// position. Zero for uncached operations whose paths bake their own fraction. + /// + public Vector2 SubPixelOffset { get; set; } + + /// + /// Gets or sets the cache key identifying the glyph path in the text cache. + /// + public RichTextGlyphRenderer.CacheKey GlyphKey { get; set; } + + /// + /// Gets or sets a value indicating whether is valid. This is + /// for uncached operations such as path-following text and decorations. + /// + public bool HasGlyphKey { get; set; } + + /// + /// Gets or sets the fill rule used to resolve the interior of . + /// public IntersectionRule IntersectionRule { get; set; } + /// + /// Gets or sets the render pass used to order operations: fills paint beneath outlines, + /// and outlines beneath decorations. Emission order is preserved within each pass. + /// public byte RenderPass { get; set; } + /// + /// Gets or sets the brush used by operations. + /// public Brush? Brush { get; set; } + /// + /// Gets or sets the pen used by operations. + /// public Pen? Pen { get; set; } + /// + /// Gets or sets the alpha composition mode captured from the active text run or graphics options. + /// public PixelAlphaCompositionMode PixelAlphaCompositionMode { get; set; } + /// + /// Gets or sets the color blending mode captured from the active text run or graphics options. + /// public PixelColorBlendingMode PixelColorBlendingMode { get; set; } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs index 09f456dcb..2212aa21f 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptions.cs @@ -7,12 +7,16 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Provides options for influencing drawing operations, combining graphics rendering settings, -/// shape fill-rule behavior, and an optional coordinate transform. +/// the fill-rule intersection mode, and an optional coordinate transform. /// public class DrawingOptions { + /// + /// The default perceptual contrast boost applied to antialiased text rendering. + /// + public const float DefaultTextContrast = 0.5F; + private GraphicsOptions graphicsOptions; - private ShapeOptions shapeOptions; /// /// Initializes a new instance of the class. @@ -20,21 +24,28 @@ public class DrawingOptions public DrawingOptions() { this.graphicsOptions = new GraphicsOptions(); - this.shapeOptions = new ShapeOptions(); this.Transform = Matrix4x4.Identity; } + /// + /// Initializes a new instance of the class with explicit values. + /// + /// The graphics rendering options. + /// The fill rule used to determine the interior of paths. + /// The transform matrix applied to vector output before rasterization. + /// The perceptual contrast boost applied to antialiased text rendering. internal DrawingOptions( GraphicsOptions graphicsOptions, - ShapeOptions shapeOptions, - Matrix4x4 transform) + IntersectionRule intersectionRule, + Matrix4x4 transform, + float textContrast) { DebugGuard.NotNull(graphicsOptions, nameof(graphicsOptions)); - DebugGuard.NotNull(shapeOptions, nameof(shapeOptions)); this.graphicsOptions = graphicsOptions; - this.shapeOptions = shapeOptions; + this.IntersectionRule = intersectionRule; this.Transform = transform; + this.TextContrast = textContrast; } /// @@ -52,17 +63,10 @@ public GraphicsOptions GraphicsOptions } /// - /// Gets or sets the shape options that control fill-rule intersection mode and boolean clipping behavior. + /// Gets or sets the fill rule used to determine which regions of a self-intersecting or + /// multi-contour path are inside the filled area. Defaults to . /// - public ShapeOptions ShapeOptions - { - get => this.shapeOptions; - set - { - Guard.NotNull(value, nameof(this.ShapeOptions)); - this.shapeOptions = value; - } - } + public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.NonZero; /// /// Gets or sets the transform matrix applied to vector output before rasterization. @@ -70,4 +74,19 @@ public ShapeOptions ShapeOptions /// Defaults to . /// public Matrix4x4 Transform { get; set; } + + /// + /// Gets or sets the perceptual contrast boost applied to antialiased text rendering. + /// Coverage is remapped through an S-curve that darkens mostly covered pixels and + /// lightens mostly empty ones, so glyph stems solidify while counters and gaps stay + /// bright; empty, half-covered, and fully covered pixels are unchanged. 0 + /// disables the boost and renders text identically to plain vector fills; 1 + /// applies the full smoothstep remap. Because the curve lightens sub-half coverage, + /// high values gradually thin hairline stems in very light faces at very small sizes; + /// lower the value if hairline preservation matters more than contrast. The boost + /// applies only to text drawn through the text APIs; general vector fills and strokes + /// are never affected. + /// Defaults to . + /// + public float TextContrast { get; set; } = DefaultTextContrast; } diff --git a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs index 0e233a62a..61f02942c 100644 --- a/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs +++ b/src/ImageSharp.Drawing/Processing/DrawingOptionsDefaultsExtensions.cs @@ -16,13 +16,17 @@ public static class DrawingOptionsDefaultsExtensions /// The image processing context to retrieve defaults from. /// The globally configured default options. public static DrawingOptions GetDrawingOptions(this IImageProcessingContext context) - => new(context.GetGraphicsOptions(), new ShapeOptions(), Matrix4x4.Identity); + => new(context.GetGraphicsOptions(), IntersectionRule.NonZero, Matrix4x4.Identity, DrawingOptions.DefaultTextContrast); /// - /// Clones the path graphic options and applies changes required to force clearing. + /// Clones the drawing options and applies changes required to force clearing. /// - /// The drawing options to clone - /// A clone of shapeOptions with ColorBlendingMode, AlphaCompositionMode, and BlendPercentage set + /// The drawing options to clone. + /// + /// A clone of with , + /// , and + /// forced so the source replaces the destination. + /// internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawingOptions) { GraphicsOptions options = drawingOptions.GraphicsOptions.DeepClone(); @@ -30,6 +34,6 @@ internal static DrawingOptions CloneForClearOperation(this DrawingOptions drawin options.AlphaCompositionMode = PixelAlphaCompositionMode.Src; options.BlendPercentage = 1F; - return new DrawingOptions(options, drawingOptions.ShapeOptions, drawingOptions.Transform); + return new DrawingOptions(options, drawingOptions.IntersectionRule, drawingOptions.Transform, drawingOptions.TextContrast); } } diff --git a/src/ImageSharp.Drawing/Processing/DrawingTextCache.cs b/src/ImageSharp.Drawing/Processing/DrawingTextCache.cs new file mode 100644 index 000000000..ebdba651a --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/DrawingTextCache.cs @@ -0,0 +1,490 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Stores reusable text drawing data shared by one or more drawing canvases. +/// +/// +/// Two tiers are cached. The glyph cache holds one flattened outline per glyph, keyed by glyph id, +/// size and pen; it is the base layer that avoids re-flattening the same glyph and is shared by +/// every run. The run-path cache is derived from it: the glyphs of a whole uniform run merged into a +/// single positioned path, so that redrawing the run collapses to one composition command instead of +/// one per glyph. The run path is keyed in run-local space, so the same run content drawn at any +/// position, including a fractionally scrolled one, is a hit. The cost is memory, because the merged +/// path holds a copy of each glyph's geometry; far fewer run paths than glyph entries are kept, only +/// whole-run repeats benefit, and a run that differs by a single glyph misses and falls back to the +/// per-glyph commands. +/// +public sealed class DrawingTextCache +{ + /// + /// The default maximum number of text cache entries. + /// + public const int DefaultCapacity = 16384; + + /// + /// Divisor applied to to derive the run-path capacity. + /// Run paths can retain large combined geometry, so keep fewer of them than + /// individual glyph entries. + /// + private const int RunPathCapacityDivisor = 4; + + // Both caches are LRU: the dictionary provides O(1) lookup while the linked list + // tracks usage order (most recently used at the head, eviction from the tail). + + /// + /// The base glyph cache: one flattened glyph outline per key, keyed by + /// (glyph id, size and pen) and shared across all + /// runs. The run-path cache is assembled from these entries. + /// + private readonly Dictionary> entries = []; + + /// + /// Usage-ordered list of glyph entries; least recently used at the tail. + /// + private readonly LinkedList usage = new(); + + /// + /// The derived run-path cache: a whole uniform run's glyphs merged into one positioned path, + /// keyed run-locally by so a repeat at any position hits. This + /// collapses a run to a single composition command at the cost of storing the merged geometry. + /// + private readonly Dictionary> runPathEntries = []; + + /// + /// Usage-ordered list of run-path entries; least recently used at the tail. + /// + private readonly LinkedList runPathUsage = new(); + + /// + /// Maximum number of run-path entries, derived from . + /// + private readonly int runPathCapacity; + + /// + /// Initializes a new instance of the class. + /// + public DrawingTextCache() + : this(DefaultCapacity) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum number of text cache entries. + public DrawingTextCache(int capacity) + { + Guard.MustBeGreaterThan(capacity, 0, nameof(capacity)); + + this.Capacity = capacity; + this.runPathCapacity = Math.Max(1, capacity / RunPathCapacityDivisor); + } + + /// + /// Gets the maximum number of glyph cache entries. The smaller run-path cache + /// capacity is derived from this value. + /// + public int Capacity { get; } + + /// + /// Gets the number of glyph cache entries. Run-path entries are not included. + /// + public int Count => this.entries.Count; + + /// + /// Gets the reusable drawing-operation scratch list handed to text renderers. Canvases are + /// per-frame objects while this cache survives across frames, so hosting the scratch here + /// keeps its capacity instead of regrowing a list of large operation structs every draw. + /// The list is cleared at the start of each text draw; like the caches on this type it + /// assumes single-threaded use. + /// + internal List OperationScratch { get; } = []; + + /// + /// Gets the reusable render-pass sort buffer used when text operations are lowered to + /// composition commands, hosted here for the same lifetime reason as + /// . The consuming batcher retains the commands, never this + /// buffer. + /// + internal List<(byte RenderPass, int Sequence, Backends.CompositionSceneCommand Command)> CommandSortScratch { get; } = []; + + /// + /// Removes all cached text drawing data. + /// + public void Clear() + { + this.entries.Clear(); + this.usage.Clear(); + this.runPathEntries.Clear(); + this.runPathUsage.Clear(); + this.OperationScratch.Clear(); + this.CommandSortScratch.Clear(); + } + + /// + /// Attempts to get cached glyph drawing data for the specified key. + /// + /// The glyph cache key. + /// The cached glyph drawing data when available. + /// + /// when cached data exists; otherwise, . + /// + internal bool TryGetValue(RichTextGlyphRenderer.CacheKey key, [NotNullWhen(true)] out List? value) + { + if (!this.entries.TryGetValue(key, out LinkedListNode? node)) + { + value = null; + return false; + } + + // Move the hit to the head so the least recently used entry stays at the tail. + this.usage.Remove(node); + this.usage.AddFirst(node); + value = node.Value.Value; + return true; + } + + /// + /// Gets existing glyph drawing data for the specified key, or creates a new cache entry. + /// + /// The glyph cache key. + /// + /// The glyph drawing data associated with . + /// + internal List GetOrAdd(RichTextGlyphRenderer.CacheKey key) + { + if (this.TryGetValue(key, out List? value)) + { + return value; + } + + value = []; + LinkedListNode node = new(new Entry(key, value)); + this.usage.AddFirst(node); + this.entries.Add(key, node); + + // Evict the least recently used entry once over capacity. + if (this.entries.Count > this.Capacity) + { + LinkedListNode last = this.usage.Last!; + this.usage.RemoveLast(); + _ = this.entries.Remove(last.Value.Key); + } + + return value; + } + + /// + /// Attempts to get a cached positioned glyph-run path. + /// + /// The positioned run path key. + /// The cached positioned path when available. + /// + /// when cached data exists; otherwise, . + /// + internal bool TryGetRunPath(RunPathCacheKey key, [NotNullWhen(true)] out IPath? path) + { + if (!this.runPathEntries.TryGetValue(key, out LinkedListNode? node)) + { + path = null; + return false; + } + + // Move the hit to the head so the least recently used entry stays at the tail. + this.runPathUsage.Remove(node); + this.runPathUsage.AddFirst(node); + path = node.Value.Path; + return true; + } + + /// + /// Stores a positioned glyph-run path. + /// + /// The positioned run path key. + /// The positioned path to cache. + internal void AddRunPath(RunPathCacheKey key, IPath path) + { + LinkedListNode node = new(new RunPathEntry(key, path)); + this.runPathUsage.AddFirst(node); + this.runPathEntries.Add(key, node); + + // Evict the least recently used entry once over capacity. + if (this.runPathEntries.Count > this.runPathCapacity) + { + LinkedListNode last = this.runPathUsage.Last!; + this.runPathUsage.RemoveLast(); + _ = this.runPathEntries.Remove(last.Value.Key); + } + } + + /// + /// A glyph cache entry pairing the key with its render data. The key is stored so + /// that eviction from the usage list can also remove the dictionary entry. + /// + private readonly struct Entry + { + /// + /// Initializes a new instance of the struct. + /// + /// The glyph cache key. + /// The glyph render data list. + public Entry(RichTextGlyphRenderer.CacheKey key, List value) + { + this.Key = key; + this.Value = value; + } + + /// + /// Gets the glyph cache key. + /// + public RichTextGlyphRenderer.CacheKey Key { get; } + + /// + /// Gets the glyph render data list (one entry per glyph layer). + /// + public List Value { get; } + } + + /// + /// Identifies a positioned glyph-run path. + /// + internal readonly struct RunPathCacheKey : IEquatable + { + /// + /// The positioned glyph path entries; only the first items belong to the key. + /// + private readonly RunPathCacheEntry[] entries; + + /// + /// The number of valid entries in . + /// + private readonly int count; + + /// + /// The precomputed hash of all valid entries. + /// + private readonly int hashCode; + + /// + /// Initializes a new instance of the struct. + /// + /// The positioned glyph path entries. + /// The number of entries that belong to the key. + public RunPathCacheKey(RunPathCacheEntry[] entries, int count) + { + this.entries = entries; + this.count = count; + + // Precompute the hash once: keys are immutable and hashed on every + // dictionary lookup, and the cheap hash comparison in Equals lets us + // skip the per-entry comparison loop for non-matching keys. + HashCode hash = default; + for (int i = 0; i < count; i++) + { + hash.Add(entries[i]); + } + + this.hashCode = hash.ToHashCode(); + } + + /// + /// Gets the number of glyph path entries in the key. + /// + public int Count => this.count; + + /// + /// Determines whether two instances are equal. + /// + /// The first key to compare. + /// The second key to compare. + /// + /// if the keys are equal; otherwise, . + /// + public static bool operator ==(RunPathCacheKey left, RunPathCacheKey right) => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The first key to compare. + /// The second key to compare. + /// + /// if the keys differ; otherwise, . + /// + public static bool operator !=(RunPathCacheKey left, RunPathCacheKey right) => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is RunPathCacheKey key && this.Equals(key); + + /// + public bool Equals(RunPathCacheKey other) + { + if (this.hashCode != other.hashCode || this.count != other.count) + { + return false; + } + + for (int i = 0; i < this.count; i++) + { + if (this.entries[i] != other.entries[i]) + { + return false; + } + } + + return true; + } + + /// + public override int GetHashCode() => this.hashCode; + } + + /// + /// Identifies one positioned glyph path inside a cached run path. + /// + internal readonly struct RunPathCacheEntry : IEquatable + { + /// + /// The reciprocal of the quantization step applied to + /// for key comparison. The exact value is used when baking geometry; quantizing only + /// the comparison absorbs the float noise that absolute-position subtraction introduces, + /// so rigidly moved runs keep matching. Runs whose exact relative layouts differ by + /// less than half a step share the first occurrence's geometry. + /// + private const float RelativeLocationAccuracyMultiple = 8F; + + /// + /// Initializes a new instance of the struct. + /// + /// The local glyph path. + /// The exact location relative to the run origin, including the fractional component. + /// The stable glyph cache key. + /// A value indicating whether is valid. + public RunPathCacheEntry( + IPath path, + Vector2 relativeLocation, + RichTextGlyphRenderer.CacheKey glyphKey, + bool hasGlyphKey) + { + this.Path = path; + this.RelativeLocation = relativeLocation; + this.GlyphKey = glyphKey; + this.HasGlyphKey = hasGlyphKey; + } + + /// + /// Gets the local glyph path. + /// + public IPath Path { get; } + + /// + /// Gets the exact location relative to the run origin, including the fractional + /// component. Run-relative locations make the cache key position independent: the same + /// run content drawn at a different absolute location, even a fractionally scrolled one, + /// produces the same key. + /// + public Vector2 RelativeLocation { get; } + + /// + /// Gets the stable glyph cache key. + /// + public RichTextGlyphRenderer.CacheKey GlyphKey { get; } + + /// + /// Gets a value indicating whether is valid. + /// + public bool HasGlyphKey { get; } + + /// + /// Determines whether two instances are equal. + /// + /// The first entry to compare. + /// The second entry to compare. + /// + /// if the entries are equal; otherwise, . + /// + public static bool operator ==(RunPathCacheEntry left, RunPathCacheEntry right) => left.Equals(right); + + /// + /// Determines whether two instances are not equal. + /// + /// The first entry to compare. + /// The second entry to compare. + /// + /// if the entries differ; otherwise, . + /// + public static bool operator !=(RunPathCacheEntry left, RunPathCacheEntry right) => !(left == right); + + /// + public override bool Equals(object? obj) + => obj is RunPathCacheEntry entry && this.Equals(entry); + + /// + public bool Equals(RunPathCacheEntry other) + { + // Prefer the stable glyph key: it matches identical glyphs across separate + // renders where the IPath instances differ. Without a key (e.g. uncached + // path-based text) fall back to path reference identity, which only matches + // within the same render. + if (this.HasGlyphKey && other.HasGlyphKey) + { + return this.GlyphKey.Equals(other.GlyphKey) + && QuantizeRelativeLocation(this.RelativeLocation) == QuantizeRelativeLocation(other.RelativeLocation); + } + + return ReferenceEquals(this.Path, other.Path) + && QuantizeRelativeLocation(this.RelativeLocation) == QuantizeRelativeLocation(other.RelativeLocation); + } + + /// + public override int GetHashCode() + => this.HasGlyphKey + ? HashCode.Combine(this.GlyphKey, QuantizeRelativeLocation(this.RelativeLocation)) + : HashCode.Combine(this.Path, QuantizeRelativeLocation(this.RelativeLocation)); + + /// + /// Quantizes a run-relative location to the key comparison grid. + /// + /// The exact run-relative location. + /// The location rounded to the comparison grid. + private static Vector2 QuantizeRelativeLocation(Vector2 relativeLocation) + => new( + MathF.Round(relativeLocation.X * RelativeLocationAccuracyMultiple) / RelativeLocationAccuracyMultiple, + MathF.Round(relativeLocation.Y * RelativeLocationAccuracyMultiple) / RelativeLocationAccuracyMultiple); + } + + /// + /// A run-path cache entry pairing the key with the combined positioned path. The key + /// is stored so that eviction from the usage list can also remove the dictionary entry. + /// + private readonly struct RunPathEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The positioned run path key. + /// The combined positioned path. + public RunPathEntry(RunPathCacheKey key, IPath path) + { + this.Key = key; + this.Path = path; + } + + /// + /// Gets the positioned run path key. + /// + public RunPathCacheKey Key { get; } + + /// + /// Gets the combined positioned path. + /// + public IPath Path { get; } + } +} diff --git a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs index 8fb79aa91..665a820ce 100644 --- a/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/EllipticGradientBrush.cs @@ -13,7 +13,9 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public sealed class EllipticGradientBrush : GradientBrush { - /// + /// + /// Initializes a new instance of the class. + /// /// The center of the elliptical gradient and 0 for the color stops. /// The end point of the reference axis of the ellipse. /// @@ -52,7 +54,7 @@ public EllipticGradientBrush( public float AxisRatio { get; } /// - public override Brush Transform(Matrix4x4 matrix) + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) { PointF tc = PointF.Transform(this.Center, matrix); PointF tRef = PointF.Transform(this.ReferenceAxisEnd, matrix); @@ -84,18 +86,36 @@ public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, - RectangleF region) => - new EllipticGradientBrushRenderer( + RectangleF region) + { + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + return new EllipticGradientBrushRenderer>( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + } + + return new EllipticGradientBrushRenderer>( configuration, options, canvasWidth, this, this.ColorStopsArray, this.RepetitionMode); + } - /// - private sealed class EllipticGradientBrushRenderer : GradientBrushRenderer + /// + /// The elliptic gradient brush applicator. + /// + /// The pixel format. + /// The destination representation encoder. + private sealed class EllipticGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { private readonly PointF center; @@ -108,7 +128,7 @@ private sealed class EllipticGradientBrushRenderer : GradientBrushRender private readonly float secondRadiusSquared; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. @@ -142,15 +162,20 @@ public EllipticGradientBrushRenderer( /// protected override float PositionOnGradient(float x, float y) { + // Translate the sample into center-relative coordinates, then rotate it by the + // negated reference-axis angle so the reference axis aligns with local x before + // measuring against the axis radii. Rotating by the positive angle instead would + // mirror the ellipse for any orientation that is not a multiple of 90 degrees. float x0 = x - this.center.X; float y0 = y - this.center.Y; - float xR = (x0 * this.cosRotation) - (y0 * this.sinRotation); - float yR = (x0 * this.sinRotation) + (y0 * this.cosRotation); + float xR = (x0 * this.cosRotation) + (y0 * this.sinRotation); + float yR = (y0 * this.cosRotation) - (x0 * this.sinRotation); float xSquared = xR * xR; float ySquared = yR * yR; + // Normalized elliptical distance: sqrt((x/a)^2 + (y/b)^2), 0 at the center. return MathF.Sqrt((xSquared / this.referenceRadiusSquared) + (ySquared / this.secondRadiusSquared)); } } diff --git a/src/ImageSharp.Drawing/Processing/GradientBrush.cs b/src/ImageSharp.Drawing/Processing/GradientBrush.cs index 2376b3e21..4070d1dde 100644 --- a/src/ImageSharp.Drawing/Processing/GradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/GradientBrush.cs @@ -6,12 +6,17 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Base class for Gradient brushes +/// Base class for gradient brushes. +/// Derived brushes define a parameterization that maps each point to a scalar gradient +/// position; this base class maps that position to a color via the sorted color stops +/// and the . /// public abstract class GradientBrush : Brush { - /// - /// Defines how the colors are repeated beyond the interval [0..1] + /// + /// Initializes a new instance of the class. + /// + /// Defines how the colors are repeated beyond the interval [0..1]. /// The gradient colors. protected GradientBrush(GradientRepetitionMode repetitionMode, params ColorStop[] colorStops) { @@ -57,6 +62,9 @@ public override int GetHashCode() /// is not stable and can reorder /// equal-ratio color stops, producing non-deterministic gradient results. /// + /// The element type of the collection. + /// The array to sort in place. + /// The comparison used to order the elements. private static void InsertionSort(T[] collection, Comparison comparison) { int count = collection.Length; @@ -75,20 +83,22 @@ private static void InsertionSort(T[] collection, Comparison comparison) } /// - /// Base class for gradient brush applicators + /// Base class for gradient brush applicators. /// /// The pixel format. - internal abstract class GradientBrushRenderer : BrushRenderer + /// The destination representation encoder. + internal abstract class GradientBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { private static readonly TPixel Transparent = Color.Transparent.ToPixel(); - private readonly ColorStop[] colorStops; + private readonly GradientColorStop[] colorStops; private readonly GradientRepetitionMode repetitionMode; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. @@ -103,17 +113,38 @@ protected GradientBrushRenderer( GradientRepetitionMode repetitionMode) : base(configuration, options, canvasWidth) { - this.colorStops = colorStops; + this.colorStops = new GradientColorStop[colorStops.Length]; + + // CSS Color 4 requires alpha to be premultiplied before color interpolation. + // Cache associated stop vectors once so the result is independent of TPixel storage + // and the per-pixel interpolation loop does not repeat the conversion. + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + for (int i = 0; i < colorStops.Length; i++) + { + ColorStop stop = colorStops[i]; + this.colorStops[i] = new GradientColorStop(stop.Ratio, stop.Color.ToScaledVector4(PixelAlphaRepresentation.Associated)); + } + this.repetitionMode = repetitionMode; } + /// + /// Gets the gradient color for the pixel at the given device coordinate. + /// + /// The x-coordinate of the pixel in device space. + /// The y-coordinate of the pixel in device space. + /// The blended gradient color converted to . internal TPixel this[int x, int y] { get { + // Evaluate at pixel centers so gradient sampling lines up with the + // rasterized coverage positions. float fx = x + 0.5f; float fy = y + 0.5f; + // NaN signals that the parameterization is undefined at this point + // (e.g. outside the valid branch of a conic); such pixels stay transparent. float positionOnCompleteGradient = this.PositionOnGradient(fx, fy); if (float.IsNaN(positionOnCompleteGradient)) { @@ -126,6 +157,8 @@ protected GradientBrushRenderer( positionOnCompleteGradient %= 1; break; case GradientRepetitionMode.Reflect: + // Fold every second period back on itself so alternating + // repetitions run the stops in reverse order. positionOnCompleteGradient %= 2; if (positionOnCompleteGradient > 1) { @@ -147,21 +180,16 @@ protected GradientBrushRenderer( break; } - (ColorStop from, ColorStop to) = this.GetGradientSegment(positionOnCompleteGradient); + (GradientColorStop from, GradientColorStop to) = this.GetGradientSegment(positionOnCompleteGradient); - if (from.Color.Equals(to.Color)) + if (from.Color == to.Color) { - return from.Color.ToPixel(); + return TEncoder.Encode(from.Color); } float onLocalGradient = (positionOnCompleteGradient - from.Ratio) / (to.Ratio - from.Ratio); - // TODO: This should use premultiplied vectors to avoid bad blends e.g. red -> brown <- green. - return Color.FromScaledVector( - Vector4.Lerp( - from.Color.ToScaledVector4(), - to.Color.ToScaledVector4(), - onLocalGradient)).ToPixel(); + return TEncoder.Encode(Vector4.Lerp(from.Color, to.Color, onLocalGradient)); } } @@ -173,40 +201,27 @@ public override void Apply( int y, BrushWorkspace workspace) { - Span amounts = workspace.GetAmounts(scanline.Length); Span overlays = workspace.GetOverlays(scanline.Length); - float blendPercentage = this.Options.BlendPercentage; // TODO: Remove bounds checks. - if (blendPercentage < 1) + for (int i = 0; i < scanline.Length; i++) { - for (int i = 0; i < scanline.Length; i++) - { - amounts[i] = scanline[i] * blendPercentage; - overlays[i] = this[x + i, y]; - } - } - else - { - for (int i = 0; i < scanline.Length; i++) - { - amounts[i] = scanline[i]; - overlays[i] = this[x + i, y]; - } + overlays[i] = this[x + i, y]; } - this.Blender.Blend( + this.Blender.BlendWithCoverage( this.Configuration, destinationRow, destinationRow, overlays, - amounts, + this.Options.BlendPercentage, + scanline, workspace.GetBlendScratch(scanline.Length, 3)); } /// /// Calculates the position on the gradient for a given point. - /// This method is abstract as it's content depends on the shape of the gradient. + /// This method is abstract as its content depends on the shape of the gradient. /// /// The x-coordinate of the point. /// The y-coordinate of the point. @@ -218,12 +233,21 @@ public override void Apply( /// protected abstract float PositionOnGradient(float x, float y); - private (ColorStop From, ColorStop To) GetGradientSegment(float positionOnCompleteGradient) + /// + /// Finds the pair of adjacent color stops bracketing the given gradient position. + /// Positions before the first stop return the first stop twice; positions after the + /// last stop return the last stop twice, which yields the stable edge colors used + /// by . + /// + /// The position on the gradient, after repetition handling. + /// The bracketing stops; equal when the position lies outside the stop range. + private (GradientColorStop From, GradientColorStop To) GetGradientSegment(float positionOnCompleteGradient) { - ColorStop localGradientFrom = this.colorStops[0]; - ColorStop localGradientTo = default; + // Stop counts are tiny, so a linear scan over the sorted stops beats a binary search. + GradientColorStop localGradientFrom = this.colorStops[0]; + GradientColorStop localGradientTo = default; - foreach (ColorStop colorStop in this.colorStops) + foreach (GradientColorStop colorStop in this.colorStops) { localGradientTo = colorStop; @@ -238,5 +262,23 @@ public override void Apply( return (localGradientFrom, localGradientTo); } + + /// + /// Stores one gradient stop in the associated representation used for interpolation. + /// + /// The stop position. + /// The associated stop color. + private readonly struct GradientColorStop(float ratio, Vector4 color) + { + /// + /// Gets the stop position. + /// + public float Ratio { get; } = ratio; + + /// + /// Gets the associated stop color. + /// + public Vector4 Color { get; } = color; + } } } diff --git a/src/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs b/src/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs index 879062560..d19deb01d 100644 --- a/src/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs +++ b/src/ImageSharp.Drawing/Processing/GradientRepetitionMode.cs @@ -22,12 +22,12 @@ public enum GradientRepetitionMode /// /// Reflect the gradient. /// Similar to , but each other repetition uses inverse order of s. - /// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->White... + /// Used on a Black-White gradient, Reflect leads to Black->{gray}->White->{gray}->Black... /// Reflect, /// - /// With DontFill a gradient does not touch any pixel beyond it's borders. + /// With DontFill a gradient does not touch any pixel beyond its borders. /// For the this is beyond the orthogonal through start and end, /// For and it's beyond 1.0. /// diff --git a/src/ImageSharp.Drawing/Processing/IGradientPixelEncoder.cs b/src/ImageSharp.Drawing/Processing/IGradientPixelEncoder.cs new file mode 100644 index 000000000..3c634f2c8 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/IGradientPixelEncoder.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Encodes an associated gradient result in the destination pixel representation. +/// +/// The destination pixel format. +internal interface IGradientPixelEncoder + where TPixel : unmanaged, IPixel +{ + /// + /// Converts an associated scaled vector to the destination pixel representation. + /// + /// The associated scaled vector. + /// The destination pixel. + public static abstract TPixel Encode(Vector4 color); +} + +/// +/// Encodes associated gradient results for an associated-alpha destination. +/// +/// The destination pixel format. +internal readonly struct AssociatedGradientPixelEncoder : IGradientPixelEncoder + where TPixel : unmanaged, IPixel +{ + /// + public static TPixel Encode(Vector4 color) => TPixel.FromScaledVector4(color); +} + +/// +/// Encodes associated gradient results for an unassociated-alpha destination. +/// +/// The destination pixel format. +internal readonly struct UnassociatedGradientPixelEncoder : IGradientPixelEncoder + where TPixel : unmanaged, IPixel +{ + /// + public static TPixel Encode(Vector4 color) + { + // CSS Color 4 undoes premultiplication only after interpolation. Straight targets require + // that conversion here, while associated targets store the interpolated vector directly. + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + float alpha = color.W; + if (alpha > 0) + { + color /= alpha; + color.W = alpha; + } + else + { + color = Vector4.Zero; + } + + return TPixel.FromScaledVector4(color); + } +} diff --git a/src/ImageSharp.Drawing/Processing/ImageBrush.cs b/src/ImageSharp.Drawing/Processing/ImageBrush.cs index ac0b070ef..9ef93e5b4 100644 --- a/src/ImageSharp.Drawing/Processing/ImageBrush.cs +++ b/src/ImageSharp.Drawing/Processing/ImageBrush.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Numerics; using System.Runtime.CompilerServices; namespace SixLabors.ImageSharp.Drawing.Processing; @@ -50,10 +51,75 @@ public ImageBrush(Image image, RectangleF region, Point offset) : base(image, region, offset) => this.SourceImage = image; + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + public ImageBrush(Image image, WrapMode wrapX, WrapMode wrapY) + : base(image, wrapX, wrapY) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// An offset to apply to the image while drawing the texture. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + public ImageBrush(Image image, Point offset, WrapMode wrapX, WrapMode wrapY) + : base(image, offset, wrapX, wrapY) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The region of interest within the source image. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + public ImageBrush(Image image, RectangleF region, WrapMode wrapX, WrapMode wrapY) + : base(image, region, wrapX, wrapY) + => this.SourceImage = image; + + /// + /// Initializes a new instance of the class. + /// + /// The source image to draw. + /// The region of interest within the source image. + /// An offset to apply to the image while drawing the texture. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + public ImageBrush(Image image, RectangleF region, Point offset, WrapMode wrapX, WrapMode wrapY) + : base(image, region, offset, wrapX, wrapY) + => this.SourceImage = image; + /// /// Gets the typed source image used by this brush. /// public Image SourceImage { get; } + + /// + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) + { + // The texture pixels are not transformed by the matrix; the brush anchors to the + // interest region origin, so when preparation moves that origin the offset must + // shift by the same delta to stop the texture visually sliding. + int offsetX = sourceInterest.X - preparedInterest.X; + int offsetY = sourceInterest.Y - preparedInterest.Y; + if (offsetX == 0 && offsetY == 0) + { + return this; + } + + return new ImageBrush( + this.SourceImage, + this.SourceRegion, + new Point(this.Offset.X + offsetX, this.Offset.Y + offsetY), + this.WrapX, + this.WrapY); + } } /// @@ -75,7 +141,7 @@ protected ImageBrush(Image image) /// /// The image. /// - /// An offset to apply the to image image while drawing apply the texture. + /// An offset to apply to the image while drawing the texture. /// protected ImageBrush(Image image, Point offset) : this(image, image.Bounds, offset) @@ -104,13 +170,71 @@ protected ImageBrush(Image image, RectangleF region) /// This overrides any region used to initialize the brush applicator. /// /// - /// An offset to apply the to image image while drawing apply the texture. + /// An offset to apply to the image while drawing the texture. /// protected ImageBrush(Image image, RectangleF region, Point offset) + : this(image, region, offset, WrapMode.Repeat, WrapMode.Repeat) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + protected ImageBrush(Image image, WrapMode wrapX, WrapMode wrapY) + : this(image, image.Bounds, Point.Empty, wrapX, wrapY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// An offset to apply to the image while drawing the texture. + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + protected ImageBrush(Image image, Point offset, WrapMode wrapX, WrapMode wrapY) + : this(image, image.Bounds, offset, wrapX, wrapY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + protected ImageBrush(Image image, RectangleF region, WrapMode wrapX, WrapMode wrapY) + : this(image, region, Point.Empty, wrapX, wrapY) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The image. + /// + /// The region of interest. + /// This overrides any region used to initialize the brush applicator. + /// + /// + /// An offset to apply to the image while drawing the texture. + /// + /// The wrap mode used when sampling horizontally beyond the source region. + /// The wrap mode used when sampling vertically beyond the source region. + protected ImageBrush(Image image, RectangleF region, Point offset, WrapMode wrapX, WrapMode wrapY) { this.UntypedImage = image; this.SourceRegion = RectangleF.Intersect(image.Bounds, region); this.Offset = offset; + this.WrapX = wrapX; + this.WrapY = wrapY; } /// @@ -128,19 +252,32 @@ protected ImageBrush(Image image, RectangleF region, Point offset) /// public Point Offset { get; } + /// + /// Gets the wrap mode used when sampling horizontally beyond the . + /// + public WrapMode WrapX { get; } + + /// + /// Gets the wrap mode used when sampling vertically beyond the . + /// + public WrapMode WrapY { get; } + /// public override bool Equals(Brush? other) { if (other is ImageBrush ib) { - return ib.UntypedImage == this.UntypedImage && ib.SourceRegion == this.SourceRegion; + return ib.UntypedImage == this.UntypedImage + && ib.SourceRegion == this.SourceRegion + && ib.WrapX == this.WrapX + && ib.WrapY == this.WrapY; } return false; } /// - public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion); + public override int GetHashCode() => HashCode.Combine(this.UntypedImage, this.SourceRegion, this.WrapX, this.WrapY); /// public override BrushRenderer CreateRenderer( @@ -151,7 +288,7 @@ public override BrushRenderer CreateRenderer( { if (this.UntypedImage is Image image) { - return new ImageBrushRenderer(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset); + return new ImageBrushRenderer(configuration, options, canvasWidth, image, region, this.SourceRegion, this.Offset, this.WrapX, this.WrapY); } // This will never be hit as the brush is always normalized by the drawing canvas @@ -179,6 +316,16 @@ private sealed class ImageBrushRenderer : BrushRenderer /// private readonly Rectangle sourceRegion; + /// + /// The wrap mode applied horizontally when sampling beyond the source region. + /// + private readonly WrapMode wrapX; + + /// + /// The wrap mode applied vertically when sampling beyond the source region. + /// + private readonly WrapMode wrapY; + /// /// The Y offset. /// @@ -199,6 +346,8 @@ private sealed class ImageBrushRenderer : BrushRenderer /// The region of the target image we will be drawing to. /// The region of the source image we will be using to source pixels to draw from. /// An offset to apply to the texture while drawing. + /// The horizontal wrap mode. + /// The vertical wrap mode. public ImageBrushRenderer( Configuration configuration, GraphicsOptions options, @@ -206,23 +355,39 @@ public ImageBrushRenderer( Image image, RectangleF targetRegion, RectangleF sourceRegion, - Point offset) + Point offset, + WrapMode wrapX, + WrapMode wrapY) : base(configuration, options, canvasWidth) { this.sourceFrame = image.Frames.RootFrame; this.sourceRegion = Rectangle.Intersect(image.Bounds, (Rectangle)sourceRegion); + this.wrapX = wrapX; + this.wrapY = wrapY; this.offsetY = (int)MathF.Floor(targetRegion.Top) + offset.Y; this.offsetX = (int)MathF.Floor(targetRegion.Left) + offset.X; } + /// + /// Gets the texture pixel for the given device coordinate after applying the + /// brush origin offset and the per-axis wrap modes. + /// + /// The x-coordinate of the pixel in device space. + /// The y-coordinate of the pixel in device space. + /// The sampled pixel, or transparent when outside an unwrapped region. internal TPixel this[int x, int y] { get { - int srcX = ((x - this.offsetX) % this.sourceRegion.Width) + this.sourceRegion.X; - int srcY = ((y - this.offsetY) % this.sourceRegion.Height) + this.sourceRegion.Y; - return this.sourceFrame[srcX, srcY]; + if (TryWrap(x - this.offsetX, this.sourceRegion.Width, this.sourceRegion.X, this.wrapX, out int srcX) + && TryWrap(y - this.offsetY, this.sourceRegion.Height, this.sourceRegion.Y, this.wrapY, out int srcY)) + { + return this.sourceFrame[srcX, srcY]; + } + + // None wrap mode outside the source region samples as transparent. + return default; } } @@ -234,33 +399,93 @@ public override void Apply( int y, BrushWorkspace workspace) { - Span amountSpan = workspace.GetAmounts(scanline.Length); + Span coverageSpan = workspace.GetAmounts(scanline.Length); Span overlaySpan = workspace.GetOverlays(scanline.Length); - int offsetX = x - this.offsetX; - int sourceY = ((((y - this.offsetY) % this.sourceRegion.Height) // clamp the number between -height and +height - + this.sourceRegion.Height) % this.sourceRegion.Height) // clamp the number between 0 and +height - + this.sourceRegion.Y; - Span sourceRow = this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY); + int baseX = x - this.offsetX; + bool rowInRange = TryWrap(y - this.offsetY, this.sourceRegion.Height, this.sourceRegion.Y, this.wrapY, out int sourceY); + Span sourceRow = rowInRange + ? this.sourceFrame.PixelBuffer.DangerousGetRowSpan(sourceY) + : default; for (int i = 0; i < scanline.Length; i++) { - amountSpan[i] = scanline[i] * this.Options.BlendPercentage; - - int sourceX = ((((i + offsetX) % this.sourceRegion.Width) // clamp the number between -width and +width - + this.sourceRegion.Width) % this.sourceRegion.Width) // clamp the number between 0 and +width - + this.sourceRegion.X; - - overlaySpan[i] = sourceRow[sourceX]; + if (rowInRange && TryWrap(baseX + i, this.sourceRegion.Width, this.sourceRegion.X, this.wrapX, out int sourceX)) + { + coverageSpan[i] = scanline[i]; + overlaySpan[i] = sourceRow[sourceX]; + } + else + { + // None wrap mode outside the source region: contribute nothing. + coverageSpan[i] = 0; + overlaySpan[i] = default; + } } - this.Blender.Blend( + this.Blender.BlendWithCoverage( this.Configuration, destinationRow, destinationRow, overlaySpan, - amountSpan, + this.Options.BlendPercentage, + coverageSpan, workspace.GetBlendScratch(scanline.Length, 3)); } + + /// + /// Maps a target coordinate (relative to the brush origin) to a source coordinate within the + /// region, applying the per-axis wrap mode. Returns for + /// when the coordinate falls outside the region. + /// + /// The brush-origin-relative coordinate to map. + /// The size of the source region along this axis. + /// The start of the source region along this axis, in source image space. + /// The wrap mode to apply along this axis. + /// Receives the mapped coordinate in source image space. + /// if the coordinate maps to a source pixel; otherwise . + private static bool TryWrap(int coordinate, int size, int regionStart, WrapMode mode, out int source) + { + if (size <= 0) + { + source = regionStart; + return false; + } + + switch (mode) + { + case WrapMode.None: + if (coordinate < 0 || coordinate >= size) + { + source = regionStart; + return false; + } + + source = coordinate + regionStart; + return true; + + case WrapMode.Mirror: + // Wrap into a double period using a true (non-negative) modulo, then + // reflect the second half so adjacent tiles are mirror images. + int period = size * 2; + int mirrored = ((coordinate % period) + period) % period; + if (mirrored >= size) + { + mirrored = period - 1 - mirrored; + } + + source = mirrored + regionStart; + return true; + + case WrapMode.Clamp: + source = Math.Clamp(coordinate, 0, size - 1) + regionStart; + return true; + + default: // Repeat + // True modulo so negative coordinates tile correctly. + source = (((coordinate % size) + size) % size) + regionStart; + return true; + } + } } } diff --git a/src/ImageSharp.Drawing/Processing/LayerEffect.cs b/src/ImageSharp.Drawing/Processing/LayerEffect.cs new file mode 100644 index 000000000..2c54fd4e1 --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/LayerEffect.cs @@ -0,0 +1,379 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Describes an image effect applied to the contents of a compositing layer when it is restored. +/// The layer isolates the content the effect operates on, so the effect sees only what was drawn +/// into the layer, against transparency, before the result is composited onto the canvas. +/// +public abstract class LayerEffect +{ + private readonly bool isPassThrough; + private readonly GraphicsOptions? writeBackOptions; + private readonly Point writeBackOffset; + private readonly Action operation; + private readonly int reach; + + /// + /// Initializes a new instance of the class. + /// + /// The operation that implements the effect. + protected LayerEffect(Action operation) + { + Guard.NotNull(operation, nameof(operation)); + this.operation = operation; + } + + /// + /// Initializes a new instance of the class with the observable + /// behavior of another effect when this effect is not executed natively. + /// + /// The effect whose behavior is used as the fallback. + protected LayerEffect(LayerEffect fallbackEffect) + { + Guard.NotNull(fallbackEffect, nameof(fallbackEffect)); + this.isPassThrough = fallbackEffect.IsPassThrough; + this.writeBackOptions = fallbackEffect.WriteBackOptions; + this.writeBackOffset = fallbackEffect.WriteBackOffset; + this.operation = fallbackEffect.CreateOperation(); + this.reach = fallbackEffect.Reach; + } + + /// + /// Gets the distance, in pixels, the effect can push content beyond its source region. Layer + /// and processing bounds are expanded by this reach so the effect output is not cut off. + /// Effects constructed over a fallback inherit the fallback's reach. + /// + public virtual int Reach => this.reach; + + /// + /// Gets a value indicating whether the effect leaves the layer content unchanged, in which + /// case its application is skipped entirely. + /// + public virtual bool IsPassThrough => this.isPassThrough; + + /// + /// Gets the graphics options used to composite the processed pixels back onto the layer, or + /// to replace the processed region outright. + /// + public virtual GraphicsOptions? WriteBackOptions => this.writeBackOptions; + + /// + /// Gets the offset, in pixels, at which the processed pixels are written back relative to the + /// region they were read from. + /// + public virtual Point WriteBackOffset => this.writeBackOffset; + + /// + /// Creates the image-processing operation that transforms the layer snapshot into the effect + /// output. + /// + /// The operation to run against the layer snapshot. + internal Action CreateOperation() => this.operation; +} + +/// +/// Blurs the contents of a compositing layer when it is restored. +/// +public sealed class BlurLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels; zero leaves the content unchanged. + public BlurLayerEffect(float sigma) + : base(context => context.GaussianBlur(sigma)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Sigma = sigma; + } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + public override int Reach => (int)MathF.Ceiling(this.Sigma * 3F) + 1; + + /// + public override bool IsPassThrough => this.Sigma == 0; + + /// + public override GraphicsOptions? WriteBackOptions => null; + + /// + public override Point WriteBackOffset => default; +} + +/// +/// Composites a drop shadow beneath the contents of a compositing layer when it is restored. The +/// shadow is the content's own silhouette tinted with the shadow colour, blurred, and offset. +/// +public sealed class DropShadowLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The shadow offset, in pixels. + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// + /// The shadow colour. Its alpha scales the content's alpha, so a semi-transparent colour draws + /// a semi-transparent shadow. + /// + public DropShadowLayerEffect(Point offset, float sigma, Color color) + : base(CreateOperation(sigma, color)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + public override int Reach + => (int)MathF.Ceiling(this.Sigma * 3F) + Math.Max(Math.Abs(this.Offset.X), Math.Abs(this.Offset.Y)) + 1; + + /// + public override GraphicsOptions? WriteBackOptions + => new() { AlphaCompositionMode = PixelAlphaCompositionMode.DestOver }; + + /// + public override Point WriteBackOffset => this.Offset; + + /// + /// Creates the CPU drop-shadow operation for the supplied effect values. + /// + /// The Gaussian blur sigma. + /// The shadow colour. + /// The configured image-processing operation. + private static Action CreateOperation(float sigma, Color color) + { + // The tint replaces the snapshot's colour with the constant shadow colour and scales its + // alpha. The colour filter operates on straight alpha, so with the RGB rows zeroed the + // constant row sets the colour outright; because the colour is then constant across the + // snapshot, the blur only spreads alpha and cannot bleed foreign colours into the shadow. + Vector4 vector = color.ToPixel().ToScaledVector4(); + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + return context => + { + context.Filter(tint); + if (sigma > 0) + { + context.GaussianBlur(sigma); + } + }; + } +} + +/// +/// Composites a glow beneath the contents of a compositing layer when it is restored: the content's +/// own silhouette tinted with the glow colour and blurred, spreading evenly in all directions. +/// +public sealed class GlowLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The Gaussian blur sigma, in pixels, controlling the glow's spread. + /// + /// The glow colour. Its alpha scales the content's alpha, so a semi-transparent colour draws a + /// fainter glow. + /// + public GlowLayerEffect(float sigma, Color color) + : base(CreateOperation(sigma, color)) + { + Guard.MustBeGreaterThan(sigma, 0, nameof(sigma)); + this.Sigma = sigma; + this.Color = color; + } + + /// + /// Gets the Gaussian blur sigma, in pixels, controlling the glow's spread. + /// + public float Sigma { get; } + + /// + /// Gets the glow colour. + /// + public Color Color { get; } + + /// + public override int Reach => (int)MathF.Ceiling(this.Sigma * 3F) + 1; + + /// + public override GraphicsOptions? WriteBackOptions + => new() { AlphaCompositionMode = PixelAlphaCompositionMode.DestOver }; + + /// + public override Point WriteBackOffset => default; + + /// + /// Creates the CPU glow operation for the supplied effect values. + /// + /// The Gaussian blur sigma. + /// The glow colour. + /// The configured image-processing operation. + private static Action CreateOperation(float sigma, Color color) + { + // The tint replaces the snapshot's colour with the constant glow colour and scales its + // alpha. The colour filter operates on straight alpha, so with the RGB rows zeroed the + // constant row sets the colour outright; because the colour is then constant across the + // snapshot, the blur only spreads alpha and cannot bleed foreign colours into the glow. + Vector4 vector = color.ToPixel().ToScaledVector4(); + ColorMatrix tint = default; + tint.M44 = vector.W; + tint.M51 = vector.X; + tint.M52 = vector.Y; + tint.M53 = vector.Z; + + return context => + { + context.Filter(tint); + context.GaussianBlur(sigma); + }; + } +} + +/// +/// Composites a shadow inside the contents of a compositing layer when it is restored: the darkness +/// outside the content's silhouette is tinted with the shadow colour, blurred, offset, and clipped +/// onto the content itself, so the shadow hugs the content's inside edges. +/// +public sealed class InnerShadowLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// + /// The shadow offset, in pixels. Positive values shade the content's top and left inside edges, + /// matching a CSS inset shadow. + /// + /// The Gaussian blur sigma, in pixels; zero draws a hard shadow. + /// + /// The shadow colour. Its alpha scales the shadow's alpha, so a semi-transparent colour draws a + /// semi-transparent shadow. + /// + public InnerShadowLayerEffect(Point offset, float sigma, Color color) + : base(CreateOperation(sigma, color)) + { + Guard.MustBeGreaterThanOrEqualTo(sigma, 0, nameof(sigma)); + this.Offset = offset; + this.Sigma = sigma; + this.Color = color; + } + + /// + /// Gets the shadow offset, in pixels. + /// + public Point Offset { get; } + + /// + /// Gets the Gaussian blur sigma, in pixels. + /// + public float Sigma { get; } + + /// + /// Gets the shadow colour. + /// + public Color Color { get; } + + /// + public override int Reach + => (int)MathF.Ceiling(this.Sigma * 3F) + Math.Max(Math.Abs(this.Offset.X), Math.Abs(this.Offset.Y)) + 1; + + /// + public override GraphicsOptions? WriteBackOptions + => new() { AlphaCompositionMode = PixelAlphaCompositionMode.SrcAtop }; + + /// + public override Point WriteBackOffset => this.Offset; + + /// + /// Creates the CPU inner-shadow operation for the supplied effect values. + /// + /// The Gaussian blur sigma. + /// The shadow colour. + /// The configured image-processing operation. + private static Action CreateOperation(float sigma, Color color) + { + // The shadow is the blurred INVERSE of the content's silhouette: everywhere the content is + // not, tinted with the shadow colour. The matrix maps alpha to colourAlpha * (1 - alpha) + // with the constant RGB rows carrying the shadow colour, the blur feathers the boundary, + // and the SrcAtop write-back clips the result onto the content so only the parts that + // reach inside its edges remain. + Vector4 vector = color.ToPixel().ToScaledVector4(); + ColorMatrix invertedTint = default; + invertedTint.M44 = -vector.W; + invertedTint.M54 = vector.W; + invertedTint.M51 = vector.X; + invertedTint.M52 = vector.Y; + invertedTint.M53 = vector.Z; + + return context => + { + context.Filter(invertedTint); + if (sigma > 0) + { + context.GaussianBlur(sigma); + } + }; + } +} + +/// +/// Transforms the colours of a compositing layer's contents through a colour matrix when the layer +/// is restored. Covers the CSS filter family: grayscale, sepia, saturation, hue rotation, +/// brightness, contrast, and invert are all colour matrices. +/// +public sealed class ColorMatrixLayerEffect : LayerEffect +{ + /// + /// Initializes a new instance of the class. + /// + /// The colour matrix applied to the layer content. + public ColorMatrixLayerEffect(ColorMatrix matrix) + : base(context => context.Filter(matrix)) + => this.Matrix = matrix; + + /// + /// Gets the colour matrix applied to the layer content. + /// + public ColorMatrix Matrix { get; } + + /// + public override int Reach => 0; + + /// + public override GraphicsOptions? WriteBackOptions => null; + + /// + public override Point WriteBackOffset => default; + + /// + public override bool IsPassThrough => this.Matrix == ColorMatrix.Identity; +} diff --git a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs index e378cabdc..b59dd7896 100644 --- a/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/LinearGradientBrush.cs @@ -65,7 +65,7 @@ public LinearGradientBrush( public PointF EndPoint { get; } /// - public override Brush Transform(Matrix4x4 matrix) + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) => new LinearGradientBrush( PointF.Transform(this.StartPoint, matrix), PointF.Transform(this.EndPoint, matrix), @@ -136,20 +136,35 @@ public override BrushRenderer CreateRenderer( GraphicsOptions options, int canvasWidth, RectangleF region) - => new LinearGradientBrushRenderer( + { + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + return new LinearGradientBrushRenderer>( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + } + + return new LinearGradientBrushRenderer>( configuration, options, canvasWidth, this, this.ColorStopsArray, this.RepetitionMode); + } /// /// Implements the gradient application logic for . /// /// The pixel format. - private sealed class LinearGradientBrushRenderer : GradientBrushRenderer + /// The destination representation encoder. + private sealed class LinearGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { private readonly PointF start; private readonly float alongX; @@ -157,7 +172,7 @@ private sealed class LinearGradientBrushRenderer : GradientBrushRenderer private readonly float alongsSquared; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The ImageSharp configuration. /// The graphics options. @@ -186,9 +201,13 @@ protected override float PositionOnGradient(float x, float y) { if (this.alongsSquared == 0f) { + // Degenerate zero-length axis: treat every point as sitting at the gradient end. return 1f; } + // Scalar projection onto the gradient axis: + // t = dot(p - start, axis) / |axis|^2, giving 0 at the start point and 1 at the + // end point. Values outside [0..1] are handled by the repetition mode. float deltaX = x - this.start.X; float deltaY = y - this.start.Y; return ((deltaX * this.alongX) + (deltaY * this.alongY)) / this.alongsSquared; diff --git a/src/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs b/src/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs index 276a9bad2..c91682124 100644 --- a/src/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs +++ b/src/ImageSharp.Drawing/Processing/PaintProcessor{TPixel}.cs @@ -37,6 +37,9 @@ public PaintProcessor( /// protected override void OnFrameApply(ImageFrame source) { + // The callback only records work. Disposing the canvas finalizes open state + // (layers, clips) and replays the recorded timeline into the frame, so the + // using scope is what commits the painting. using DrawingCanvas canvas = source.CreateCanvas(this.Configuration, this.definition.Options); this.action(canvas); } diff --git a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs index 423c6d2dc..c84cf3bfd 100644 --- a/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PathGradientBrush.cs @@ -9,6 +9,10 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// /// Provides an implementation of a brush for painting gradients between multiple color positions in 2D coordinates. /// +/// +/// Colors are assigned to the polygon points in order; if fewer colors than points are +/// supplied the colors are reused cyclically. +/// public sealed class PathGradientBrush : Brush { private readonly PointF[] points; @@ -77,7 +81,7 @@ public PathGradientBrush(PointF[] points, Color[] colors, Color centerColor) public bool HasExplicitCenterColor { get; } /// - public override Brush Transform(Matrix4x4 matrix) + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) { if (matrix.IsIdentity) { @@ -118,37 +122,58 @@ public override BrushRenderer CreateRenderer( GraphicsOptions options, int canvasWidth, RectangleF region) - => new PathGradientBrushRenderer( + { + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + return new PathGradientBrushRenderer>( + configuration, + options, + canvasWidth, + this.edges, + this.CenterColor, + this.HasExplicitCenterColor); + } + + return new PathGradientBrushRenderer>( configuration, options, canvasWidth, this.edges, this.CenterColor, this.HasExplicitCenterColor); + } + /// + /// Computes the default center color as the arithmetic mean of the point colors + /// in scaled vector space. + /// + /// The point colors to average. + /// The averaged color. private static Color CalculateCenterColor(Color[] colors) { Guard.NotNull(colors, nameof(colors)); Guard.MustBeGreaterThan(colors.Length, 0, nameof(colors)); - return Color.FromScaledVector(colors.Select(c => c.ToScaledVector4()).Aggregate((p1, p2) => p1 + p2) / colors.Length); - } - - private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length(); - - private readonly struct Intersection - { - public Intersection(PointF point, float distance) + Vector4 sum = Vector4.Zero; + foreach (Color color in colors) { - this.Point = point; - this.Distance = distance; + // CSS Color 4 requires alpha to be premultiplied before color interpolation. The + // implicit center therefore averages associated values just like the rendered gradient. + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + sum += color.ToScaledVector4(PixelAlphaRepresentation.Associated); } - public PointF Point { get; } - - public float Distance { get; } + return Color.FromScaledVector(sum / colors.Length, PixelAlphaRepresentation.Associated); } + /// + /// Computes the Euclidean distance between two points. + /// + /// The first point. + /// The second point. + /// The distance between the points. + private static float DistanceBetween(Vector2 p1, Vector2 p2) => (p2 - p1).Length(); + /// /// An edge of the polygon that represents the gradient area. /// @@ -156,39 +181,68 @@ private class Edge : IEquatable { private readonly float length; + /// + /// Initializes a new instance of the class. + /// + /// The start point of the edge. + /// The end point of the edge. + /// The color at the start point. + /// The color at the end point. public Edge(Vector2 start, Vector2 end, Color startColor, Color endColor) { this.Start = start; this.End = end; - this.StartColor = startColor.ToScaledVector4(); - this.EndColor = endColor.ToScaledVector4(); + this.StartColor = startColor; + this.EndColor = endColor; this.length = DistanceBetween(this.End, this.Start); } + /// + /// Gets the start point of the edge. + /// public Vector2 Start { get; } + /// + /// Gets the end point of the edge. + /// public Vector2 End { get; } - public Vector4 StartColor { get; } + /// + /// Gets the color at the start point. + /// + public Color StartColor { get; } - public Vector4 EndColor { get; } + /// + /// Gets the color at the end point. + /// + public Color EndColor { get; } + /// + /// Tests whether the segment from to + /// intersects this edge, ignoring collinear overlaps. + /// + /// The start point of the query segment. + /// The end point of the query segment. + /// Receives the intersection point when the segments intersect. + /// if the segments intersect; otherwise . public bool Intersect( Vector2 start, Vector2 end, ref Vector2 ip) => PolygonUtilities.LineSegmentToLineSegmentIgnoreCollinear(start, end, this.Start, this.End, ref ip); - public Vector4 ColorAt(float distance) - { - float ratio = this.length > 0 ? distance / this.length : 0; - - return Vector4.Lerp(this.StartColor, this.EndColor, ratio); - } + /// + /// Gets the interpolation ratio at the given point, measured by its distance from the edge start. + /// + /// The point on or near the edge. + /// The interpolation ratio from the edge start to the edge end. + public float GetInterpolationRatio(PointF point) - public Vector4 ColorAt(PointF point) => this.ColorAt(DistanceBetween(point, this.Start)); + // Zero-length edges have no gradient direction; use the start color. + => this.length > 0 ? DistanceBetween(point, this.Start) / this.length : 0; + /// public bool Equals(Edge? other) => other != null && other.Start == this.Start && @@ -196,8 +250,10 @@ public bool Equals(Edge? other) other.StartColor.Equals(this.StartColor) && other.EndColor.Equals(this.EndColor); + /// public override bool Equals(object? obj) => this.Equals(obj as Edge); + /// public override int GetHashCode() => HashCode.Combine(this.Start, this.End, this.StartColor, this.EndColor); } @@ -206,8 +262,10 @@ public override int GetHashCode() /// The path gradient brush applicator. /// /// The pixel format. - private sealed class PathGradientBrushRenderer : BrushRenderer + /// The destination representation encoder. + private sealed class PathGradientBrushRenderer : BrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { private readonly Vector2 center; @@ -219,12 +277,14 @@ private sealed class PathGradientBrushRenderer : BrushRenderer private readonly IList edges; + private readonly (Vector4 StartColor, Vector4 EndColor)[] edgeColors; + private readonly TPixel centerPixel; private readonly TPixel transparentPixel; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. @@ -242,16 +302,34 @@ public PathGradientBrushRenderer( : base(configuration, options, canvasWidth) { this.edges = edges; + this.edgeColors = new (Vector4 StartColor, Vector4 EndColor)[edges.Count]; Vector2[] points = [.. edges.Select(s => s.Start)]; + // CSS Color 4 requires alpha to be premultiplied before color interpolation. Cache + // associated inputs without packing through TPixel so destination storage cannot + // change the gradient and conversion remains outside the hot loop. + // https://www.w3.org/TR/css-color-4/#interpolation-alpha + for (int i = 0; i < edges.Count; i++) + { + this.edgeColors[i] = ( + edges[i].StartColor.ToScaledVector4(PixelAlphaRepresentation.Associated), + edges[i].EndColor.ToScaledVector4(PixelAlphaRepresentation.Associated)); + } + this.center = points.Aggregate((p1, p2) => p1 + p2) / edges.Count; - this.centerColor = centerColor.ToScaledVector4(); this.hasSpecialCenterColor = hasSpecialCenterColor; - this.centerPixel = centerColor.ToPixel(); + this.centerColor = centerColor.ToScaledVector4(PixelAlphaRepresentation.Associated); + this.centerPixel = TEncoder.Encode(this.centerColor); this.maxDistance = points.Select(p => p - this.center).Max(d => d.Length()); this.transparentPixel = Color.Transparent.ToPixel(); } + /// + /// Gets the gradient color for the pixel at the given device coordinate. + /// + /// The x-coordinate of the pixel in device space. + /// The y-coordinate of the pixel in device space. + /// The gradient color, or transparent where the gradient is undefined. internal TPixel this[int x, int y] { get @@ -264,6 +342,8 @@ public PathGradientBrushRenderer( return this.centerPixel; } + // Triangles without an explicit center color can be shaded exactly + // with barycentric interpolation of the three vertex colors. if (this.edges.Count == 3 && !this.hasSpecialCenterColor) { if (!FindPointOnTriangle( @@ -277,17 +357,21 @@ public PathGradientBrushRenderer( return this.transparentPixel; } - Vector4 pointColor = ((1 - u - v) * this.edges[0].StartColor) - + (u * this.edges[0].EndColor) - + (v * this.edges[2].StartColor); + Vector4 pointColor = ((1 - u - v) * this.edgeColors[0].StartColor) + + (u * this.edgeColors[0].EndColor) + + (v * this.edgeColors[2].StartColor); - return TPixel.FromScaledVector4(pointColor); + return TEncoder.Encode(pointColor); } + // General case: cast a ray from the sample point away from the center and + // find the closest edge it hits. The final color interpolates twice: + // along the edge for the boundary color, then between that boundary color + // and the center color by the point's relative distance to the center. Vector2 direction = Vector2.Normalize(point - this.center); Vector2 end = point + (direction * this.maxDistance); - (Edge Edge, Vector2 Point)? isc = this.FindIntersection(point, end); + (Edge Edge, int EdgeIndex, Vector2 Point)? isc = this.FindIntersection(point, end); if (!isc.HasValue) { @@ -295,14 +379,18 @@ public PathGradientBrushRenderer( } Vector2 intersection = isc.Value.Point; - Vector4 edgeColor = isc.Value.Edge.ColorAt(intersection); + int edgeIndex = isc.Value.EdgeIndex; + (Vector4 startColor, Vector4 endColor) = this.edgeColors[edgeIndex]; + Vector4 edgeColor = Vector4.Lerp(startColor, endColor, isc.Value.Edge.GetInterpolationRatio(intersection)); + + // ratio is 0 on the boundary (edge color) and 1 at the center (center color). float length = DistanceBetween(intersection, this.center); float ratio = length > 0 ? DistanceBetween(intersection, point) / length : 0; Vector4 color = Vector4.Lerp(edgeColor, this.centerColor, ratio); - return TPixel.FromScaledVector4(color); + return TEncoder.Encode(color); } } @@ -314,47 +402,43 @@ public override void Apply( int y, BrushWorkspace workspace) { - Span amounts = workspace.GetAmounts(scanline.Length); Span overlays = workspace.GetOverlays(scanline.Length); - float blendPercentage = this.Options.BlendPercentage; // TODO: Remove bounds checks. - if (blendPercentage < 1) + for (int i = 0; i < scanline.Length; i++) { - for (int i = 0; i < scanline.Length; i++) - { - amounts[i] = scanline[i] * blendPercentage; - overlays[i] = this[x + i, y]; - } - } - else - { - for (int i = 0; i < scanline.Length; i++) - { - amounts[i] = scanline[i]; - overlays[i] = this[x + i, y]; - } + overlays[i] = this[x + i, y]; } - this.Blender.Blend( + this.Blender.BlendWithCoverage( this.Configuration, destinationRow, destinationRow, overlays, - amounts, + this.Options.BlendPercentage, + scanline, workspace.GetBlendScratch(scanline.Length, 3)); } - private (Edge Edge, Vector2 Point)? FindIntersection( - PointF start, - PointF end) + /// + /// Finds the polygon edge whose intersection with the given segment lies + /// closest to the segment start. + /// + /// The start point of the query segment. + /// The end point of the query segment. + /// The closest intersected edge, its index, and the intersection point, or when no edge is hit. + private (Edge Edge, int EdgeIndex, Vector2 Point)? FindIntersection(PointF start, PointF end) { Vector2 ip = default; Vector2 closestIntersection = default; Edge? closestEdge = null; + int closestEdgeIndex = 0; float minDistance = float.MaxValue; - foreach (Edge edge in this.edges) + + for (int i = 0; i < this.edges.Count; i++) { + Edge edge = this.edges[i]; + if (!edge.Intersect(start, end, ref ip)) { continue; @@ -365,13 +449,25 @@ public override void Apply( { minDistance = d; closestEdge = edge; + closestEdgeIndex = i; closestIntersection = ip; } } - return closestEdge != null ? (closestEdge, closestIntersection) : null; + return closestEdge != null ? (closestEdge, closestEdgeIndex, closestIntersection) : null; } + /// + /// Computes the barycentric coordinates of a point relative to a triangle, + /// rejecting points that lie outside it. + /// + /// The first triangle vertex. + /// The second triangle vertex. + /// The third triangle vertex. + /// The point to test. + /// Receives the barycentric weight of . + /// Receives the barycentric weight of . + /// if the point lies inside the triangle; otherwise . private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vector2 point, out float u, out float v) { Vector2 e1 = v2 - v1; @@ -382,6 +478,8 @@ private static bool FindPointOnTriangle(Vector2 v1, Vector2 v2, Vector2 v3, Vect Vector2 pv2 = point - v2; Vector2 pv3 = point - v3; + // The z-components of these cross products give the point's side of each + // edge; mixed signs mean the point is outside the triangle. Vector3 d1 = Vector3.Cross(new Vector3(e1.X, e1.Y, 0), new Vector3(pv1.X, pv1.Y, 0)); Vector3 d2 = Vector3.Cross(new Vector3(e2.X, e2.Y, 0), new Vector3(pv2.X, pv2.Y, 0)); Vector3 d3 = Vector3.Cross(new Vector3(e3.X, e3.Y, 0), new Vector3(pv3.X, pv3.Y, 0)); diff --git a/src/ImageSharp.Drawing/Processing/PatternBrush.cs b/src/ImageSharp.Drawing/Processing/PatternBrush.cs index a05d7b498..d5e152797 100644 --- a/src/ImageSharp.Drawing/Processing/PatternBrush.cs +++ b/src/ImageSharp.Drawing/Processing/PatternBrush.cs @@ -29,9 +29,9 @@ public sealed class PatternBrush : Brush /// /// Initializes a new instance of the class. /// - /// Color of the fore. - /// Color of the back. - /// The pattern. + /// The color used where the pattern flag is . + /// The color used where the pattern flag is . + /// The pattern flags, indexed [row, column]. public PatternBrush(Color foreColor, Color backColor, bool[,] pattern) : this(foreColor, backColor, new DenseMatrix(pattern)) { @@ -40,9 +40,9 @@ public PatternBrush(Color foreColor, Color backColor, bool[,] pattern) /// /// Initializes a new instance of the class. /// - /// Color of the fore. - /// Color of the back. - /// The pattern. + /// The color used where the pattern flag is . + /// The color used where the pattern flag is . + /// The pattern flags, indexed [row, column]. internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix pattern) { this.Pattern = new DenseMatrix(pattern.Columns, pattern.Rows); @@ -60,9 +60,10 @@ internal PatternBrush(Color foreColor, Color backColor, in DenseMatrix pat } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class, + /// sharing the pattern matrix of an existing brush. /// - /// The brush. + /// The brush to copy the pattern from. internal PatternBrush(PatternBrush brush) => this.Pattern = brush.Pattern; /// @@ -122,10 +123,18 @@ public PatternBrushRenderer( : base(configuration, options, canvasWidth) => this.pattern = pattern; + /// + /// Gets the pattern pixel for the given device coordinate. + /// + /// The x-coordinate of the pixel in device space. + /// The y-coordinate of the pixel in device space. + /// The pattern pixel at the tiled position. internal TPixel this[int x, int y] { get { + // The pattern tiles from the canvas origin (not the shape), so + // adjacent fills line up seamlessly. x %= this.pattern.Columns; y %= this.pattern.Rows; @@ -143,23 +152,21 @@ public override void Apply( BrushWorkspace workspace) { int patternY = y % this.pattern.Rows; - Span amounts = workspace.GetAmounts(scanline.Length); Span overlays = workspace.GetOverlays(scanline.Length); for (int i = 0; i < scanline.Length; i++) { - amounts[i] = Math.Clamp(scanline[i] * this.Options.BlendPercentage, 0, 1F); - int patternX = (x + i) % this.pattern.Columns; overlays[i] = this.pattern[patternY, patternX]; } - this.Blender.Blend( + this.Blender.BlendWithCoverage( this.Configuration, destinationRow, destinationRow, overlays, - amounts, + this.Options.BlendPercentage, + scanline, workspace.GetBlendScratch(scanline.Length, 3)); } } diff --git a/src/ImageSharp.Drawing/Processing/PatternPen.cs b/src/ImageSharp.Drawing/Processing/PatternPen.cs index 87a1743a3..e71944a1f 100644 --- a/src/ImageSharp.Drawing/Processing/PatternPen.cs +++ b/src/ImageSharp.Drawing/Processing/PatternPen.cs @@ -4,14 +4,14 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Defines a pen that can apply a pattern to a line with a set brush and thickness +/// Defines a pen that can apply a pattern to a line with a set brush and thickness. /// /// -/// The pattern will be in to the form of +/// The pattern is expressed in the form /// -/// new float[]{ 1f, 2f, 0.5f} +/// [1f, 2f, 0.5f] /// -/// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections. +/// this will be converted into a pattern that is 3.5 times longer than the width with 3 sections. /// /// Section 1 will be width long (making a square) and will be filled by the brush. /// Section 2 will be width * 2 long and will be empty. @@ -31,6 +31,17 @@ public PatternPen(Color color, float[] strokePattern) { } + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// The stroke pattern. + /// The distance into the stroke pattern, expressed as a multiple of the stroke width. + public PatternPen(Color color, float[] strokePattern, float strokePatternOffset) + : base(new PenOptions(color, 1, strokePattern, strokePatternOffset)) + { + } + /// /// Initializes a new instance of the class. /// @@ -42,6 +53,18 @@ public PatternPen(Color color, float strokeWidth, float[] strokePattern) { } + /// + /// Initializes a new instance of the class. + /// + /// The color. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + /// The distance into the stroke pattern, expressed as a multiple of . + public PatternPen(Color color, float strokeWidth, float[] strokePattern, float strokePatternOffset) + : base(new PenOptions(color, strokeWidth, strokePattern, strokePatternOffset)) + { + } + /// /// Initializes a new instance of the class. /// @@ -53,6 +76,18 @@ public PatternPen(Brush strokeFill, float strokeWidth, float[] strokePattern) { } + /// + /// Initializes a new instance of the class. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + /// The distance into the stroke pattern, expressed as a multiple of . + public PatternPen(Brush strokeFill, float strokeWidth, float[] strokePattern, float strokePatternOffset) + : base(new PenOptions(strokeFill, strokeWidth, strokePattern, strokePatternOffset)) + { + } + /// /// Initializes a new instance of the class. /// @@ -75,5 +110,5 @@ public override bool Equals(Pen? other) /// public override IPath GeneratePath(IPath path, float strokeWidth) - => path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokeOptions); + => path.GenerateOutline(strokeWidth, this.StrokePattern.Span, this.StrokePatternOffset, this.StrokeOptions); } diff --git a/src/ImageSharp.Drawing/Processing/Pen.cs b/src/ImageSharp.Drawing/Processing/Pen.cs index 8654625b9..bf3de7d9c 100644 --- a/src/ImageSharp.Drawing/Processing/Pen.cs +++ b/src/ImageSharp.Drawing/Processing/Pen.cs @@ -7,11 +7,11 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// The base class for pens that can apply a pattern to a line with a set brush and thickness /// /// -/// The pattern will be in to the form of +/// The pattern is expressed in the form /// -/// new float[]{ 1f, 2f, 0.5f} +/// [1f, 2f, 0.5f] /// -/// this will be converted into a pattern that is 3.5 times longer that the width with 3 sections. +/// this will be converted into a pattern that is 3.5 times longer than the width with 3 sections. /// /// Section 1 will be width long (making a square) and will be filled by the brush. /// Section 2 will be width * 2 long and will be empty. @@ -21,6 +21,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public abstract class Pen : IEquatable { + // Dash pattern segment lengths, expressed as multiples of the stroke width. private readonly float[] pattern; /// @@ -58,6 +59,7 @@ protected Pen(Brush strokeFill, float strokeWidth, float[] strokePattern) this.StrokeFill = strokeFill; this.StrokeWidth = strokeWidth; this.pattern = strokePattern; + this.StrokePatternOffset = 0; this.StrokeOptions = new StrokeOptions(); } @@ -70,6 +72,7 @@ protected Pen(PenOptions options) this.StrokeFill = options.StrokeFill; this.StrokeWidth = options.StrokeWidth; this.pattern = options.StrokePattern; + this.StrokePatternOffset = options.StrokePatternOffset; this.StrokeOptions = options.StrokeOptions ?? new StrokeOptions(); } @@ -82,21 +85,24 @@ protected Pen(PenOptions options) /// public ReadOnlyMemory StrokePattern => this.pattern; + /// + public float StrokePatternOffset { get; } + /// public StrokeOptions StrokeOptions { get; } /// - /// Applies the styling from the pen to a path and generate a new path with the final vector. + /// Applies the styling from the pen to a path and generates a new path with the final vector. /// - /// The source path + /// The source path. /// The with the pen styling applied. public IPath GeneratePath(IPath path) => this.GeneratePath(path, this.StrokeWidth); /// - /// Applies the styling from the pen to a path and generate a new path with the final vector. + /// Applies the styling from the pen to a path and generates a new path with the final vector. /// - /// The source path + /// The source path. /// The stroke width in the path's local coordinate space before any drawing transform is applied. /// The with the pen styling applied. public abstract IPath GeneratePath(IPath path, float strokeWidth); @@ -106,6 +112,7 @@ public virtual bool Equals(Pen? other) => other != null && this.StrokeWidth == other.StrokeWidth && this.StrokeFill.Equals(other.StrokeFill) + && this.StrokePatternOffset == other.StrokePatternOffset && this.StrokeOptions.Equals(other.StrokeOptions) && this.StrokePattern.Span.SequenceEqual(other.StrokePattern.Span); @@ -114,5 +121,5 @@ public virtual bool Equals(Pen? other) /// public override int GetHashCode() - => HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokeOptions, this.pattern); + => HashCode.Combine(this.StrokeWidth, this.StrokeFill, this.StrokePatternOffset, this.StrokeOptions, this.pattern); } diff --git a/src/ImageSharp.Drawing/Processing/PenOptions.cs b/src/ImageSharp.Drawing/Processing/PenOptions.cs index dcdba93be..49205eb03 100644 --- a/src/ImageSharp.Drawing/Processing/PenOptions.cs +++ b/src/ImageSharp.Drawing/Processing/PenOptions.cs @@ -38,6 +38,18 @@ public PenOptions(Color color, float strokeWidth, float[]? strokePattern) { } + /// + /// Initializes a new instance of the struct. + /// + /// The color. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + /// The distance into the stroke pattern, expressed as a multiple of . + public PenOptions(Color color, float strokeWidth, float[] strokePattern, float strokePatternOffset) + : this(new SolidBrush(color), strokeWidth, strokePattern, strokePatternOffset) + { + } + /// /// Initializes a new instance of the struct. /// @@ -51,9 +63,23 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern) this.StrokeFill = strokeFill; this.StrokeWidth = strokeWidth; this.StrokePattern = strokePattern ?? Pens.EmptyPattern; + this.StrokePatternOffset = 0; this.StrokeOptions = new StrokeOptions(); } + /// + /// Initializes a new instance of the struct. + /// + /// The brush used to fill the stroke outline. + /// The stroke width in the path's local coordinate space before any drawing transform is applied. + /// The stroke pattern. + /// The distance into the stroke pattern, expressed as a multiple of . + public PenOptions(Brush strokeFill, float strokeWidth, float[] strokePattern, float strokePatternOffset) + : this(strokeFill, strokeWidth, strokePattern) + { + this.StrokePatternOffset = strokePatternOffset; + } + /// /// Gets the brush used to fill the stroke outline. Defaults to . /// @@ -65,10 +91,17 @@ public PenOptions(Brush strokeFill, float strokeWidth, float[]? strokePattern) public float StrokeWidth { get; } /// - /// Gets the stroke pattern. + /// Gets the stroke pattern: alternating filled and empty segment lengths expressed as + /// multiples of , starting with a filled segment. + /// An empty pattern produces a continuous stroke. /// public float[] StrokePattern { get; } + /// + /// Gets or sets the distance into the stroke pattern, expressed as a multiple of . + /// + public float StrokePatternOffset { get; set; } + /// /// Gets or sets the stroke geometry options used to stroke paths drawn with this pen. /// diff --git a/src/ImageSharp.Drawing/Processing/Pens.cs b/src/ImageSharp.Drawing/Processing/Pens.cs index b266b2018..7a86b7d67 100644 --- a/src/ImageSharp.Drawing/Processing/Pens.cs +++ b/src/ImageSharp.Drawing/Processing/Pens.cs @@ -8,6 +8,8 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// public static class Pens { + // Pattern segment lengths are multiples of the stroke width (see Pen remarks): + // alternating filled and empty sections starting with a filled one. private static readonly float[] DashDotPattern = [3f, 1f, 1f, 1f]; private static readonly float[] DashDotDotPattern = [3f, 1f, 1f, 1f, 1f, 1f]; private static readonly float[] DottedPattern = [1f, 1f]; @@ -15,21 +17,21 @@ public static class Pens internal static readonly float[] EmptyPattern = []; /// - /// Create a solid pen without any drawing patterns + /// Creates a solid pen without any drawing pattern. /// /// The color. /// The . public static SolidPen Solid(Color color) => new(color); /// - /// Create a solid pen without any drawing patterns + /// Creates a solid pen without any drawing pattern. /// /// The brush. /// The . public static SolidPen Solid(Brush brush) => new(brush); /// - /// Create a solid pen without any drawing patterns + /// Creates a solid pen without any drawing pattern. /// /// The color. /// The width. @@ -37,7 +39,7 @@ public static class Pens public static SolidPen Solid(Color color, float width) => new(color, width); /// - /// Create a solid pen without any drawing patterns + /// Creates a solid pen without any drawing pattern. /// /// The brush. /// The width. @@ -45,7 +47,7 @@ public static class Pens public static SolidPen Solid(Brush brush, float width) => new(brush, width); /// - /// Create a pen with a 'Dash' drawing patterns + /// Creates a pen with a 'Dash' drawing pattern. /// /// The color. /// The width. @@ -53,7 +55,7 @@ public static class Pens public static PatternPen Dash(Color color, float width) => new(color, width, DashedPattern); /// - /// Create a pen with a 'Dash' drawing patterns + /// Creates a pen with a 'Dash' drawing pattern. /// /// The brush. /// The width. @@ -61,7 +63,7 @@ public static class Pens public static PatternPen Dash(Brush brush, float width) => new(brush, width, DashedPattern); /// - /// Create a pen with a 'Dot' drawing patterns + /// Creates a pen with a 'Dot' drawing pattern. /// /// The color. /// The width. @@ -69,7 +71,7 @@ public static class Pens public static PatternPen Dot(Color color, float width) => new(color, width, DottedPattern); /// - /// Create a pen with a 'Dot' drawing patterns + /// Creates a pen with a 'Dot' drawing pattern. /// /// The brush. /// The width. @@ -77,7 +79,7 @@ public static class Pens public static PatternPen Dot(Brush brush, float width) => new(brush, width, DottedPattern); /// - /// Create a pen with a 'Dash Dot' drawing patterns + /// Creates a pen with a 'Dash Dot' drawing pattern. /// /// The color. /// The width. @@ -85,7 +87,7 @@ public static class Pens public static PatternPen DashDot(Color color, float width) => new(color, width, DashDotPattern); /// - /// Create a pen with a 'Dash Dot' drawing patterns + /// Creates a pen with a 'Dash Dot' drawing pattern. /// /// The brush. /// The width. @@ -93,7 +95,7 @@ public static class Pens public static PatternPen DashDot(Brush brush, float width) => new(brush, width, DashDotPattern); /// - /// Create a pen with a 'Dash Dot Dot' drawing patterns + /// Creates a pen with a 'Dash Dot Dot' drawing pattern. /// /// The color. /// The width. @@ -101,7 +103,7 @@ public static class Pens public static PatternPen DashDotDot(Color color, float width) => new(color, width, DashDotDotPattern); /// - /// Create a pen with a 'Dash Dot Dot' drawing patterns + /// Creates a pen with a 'Dash Dot Dot' drawing pattern. /// /// The brush. /// The width. diff --git a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs index 3fcd78664..0e030721c 100644 --- a/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RadialGradientBrush.cs @@ -84,7 +84,7 @@ public RadialGradientBrush( public bool IsTwoCircle => this.Center1.HasValue && this.Radius1.HasValue; /// - public override Brush Transform(Matrix4x4 matrix) + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) { PointF tc0 = PointF.Transform(this.Center0, matrix); float scale = MatrixUtilities.GetAverageScale(in matrix); @@ -122,7 +122,22 @@ public override BrushRenderer CreateRenderer( GraphicsOptions options, int canvasWidth, RectangleF region) - => new RadialGradientBrushRenderer( + { + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + return new RadialGradientBrushRenderer>( + configuration, + options, + canvasWidth, + this.Center0, + this.Radius0, + this.Center1, + this.Radius1, + this.ColorStopsArray, + this.RepetitionMode); + } + + return new RadialGradientBrushRenderer>( configuration, options, canvasWidth, @@ -132,13 +147,19 @@ public override BrushRenderer CreateRenderer( this.Radius1, this.ColorStopsArray, this.RepetitionMode); + } /// /// The radial gradient brush applicator. /// - private sealed class RadialGradientBrushRenderer : GradientBrushRenderer + /// The pixel format. + /// The destination representation encoder. + private sealed class RadialGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { + // Tolerance (1/4096) for classifying near-degenerate circle configurations + // such as equal radii or a focal point sitting on the limiting circle. private const float GradientEpsilon = 1F / (1 << 12); // Single-circle fields @@ -159,7 +180,7 @@ private sealed class RadialGradientBrushRenderer : GradientBrushRenderer private readonly bool isSwapped; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. @@ -221,6 +242,8 @@ protected override float PositionOnGradient(float x, float y) { if (!this.isTwoCircle) { + // Single-circle form: the parameter is simply distance from the + // center divided by the radius. float ux = x - this.c0x, uy = y - this.c0y; return MathF.Sqrt((ux * ux) + (uy * uy)) / this.r0; } @@ -297,6 +320,17 @@ protected override float PositionOnGradient(float x, float y) return this.isSwapped ? 1F - t : t; } + /// + /// Classifies the two-circle gradient into one of the canonical conic cases + /// (strip, focal-on-circle, wide or narrow cone) and builds the change-of-basis + /// transform that lets evaluate it with + /// closed-form expressions. + /// + /// The center of the starting circle. + /// The radius of the starting circle. + /// The center of the ending circle. + /// The radius of the ending circle. + /// The precomputed evaluation parameters. private static ConicalGradientParameters CreateConicalGradientParameters( PointF center0, float radius0, @@ -391,8 +425,21 @@ private static ConicalGradientParameters CreateConicalGradientParameters( isSwapped: isSwapped); } + /// + /// Computes the Euclidean distance between two points. + /// + /// The first point. + /// The second point. + /// The distance between the points. private static float Distance(Vector2 p0, Vector2 p1) => Vector2.Distance(p0, p1); + /// + /// Builds the transform that maps the segment from to + /// onto the unit line from (0, 0) to (1, 0). + /// + /// The point mapped to the origin. + /// The point mapped to (1, 0). + /// The change-of-basis transform. private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1) { // Build a change-of-basis that sends the segment p0->p1 to the @@ -403,6 +450,13 @@ private static Matrix3x2 TwoPointToUnitLine(PointF p0, PointF p1) return inverse * FromPoly2(new PointF(0F, 0F), new PointF(1F, 0F)); } + /// + /// Builds an affine frame with as the origin and the vector + /// from to as one axis. + /// + /// The origin of the frame. + /// The point defining the primary axis direction and length. + /// The affine frame matrix. private static Matrix3x2 FromPoly2(PointF p0, PointF p1) // This affine frame uses p0 as the origin and p0->p1 as one axis. @@ -415,8 +469,21 @@ private static Matrix3x2 FromPoly2(PointF p0, PointF p1) p0.X, p0.Y); + /// + /// Precomputed parameters describing a two-circle gradient in its canonical frame. + /// private readonly struct ConicalGradientParameters { + /// + /// Initializes a new instance of the struct. + /// + /// The transform mapping user space into the canonical evaluation frame. + /// The focal point position along the start-to-end axis. + /// The end-circle radius in the canonical frame, or the squared half-width for strips. + /// Whether both circles share the same radius (strip case). + /// Whether both circles share the same center (concentric case). + /// Whether the focal point lies exactly on the limiting circle. + /// Whether the start and end circles were exchanged during normalization. public ConicalGradientParameters( Matrix3x2 transform, float focalX, @@ -435,18 +502,40 @@ public ConicalGradientParameters( this.IsSwapped = isSwapped; } + /// + /// Gets the transform mapping user space into the canonical evaluation frame. + /// public Matrix3x2 Transform { get; } + /// + /// Gets the focal point position along the start-to-end axis. + /// Values outside [0..1] correspond to cones whose focus lies beyond an endpoint. + /// public float FocalX { get; } + /// + /// Gets the end-circle radius in the canonical frame, or the squared half-width for strips. + /// public float Radius { get; } + /// + /// Gets a value indicating whether both circles share the same radius (strip case). + /// public bool IsStrip { get; } + /// + /// Gets a value indicating whether both circles share the same center (concentric case). + /// public bool IsCircular { get; } + /// + /// Gets a value indicating whether the focal point lies exactly on the limiting circle. + /// public bool IsFocalOnCircle { get; } + /// + /// Gets a value indicating whether the start and end circles were exchanged during normalization. + /// public bool IsSwapped { get; } } } diff --git a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs index f67274bca..0b9fb5f8d 100644 --- a/src/ImageSharp.Drawing/Processing/RecolorBrush.cs +++ b/src/ImageSharp.Drawing/Processing/RecolorBrush.cs @@ -2,21 +2,23 @@ // Licensed under the Six Labors Split License. using System.Numerics; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Provides an implementation of a brush that can recolor an image +/// Provides an implementation of a brush that can recolor an image. +/// Pixels within of are blended +/// towards , with the blend strength falling off as the +/// color distance approaches the threshold. /// public sealed class RecolorBrush : Brush { /// /// Initializes a new instance of the class. /// - /// Color of the source. - /// Color of the target. - /// The threshold as a value between 0 and 1. + /// The color to match against existing pixels. + /// The color to recolor matched pixels with. + /// The color-matching threshold as a value between 0 and 1. public RecolorBrush(Color sourceColor, Color targetColor, float threshold) { this.SourceColor = sourceColor; @@ -25,17 +27,17 @@ public RecolorBrush(Color sourceColor, Color targetColor, float threshold) } /// - /// Gets the threshold. + /// Gets the color-matching threshold as a value between 0 and 1. /// public float Threshold { get; } /// - /// Gets the source color. + /// Gets the color to match against existing pixels. /// public Color SourceColor { get; } /// - /// Gets the target color. + /// Gets the color to recolor matched pixels with. /// public Color TargetColor { get; } @@ -45,13 +47,18 @@ public override BrushRenderer CreateRenderer( GraphicsOptions options, int canvasWidth, RectangleF region) - => new RecolorBrushRenderer( + { + Vector4 sourceColor = this.SourceColor.ToScaledVector4(TPixel.GetPixelTypeInfo().AlphaRepresentation); + TPixel targetColor = this.TargetColor.ToPixel(); + + return new RecolorBrushRenderer( configuration, options, canvasWidth, - this.SourceColor.ToPixel(), - this.TargetColor.ToPixel(), + sourceColor, + targetColor, this.Threshold); + } /// public override bool Equals(Brush? other) @@ -85,28 +92,25 @@ private sealed class RecolorBrushRenderer : BrushRenderer /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. - /// The options + /// The graphics options. /// The canvas width for the current render pass. - /// Color of the source. - /// Color of the target. - /// The threshold . + /// The color to match, expressed in the destination pixel format's native alpha representation. + /// The color to recolor matched pixels with. + /// The color-matching threshold as a value between 0 and 1. public RecolorBrushRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, - TPixel sourceColor, + Vector4 sourceColor, TPixel targetColor, float threshold) : base(configuration, options, canvasWidth) { - this.sourceColor = sourceColor.ToScaledVector4(); + this.sourceColor = sourceColor; this.targetColorPixel = targetColor; - // TODO: Review this. We can skip the conversion from/to Vector4. - // Lets hack a min max extremes for a color space by letting the IPackedPixel clamp our values to something in the correct spaces :) - TPixel maxColor = TPixel.FromVector4(new Vector4(float.MaxValue)); - TPixel minColor = TPixel.FromVector4(new Vector4(float.MinValue)); - this.threshold = Vector4.DistanceSquared(maxColor.ToVector4(), minColor.ToVector4()) * threshold; + // Matching happens in squared-distance space so no per-pixel sqrt is needed. + this.threshold = threshold * Vector4.DistanceSquared(Vector4.Zero, Vector4.One); } /// @@ -117,26 +121,33 @@ public override void Apply( int y, BrushWorkspace workspace) { - Span amounts = workspace.GetAmounts(scanline.Length); Span overlays = workspace.GetOverlays(scanline.Length); for (int i = 0; i < scanline.Length; i++) { - amounts[i] = scanline[i] * this.Options.BlendPercentage; + // The brush reads the already-composed destination pixel: recoloring is a + // function of what is currently on the canvas, not of a source texture. TPixel result = destinationRow[i]; - Vector4 background = result.ToVector4(); + + // Keep both operands in TPixel's native representation. The constrained call is + // statically specialized and adds no representation branch to the pixel loop. + Vector4 background = result.ToScaledVector4(); float distance = Vector4.DistanceSquared(background, this.sourceColor); + + // Blend strength falls off linearly with squared distance: an exact match + // is fully recolored while a match at the threshold is left untouched. overlays[i] = distance <= this.threshold ? this.Blender.Blend(result, this.targetColorPixel, (this.threshold - distance) / this.threshold) : result; } - this.Blender.Blend( + this.Blender.BlendWithCoverage( this.Configuration, destinationRow, destinationRow, overlays, - amounts, + this.Options.BlendPercentage, + scanline, workspace.GetBlendScratch(scanline.Length, 3)); } } diff --git a/src/ImageSharp.Drawing/Processing/RichGlyphOptions.cs b/src/ImageSharp.Drawing/Processing/RichGlyphOptions.cs new file mode 100644 index 000000000..596482f3b --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/RichGlyphOptions.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Provides for rendering a glyph by id with per-glyph +/// and support. The paint is carried to the renderer via a +/// , so it can vary per glyph across a run. +/// +public class RichGlyphOptions : GlyphOptions +{ + /// + public Brush? Brush { get; set; } + + /// + public Pen? Pen { get; set; } + + /// + public Pen? StrikeoutPen { get; set; } + + /// + public Pen? UnderlinePen { get; set; } + + /// + public Pen? OverlinePen { get; set; } + + /// + protected override TextRun CreateTextRun() + => new RichTextRun + { + Start = this.GraphemeIndex, + End = this.GraphemeIndex + 1, + Font = this.Font, + TextAttributes = this.TextAttributes, + TextDecorations = this.TextDecorations, + Brush = this.Brush, + Pen = this.Pen, + StrikeoutPen = this.StrikeoutPen, + UnderlinePen = this.UnderlinePen, + OverlinePen = this.OverlinePen + }; +} diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs index c35c40856..41bed0c5a 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.Brushes.cs @@ -20,7 +20,9 @@ internal sealed partial class RichTextGlyphRenderer /// The paint definition coming from the interpreter. /// A transform to apply to the brush coordinates. /// The resulting brush, or if the paint is unsupported. - /// if a brush could be created; otherwise, . + /// + /// if a brush could be created; otherwise, . + /// public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 transform, [NotNullWhen(true)] out Brush? brush) { brush = null; @@ -53,7 +55,9 @@ public static bool TryCreateBrush([NotNullWhen(true)] Paint? paint, Matrix4x4 tr /// The linear gradient paint. /// The transform to apply to the gradient points. /// The resulting brush. - /// if created; otherwise, . + /// + /// if created; otherwise, . + /// private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). @@ -94,7 +98,9 @@ private static bool TryCreateLinearGradientBrush(LinearGradientPaint paint, Matr /// The radial gradient paint. /// The transform to apply to the gradient center point. /// The resulting brush. - /// if created; otherwise, . + /// + /// if created; otherwise, . + /// private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). @@ -127,7 +133,9 @@ private static bool TryCreateRadialGradientBrush(RadialGradientPaint paint, Matr /// The sweep gradient paint. /// The transform to apply to the gradient center point. /// The resulting brush. - /// if created; otherwise, . + /// + /// if created; otherwise, . + /// private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix4x4 transform, out Brush? brush) { // Map gradient stops (apply paint opacity multiplier to each stop's alpha). @@ -151,7 +159,9 @@ private static bool TryCreateSweepGradientBrush(SweepGradientPaint paint, Matrix /// Maps an to . /// /// The spread method. - /// The repetition mode. + /// + /// The repetition mode. + /// private static GradientRepetitionMode MapSpread(SpreadMethod spread) => spread switch { @@ -167,7 +177,9 @@ private static GradientRepetitionMode MapSpread(SpreadMethod spread) /// /// The source stops. /// The paint opacity in range [0,1]. - /// An array of . + /// + /// An array of . + /// private static ColorStop[] ToColorStops(ReadOnlySpan stops, float paintOpacity) { if (stops.Length == 0) @@ -192,7 +204,9 @@ private static ColorStop[] ToColorStops(ReadOnlySpan stops, float /// /// The glyph color. /// The opacity multiplier in range [0,1]. - /// The ImageSharp color. + /// + /// The ImageSharp color. + /// private static Color ToColor(in GlyphColor c, float opacity) { float a = Math.Clamp(c.A / 255f * Math.Clamp(opacity, 0f, 1f), 0f, 1f); diff --git a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs index b84a475ab..ed7ff87c5 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextGlyphRenderer.cs @@ -6,6 +6,8 @@ using SixLabors.Fonts; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Helpers; +using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Text; namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; @@ -13,71 +15,109 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Processors.Text; /// /// Allows the rendering of rich text configured via . /// -internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposable +internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder { // --- Render-pass ordering constants --- // Within DrawTextOperations, operations are sorted first by RenderPass so that // fills paint beneath outlines, and outlines beneath decorations. + + /// + /// Render pass for glyph fills; painted first (bottom). + /// private const byte RenderOrderFill = 0; + + /// + /// Render pass for glyph outlines; painted above fills. + /// private const byte RenderOrderOutline = 1; + + /// + /// Render pass for text decorations; painted last (top). + /// private const byte RenderOrderDecoration = 2; + /// + /// The drawing options (transform, graphics options) supplied by the caller. + /// private readonly DrawingOptions drawingOptions; - /// The default pen supplied by the caller (e.g. from DrawText(..., pen)). + /// + /// The default pen supplied by the caller (e.g. from DrawText(..., pen)). + /// private readonly Pen? defaultPen; - /// The default brush supplied by the caller (e.g. from DrawText(..., brush)). + /// + /// The default brush supplied by the caller (e.g. from DrawText(..., brush)). + /// private readonly Brush? defaultBrush; /// - /// When the text is laid out along a path, this holds the path internals - /// for point-along-path queries. for normal (linear) text. + /// Set once this renderer's disposal has run; guards the override independently of the base guard. /// - private readonly IPathInternals? path; private bool isDisposed; // --- Per-glyph mutable state reset in BeginGlyph --- - /// The (or ) governing the current glyph. + /// + /// The (or ) governing the current glyph. + /// private TextRun? currentTextRun; - /// Brush resolved from the current , or . + /// + /// Brush resolved from the current , or . + /// private Brush? currentBrush; - /// Pen resolved from the current , or . + /// + /// Pen resolved from the current , or . + /// private Pen? currentPen; - /// The fill rule for the current color layer (COLR). + /// + /// The fill rule for the current color layer (COLR). + /// private FillRule currentFillRule; - /// Alpha composition mode active for the current glyph/layer. + /// + /// Alpha composition mode active for the current glyph/layer. + /// private PixelAlphaCompositionMode currentCompositionMode; - /// Color blending mode active for the current glyph/layer. + /// + /// Color blending mode active for the current glyph/layer. + /// private PixelColorBlendingMode currentBlendingMode; - /// Whether the current glyph uses vertical layout (affects decoration orientation). - private bool currentDecorationIsVertical; - - /// Set to when is called, cleared in . + /// + /// Set to when is called, cleared in . + /// private bool hasLayer; // --- Glyph outline cache --- - // Glyphs that share the same CacheKey (same glyph id, sub-pixel position quantized - // to 1/AccuracyMultiple, pen reference, etc.) reuse the translated IPath from the - // first occurrence. This avoids re-building the full outline for repeated characters. + // Glyphs that share the same CacheKey (same glyph id, size, pen reference, etc.) reuse + // the anchored IPath from the first occurrence. This avoids re-building the full outline + // for repeated characters. // - // AccuracyMultiple = 8 means sub-pixel positions are quantized to 1/8 px steps. - // Benchmarked to give <0.2% image difference vs. uncached, with >60% cache hit ratio. - private const float AccuracyMultiple = 8; + // Position is intentionally absent from the key: cached paths are anchored at their exact + // outline origin, and each emitted operation carries the pixel-snapped location plus the + // fractional remainder (DrawingOperation.SubPixelOffset). The backends apply the remainder + // as a residual translation, so one cache entry renders exactly at every position. + // The key's size component stays quantized (1/SizeAccuracyMultiple px) because transformed + // bounds sizes pick up float noise under translation. - /// Maps cache keys to their list of entries (one per layer). - /// Owned by the enclosing and shared across every DrawText - /// call on that canvas, so glyph outlines persist beyond a single text draw. - private readonly Dictionary> glyphCache; + /// + /// The reciprocal of the quantization step applied to the cache key's size component. + /// + private const float SizeAccuracyMultiple = 8; + + /// + /// Cache storing reusable glyph outline entries. + /// + private readonly DrawingTextCache glyphCache; - /// Read cursor into the cached layer list for layered cache hits. + /// + /// Read cursor into the cached layer list for layered cache hits. + /// private int cacheReadIndex; /// @@ -92,9 +132,18 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa /// private readonly bool noCache; - /// The cache key computed for the current glyph in . + /// + /// The cache key computed for the current glyph in . + /// private CacheKey currentCacheKey; + /// + /// The cache entries for the current glyph key. Assigned on a cache hit and only read on + /// hit paths; the miss path appends through the cache-owned list instead, so no per-glyph + /// list is allocated here. + /// + private List? currentCacheEntries; + /// /// The transformed (post-) bounding-box location /// of the current glyph. Stored so can compute @@ -109,21 +158,26 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder, IDisposa /// Optional path to draw the text along. /// Default pen for outlined text, or for fill-only. /// Default brush for filled text, or for outline-only. - /// Caller-owned per-canvas glyph cache shared across renderer - /// instances so glyph outlines persist beyond a single text draw. + /// Caller-owned glyph cache shared across renderer instances. + /// + /// The caller-owned operation list this renderer emits into. Passing the canvas's reusable + /// list keeps its capacity across draws instead of regrowing a fresh list per call; it is + /// cleared in and must not be shared by concurrently live renderers. + /// public RichTextGlyphRenderer( DrawingOptions drawingOptions, IPath? path, Pen? pen, Brush? brush, - Dictionary> glyphCache) + DrawingTextCache glyphCache, + List operations) : base(drawingOptions.Transform) { this.drawingOptions = drawingOptions; this.defaultPen = pen; this.defaultBrush = brush; this.glyphCache = glyphCache; - this.DrawingOperations = []; + this.DrawingOperations = operations; this.currentCompositionMode = drawingOptions.GraphicsOptions.AlphaCompositionMode; this.currentBlendingMode = drawingOptions.GraphicsOptions.ColorBlendingMode; @@ -133,14 +187,22 @@ public RichTextGlyphRenderer( // so cache hits are vanishingly rare; disable caching entirely. this.rasterizationRequired = true; this.noCache = true; - if (path is IPathInternals internals) - { - this.path = internals; - } - else - { - this.path = new ComplexPolygon(path); - } + this.TextPath = path; + } + else if (!MatrixUtilities.IsAffine(drawingOptions.Transform)) + { + // Projective transforms distort each glyph by its absolute position (a glyph left + // of the vanishing point shears the opposite way to one on the right), so the + // position-free cache key would share outlines between differently-distorted + // instances. Build every outline from scratch instead. + // + // Potential upgrade if projective text ever profiles hot: key on a quantized local + // distortion signature instead of disabling the cache - the 2x2 Jacobian of the + // projective map evaluated at the glyph anchor (post perspective-divide). Instances + // with the same quantized Jacobian share shape to first order, restoring hits for + // same-row repeats with an error bounded by the quantization step. + this.rasterizationRequired = true; + this.noCache = true; } } @@ -151,6 +213,14 @@ public RichTextGlyphRenderer( /// public List DrawingOperations { get; } + /// + /// Gets a value indicating whether per-grapheme glyph collections are aggregated. + /// This renderer emits drawing operations directly and never reads + /// , so the aggregate would be one discarded builder + /// and two lists per grapheme per draw. + /// + protected override bool CollectsGlyphPaths => false; + /// protected override void BeginText(in FontRectangle bounds) => this.DrawingOperations.Clear(); @@ -163,7 +233,7 @@ protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererPara // 2. Layered or decorated cache hit: reuse cached path, return true for EndGlyph/SetDecoration. // 3. Cache miss: rasterize from scratch. this.cacheReadIndex = 0; - this.currentDecorationIsVertical = parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + this.currentCacheEntries = null; this.currentTextRun = parameters.TextRun; if (parameters.TextRun is RichTextRun drawingRun) { @@ -178,31 +248,29 @@ protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererPara if (!this.noCache) { - // Transform the font-metric bounds by the drawing transform so that the - // sub-pixel position and size reflect the final screen coordinates. - // Quantize to 1/AccuracyMultiple px steps for cache key comparison. + // Transform the font-metric bounds by the drawing transform so that the size + // reflects the final screen coordinates. Only the quantized size enters the key: + // cached paths are position independent, and quantizing absorbs the float noise + // that transformed sizes pick up under translation. RectangleF currentBounds = RectangleF.Transform( new RectangleF(bounds.Location, new SizeF(bounds.Width, bounds.Height)), this.drawingOptions.Transform); this.currentTransformedBoundsLocation = currentBounds.Location; - PointF currentBoundsDelta = currentBounds.Location - ClampToPixel(currentBounds.Location); - PointF subPixelLocation = new( - MathF.Round(currentBoundsDelta.X * AccuracyMultiple) / AccuracyMultiple, - MathF.Round(currentBoundsDelta.Y * AccuracyMultiple) / AccuracyMultiple); - - SizeF subPixelSize = new( - MathF.Round(currentBounds.Width * AccuracyMultiple) / AccuracyMultiple, - MathF.Round(currentBounds.Height * AccuracyMultiple) / AccuracyMultiple); + SizeF quantizedSize = new( + MathF.Round(currentBounds.Width * SizeAccuracyMultiple) / SizeAccuracyMultiple, + MathF.Round(currentBounds.Height * SizeAccuracyMultiple) / SizeAccuracyMultiple); this.currentCacheKey = CacheKey.FromParameters( parameters, - new RectangleF(subPixelLocation, subPixelSize), + quantizedSize, this.currentPen ?? this.defaultPen); if (this.glyphCache.TryGetValue(this.currentCacheKey, out List? cachedEntries)) { + this.currentCacheEntries = cachedEntries; + if (cachedEntries.Count > 0 && !cachedEntries[0].IsLayered && this.EnabledDecorations() == TextDecorations.None) { @@ -214,15 +282,17 @@ protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererPara } // Layered or decorated cache hit: let the normal flow handle - // per-layer state and decoration callbacks. + // per-layer state and decoration callbacks. For decorated non-layered glyphs the + // decoded outline is not needed, because the cached path and its stored anchor + // position the glyph; skipping the build avoids a discarded path graph per glyph + // per draw. Layered glyphs keep building: each layer's exact built bounds anchor + // that layer, and COLR components interleave layers across glyph callbacks. this.rasterizationRequired = false; + this.OutlineBuildRequired = cachedEntries[0].IsLayered; return true; } } - // Transform the glyph vectors using the original bounds - // The default transform will automatically be applied. - this.TransformGlyph(in bounds); this.rasterizationRequired = true; return true; } @@ -245,9 +315,9 @@ protected override void BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? cl /// protected override void EndLayer() { - // Finalizes a color layer. On a cache miss, translates the built path to local - // coordinates and stores it for future hits. On a cache hit, reads the stored - // path and adjusts the render location using sub-pixel delta compensation. + // Finalizes a color layer. On a cache miss, anchors the built path at its exact + // outline origin and stores it for future hits. On a cache hit, reuses the stored + // path; this instance's own outline origin positions it exactly. GlyphRenderData renderData = default; IPath? fillPath = null; @@ -263,27 +333,35 @@ protected override void EndLayer() // Any drawing of outlines is ignored as that doesn't really make sense. bool renderFill = this.currentBrush != null; - // Path has already been added to the collection via the base class. + // Path has already been added to the collection via the base class. Layered glyphs + // always build their decoded outline, on hits too: each layer is anchored by its own + // exact built bounds, and COLR components interleave layers across glyph callbacks. IPath path = this.CurrentPaths[^1]; - Point renderLocation = ClampToPixel(path.Bounds.Location); + PointF boundsLocation = path.Bounds.Location; + Point renderLocation = ClampToPixel(boundsLocation); + Vector2 subPixelOffset = (Vector2)(boundsLocation - renderLocation); if (this.noCache || this.rasterizationRequired) { + renderData.IsLayered = true; + if (path.Bounds.Equals(RectangleF.Empty)) { + // Empty layers still append a marker entry so hit replays consume exactly one + // entry per layer callback and stay aligned with the font engine's sequence. + if (!this.noCache) + { + this.UpdateCache(renderData); + } + return; } if (renderFill) { - renderData.FillPath = path.Translate(-renderLocation); + renderData.FillPath = path.Translate(-boundsLocation.X, -boundsLocation.Y); fillPath = renderData.FillPath; } - // Capture the delta between the location and the truncated render location. - // We can use this to offset the render location on the next instance of this glyph. - renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); - renderData.IsLayered = true; - if (!this.noCache) { this.UpdateCache(renderData); @@ -291,35 +369,11 @@ protected override void EndLayer() } else { - renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; - - // Offset the render location by the delta from the cached glyph and this one. - Vector2 previousDelta = renderData.LocationDelta; - Vector2 currentLocation = path.Bounds.Location; - Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); - - if (previousDelta.Y > currentDelta.Y) - { - // Move the location down to match the previous location offset. - currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); - } - else if (previousDelta.Y < currentDelta.Y) - { - // Move the location up to match the previous location offset. - currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); - } - else if (previousDelta.X > currentDelta.X) - { - // Move the location right to match the previous location offset. - currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); - } - else if (previousDelta.X < currentDelta.X) - { - // Move the location left to match the previous location offset. - currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); - } - - renderLocation = ClampToPixel(currentLocation); + // Cache hit: the stored path is anchored at its exact origin, so this instance's + // own outline origin (snapped location + fractional remainder) positions it + // exactly; no sub-pixel compensation is required. The entries were assigned by the + // hit in BeginGlyph. + renderData = this.currentCacheEntries![this.cacheReadIndex++]; if (renderFill && renderData.FillPath is not null) { @@ -335,6 +389,9 @@ protected override void EndLayer() Kind = DrawingOperationKind.Fill, Path = fillPath, RenderLocation = renderLocation, + SubPixelOffset = subPixelOffset, + GlyphKey = this.currentCacheKey, + HasGlyphKey = !this.noCache, IntersectionRule = fillRule, Brush = this.currentBrush, RenderPass = RenderOrderFill, @@ -381,11 +438,10 @@ public override TextDecorations EnabledDecorations() /// public override void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { - // Emits a DrawingOperation for a text decoration. Resolves the decoration pen - // from the current RichTextRun, re-scales the base-class path when the pen's - // stroke width differs from the font-metric thickness, and anchors the scaling - // per decoration type (overline to bottom edge, underline to top edge, strikeout to center). - // Decorations are not cached. + // Emits a DrawingOperation for one carved decoration segment. The base class has already + // built the rectangle path at the drawn thickness; here we resolve the pen from the run + // that was captured while the glyph was live (carving happens a glyph later) and fill the + // path. Decorations are not cached. if (thickness == 0) { return; @@ -393,17 +449,17 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star Brush? brush = null; Pen? pen = null; - if (this.currentTextRun is RichTextRun drawingRun) + if (this.CurrentDecorationRun is RichTextRun drawingRun) { brush = drawingRun.Brush; if (textDecorations == TextDecorations.Strikeout) { - pen = drawingRun.StrikeoutPen ?? pen; + pen = drawingRun.StrikeoutPen; } else if (textDecorations == TextDecorations.Underline) { - pen = drawingRun.UnderlinePen ?? pen; + pen = drawingRun.UnderlinePen; } else if (textDecorations == TextDecorations.Overline) { @@ -411,69 +467,20 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star } } - // Always respect the pen stroke width if explicitly set. - float originalThickness = thickness; - if (pen is not null) - { - // Clamp the thickness to whole pixels. - thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth)); - } - else - { - // The thickness of the line has already been clamped in the base class. - pen = new SolidPen((brush ?? this.defaultBrush)!, thickness); - } + // The stroke width is already reflected in the built path; only the fill is taken from the pen. + pen ??= new SolidPen((brush ?? this.defaultBrush)!, thickness); // Path has already been added to the collection via the base class. IPath path = this.CurrentPaths[^1]; - IPath outline = path; - - if (originalThickness != thickness) - { - // Respect edge anchoring per decoration type: - // - Overline: keep the base edge fixed (bottom in horizontal; left in vertical) - // - Underline: keep the top edge fixed (top in horizontal; right in vertical) - // - Strikeout: keep the center fixed (default behavior) - float ratio = thickness / originalThickness; - if (ratio != 1f) - { - Vector2 scale = this.currentDecorationIsVertical - ? new Vector2(ratio, 1f) - : new Vector2(1f, ratio); - - RectangleF b = path.Bounds; - Vector2 center = new(b.Left + (b.Width * 0.5f), b.Top + (b.Height * 0.5f)); - Vector2 anchor = center; - - if (textDecorations == TextDecorations.Overline) - { - anchor = this.currentDecorationIsVertical - ? new Vector2(b.Left, center.Y) // vertical: anchor left edge - : new Vector2(center.X, b.Bottom); // horizontal: anchor bottom edge - } - else if (textDecorations == TextDecorations.Underline) - { - anchor = this.currentDecorationIsVertical - ? new Vector2(b.Right, center.Y) // vertical: anchor right edge - : new Vector2(center.X, b.Top); // horizontal: anchor top edge - } - - // Scale about the chosen anchor so the fixed edge stays in place. - outline = outline.Transform(Matrix4x4.CreateScale(scale.X, scale.Y, 1, new Vector3(anchor, 0))); - } - } - - // Render the path here. Decorations are un-cached. - Point renderLocation = ClampToPixel(outline.Bounds.Location); - IPath decorationPath = outline.Translate(-renderLocation); - Brush decorationBrush = pen.StrokeFill; + Point renderLocation = ClampToPixel(path.Bounds.Location); + IPath decorationPath = path.Translate(-renderLocation); this.DrawingOperations.Add(new DrawingOperation { Kind = DrawingOperationKind.Fill, Path = decorationPath, RenderLocation = renderLocation, IntersectionRule = IntersectionRule.NonZero, - Brush = decorationBrush, + Brush = pen.StrokeFill, RenderPass = RenderOrderDecoration }); } @@ -482,9 +489,10 @@ public override void SetDecoration(TextDecorations textDecorations, Vector2 star protected override void EndGlyph() { // If hasLayer is set, layers were already handled by EndLayer; skip. - // Otherwise, on a cache miss the built path is translated to local coordinates, + // Otherwise, on a cache miss the built path is anchored at its exact outline origin, // stored for future hits, and emitted as fill and/or outline DrawingOperations. - // On a cache hit the stored path is reused with sub-pixel delta compensation. + // On a cache hit the stored path is reused; this instance's own outline origin + // (snapped location + fractional remainder) positions it exactly. if (this.hasLayer) { // The layer has already been rendered. @@ -518,30 +526,43 @@ protected override void EndGlyph() renderOutline = true; } - // Path has already been added to the collection via the base class. - IPath path = this.CurrentPaths[^1]; - Point renderLocation = ClampToPixel(path.Bounds.Location); + Point renderLocation; + Vector2 subPixelOffset; if (this.noCache || this.rasterizationRequired) { + // Path has already been added to the collection via the base class. + // The path is anchored at its exact outline origin so one cache entry serves every + // position; the pixel-snapped location and the fractional remainder are carried on + // the emitted operations and reapplied by the backends. + IPath path = this.CurrentPaths[^1]; + PointF boundsLocation = path.Bounds.Location; + renderLocation = ClampToPixel(boundsLocation); + subPixelOffset = (Vector2)(boundsLocation - renderLocation); + if (path.Bounds.Equals(RectangleF.Empty)) { + // Ink-free glyphs (spaces, control glyphs) emit nothing, but they must still + // populate the cache: an unpopulated key takes the full transform-and-decode + // miss path again on every later draw of the same text. The default marker has + // no fill path, so the cached hit paths recognize it and emit nothing. + if (!this.noCache) + { + this.UpdateCache(renderData); + } + return; } - IPath localPath = path.Translate(-renderLocation); + IPath localPath = path.Translate(-boundsLocation.X, -boundsLocation.Y); if (renderFill || renderOutline) { renderData.FillPath = localPath; glyphPath = renderData.FillPath; } - // Capture the delta between the location and the truncated render location. - // We can use this to offset the render location on the next instance of this glyph. - renderData.LocationDelta = (Vector2)(path.Bounds.Location - renderLocation); - // Store the offset between outline bounds and font metric bounds so that - // cache hits in BeginGlyph can accurately estimate the path location. - renderData.BoundsOffset = (Vector2)(path.Bounds.Location - this.currentTransformedBoundsLocation); + // cache hits can accurately estimate the path location. + renderData.BoundsOffset = (Vector2)(boundsLocation - this.currentTransformedBoundsLocation); if (!this.noCache) { @@ -550,42 +571,19 @@ protected override void EndGlyph() } else { - renderData = this.glyphCache[this.currentCacheKey][this.cacheReadIndex++]; - - // Offset the render location by the delta from the cached glyph and this one. - Vector2 previousDelta = renderData.LocationDelta; - Vector2 currentLocation = path.Bounds.Location; - Vector2 currentDelta = path.Bounds.Location - ClampToPixel(path.Bounds.Location); - - if (previousDelta.Y > currentDelta.Y) - { - // Move the location down to match the previous location offset. - currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); - } - else if (previousDelta.Y < currentDelta.Y) - { - // Move the location up to match the previous location offset. - currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); - } - else if (previousDelta.X > currentDelta.X) - { - // Move the location right to match the previous location offset. - currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); - } - else if (previousDelta.X < currentDelta.X) - { - // Move the location left to match the previous location offset. - currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); - } - - renderLocation = ClampToPixel(currentLocation); - - if (renderFill && renderData.FillPath is not null) - { - glyphPath = renderData.FillPath; - } + // Cache hit: the base class skipped building the decoded outline, so derive the + // position estimate from the anchor stored with the cached entry, exactly as the + // fast path does. The stored path is anchored at its exact origin, so the snapped + // estimate plus its fractional remainder positions it exactly. The entries were + // assigned by the hit in BeginGlyph. + renderData = this.currentCacheEntries![this.cacheReadIndex++]; + PointF estimatedPathLocation = new( + this.currentTransformedBoundsLocation.X + renderData.BoundsOffset.X, + this.currentTransformedBoundsLocation.Y + renderData.BoundsOffset.Y); + renderLocation = ClampToPixel(estimatedPathLocation); + subPixelOffset = (Vector2)(estimatedPathLocation - renderLocation); - if (renderOutline && renderData.FillPath is not null) + if ((renderFill || renderOutline) && renderData.FillPath is not null) { glyphPath = renderData.FillPath; } @@ -599,6 +597,9 @@ protected override void EndGlyph() Kind = DrawingOperationKind.Fill, Path = glyphPath, RenderLocation = renderLocation, + SubPixelOffset = subPixelOffset, + GlyphKey = this.currentCacheKey, + HasGlyphKey = !this.noCache, IntersectionRule = fillRule, Brush = this.currentBrush, RenderPass = RenderOrderFill, @@ -615,6 +616,9 @@ protected override void EndGlyph() Kind = DrawingOperationKind.Draw, Path = glyphPath, RenderLocation = renderLocation, + SubPixelOffset = subPixelOffset, + GlyphKey = this.currentCacheKey, + HasGlyphKey = !this.noCache, IntersectionRule = outlineRule, Pen = this.currentPen, RenderPass = RenderOrderOutline, @@ -636,10 +640,13 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren { // Estimate the outline bounds location using the stored offset between // the outline bounds and the font metric bounds from the original glyph. + // The cached path is anchored at its exact outline origin, so the snapped + // estimate plus its fractional remainder positions it exactly. PointF estimatedPathLocation = new( currentBoundsLocation.X + renderData.BoundsOffset.X, currentBoundsLocation.Y + renderData.BoundsOffset.Y); - Point renderLocation = ComputeCacheHitRenderLocation(estimatedPathLocation, renderData.LocationDelta); + Point renderLocation = ClampToPixel(estimatedPathLocation); + Vector2 subPixelOffset = (Vector2)(estimatedPathLocation - renderLocation); // Fix up the text runs colors. Brush? brush = this.currentBrush; @@ -664,6 +671,9 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren Kind = DrawingOperationKind.Fill, Path = glyphPath, RenderLocation = renderLocation, + SubPixelOffset = subPixelOffset, + GlyphKey = this.currentCacheKey, + HasGlyphKey = !this.noCache, IntersectionRule = fillRule, Brush = brush, RenderPass = RenderOrderFill, @@ -680,6 +690,9 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren Kind = DrawingOperationKind.Draw, Path = glyphPath, RenderLocation = renderLocation, + SubPixelOffset = subPixelOffset, + GlyphKey = this.currentCacheKey, + HasGlyphKey = !this.noCache, IntersectionRule = outlineRule, Pen = pen, RenderPass = RenderOrderOutline, @@ -689,127 +702,47 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren } } - /// - /// Computes the pixel-snapped render location for a cache-hit glyph by compensating - /// for the sub-pixel delta difference between the original cached glyph and the - /// current instance. This keeps glyphs visually aligned even when their sub-pixel - /// positions differ slightly. - /// - /// The estimated outline bounds origin for the current glyph. - /// The sub-pixel delta recorded when the path was first cached. - /// A pixel-snapped render location. - private static Point ComputeCacheHitRenderLocation(PointF pathLocation, Vector2 previousDelta) - { - Vector2 currentLocation = (Vector2)pathLocation; - Vector2 currentDelta = currentLocation - (Vector2)ClampToPixel(pathLocation); - - if (previousDelta.Y > currentDelta.Y) - { - currentLocation += new Vector2(0, previousDelta.Y - currentDelta.Y); - } - else if (previousDelta.Y < currentDelta.Y) - { - currentLocation -= new Vector2(0, currentDelta.Y - previousDelta.Y); - } - else if (previousDelta.X > currentDelta.X) - { - currentLocation += new Vector2(previousDelta.X - currentDelta.X, 0); - } - else if (previousDelta.X < currentDelta.X) - { - currentLocation -= new Vector2(currentDelta.X - previousDelta.X, 0); - } - - return ClampToPixel(currentLocation); - } - /// /// Stores a entry in the glyph cache under the /// current key. Creates the cache list on first insertion for a given key. /// + /// The render data to append to the current key's entry list. private void UpdateCache(GlyphRenderData renderData) { - if (!this.glyphCache.TryGetValue(this.currentCacheKey, out List? _)) - { - this.glyphCache[this.currentCacheKey] = []; - } - - this.glyphCache[this.currentCacheKey].Add(renderData); + this.glyphCache.GetOrAdd(this.currentCacheKey).Add(renderData); } - /// - public void Dispose() => this.Dispose(true); - /// /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Point ClampToPixel(PointF point) => Point.Truncate(point); - /// - /// Applies the path-based transform to the - /// for the current glyph, positioning it along the text path (if any) or - /// leaving the identity transform for linear text. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void TransformGlyph(in FontRectangle bounds) - => this.Builder.SetTransform(this.ComputeTransform(in bounds)); - - /// - /// Computes the combined translation + rotation matrix that places a glyph - /// along the text path. For linear text (no path), returns . - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private Matrix4x4 ComputeTransform(in FontRectangle bounds) + /// + protected override void Dispose(bool disposing) { - if (this.path is null) + if (this.isDisposed) { - return Matrix4x4.Identity; + return; } - // Find the point of this intersection along the given path. - // We want to find the point on the path that is closest to the center-bottom side of the glyph. - Vector2 half = new(bounds.Width * .5F, 0); - SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); - - // Now offset to our target point since we're aligning the top-left location of our glyph against the path. - Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - return Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) - * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); - } - - /// - /// Releases managed resources (glyph cache and drawing operations list). - /// - /// to release managed resources. - private void Dispose(bool disposing) - { - if (!this.isDisposed) + this.isDisposed = true; + if (disposing) { - if (disposing) - { - // The glyph cache is owned by the canvas and outlives this renderer. - this.DrawingOperations.Clear(); - } - - this.isDisposed = true; + // The glyph cache is owned outside this renderer and outlives this draw call. + this.DrawingOperations.Clear(); } + + base.Dispose(disposing); } /// - /// Per-layer cached data for a rasterized glyph. Stores the locally-translated - /// path and the sub-pixel deltas needed to reposition the path at a different - /// screen location on a cache hit. + /// Per-layer cached data for a rasterized glyph. Stores the path anchored at its exact + /// outline origin; cache hits position it via their own snapped location and fractional + /// remainder, so no per-hit compensation state is required. /// internal struct GlyphRenderData { - /// - /// The fractional-pixel offset between the path's bounding-box origin - /// and the truncated (pixel-snapped) render location. Used to compensate - /// for sub-pixel position differences between cache hits. - /// - public Vector2 LocationDelta; - /// /// The offset between the outline path's bounding-box origin and the /// font-metric bounds origin. Stored on first rasterization so that @@ -819,8 +752,9 @@ internal struct GlyphRenderData public Vector2 BoundsOffset; /// - /// The glyph outline path translated to local coordinates (origin at 0,0). - /// Shared across all cache hits for the same . + /// The glyph outline path anchored at its exact outline origin (origin at 0,0 with + /// no baked sub-pixel fraction). Shared across all cache hits for the same + /// regardless of position. /// public IPath? FillPath; @@ -837,49 +771,78 @@ internal struct GlyphRenderData /// Identifies a unique glyph variant for caching purposes. Two glyphs with the same /// share identical outline geometry and can reuse the same /// . The key includes the glyph id, font metrics, - /// sub-pixel position (quantized to ), and the pen reference - /// (since stroke width affects the outline path). + /// the transformed size (quantized to ), and the pen + /// reference (since stroke width affects the outline path). Position is intentionally + /// excluded: cached paths are anchored at their exact outline origin and repositioned + /// per operation. /// internal readonly struct CacheKey : IEquatable { - /// Gets the font family name. + /// + /// Gets the font family name. + /// public string Font { get; init; } - /// Gets the glyph color variant (normal, COLR, etc.). + /// + /// Gets the glyph color variant (normal, COLR, etc.). + /// public GlyphColor GlyphColor { get; init; } - /// Gets the glyph type (simple, composite, etc.). + /// + /// Gets the glyph type (simple, composite, etc.). + /// public GlyphType GlyphType { get; init; } - /// Gets the font style (regular, bold, italic, etc.). + /// + /// Gets the font style (regular, bold, italic, etc.). + /// public FontStyle FontStyle { get; init; } - /// Gets the glyph index within the font. + /// + /// Gets the glyph index within the font. + /// public ushort GlyphId { get; init; } - /// Gets the composite glyph parent index (0 for non-composite). + /// + /// Gets the composite glyph parent index (0 for non-composite). + /// public ushort CompositeGlyphId { get; init; } - /// Gets the Unicode code point this glyph represents. + /// + /// Gets the Unicode code point this glyph represents. + /// public CodePoint CodePoint { get; init; } - /// Gets the em-size at which the glyph is rendered. + /// + /// Gets the em-size at which the glyph is rendered. + /// public float PointSize { get; init; } - /// Gets the DPI used for rendering. + /// + /// Gets the DPI used for rendering. + /// public float Dpi { get; init; } - /// Gets the layout mode (horizontal, vertical, vertical-rotated). + /// + /// Gets the layout mode (horizontal, vertical, vertical-rotated). + /// public GlyphLayoutMode LayoutMode { get; init; } - /// Gets any text attributes (e.g. superscript/subscript) that affect rendering. + /// + /// Gets any text attributes (e.g. superscript/subscript) that affect rendering. + /// public TextAttributes TextAttributes { get; init; } - /// Gets text decorations that may influence outline geometry. + /// + /// Gets text decorations that may influence outline geometry. + /// public TextDecorations TextDecorations { get; init; } - /// Gets the quantized sub-pixel bounds used for position-sensitive cache lookup. - public RectangleF Bounds { get; init; } + /// + /// Gets the quantized transformed size. Distinguishes scale variants of the same glyph + /// while quantization absorbs the float noise transformed sizes pick up under translation. + /// + public SizeF Size { get; init; } /// /// Gets the pen reference used for outlined text. Compared by reference equality @@ -889,22 +852,40 @@ internal struct GlyphRenderData /// public Pen? PenReference { get; init; } + /// + /// Determines whether two instances are equal. + /// + /// The first key to compare. + /// The second key to compare. + /// + /// if the keys are equal; otherwise, . + /// public static bool operator ==(CacheKey left, CacheKey right) => left.Equals(right); + /// + /// Determines whether two instances are not equal. + /// + /// The first key to compare. + /// The second key to compare. + /// + /// if the keys differ; otherwise, . + /// public static bool operator !=(CacheKey left, CacheKey right) => !(left == right); /// - /// Creates a from glyph renderer parameters and quantized bounds. - /// The grapheme index is intentionally excluded because it varies per glyph instance - /// while the outline geometry remains the same for matching glyph+position. + /// Creates a from glyph renderer parameters and the quantized + /// transformed size. The grapheme index is intentionally excluded because it varies per + /// glyph instance while the outline geometry remains the same for matching glyphs. /// /// The glyph renderer parameters from the font engine. - /// Quantized sub-pixel bounds for position-sensitive lookup. + /// The quantized transformed size distinguishing scale variants. /// The pen reference for outlined text, or . - /// A new cache key. + /// + /// A new cache key. + /// public static CacheKey FromParameters( in GlyphRendererParameters parameters, - RectangleF bounds, + SizeF size, Pen? penReference) => new() { @@ -921,13 +902,15 @@ public static CacheKey FromParameters( LayoutMode = parameters.LayoutMode, TextAttributes = parameters.TextRun.TextAttributes, TextDecorations = parameters.TextRun.TextDecorations, - Bounds = bounds, + Size = size, PenReference = penReference }; + /// public override bool Equals(object? obj) => obj is CacheKey key && this.Equals(key); + /// public bool Equals(CacheKey other) => this.Font == other.Font && this.GlyphColor.Equals(other.GlyphColor) && @@ -941,11 +924,13 @@ public bool Equals(CacheKey other) this.LayoutMode == other.LayoutMode && this.TextAttributes == other.TextAttributes && this.TextDecorations == other.TextDecorations && - this.Bounds.Equals(other.Bounds) && + this.Size.Equals(other.Size) && ReferenceEquals(this.PenReference, other.PenReference); + /// public override int GetHashCode() { + // Must match Equals: the pen is hashed by reference identity, not value. HashCode hash = default; hash.Add(this.Font); hash.Add(this.GlyphColor); @@ -959,7 +944,7 @@ public override int GetHashCode() hash.Add(this.LayoutMode); hash.Add(this.TextAttributes); hash.Add(this.TextDecorations); - hash.Add(this.Bounds); + hash.Add(this.Size); hash.Add(this.PenReference is null ? 0 : RuntimeHelpers.GetHashCode(this.PenReference)); return hash.ToHashCode(); } diff --git a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs index 0929b6e60..3d12a78c9 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextOptions.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextOptions.cs @@ -26,6 +26,8 @@ public RichTextOptions(Font font) public RichTextOptions(RichTextOptions options) : base(options) { + // Copy each run into a fresh instance so later mutation of the source runs + // cannot leak into this options instance (and vice versa). List runs = new(options.TextRuns.Count); foreach (RichTextRun run in options.TextRuns) { diff --git a/src/ImageSharp.Drawing/Processing/RichTextRun.cs b/src/ImageSharp.Drawing/Processing/RichTextRun.cs index ab0a431b3..40688647b 100644 --- a/src/ImageSharp.Drawing/Processing/RichTextRun.cs +++ b/src/ImageSharp.Drawing/Processing/RichTextRun.cs @@ -34,4 +34,28 @@ public class RichTextRun : TextRun /// Gets or sets the pen used for drawing overline features for this run. /// public Pen? OverlinePen { get; set; } + + /// + public override TextDecorationOptions? GetDecorationOptions(TextDecorations decoration) + { + Pen? pen = decoration switch + { + TextDecorations.Underline => this.UnderlinePen, + TextDecorations.Strikeout => this.StrikeoutPen, + TextDecorations.Overline => this.OverlinePen, + _ => null, + }; + + if (pen is null) + { + return null; + } + + // Report the same whole-pixel stroke width the renderer paints so the skip-ink gaps and + // measurement band clear the width that is actually drawn, not the font-metric thickness. + return new TextDecorationOptions + { + Thickness = MathF.Max(1F, (float)Math.Round(pen.StrokeWidth)), + }; + } } diff --git a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs b/src/ImageSharp.Drawing/Processing/ShapeOptions.cs deleted file mode 100644 index 79e0b6dc2..000000000 --- a/src/ImageSharp.Drawing/Processing/ShapeOptions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing.Processing; - -/// -/// Provides options for controlling how vector shapes are interpreted during rasterization, -/// including the fill-rule intersection mode and boolean clipping operations. -/// -public class ShapeOptions : IDeepCloneable -{ - /// - /// Initializes a new instance of the class. - /// - public ShapeOptions() - { - } - - private ShapeOptions(ShapeOptions source) - { - this.IntersectionRule = source.IntersectionRule; - this.BooleanOperation = source.BooleanOperation; - } - - /// - /// Gets or sets the boolean clipping operation used when a clipping path is applied. - /// Determines how the clip shape interacts with the target region - /// (e.g. subtracts the clip shape). - /// - /// Defaults to . - /// - public BooleanOperation BooleanOperation { get; set; } = BooleanOperation.Difference; - - /// - /// Gets or sets the fill rule that determines how overlapping or nested contours affect coverage. - /// fills any region with a non-zero winding number; - /// alternates fill/hole for each contour crossing. - /// - /// Defaults to . - /// - public IntersectionRule IntersectionRule { get; set; } = IntersectionRule.NonZero; - - /// - public ShapeOptions DeepClone() => new(this); -} diff --git a/src/ImageSharp.Drawing/Processing/SolidBrush.cs b/src/ImageSharp.Drawing/Processing/SolidBrush.cs index c4c5258d3..a28e076c7 100644 --- a/src/ImageSharp.Drawing/Processing/SolidBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SolidBrush.cs @@ -83,34 +83,16 @@ public override void Apply( scanline = scanline[..destinationRow.Length]; } - Configuration configuration = this.Configuration; - if (this.Options.BlendPercentage == 1F) - { - this.Blender.Blend( - configuration, - destinationRow, - destinationRow, - this.color, - scanline, - workspace.GetBlendScratch(scanline.Length, 2)); - } - else - { - Span amounts = workspace.GetAmounts(scanline.Length); - - for (int i = 0; i < scanline.Length; i++) - { - amounts[i] = scanline[i] * this.Options.BlendPercentage; - } - - this.Blender.Blend( - configuration, - destinationRow, - destinationRow, - this.color, - amounts, - workspace.GetBlendScratch(scanline.Length, 2)); - } + // The single-color overlay overload only needs two Vector4 scratch rows, + // unlike the per-pixel overlay path which needs three. + this.Blender.BlendWithCoverage( + this.Configuration, + destinationRow, + destinationRow, + this.color, + this.Options.BlendPercentage, + scanline, + workspace.GetBlendScratch(scanline.Length, 2)); } } } diff --git a/src/ImageSharp.Drawing/Processing/SolidPen.cs b/src/ImageSharp.Drawing/Processing/SolidPen.cs index a3f3165bf..9d29923af 100644 --- a/src/ImageSharp.Drawing/Processing/SolidPen.cs +++ b/src/ImageSharp.Drawing/Processing/SolidPen.cs @@ -4,7 +4,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing; /// -/// Defines a pen that can apply a pattern to a line with a set brush and thickness. +/// Defines a pen that draws a continuous, unpatterned stroke with a set brush and thickness. /// public class SolidPen : Pen { diff --git a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs index ba4fc59da..7e4a4c5d3 100644 --- a/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs +++ b/src/ImageSharp.Drawing/Processing/SweepGradientBrush.cs @@ -55,7 +55,7 @@ public SweepGradientBrush( public float EndAngleDegrees { get; } /// - public override Brush Transform(Matrix4x4 matrix) + public override Brush Transform(Matrix4x4 matrix, Rectangle sourceInterest, Rectangle preparedInterest) { PointF tc = PointF.Transform(this.Center, matrix); @@ -217,23 +217,36 @@ public override BrushRenderer CreateRenderer( Configuration configuration, GraphicsOptions options, int canvasWidth, - RectangleF region) => + RectangleF region) + { + if (TPixel.GetPixelTypeInfo().AlphaRepresentation == PixelAlphaRepresentation.Associated) + { + return new SweepGradientBrushRenderer>( + configuration, + options, + canvasWidth, + this, + this.ColorStopsArray, + this.RepetitionMode); + } - // The renderer precomputes the angular interval once and then samples it per pixel. - new SweepGradientBrushRenderer( + return new SweepGradientBrushRenderer>( configuration, options, canvasWidth, this, this.ColorStopsArray, this.RepetitionMode); + } /// /// The sweep (conic) gradient brush applicator. /// /// The pixel format. - private sealed class SweepGradientBrushRenderer : GradientBrushRenderer + /// The destination representation encoder. + private sealed class SweepGradientBrushRenderer : GradientBrushRenderer where TPixel : unmanaged, IPixel + where TEncoder : struct, IGradientPixelEncoder { private const float Tau = MathF.Tau; @@ -246,7 +259,7 @@ private sealed class SweepGradientBrushRenderer : GradientBrushRenderer< private readonly float endRad; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The configuration instance to use when performing operations. /// The graphics options. @@ -286,7 +299,7 @@ protected override float PositionOnGradient(float x, float y) } // Convert from y-down image space back into the brush's y-up angle convention, - // then normalize to [0, 2π) so subtraction against the stored start angle is stable. + // then normalize to [0, 2*pi) so subtraction against the stored start angle is stable. float angle = MathF.Atan2(-dy, dx); if (angle < 0f) { diff --git a/src/ImageSharp.Drawing/Processing/WrapMode.cs b/src/ImageSharp.Drawing/Processing/WrapMode.cs new file mode 100644 index 000000000..aea1284bd --- /dev/null +++ b/src/ImageSharp.Drawing/Processing/WrapMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Processing; + +/// +/// Defines how an samples beyond its source region along a single axis. +/// +public enum WrapMode +{ + /// + /// The source region is not repeated; pixels outside it are left transparent. + /// + None = 0, + + /// + /// The source region is tiled, repeating edge-to-edge. + /// + Repeat = 1, + + /// + /// The source region is tiled, mirroring every other repetition. + /// + Mirror = 2, + + /// + /// The source region is not repeated; the nearest edge pixel is sampled for coordinates outside it. + /// + Clamp = 3, +} diff --git a/src/ImageSharp.Drawing/RectanglePolygon.cs b/src/ImageSharp.Drawing/RectanglePolygon.cs index 352fea7c9..4d2cf9e09 100644 --- a/src/ImageSharp.Drawing/RectanglePolygon.cs +++ b/src/ImageSharp.Drawing/RectanglePolygon.cs @@ -2,19 +2,33 @@ // Licensed under the Six Labors Split License. using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; namespace SixLabors.ImageSharp.Drawing; /// /// A closed rectangular path defined by four straight edges. /// -public sealed class RectanglePolygon : IPath, ISimplePath, IPathInternals +public sealed class RectanglePolygon : IPath, ISimplePath { + /// + /// The top-left corner of the rectangle. + /// private readonly Vector2 topLeft; + + /// + /// The bottom-right corner of the rectangle. + /// private readonly Vector2 bottomRight; + + /// + /// The four corner points in clockwise order starting at the top-left. + /// private readonly PointF[] points; - private readonly float halfLength; - private readonly float length; + + /// + /// The per-scale cache of retained linear geometry. + /// private LinearGeometryCache geometryCache; /// @@ -53,8 +67,6 @@ public RectanglePolygon(PointF topLeft, PointF bottomRight) new Vector2(this.topLeft.X, this.bottomRight.Y) ]; - this.halfLength = this.Size.Width + this.Size.Height; - this.length = this.halfLength * 2; this.Bounds = new RectangleF(this.Location, this.Size); } @@ -82,7 +94,7 @@ public RectanglePolygon(RectangleF rectangle) } /// - /// Gets the location. + /// Gets the top-left location of the rectangle. /// public PointF Location { get; } @@ -92,7 +104,7 @@ public RectanglePolygon(RectangleF rectangle) public float Left => this.X; /// - /// Gets the x-coordinate. + /// Gets the x-coordinate of the top-left corner. /// public float X => this.topLeft.X; @@ -107,7 +119,7 @@ public RectanglePolygon(RectangleF rectangle) public float Top => this.Y; /// - /// Gets the y-coordinate. + /// Gets the y-coordinate of the top-left corner. /// public float Y => this.topLeft.Y; @@ -152,6 +164,7 @@ public RectanglePolygon(RectangleF rectangle) /// Converts a polygon to a rectangle polygon from its bounds. /// /// The polygon to convert. + /// The rectangle polygon covering the source polygon's bounds. public static explicit operator RectanglePolygon(Polygon polygon) => new(polygon.Bounds.X, polygon.Bounds.Y, polygon.Bounds.Width, polygon.Bounds.Height); @@ -163,53 +176,30 @@ public IPath Transform(Matrix4x4 matrix) return this; } - // Rectangles may be rotated and skewed which means they will then need representing by a polygon - return new Polygon(new LinearLineSegment(this.points).Transform(matrix)); - } - - /// - SegmentInfo IPathInternals.PointAlongPath(float distance) - { - distance %= this.length; - - if (distance < this.Width) - { - // we are on the top stretch - return new SegmentInfo - { - Point = new Vector2(this.Left + distance, this.Top), - Angle = MathF.PI - }; - } - - distance -= this.Width; - if (distance < this.Height) - { - // down on right - return new SegmentInfo - { - Point = new Vector2(this.Right, this.Top + distance), - Angle = -MathF.PI / 2 - }; - } - - distance -= this.Height; - if (distance < this.Width) + if (MatrixUtilities.PreservesAxisAlignedRectangles(matrix)) { - // bottom right to left - return new SegmentInfo - { - Point = new Vector2(this.Right - distance, this.Bottom), - Angle = 0 - }; + // Keep the rectangle type when possible so later clip/scissor code can still + // recognize the shape without flattening a generic polygon. + Vector2 topRight = new(this.bottomRight.X, this.topLeft.Y); + Vector2 bottomLeft = new(this.topLeft.X, this.bottomRight.Y); + + // Transform all four corners rather than transforming location/size. Negative + // scales and axis swaps can invert edges, so the bounds must be recomputed. + Vector2 p0 = Vector2.Transform(this.topLeft, matrix); + Vector2 p1 = Vector2.Transform(topRight, matrix); + Vector2 p2 = Vector2.Transform(this.bottomRight, matrix); + Vector2 p3 = Vector2.Transform(bottomLeft, matrix); + + float left = MathF.Min(MathF.Min(p0.X, p1.X), MathF.Min(p2.X, p3.X)); + float top = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); + float right = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); + float bottom = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); + + return new RectanglePolygon(RectangleF.FromLTRB(left, top, right, bottom)); } - distance -= this.Width; - return new SegmentInfo - { - Point = new Vector2(this.Left, this.Bottom - distance), - Angle = (float)(Math.PI / 2) - }; + // Skewed or freely rotated rectangles need polygon geometry to preserve their edges. + return new Polygon(new LinearLineSegment(this.points).Transform(matrix)); } /// @@ -224,6 +214,39 @@ public LinearGeometry ToLinearGeometry(Vector2 scale) ? hit : this.geometryCache.Store(scale, this.BuildLinearGeometry(scale)); + /// + public float ComputeLength(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeLength(); + + /// + public float ComputeArea(Vector2 scale) + => this.ToLinearGeometry(scale).ComputeArea(); + + /// + public bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale) + { + PointF scaledPoint = new(point.X * scale.X, point.Y * scale.Y); + + return this.ToLinearGeometry(scale).Contains(scaledPoint, intersectionRule); + } + + /// + public bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistance(distance, out pathPoint); + + /// + public bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint) + => this.ToLinearGeometry(scale).TryGetPathPointAtDistanceUnbounded(distance, out pathPoint); + + /// + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath path) + => this.ToLinearGeometry(scale).TryGetSegment(startDistance, stopDistance, startOnBeginFigure, out path); + + /// + /// Builds the retained four-point closed contour, scaling each corner by . + /// + /// The X/Y scale applied to the corner points. + /// The retained linear geometry. private LinearGeometry BuildLinearGeometry(Vector2 scale) { PointF p0 = new(this.points[0].X * scale.X, this.points[0].Y * scale.Y); @@ -237,37 +260,26 @@ private LinearGeometry BuildLinearGeometry(Vector2 scale) float minY = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); float maxX = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); float maxY = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); - - // Any rotation or shear in the transform can turn the axis-aligned edges into slanted ones, - // so count each edge individually rather than assuming the axis-aligned case. - int nonHorizontalSegmentCountPixelBoundary = 0; - int nonHorizontalSegmentCountPixelCenter = 0; - for (int i = 0; i < 4; i++) - { - PointF a = points[i]; - PointF b = points[(i + 1) % 4]; - if (MathF.Floor(a.Y) != MathF.Floor(b.Y)) - { - nonHorizontalSegmentCountPixelBoundary++; - } - - if (MathF.Floor(a.Y + 0.5F) != MathF.Floor(b.Y + 0.5F)) - { - nonHorizontalSegmentCountPixelCenter++; - } - } + RectangleF bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY); return new LinearGeometry( new LinearGeometryInfo { - Bounds = RectangleF.FromLTRB(minX, minY, maxX, maxY), + Bounds = bounds, ContourCount = 1, PointCount = 4, - SegmentCount = 4, - NonHorizontalSegmentCountPixelBoundary = nonHorizontalSegmentCountPixelBoundary, - NonHorizontalSegmentCountPixelCenter = nonHorizontalSegmentCountPixelCenter + SegmentCount = 4 }, - [new LinearContour { PointStart = 0, PointCount = 4, SegmentStart = 0, SegmentCount = 4, IsClosed = true }], + [new LinearContour + { + PointStart = 0, + PointCount = 4, + Bounds = bounds, + SegmentStart = 0, + SegmentCount = 4, + IsClosed = true + } + ], points); } diff --git a/src/ImageSharp.Drawing/Region.cs b/src/ImageSharp.Drawing/Region.cs new file mode 100644 index 000000000..72a1e920a --- /dev/null +++ b/src/ImageSharp.Drawing/Region.cs @@ -0,0 +1,1107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.ObjectModel; +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Helpers; + +namespace SixLabors.ImageSharp.Drawing; + +/// +/// Represents an integer region composed from axis-aligned rectangles. +/// +/// +/// The region stores exact integer coverage using a normalized rect-set model: horizontal +/// Y bands contain sorted X intervals. Region operations preserve the covered area as a +/// union of rectangles; exports that area as boundary geometry. +/// +public sealed class Region +{ + // The canonical model is the same shape used by SkRegion: a sorted set of horizontal + // Y bands, where each band owns sorted, non-overlapping X intervals. This preserves + // disjoint islands, L shapes, holes, and stair-step edges without collapsing anything + // to the bounding rectangle. + private readonly List bands = []; + + // Rectangles and the boundary path are exported views over the band model. They are + // cached because callers often ask for bounds/rectangles/path after building a region, + // but the bands remain the source of truth for region operations. + private readonly List rectangles = []; + private readonly ReadOnlyCollection rectanglesView; + private bool rectanglesValid = true; + private Rectangle bounds; + private IPath? path; + + /// + /// Initializes a new instance of the class. + /// + public Region() + => this.rectanglesView = this.rectangles.AsReadOnly(); + + /// + /// Initializes a new instance of the class containing the specified rectangle. + /// + /// The rectangle to add to the region. + public Region(Rectangle rectangle) + : this() => this.Add(rectangle); + + /// + /// Initializes a new instance of the class containing the same area as the specified region. + /// + /// The region to copy. + public Region(Region region) + : this() => this.CopyFrom(region); + + /// + /// Initializes a new instance of the class from normalized or unnormalized rectangles. + /// + /// The rectangles to union into the region. + internal Region(IReadOnlyList rectangles) + : this() + { + for (int i = 0; i < rectangles.Count; i++) + { + this.Add(rectangles[i]); + } + } + + /// + /// Gets a value indicating whether the region contains no area. + /// + public bool IsEmpty => this.bands.Count == 0; + + /// + /// Gets the bounding rectangle of the region. + /// + /// + /// Returns when the region is empty. + /// + public Rectangle Bounds => this.IsEmpty ? Rectangle.Empty : this.bounds; + + /// + /// Gets the non-overlapping rectangles that describe the region. + /// + /// + /// The rectangles are an exported view of the region area. They preserve the region shape + /// as a rect-set and are not collapsed to . + /// + public IReadOnlyList Rectangles + { + get + { + this.EnsureRectangles(); + return this.rectanglesView; + } + } + + /// + /// Adds a rectangle to the region. + /// + /// The rectangle to add. + /// + /// Rectangles with non-positive width or height do not change the region. The rectangle is + /// unioned with the existing rect-set. + /// + public void Add(Rectangle rectangle) + { + int left = rectangle.Left; + int top = rectangle.Top; + int right = rectangle.Right; + int bottom = rectangle.Bottom; + + if (left >= right || top >= bottom) + { + return; + } + + bool wasEmpty = this.IsEmpty; + + // Split existing bands at the incoming rectangle's top/bottom so the union can + // operate by merging only X intervals inside bands that exactly match the new + // rectangle's vertical span. + this.SplitAt(top); + this.SplitAt(bottom); + + int y = top; + int index = 0; + while (index < this.bands.Count && this.bands[index].Bottom <= y) + { + index++; + } + + while (y < bottom) + { + if (index < this.bands.Count && this.bands[index].Top < bottom) + { + RegionBand band = this.bands[index]; + if (y < band.Top) + { + int gapBottom = Math.Min(bottom, band.Top); + this.bands.Insert(index, new RegionBand(y, gapBottom, left, right)); + y = gapBottom; + index++; + continue; + } + + AddInterval(band.Intervals, left, right); + y = band.Bottom; + index++; + continue; + } + + this.bands.Insert(index, new RegionBand(y, bottom, left, right)); + y = bottom; + index++; + } + + this.MergeAdjacentBands(); + this.rectanglesValid = false; + this.path = null; + + this.bounds = wasEmpty + ? Rectangle.FromLTRB(left, top, right, bottom) + : Rectangle.FromLTRB( + Math.Min(this.bounds.Left, left), + Math.Min(this.bounds.Top, top), + Math.Max(this.bounds.Right, right), + Math.Max(this.bounds.Bottom, bottom)); + } + + /// + /// Removes all rectangles from the region. + /// + public void Clear() + { + this.bands.Clear(); + this.rectangles.Clear(); + this.rectanglesValid = true; + this.bounds = Rectangle.Empty; + this.path = null; + } + + /// + /// Returns a value indicating whether the region contains the specified point. + /// + /// The point to test. + /// if the region contains the point; otherwise, . + public bool Contains(Point point) => this.Contains(point.X, point.Y); + + /// + /// Returns a value indicating whether the region contains the specified point. + /// + /// The x-coordinate to test. + /// The y-coordinate to test. + /// if the region contains the point; otherwise, . + public bool Contains(int x, int y) + { + for (int i = 0; i < this.bands.Count; i++) + { + RegionBand band = this.bands[i]; + if (y < band.Top) + { + return false; + } + + if (y >= band.Bottom) + { + continue; + } + + List intervals = band.Intervals; + for (int j = 0; j < intervals.Count; j++) + { + Interval interval = intervals[j]; + if (x < interval.Left) + { + return false; + } + + if (x < interval.Right) + { + return true; + } + } + + return false; + } + + return false; + } + + /// + /// Returns a value indicating whether the region intersects the specified rectangle. + /// + /// The rectangle to test. + /// if the region intersects the rectangle; otherwise, . + public bool Intersects(Rectangle rectangle) + { + int left = rectangle.Left; + int top = rectangle.Top; + int right = rectangle.Right; + int bottom = rectangle.Bottom; + + if (left >= right || top >= bottom) + { + return false; + } + + for (int i = 0; i < this.bands.Count; i++) + { + RegionBand band = this.bands[i]; + if (band.Bottom <= top) + { + continue; + } + + if (band.Top >= bottom) + { + return false; + } + + List intervals = band.Intervals; + for (int j = 0; j < intervals.Count; j++) + { + Interval interval = intervals[j]; + if (interval.Right <= left) + { + continue; + } + + if (interval.Left >= right) + { + break; + } + + return true; + } + } + + return false; + } + + /// + /// Intersects this region with the specified rectangle. + /// + /// The rectangle to intersect with this region. + /// when the resulting region is not empty; otherwise, . + /// + /// This operation clips the existing rect-set to the rectangle. It does not replace the + /// result with one bounding rectangle. + /// + public bool Intersect(Rectangle rectangle) + { + int left = rectangle.Left; + int top = rectangle.Top; + int right = rectangle.Right; + int bottom = rectangle.Bottom; + if (left >= right || top >= bottom || this.IsEmpty) + { + this.Clear(); + return false; + } + + for (int i = 0; i < this.bands.Count;) + { + RegionBand band = this.bands[i]; + if (band.Bottom <= top || band.Top >= bottom) + { + this.bands.RemoveAt(i); + continue; + } + + band.Top = Math.Max(band.Top, top); + band.Bottom = Math.Min(band.Bottom, bottom); + + for (int j = 0; j < band.Intervals.Count;) + { + Interval interval = band.Intervals[j]; + int intervalLeft = Math.Max(interval.Left, left); + int intervalRight = Math.Min(interval.Right, right); + if (intervalLeft >= intervalRight) + { + band.Intervals.RemoveAt(j); + continue; + } + + band.Intervals[j] = new Interval(intervalLeft, intervalRight); + j++; + } + + if (band.Intervals.Count == 0) + { + this.bands.RemoveAt(i); + continue; + } + + i++; + } + + this.MergeAdjacentBands(); + this.UpdateBoundsFromBands(); + return !this.IsEmpty; + } + + /// + /// Intersects this region with the specified region. + /// + /// The region to intersect with this region. + /// when the resulting region is not empty; otherwise, . + /// + /// The operation intersects every overlapping X interval pair in every overlapping Y band + /// and unions the results. This is the rect-set form of region intersection. + /// + public bool Intersect(Region region) + { + if (this.IsEmpty || region.IsEmpty) + { + this.Clear(); + return false; + } + + Region result = new(); + int firstIndex = 0; + int secondIndex = 0; + while (firstIndex < this.bands.Count && secondIndex < region.bands.Count) + { + RegionBand first = this.bands[firstIndex]; + RegionBand second = region.bands[secondIndex]; + if (first.Bottom <= second.Top) + { + firstIndex++; + continue; + } + + if (second.Bottom <= first.Top) + { + secondIndex++; + continue; + } + + int top = Math.Max(first.Top, second.Top); + int bottom = Math.Min(first.Bottom, second.Bottom); + AddBandIntersection(result.bands, top, bottom, first.Intervals, second.Intervals); + + if (first.Bottom == bottom) + { + firstIndex++; + } + + if (second.Bottom == bottom) + { + secondIndex++; + } + } + + result.MergeAdjacentBands(); + result.UpdateBoundsFromBands(); + this.CopyFrom(result); + return !this.IsEmpty; + } + + /// + /// Creates a path representing the region. + /// + /// The path representing the region. + /// + /// The returned path describes the exact boundary of the region. Complex regions may + /// produce multiple closed figures. Returns when the region is empty. + /// + public IPath ToPath() + { + if (this.path is not null) + { + return this.path; + } + + this.EnsureRectangles(); + if (this.rectangles.Count == 0) + { + this.path = EmptyPath.ClosedPath; + return this.path; + } + + if (this.rectangles.Count == 1) + { + Rectangle rectangle = this.rectangles[0]; + this.path = new RegionPath([rectangle], ToPath(rectangle)); + return this.path; + } + + this.path = new RegionPath([.. this.rectangles], this.BuildBoundaryPath()); + return this.path; + } + + /// + /// Creates a closed path around the outside boundary of the region. + /// + /// The path describing the region boundary. + private IPath BuildBoundaryPath() + { + // Match SkRegion's boundary export shape: rectangles are first represented as + // opposing vertical edges, then linked into closed contours around the region + // boundary. Shared internal edges cancel because the rectangle list is already + // normalized into non-overlapping bands/intervals. + List edges = new(this.rectangles.Count * 2); + for (int i = 0; i < this.rectangles.Count; i++) + { + Rectangle rectangle = this.rectangles[i]; + edges.Add(new BoundaryEdge(rectangle.Left, rectangle.Bottom, rectangle.Top)); + edges.Add(new BoundaryEdge(rectangle.Right, rectangle.Top, rectangle.Bottom)); + } + + edges.Sort(static (a, b) => + { + int x = a.X.CompareTo(b.X); + return x != 0 ? x : a.Top.CompareTo(b.Top); + }); + + for (int i = 0; i < edges.Count; i++) + { + LinkBoundaryEdge(edges, i); + } + + PathBuilder builder = new(); + int remaining = edges.Count; + while (remaining > 0) + { + remaining -= ExtractBoundaryFigure(edges, builder); + } + + return builder.Build(); + } + + /// + /// Links one vertical edge to the two neighbouring vertical edges that share its end points. + /// + /// The sorted boundary edges. + /// The edge index to link. + private static void LinkBoundaryEdge(List edges, int index) + { + BoundaryEdge edge = edges[index]; + if (edge.Flags == BoundaryEdge.Complete) + { + return; + } + + if ((edge.Flags & BoundaryEdge.Y0Linked) == 0) + { + int i = index + 1; + while ((edges[i].Flags & BoundaryEdge.Y1Linked) != 0 || edge.Y0 != edges[i].Y1) + { + i++; + } + + BoundaryEdge linked = edges[i]; + linked.Next = edge; + linked.Flags |= BoundaryEdge.Y1Linked; + } + + if ((edge.Flags & BoundaryEdge.Y1Linked) == 0) + { + int i = index + 1; + while ((edges[i].Flags & BoundaryEdge.Y0Linked) != 0 || edge.Y1 != edges[i].Y0) + { + i++; + } + + BoundaryEdge linked = edges[i]; + edge.Next = linked; + linked.Flags |= BoundaryEdge.Y0Linked; + } + + edge.Flags = BoundaryEdge.Complete; + } + + /// + /// Extracts one closed boundary figure from the linked edge graph. + /// + /// The boundary edges. + /// The path builder to append to. + /// The number of consumed edges. + private static int ExtractBoundaryFigure(List edges, PathBuilder builder) + { + int index = 0; + while (edges[index].Flags == 0) + { + index++; + } + + BoundaryEdge first = edges[index]; + BoundaryEdge previous = first; + BoundaryEdge edge = first.Next!; + + _ = builder.MoveTo(previous.X, previous.Y0); + + previous.Flags = 0; + int count = 1; + while (!ReferenceEquals(edge, first)) + { + // Emit the vertical remainder of the previous edge and the horizontal + // connector to the next edge. Collinear continuations (same X, contiguous Y) + // need no intermediate points. + if (previous.X != edge.X || previous.Y1 != edge.Y0) + { + _ = builder.LineTo(previous.X, previous.Y1); + _ = builder.LineTo(edge.X, edge.Y0); + } + + previous = edge; + edge = edge.Next!; + previous.Flags = 0; + count++; + } + + _ = builder.LineTo(previous.X, previous.Y1); + _ = builder.CloseFigure(); + + return count; + } + + /// + /// Splits the band containing the specified Y coordinate. + /// + /// The Y coordinate where a band boundary is required. + private void SplitAt(int y) + { + // Region operations work on whole bands. Splitting at a Y boundary creates the + // exact band ranges needed for the next union/intersection step without changing + // the represented area. + for (int i = 0; i < this.bands.Count; i++) + { + RegionBand band = this.bands[i]; + if (y <= band.Top) + { + return; + } + + if (y >= band.Bottom) + { + continue; + } + + RegionBand lower = band.DeepClone(y, band.Bottom); + band.Bottom = y; + this.bands.Insert(i + 1, lower); + return; + } + } + + /// + /// Merges neighbouring bands that have identical X coverage. + /// + private void MergeAdjacentBands() + { + // Adjacent bands with identical X coverage represent one taller rectangle strip. + // Merging keeps the canonical representation compact and keeps exported rectangles + // stable without changing the region area. + for (int i = 1; i < this.bands.Count;) + { + RegionBand previous = this.bands[i - 1]; + RegionBand current = this.bands[i]; + + if (previous.Bottom == current.Top && IntervalsEqual(previous.Intervals, current.Intervals)) + { + previous.Bottom = current.Bottom; + this.bands.RemoveAt(i); + continue; + } + + i++; + } + } + + /// + /// Replaces this region with a copy of another region's canonical band data. + /// + /// The region to copy from. + private void CopyFrom(Region region) + { + this.bands.Clear(); + for (int i = 0; i < region.bands.Count; i++) + { + this.bands.Add(region.bands[i].DeepClone()); + } + + this.bounds = region.bounds; + this.rectangles.Clear(); + this.rectanglesValid = false; + this.path = null; + + if (region.IsEmpty) + { + this.rectanglesValid = true; + } + } + + /// + /// Recomputes exported state after a destructive band operation. + /// + private void UpdateBoundsFromBands() + { + // Bounds are a view over the represented area. Recompute from intervals after + // destructive operations so complex shapes keep their actual extents instead of + // inheriting stale operand bounds. + this.bounds = Rectangle.Empty; + this.rectanglesValid = false; + this.path = null; + + if (this.bands.Count == 0) + { + this.rectangles.Clear(); + this.rectanglesValid = true; + return; + } + + int left = int.MaxValue; + int top = this.bands[0].Top; + int right = int.MinValue; + int bottom = this.bands[^1].Bottom; + for (int i = 0; i < this.bands.Count; i++) + { + List intervals = this.bands[i].Intervals; + for (int j = 0; j < intervals.Count; j++) + { + Interval interval = intervals[j]; + left = Math.Min(left, interval.Left); + right = Math.Max(right, interval.Right); + } + } + + this.bounds = Rectangle.FromLTRB(left, top, right, bottom); + } + + /// + /// Materializes the exported rectangle view from the canonical band data. + /// + private void EnsureRectangles() + { + if (this.rectanglesValid) + { + return; + } + + this.rectangles.Clear(); + for (int i = 0; i < this.bands.Count; i++) + { + RegionBand band = this.bands[i]; + List intervals = band.Intervals; + + for (int j = 0; j < intervals.Count; j++) + { + Interval interval = intervals[j]; + this.rectangles.Add(Rectangle.FromLTRB(interval.Left, band.Top, interval.Right, band.Bottom)); + } + } + + this.rectanglesValid = true; + } + + /// + /// Unions one X interval into a sorted interval list. + /// + /// The interval list to update. + /// The left edge of the interval. + /// The right edge of the interval. + private static void AddInterval(List intervals, int left, int right) + { + // Intervals inside one band are sorted and non-overlapping. Adding one interval + // is therefore a local union operation over X coverage for that Y span. + int index = 0; + while (index < intervals.Count && intervals[index].Right < left) + { + index++; + } + + int mergedLeft = left; + int mergedRight = right; + + // Touching intervals are merged because integer regions have no gap between [a,b) and [b,c). + while (index < intervals.Count && intervals[index].Left <= mergedRight) + { + Interval interval = intervals[index]; + mergedLeft = Math.Min(mergedLeft, interval.Left); + mergedRight = Math.Max(mergedRight, interval.Right); + intervals.RemoveAt(index); + } + + intervals.Insert(index, new Interval(mergedLeft, mergedRight)); + } + + /// + /// Adds the X interval intersections for one overlapping Y band. + /// + /// The result bands receiving the intersection band. + /// The top of the overlapping Y band. + /// The bottom of the overlapping Y band. + /// The first sorted X interval list. + /// The second sorted X interval list. + private static void AddBandIntersection( + List bands, + int top, + int bottom, + List first, + List second) + { + // Both interval lists are sorted. Sweep them to produce the X-overlap intervals + // for the already-overlapped Y band. The caller unions every produced band into + // the result region, preserving L shapes and disjoint islands as a rect-set. + RegionBand? band = null; + int firstIndex = 0; + int secondIndex = 0; + while (firstIndex < first.Count && secondIndex < second.Count) + { + Interval a = first[firstIndex]; + Interval b = second[secondIndex]; + if (a.Right <= b.Left) + { + firstIndex++; + continue; + } + + if (b.Right <= a.Left) + { + secondIndex++; + continue; + } + + int left = Math.Max(a.Left, b.Left); + int right = Math.Min(a.Right, b.Right); + if (left < right) + { + band ??= new RegionBand(top, bottom); + band.Intervals.Add(new Interval(left, right)); + } + + if (a.Right == right) + { + firstIndex++; + } + + if (b.Right == right) + { + secondIndex++; + } + } + + if (band is not null) + { + bands.Add(band); + } + } + + /// + /// Compares two X interval lists for identical coverage. + /// + /// The first interval list. + /// The second interval list. + /// when both interval lists describe the same X coverage. + private static bool IntervalsEqual(List first, List second) + { + if (first.Count != second.Count) + { + return false; + } + + for (int i = 0; i < first.Count; i++) + { + if (first[i].Left != second[i].Left || first[i].Right != second[i].Right) + { + return false; + } + } + + return true; + } + + /// + /// Converts one rectangle to its boundary path. + /// + /// The rectangle to convert. + /// The rectangle path. + private static RectanglePolygon ToPath(Rectangle rectangle) + => new(rectangle); + + /// + /// Represents one filled X interval inside a region band. + /// + private readonly struct Interval + { + /// + /// Initializes a new instance of the struct. + /// + /// The inclusive left edge. + /// The exclusive right edge. + public Interval(int left, int right) + { + this.Left = left; + this.Right = right; + } + + /// + /// Gets the inclusive left edge. + /// + public int Left { get; } + + /// + /// Gets the exclusive right edge. + /// + public int Right { get; } + } + + /// + /// Represents one Y band with common X interval coverage. + /// + private sealed class RegionBand + { + // A band covers [Top, Bottom) and owns all X intervals that are filled for every + // scanline in that vertical span. + + /// + /// Initializes a new instance of the class. + /// + /// The inclusive top edge. + /// The exclusive bottom edge. + public RegionBand(int top, int bottom) + { + this.Top = top; + this.Bottom = bottom; + } + + /// + /// Initializes a new instance of the class. + /// + /// The inclusive top edge. + /// The exclusive bottom edge. + /// The inclusive left edge of the initial interval. + /// The exclusive right edge of the initial interval. + public RegionBand(int top, int bottom, int left, int right) + { + this.Top = top; + this.Bottom = bottom; + this.Intervals.Add(new Interval(left, right)); + } + + /// + /// Initializes a new instance of the class. + /// + /// The inclusive top edge. + /// The exclusive bottom edge. + /// The intervals to copy. + private RegionBand(int top, int bottom, List intervals) + { + this.Top = top; + this.Bottom = bottom; + this.Intervals.AddRange(intervals); + } + + /// + /// Gets or sets the inclusive top edge. + /// + public int Top { get; set; } + + /// + /// Gets or sets the exclusive bottom edge. + /// + public int Bottom { get; set; } + + /// + /// Gets the sorted, non-overlapping X intervals for this band. + /// + public List Intervals { get; } = []; + + /// + /// Creates a copy of this band. + /// + /// The copied band. + public RegionBand DeepClone() => new(this.Top, this.Bottom, this.Intervals); + + /// + /// Creates a copy of this band's X coverage over a different Y range. + /// + /// The inclusive top edge. + /// The exclusive bottom edge. + /// The copied band. + public RegionBand DeepClone(int top, int bottom) => new(top, bottom, this.Intervals); + } + + /// + /// Represents one vertical edge used when exporting the region boundary path. + /// + private sealed class BoundaryEdge + { + // Boundary export works by linking vertical edges into closed contours. Y0/Y1 + // preserve edge direction so the resulting path follows the outside boundary + // rather than emitting independent rectangle outlines. + + /// + /// Flag set when another edge has been linked into this edge's endpoint. + /// + public const byte Y0Linked = 0x01; + + /// + /// Flag set when this edge's endpoint has been linked to another edge. + /// + public const byte Y1Linked = 0x02; + + /// + /// Flag value indicating both endpoints are linked and the edge needs no further processing. + /// + public const byte Complete = Y0Linked | Y1Linked; + + /// + /// Initializes a new instance of the class. + /// + /// The X coordinate of the vertical edge. + /// The first Y endpoint. + /// The second Y endpoint. + public BoundaryEdge(int x, int y0, int y1) + { + this.X = x; + this.Y0 = y0; + this.Y1 = y1; + this.Top = Math.Min(y0, y1); + } + + /// + /// Gets the X coordinate of the vertical edge. + /// + public int X { get; } + + /// + /// Gets the first Y endpoint. + /// + public int Y0 { get; } + + /// + /// Gets the second Y endpoint. + /// + public int Y1 { get; } + + /// + /// Gets the topmost Y endpoint. + /// + public int Top { get; } + + /// + /// Gets or sets the edge linkage flags. + /// + public byte Flags { get; set; } + + /// + /// Gets or sets the next edge in the exported boundary contour. + /// + public BoundaryEdge? Next { get; set; } + } + + /// + /// Wraps a region boundary path with the rect-set metadata that produced it. + /// + private sealed class RegionPath : IRegionPath + { + private readonly IPath path; + + /// + /// Initializes a new instance of the class. + /// + /// The normalized rectangles that describe the same region as the boundary path. + /// The exported boundary path. + public RegionPath(Rectangle[] rectangles, IPath path) + { + this.Rectangles = rectangles; + this.path = path; + } + + /// + public IReadOnlyList Rectangles { get; } + + /// + public PathTypes PathType => this.path.PathType; + + /// + public RectangleF Bounds => this.path.Bounds; + + /// + public IPath AsClosedPath() => this; + + /// + public IEnumerable Flatten() => this.path.Flatten(); + + /// + public bool Contains(PointF point, IntersectionRule intersectionRule, Vector2 scale) + => this.path.Contains(point, intersectionRule, scale); + + /// + public bool TryGetPathPointAtDistance(float distance, Vector2 scale, out PathPoint pathPoint) + => this.path.TryGetPathPointAtDistance(distance, scale, out pathPoint); + + /// + public bool TryGetPathPointAtDistanceUnbounded(float distance, Vector2 scale, out PathPoint pathPoint) + => this.path.TryGetPathPointAtDistanceUnbounded(distance, scale, out pathPoint); + + /// + public bool TryGetSegment(float startDistance, float stopDistance, bool startOnBeginFigure, Vector2 scale, out IPath segment) + => this.path.TryGetSegment(startDistance, stopDistance, startOnBeginFigure, scale, out segment); + + /// + public LinearGeometry ToLinearGeometry(Vector2 scale) => this.path.ToLinearGeometry(scale); + + /// + public float ComputeLength(Vector2 scale) => this.path.ComputeLength(scale); + + /// + public float ComputeArea(Vector2 scale) => this.path.ComputeArea(scale); + + /// + public IPath Transform(Matrix4x4 matrix) + { + if (matrix.IsIdentity) + { + return this; + } + + if (MatrixUtilities.PreservesAxisAlignedRectangles(matrix)) + { + // RegionPath metadata is an integer rect-set. It can survive translation, + // scale, reflection, and axis swaps only while every transformed edge is + // still integer; fractional edges need the exact path geometry fallback. + Rectangle[] transformedRectangles = new Rectangle[this.Rectangles.Count]; + for (int i = 0; i < transformedRectangles.Length; i++) + { + Rectangle rectangle = this.Rectangles[i]; + + // Transform every corner so negative scales and axis swaps cannot leave + // left/right or top/bottom inverted. + Vector2 p0 = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Top), matrix); + Vector2 p1 = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Top), matrix); + Vector2 p2 = Vector2.Transform(new Vector2(rectangle.Right, rectangle.Bottom), matrix); + Vector2 p3 = Vector2.Transform(new Vector2(rectangle.Left, rectangle.Bottom), matrix); + + float left = MathF.Min(MathF.Min(p0.X, p1.X), MathF.Min(p2.X, p3.X)); + float top = MathF.Min(MathF.Min(p0.Y, p1.Y), MathF.Min(p2.Y, p3.Y)); + float right = MathF.Max(MathF.Max(p0.X, p1.X), MathF.Max(p2.X, p3.X)); + float bottom = MathF.Max(MathF.Max(p0.Y, p1.Y), MathF.Max(p2.Y, p3.Y)); + + int integerLeft = (int)left; + int integerTop = (int)top; + int integerRight = (int)right; + int integerBottom = (int)bottom; + if (left != integerLeft || top != integerTop || right != integerRight || bottom != integerBottom) + { + // Keep exact clipping semantics rather than widening a fractional + // transformed region into conservative integer rectangles. + return this.path.Transform(matrix); + } + + transformedRectangles[i] = Rectangle.FromLTRB(integerLeft, integerTop, integerRight, integerBottom); + } + + // Rebuild through Region so scaled/reflected rectangles are normalized before + // the metadata is exposed again. + return new Region(transformedRectangles).ToPath(); + } + + // Skew and free rotation turn the rect-set into ordinary path geometry. + return this.path.Transform(matrix); + } + } +} diff --git a/src/ImageSharp.Drawing/RegularPolygon.cs b/src/ImageSharp.Drawing/RegularPolygon.cs index a3c847d48..1989d1422 100644 --- a/src/ImageSharp.Drawing/RegularPolygon.cs +++ b/src/ImageSharp.Drawing/RegularPolygon.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Drawing; /// -/// A shape made up of a single path made up of one of more s +/// A regular polygon with a configurable number of equally spaced vertices placed on a circumscribing circle. /// public class RegularPolygon : Polygon { @@ -58,6 +58,15 @@ public RegularPolygon(float x, float y, int vertices, float radius) { } + /// + /// Places the vertices at equal angular steps on the circumscribing circle, + /// starting from the rotated downward radius vector. + /// + /// The center of the polygon. + /// The radius of the circumscribing circle. + /// The number of vertices; at least 3 are required. + /// The angle of rotation in degrees. + /// The linear segment describing the polygon outline. private static LinearLineSegment CreateSegment(PointF location, float radius, int vertices, float angle) { Guard.MustBeGreaterThan(vertices, 2, nameof(vertices)); diff --git a/src/ImageSharp.Drawing/RoundedRectanglePolygon.cs b/src/ImageSharp.Drawing/RoundedRectanglePolygon.cs index b24d60a04..d0b6fa216 100644 --- a/src/ImageSharp.Drawing/RoundedRectanglePolygon.cs +++ b/src/ImageSharp.Drawing/RoundedRectanglePolygon.cs @@ -16,7 +16,7 @@ public sealed class RoundedRectanglePolygon : Polygon /// The rectangle bounds. /// The x and y radius of each corner. public RoundedRectanglePolygon(RectangleF rectangle, float radius) - : this(rectangle, new SizeF(radius, radius)) + : this(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height, radius) { } @@ -26,7 +26,33 @@ public RoundedRectanglePolygon(RectangleF rectangle, float radius) /// The rectangle bounds. /// The x and y radii of each corner. public RoundedRectanglePolygon(RectangleF rectangle, SizeF radius) - : base(CreateSegments(rectangle, radius)) + : this(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height, radius) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The rectangle bounds. + /// The x and y radii of the top-left corner. + /// The x and y radii of the top-right corner. + /// The x and y radii of the bottom-right corner. + /// The x and y radii of the bottom-left corner. + public RoundedRectanglePolygon( + RectangleF rectangle, + SizeF topLeftRadius, + SizeF topRightRadius, + SizeF bottomRightRadius, + SizeF bottomLeftRadius) + : this( + rectangle.X, + rectangle.Y, + rectangle.Width, + rectangle.Height, + topLeftRadius, + topRightRadius, + bottomRightRadius, + bottomLeftRadius) { } @@ -39,7 +65,7 @@ public RoundedRectanglePolygon(RectangleF rectangle, SizeF radius) /// The rectangle height. /// The x and y radius of each corner. public RoundedRectanglePolygon(float x, float y, float width, float height, float radius) - : this(new RectangleF(x, y, width, height), radius) + : this(x, y, width, height, new SizeF(radius, radius)) { } @@ -52,10 +78,39 @@ public RoundedRectanglePolygon(float x, float y, float width, float height, floa /// The rectangle height. /// The x and y radii of each corner. public RoundedRectanglePolygon(float x, float y, float width, float height, SizeF radius) - : this(new RectangleF(x, y, width, height), radius) + : this(x, y, width, height, radius, radius, radius, radius) { } + /// + /// Initializes a new instance of the class. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radii of the top-left corner. + /// The x and y radii of the top-right corner. + /// The x and y radii of the bottom-right corner. + /// The x and y radii of the bottom-left corner. + public RoundedRectanglePolygon( + float x, + float y, + float width, + float height, + SizeF topLeftRadius, + SizeF topRightRadius, + SizeF bottomRightRadius, + SizeF bottomLeftRadius) + : base(CreateSegments(x, y, width, height, topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)) + { + } + + /// + /// Initializes a new instance of the class. + /// Used by to wrap already-transformed segments. + /// + /// The transformed segments; ownership passes to the new instance. private RoundedRectanglePolygon(ILineSegment[] segments) : base(segments, true) { @@ -79,24 +134,53 @@ public override IPath Transform(Matrix4x4 matrix) return new RoundedRectanglePolygon(segments); } - private static ILineSegment[] CreateSegments(RectangleF rectangle, SizeF radius) + /// + /// Builds the clockwise contour of alternating edges and 90 degree corner arcs. Corners with a + /// non-positive radius component are emitted square, and radii too large for the rectangle are + /// scaled down uniformly so opposing corners never overlap. + /// + /// The x-coordinate of the rectangle. + /// The y-coordinate of the rectangle. + /// The rectangle width. + /// The rectangle height. + /// The x and y radii of the top-left corner. + /// The x and y radii of the top-right corner. + /// The x and y radii of the bottom-right corner. + /// The x and y radii of the bottom-left corner. + /// The segments describing the rounded rectangle, or an empty array for a degenerate rectangle. + private static ILineSegment[] CreateSegments( + float x, + float y, + float width, + float height, + SizeF topLeftRadius, + SizeF topRightRadius, + SizeF bottomRightRadius, + SizeF bottomLeftRadius) { + RectangleF rectangle = new(x, y, width, height); float left = MathF.Min(rectangle.Left, rectangle.Right); float top = MathF.Min(rectangle.Top, rectangle.Bottom); float right = MathF.Max(rectangle.Left, rectangle.Right); float bottom = MathF.Max(rectangle.Top, rectangle.Bottom); - float width = right - left; - float height = bottom - top; + float rectangleWidth = right - left; + float rectangleHeight = bottom - top; - if (width <= 0 || height <= 0) + if (rectangleWidth <= 0 || rectangleHeight <= 0) { return []; } - float radiusX = radius.Width; - float radiusY = radius.Height; + SizeF topLeft = topLeftRadius.Width > 0F && topLeftRadius.Height > 0F ? topLeftRadius : default; + SizeF topRight = topRightRadius.Width > 0F && topRightRadius.Height > 0F ? topRightRadius : default; + SizeF bottomRight = bottomRightRadius.Width > 0F && bottomRightRadius.Height > 0F ? bottomRightRadius : default; + SizeF bottomLeft = bottomLeftRadius.Width > 0F && bottomLeftRadius.Height > 0F ? bottomLeftRadius : default; + bool hasTopLeftRadius = topLeft.Width > 0F; + bool hasTopRightRadius = topRight.Width > 0F; + bool hasBottomRightRadius = bottomRight.Width > 0F; + bool hasBottomLeftRadius = bottomLeft.Width > 0F; - if (radiusX <= 0 || radiusY <= 0) + if (!hasTopLeftRadius && !hasTopRightRadius && !hasBottomRightRadius && !hasBottomLeftRadius) { return [ @@ -108,34 +192,96 @@ private static ILineSegment[] CreateSegments(RectangleF rectangle, SizeF radius) ]; } - float radiusScale = MathF.Min(width / (radiusX + radiusX), height / (radiusY + radiusY)); + float radiiWidth = MathF.Max(topLeft.Width + topRight.Width, bottomLeft.Width + bottomRight.Width); + float radiiHeight = MathF.Max(topLeft.Height + bottomLeft.Height, topRight.Height + bottomRight.Height); + float radiusScale = MathF.Min(rectangleWidth / radiiWidth, rectangleHeight / radiiHeight); + if (radiusScale < 1F) { // Preserve the supplied corner shape while shrinking it enough that opposing corners do not overlap. - radiusX *= radiusScale; - radiusY *= radiusScale; + topLeft = new SizeF(topLeft.Width * radiusScale, topLeft.Height * radiusScale); + topRight = new SizeF(topRight.Width * radiusScale, topRight.Height * radiusScale); + bottomRight = new SizeF(bottomRight.Width * radiusScale, bottomRight.Height * radiusScale); + bottomLeft = new SizeF(bottomLeft.Width * radiusScale, bottomLeft.Height * radiusScale); + } + + PointF topLeftPoint = new(left + topLeft.Width, top); + PointF topRightPoint = new(right - topRight.Width, top); + PointF rightTopPoint = new(right, top + topRight.Height); + PointF rightBottomPoint = new(right, bottom - bottomRight.Height); + PointF bottomRightPoint = new(right - bottomRight.Width, bottom); + PointF bottomLeftPoint = new(left + bottomLeft.Width, bottom); + PointF leftBottomPoint = new(left, bottom - bottomLeft.Height); + PointF leftTopPoint = new(left, top + topLeft.Height); + int roundedCornerCount = 0; + + if (hasTopRightRadius) + { + roundedCornerCount++; + } + + if (hasBottomRightRadius) + { + roundedCornerCount++; + } + + if (hasBottomLeftRadius) + { + roundedCornerCount++; + } + + if (hasTopLeftRadius) + { + roundedCornerCount++; + } + + ILineSegment[] segments = new ILineSegment[4 + roundedCornerCount]; + int index = 0; + + segments[index++] = new LinearLineSegment(topLeftPoint, topRightPoint); + if (hasTopRightRadius) + { + segments[index++] = new ArcLineSegment( + new PointF(right - topRight.Width, top + topRight.Height), + topRight, + 0F, + -90F, + 90F); + } + + segments[index++] = new LinearLineSegment(rightTopPoint, rightBottomPoint); + if (hasBottomRightRadius) + { + segments[index++] = new ArcLineSegment( + new PointF(right - bottomRight.Width, bottom - bottomRight.Height), + bottomRight, + 0F, + 0F, + 90F); + } + + segments[index++] = new LinearLineSegment(bottomRightPoint, bottomLeftPoint); + if (hasBottomLeftRadius) + { + segments[index++] = new ArcLineSegment( + new PointF(left + bottomLeft.Width, bottom - bottomLeft.Height), + bottomLeft, + 0F, + 90F, + 90F); + } + + segments[index++] = new LinearLineSegment(leftBottomPoint, leftTopPoint); + if (hasTopLeftRadius) + { + segments[index++] = new ArcLineSegment( + new PointF(left + topLeft.Width, top + topLeft.Height), + topLeft, + 0F, + 180F, + 90F); } - SizeF cornerRadius = new(radiusX, radiusY); - PointF topLeft = new(left + radiusX, top); - PointF topRight = new(right - radiusX, top); - PointF rightTop = new(right, top + radiusY); - PointF rightBottom = new(right, bottom - radiusY); - PointF bottomRight = new(right - radiusX, bottom); - PointF bottomLeft = new(left + radiusX, bottom); - PointF leftBottom = new(left, bottom - radiusY); - PointF leftTop = new(left, top + radiusY); - - return - [ - new LinearLineSegment(topLeft, topRight), - new ArcLineSegment(new PointF(right - radiusX, top + radiusY), cornerRadius, 0F, -90F, 90F), - new LinearLineSegment(rightTop, rightBottom), - new ArcLineSegment(new PointF(right - radiusX, bottom - radiusY), cornerRadius, 0F, 0F, 90F), - new LinearLineSegment(bottomRight, bottomLeft), - new ArcLineSegment(new PointF(left + radiusX, bottom - radiusY), cornerRadius, 0F, 90F, 90F), - new LinearLineSegment(leftBottom, leftTop), - new ArcLineSegment(new PointF(left + radiusX, top + radiusY), cornerRadius, 0F, 180F, 90F) - ]; + return segments; } } diff --git a/src/ImageSharp.Drawing/SegmentEnumerator.cs b/src/ImageSharp.Drawing/SegmentEnumerator.cs index 5a12a50d5..6997b0077 100644 --- a/src/ImageSharp.Drawing/SegmentEnumerator.cs +++ b/src/ImageSharp.Drawing/SegmentEnumerator.cs @@ -13,11 +13,30 @@ namespace SixLabors.ImageSharp.Drawing; /// public ref struct SegmentEnumerator { + /// + /// The geometry whose derived segments are enumerated. + /// private readonly LinearGeometry geometry; + + /// + /// The zero-based index of the contour currently being enumerated. + /// private int contourIndex; + + /// + /// The zero-based index of the next segment to yield within the current contour. + /// private int segmentIndexInContour; + + /// + /// The most recently yielded segment. + /// private LinearSegment current; + /// + /// Initializes a new instance of the struct positioned before the first segment. + /// + /// The geometry whose derived segments are enumerated. internal SegmentEnumerator(LinearGeometry geometry) { this.geometry = geometry; @@ -48,11 +67,15 @@ public bool MoveNext() int pointIndex = pointStart + this.segmentIndexInContour; PointF start = this.geometry.Points[pointIndex]; + + // Closed contours have SegmentCount == PointCount, so the final index wraps back to + // the first stored point and forms the closing segment. Open contours have + // SegmentCount == PointCount - 1 and never reach the wrapping branch. PointF end = this.segmentIndexInContour == contour.PointCount - 1 ? this.geometry.Points[pointStart] : this.geometry.Points[pointIndex + 1]; - this.current = CreateSegment(start, end); + this.current = CreateSegment(start, end, this.contourIndex); this.segmentIndexInContour++; return true; } @@ -64,11 +87,19 @@ public bool MoveNext() return false; } - private static LinearSegment CreateSegment(PointF start, PointF end) + /// + /// Creates a segment with its precomputed per-segment metadata. + /// + /// The segment start point. + /// The segment end point. + /// The zero-based index of the owning contour. + /// The derived . + private static LinearSegment CreateSegment(PointF start, PointF end, int contourIndex) => new() { Start = start, End = end, + ContourIndex = contourIndex, MinY = MathF.Min(start.Y, end.Y), MaxY = MathF.Max(start.Y, end.Y), IsHorizontal = start.Y == end.Y diff --git a/src/ImageSharp.Drawing/SegmentInfo.cs b/src/ImageSharp.Drawing/SegmentInfo.cs deleted file mode 100644 index 632ba035e..000000000 --- a/src/ImageSharp.Drawing/SegmentInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -namespace SixLabors.ImageSharp.Drawing; - -/// -/// Returns metadata about the point along a path. -/// -public readonly struct SegmentInfo -{ - /// - /// Gets the point on the path - /// - public PointF Point { get; init; } - - /// - /// Gets the angle of the segment. Measured in radians. - /// - public float Angle { get; init; } -} diff --git a/src/ImageSharp.Drawing/SplitPathExtensions.cs b/src/ImageSharp.Drawing/SplitPathExtensions.cs index b551121dc..08d768bb4 100644 --- a/src/ImageSharp.Drawing/SplitPathExtensions.cs +++ b/src/ImageSharp.Drawing/SplitPathExtensions.cs @@ -25,7 +25,19 @@ public static class SplitPathExtensions /// The dash pattern. Each element is a multiple of . /// A path containing the "on" dash segments. public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern) - => path.GenerateDashes(strokeWidth, pattern, startOff: false); + => path.GenerateDashes(strokeWidth, pattern, startOff: false, offset: 0); + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// The distance into the dash pattern, expressed as a multiple of . + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, float offset) + => path.GenerateDashes(strokeWidth, pattern, false, offset); /// /// Splits the given path into dash segments based on the provided pattern. @@ -37,6 +49,19 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS /// Whether the first item in the pattern is off rather than on. /// A path containing the "on" dash segments. public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, bool startOff) + => path.GenerateDashes(strokeWidth, pattern, startOff, offset: 0); + + /// + /// Splits the given path into dash segments based on the provided pattern. + /// Returns a composite path containing only the "on" segments as open sub-paths. + /// + /// The centerline path to split. + /// The stroke width (pattern elements are multiples of this). + /// The dash pattern. Each element is a multiple of . + /// Whether the first item in the pattern is off rather than on. + /// The distance into the dash pattern, expressed as a multiple of . + /// A path containing the "on" dash segments. + public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlySpan pattern, bool startOff, float offset) { if (pattern.Length < 2) { @@ -58,6 +83,12 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS return path; } + float patternOffset = (offset * strokeWidth) % patternLength; + if (patternOffset < 0) + { + patternOffset += patternLength; + } + IEnumerable simplePaths = path.Flatten(); List segments = []; List buffer = new(64); @@ -66,7 +97,24 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS { bool online = !startOff; int patternPos = 0; - float targetLength = pattern[patternPos] * strokeWidth; + float currentOffset = patternOffset; + + // Phase is normalized once per contour so the main walker can keep + // the existing single-pass split logic. + while (currentOffset > eps) + { + float patternElementLength = MathF.Abs(pattern[patternPos]) * strokeWidth; + if (currentOffset + eps < patternElementLength) + { + break; + } + + currentOffset -= patternElementLength; + online = !online; + patternPos = (patternPos + 1) % pattern.Length; + } + + float targetLength = (MathF.Abs(pattern[patternPos]) * strokeWidth) - currentOffset; ReadOnlySpan pts = p.Points.Span; if (pts.Length < 2) @@ -143,7 +191,7 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS current = next; ei++; patternPos = (patternPos + 1) % pattern.Length; - targetLength = pattern[patternPos] * strokeWidth; + targetLength = MathF.Abs(pattern[patternPos]) * strokeWidth; continue; } @@ -162,7 +210,7 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS online = !online; current = split; // continue along the same geometric segment patternPos = (patternPos + 1) % pattern.Length; - targetLength = pattern[patternPos] * strokeWidth; + targetLength = MathF.Abs(pattern[patternPos]) * strokeWidth; } // Flush the tail of the last dash span, if any. @@ -180,7 +228,7 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS if (segments.Count == 0) { - return path; + return Path.Empty; } if (segments.Count == 1) @@ -191,8 +239,15 @@ public static IPath GenerateDashes(this IPath path, float strokeWidth, ReadOnlyS return new ComplexPolygon(segments); } + /// + /// Emits the buffered dash span as an open sub-path. + /// + /// The accumulated dash span points. + /// The list receiving the emitted sub-paths. private static void FlushBuffer(List buffer, List segments) { + // Spans whose first and last points coincide have no drawable extent + // (or would form a degenerate loop), so they are dropped. if (buffer.Count >= 2 && buffer[0] != buffer[^1]) { segments.Add(new Path(new LinearLineSegment([.. buffer]))); diff --git a/src/ImageSharp.Drawing/StarPolygon.cs b/src/ImageSharp.Drawing/StarPolygon.cs index 68a27955a..b89b63c70 100644 --- a/src/ImageSharp.Drawing/StarPolygon.cs +++ b/src/ImageSharp.Drawing/StarPolygon.cs @@ -62,6 +62,16 @@ public StarPolygon(float x, float y, int prongs, float innerRadii, float outerRa { } + /// + /// Places two vertices per prong, alternating between the inner and outer radii at equal + /// angular steps around the center. + /// + /// The center point of the star. + /// The inner star radius. + /// The outer star radius. + /// The number of star prongs; at least 3 are required. + /// The angle of rotation in degrees. + /// The linear segment describing the star outline. private static LinearLineSegment CreateSegment(Vector2 location, float innerRadii, float outerRadii, int prongs, float angle) { Guard.MustBeGreaterThan(prongs, 2, nameof(prongs)); diff --git a/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs index 0870a5744..fa53905b0 100644 --- a/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Text/BaseGlyphBuilder.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Numerics; using System.Runtime.CompilerServices; using SixLabors.Fonts; @@ -12,8 +13,10 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// /// Defines a base rendering surface that Fonts can use to generate shapes. /// -internal class BaseGlyphBuilder : IGlyphRenderer +internal class BaseGlyphBuilder : IGlyphRenderer, IDisposable { + private bool isDisposed; + /// /// The last point emitted by MoveTo / LineTo / curve commands. /// Used as the implicit start of the next segment. @@ -27,48 +30,136 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// private GlyphRendererParameters parameters; - // Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for this glyph. - // When true, EndGlyph skips its default single-layer path capture because layers - // already contributed their paths individually. + /// + /// Tracks whether geometry was emitted inside BeginLayer/EndLayer pairs for + /// this glyph. When , EndGlyph skips its default single-layer + /// path capture because layers already contributed their paths individually. + /// private bool usedLayers; - // Tracks whether we are currently inside a layer block. - // Guards against unbalanced EndLayer calls. + /// + /// Tracks whether we are currently inside a layer block. + /// Guards against unbalanced EndLayer calls. + /// private bool inLayer; // --- Per-GRAPHEME layered capture --- // A grapheme cluster (e.g. a base glyph + COLR v0 color layers) may span // multiple BeginGlyph/EndGlyph calls. These fields aggregate all layers // belonging to the same grapheme into a single GlyphPathCollection. + + /// + /// Accumulates paths and layer descriptors for the grapheme currently being rendered; + /// when no grapheme is in progress. + /// private GlyphPathCollection.Builder? graphemeBuilder; + + /// + /// The number of paths added to so far. + /// The next layer span starts at this index. + /// private int graphemePathCount; + + /// + /// The grapheme index of the aggregate in progress, or -1 when none. + /// A change in index during BeginGlyph flushes the previous aggregate. + /// private int currentGraphemeIndex = -1; + + /// + /// Completed per-grapheme collections in rendering order; exposed via . + /// private readonly List currentGlyphs = []; - // Previous decoration details per decoration type, used to stitch adjacent - // decorations together and eliminate sub-pixel gaps between glyphs. - private TextDecorationDetails? previousUnderlineTextDecoration; - private TextDecorationDetails? previousOverlineTextDecoration; - private TextDecorationDetails? previousStrikeoutTextDecoration; + // Skip-ink is carved here, across glyphs, rather than in the font engine, because a gap must be + // widened by the drawn thickness and can extend past a glyph boundary into its neighbours. Each + // decoration type keeps a two-glyph window: the middle glyph is carved against both neighbours + // once the following glyph arrives, and the last glyph is flushed in EndText. + + /// + /// The underline window (previous and current glyph). + /// + private DecorationLane underlineLane; + + /// + /// The overline window (previous and current glyph). + /// + private DecorationLane overlineLane; + + /// + /// The strikeout window (previous and current glyph). + /// + private DecorationLane strikeoutLane; + + /// + /// The along-line distance in pixels under which two carved pieces count as contiguous and + /// merge. Pen-derived cell boundaries drift by ULPs between one cell's end and the next + /// cell's start; genuine spacing is orders of magnitude larger. + /// + private const float DecorationContiguityEpsilon = 1F / 32F; + + /// + /// Reusable buffer of widened ink gaps gathered while carving one glyph's decoration. + /// + private readonly List<(float Start, float End)> decorationGaps = []; // Per-layer (within current grapheme) bookkeeping: + + /// + /// Index into the grapheme path list where the current layer's span begins. + /// private int layerStartIndex; + + /// + /// Paint supplied by BeginLayer; means the default foreground. + /// private Paint? currentLayerPaint; + + /// + /// Fill rule supplied by BeginLayer for the current layer. + /// private FillRule currentLayerFillRule; + + /// + /// Clip quad supplied by BeginLayer (COLR v1); applied in EndLayer. + /// private ClipQuad? currentClipBounds; + /// + /// Dedicated builder for carved decoration segments. The sliding window emits a glyph's + /// segments after the next glyph has already retargeted (path text + /// sets a per-glyph transform), so decoration geometry is built here under the transform + /// captured with the owning glyph instead of whatever currently holds. + /// + private readonly PathBuilder decorationBuilder = new(); + + /// + /// The transform every glyph point receives after any per-glyph placement, mirroring the + /// default transform baked into . Held separately because path + /// decoration bands are built in placement space and must compose with it explicitly. + /// + private readonly Matrix4x4 baseTransform; + /// /// Initializes a new instance of the class /// with an identity transform. /// - public BaseGlyphBuilder() => this.Builder = new PathBuilder(); + public BaseGlyphBuilder() + { + this.baseTransform = Matrix4x4.Identity; + this.Builder = new PathBuilder(); + } /// /// Initializes a new instance of the class /// with the specified transform applied to all incoming glyph geometry. /// /// A matrix transform applied to every point received from the font engine. - public BaseGlyphBuilder(Matrix4x4 transform) => this.Builder = new PathBuilder(transform); + public BaseGlyphBuilder(Matrix4x4 transform) + { + this.baseTransform = transform; + this.Builder = new PathBuilder(transform); + } /// /// Gets the flattened paths captured for all glyphs/graphemes. @@ -81,6 +172,30 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// public IReadOnlyList Glyphs => this.currentGlyphs; + /// + /// Gets a value indicating whether per-grapheme aggregates + /// are built while rendering. Consumers of require them; renderers that + /// emit drawing operations directly override this to skip one builder and two list + /// allocations per grapheme. + /// + protected virtual bool CollectsGlyphPaths => true; + + /// + /// Gets or sets the path that glyphs are laid out along, or for + /// straight-line text. When set, layout-contiguous decoration cells merge across the + /// per-glyph placement transforms and flush as one continuous band sampled from the path. + /// + protected IPath? TextPath { get; set; } + + /// + /// Gets or sets a value indicating whether the current glyph's decoded outline is built into + /// path objects when the glyph or layer ends. Reset to at every glyph + /// begin; caching subclasses clear it for glyphs whose cached path will be reused, skipping + /// the per-frame construction of a path graph that would be discarded. When cleared, the + /// subclass must not read the outline from for that glyph. + /// + protected bool OutlineBuildRequired { get; set; } = true; + /// /// Gets the used to accumulate outline segments /// (MoveTo, LineTo, curves) for the current glyph or layer. @@ -96,12 +211,34 @@ internal class BaseGlyphBuilder : IGlyphRenderer /// protected List CurrentPaths { get; } = []; + /// + /// Gets the text run of the glyph whose decoration segments are currently being emitted. Carving + /// happens one glyph after the run was seen, so subclasses that resolve a pen from the run must + /// read this rather than the live glyph's run. + /// + protected TextRun? CurrentDecorationRun { get; private set; } + /// /// Called by the font engine after all glyphs in the text block have been rendered. /// Flushes any in-progress grapheme aggregate and resets per-text-block state. /// void IGlyphRenderer.EndText() { + // Flush the last held glyph of each decoration window before the grapheme is finalized, so + // its carved segments are captured in the grapheme aggregate. A throw from one lane must + // still return the pooled buffers of the others. + try + { + this.FlushDecorationLane(TextDecorations.Underline, ref this.underlineLane); + this.FlushDecorationLane(TextDecorations.Overline, ref this.overlineLane); + this.FlushDecorationLane(TextDecorations.Strikeout, ref this.strikeoutLane); + } + catch + { + this.ReleaseDecorationLanes(); + throw; + } + // Finalize the last grapheme, if any: if (this.graphemeBuilder is not null && this.graphemePathCount > 0) { @@ -111,19 +248,23 @@ void IGlyphRenderer.EndText() this.graphemeBuilder = null; this.graphemePathCount = 0; this.currentGraphemeIndex = -1; - this.previousUnderlineTextDecoration = null; - this.previousOverlineTextDecoration = null; - this.previousStrikeoutTextDecoration = null; this.EndText(); } + /// + /// Called by the font engine before any glyphs are rendered for a text block. + /// Forwards to the protected override point. + /// + /// The layout bounds of the entire text block. void IGlyphRenderer.BeginText(in FontRectangle bounds) => this.BeginText(bounds); /// /// Called by the font engine before emitting outline data for a single glyph. /// Manages grapheme-cluster transitions and resets per-glyph state. /// + /// The font-metric bounding rectangle of the glyph. + /// Identifies the glyph (id, font, layout mode, text run, etc.). /// /// to have the font engine emit the full outline /// (MoveTo/LineTo/curves/EndGlyph); to skip it entirely, @@ -131,29 +272,44 @@ void IGlyphRenderer.EndText() /// bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { - // If grapheme changed, flush previous aggregate and start a new one: - if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex) + // Grapheme aggregates only exist for consumers of the built glyph collections + // (TextBuilder, DrawGlyphs). Renderers that emit drawing operations directly skip the + // whole aggregate, which otherwise allocates a builder and two lists for every grapheme. + if (this.CollectsGlyphPaths) { - if (this.graphemePathCount > 0) + // If grapheme changed, flush previous aggregate and start a new one: + if (this.graphemeBuilder is not null && this.currentGraphemeIndex != parameters.GraphemeIndex) { - this.currentGlyphs.Add(this.graphemeBuilder.Build()); + if (this.graphemePathCount > 0) + { + this.currentGlyphs.Add(this.graphemeBuilder.Build()); + } + + this.graphemeBuilder = null; + this.graphemePathCount = 0; } - this.graphemeBuilder = null; - this.graphemePathCount = 0; + if (this.graphemeBuilder is null) + { + this.graphemeBuilder = new GlyphPathCollection.Builder(); + this.currentGraphemeIndex = parameters.GraphemeIndex; + this.graphemePathCount = 0; + } } - if (this.graphemeBuilder is null) + this.parameters = parameters; + this.Builder.Clear(); + + // Path-following text positions every glyph on the path; the placement composes with + // the builder's default transform like any other glyph geometry. + if (this.TextPath is IPath textPath) { - this.graphemeBuilder = new GlyphPathCollection.Builder(); - this.currentGraphemeIndex = parameters.GraphemeIndex; - this.graphemePathCount = 0; + this.ApplyPathTransform(textPath, in bounds); } - this.parameters = parameters; - this.Builder.Clear(); this.usedLayers = false; this.inLayer = false; + this.OutlineBuildRequired = true; this.layerStartIndex = this.graphemePathCount; this.currentLayerPaint = null; @@ -163,12 +319,25 @@ bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParamete } /// - void IGlyphRenderer.BeginFigure() => this.Builder.StartFigure(); + void IGlyphRenderer.BeginFigure() + { + // Skip segment accumulation entirely when the outline will not be built: the skip-ink + // collectors observe the stream through the font engine's tee before it reaches this + // renderer, so dropping the geometry here loses nothing. + if (this.OutlineBuildRequired) + { + this.Builder.StartFigure(); + } + } /// void IGlyphRenderer.CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { - this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); + if (this.OutlineBuildRequired) + { + this.Builder.AddCubicBezier(this.currentPoint, secondControlPoint, thirdControlPoint, point); + } + this.currentPoint = point; } @@ -181,7 +350,9 @@ void IGlyphRenderer.EndGlyph() { // If the glyph did not open any explicit layer, treat its geometry as a single // implicit layer so that non-color glyphs still produce a GlyphPathCollection entry. - if (!this.usedLayers) + // When the subclass reuses a cached path, building the decoded outline here would + // allocate a path graph that nothing reads. + if (!this.usedLayers && this.OutlineBuildRequired) { IPath path = this.Builder.Build(); @@ -210,33 +381,55 @@ void IGlyphRenderer.EndGlyph() } /// - void IGlyphRenderer.EndFigure() => this.Builder.CloseFigure(); + void IGlyphRenderer.EndFigure() + { + if (this.OutlineBuildRequired) + { + this.Builder.CloseFigure(); + } + } /// void IGlyphRenderer.LineTo(Vector2 point) { - this.Builder.AddLine(this.currentPoint, point); + if (this.OutlineBuildRequired) + { + this.Builder.AddLine(this.currentPoint, point); + } + this.currentPoint = point; } /// void IGlyphRenderer.MoveTo(Vector2 point) { - this.Builder.StartFigure(); + if (this.OutlineBuildRequired) + { + this.Builder.StartFigure(); + } + this.currentPoint = point; } /// void IGlyphRenderer.ArcTo(float radiusX, float radiusY, float rotation, bool largeArc, bool sweep, Vector2 point) { - this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); + if (this.OutlineBuildRequired) + { + this.Builder.AddArc(this.currentPoint, radiusX, radiusY, rotation, largeArc, sweep, point); + } + this.currentPoint = point; } /// void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { - this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); + if (this.OutlineBuildRequired) + { + this.Builder.AddQuadraticBezier(this.currentPoint, secondControlPoint, point); + } + this.currentPoint = point; } @@ -244,6 +437,9 @@ void IGlyphRenderer.QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) /// Called by the font engine to begin a color layer within a COLR v0/v1 glyph. /// Each layer receives its own paint, fill rule, and optional clip bounds. /// + /// The paint for this color layer, or for the default foreground. + /// The fill rule to use when rasterizing this layer. + /// Optional clip quad constraining the layer region. void IGlyphRenderer.BeginLayer(Paint? paint, FillRule fillRule, ClipQuad? clipBounds) { this.usedLayers = true; @@ -269,40 +465,39 @@ void IGlyphRenderer.EndLayer() return; } - IPath path = this.Builder.Build(); - - // If the layer defines a clip quad (e.g. from COLR v1), intersect the - // built path with the quad polygon to constrain rendering. - if (this.currentClipBounds is not null) + // When the subclass reuses a cached layer path, building the decoded outline here + // would allocate a path graph that nothing reads. + if (this.OutlineBuildRequired) { - ClipQuad clip = this.currentClipBounds.Value; - PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft]; - LinearLineSegment segment = new(points); - Polygon polygon = new(segment); + IPath path = this.Builder.Build(); - ShapeOptions options = new() + // If the layer defines a clip quad (e.g. from COLR v1), intersect the + // built path with the quad polygon to constrain rendering. + if (this.currentClipBounds is not null) { - BooleanOperation = BooleanOperation.Intersection, - IntersectionRule = TextUtilities.MapFillRule(this.currentLayerFillRule) - }; + ClipQuad clip = this.currentClipBounds.Value; + PointF[] points = [clip.TopLeft, clip.TopRight, clip.BottomRight, clip.BottomLeft]; + LinearLineSegment segment = new(points); + Polygon polygon = new(segment); - path = path.Clip(options, polygon); - } + path = path.Clip(BooleanOperation.Intersection, TextUtilities.MapFillRule(this.currentLayerFillRule), polygon); + } - this.CurrentPaths.Add(path); + this.CurrentPaths.Add(path); - if (this.graphemeBuilder is not null) - { - this.graphemeBuilder.AddPath(path); - this.graphemeBuilder.AddLayer( - startIndex: this.layerStartIndex, - count: 1, - paint: this.currentLayerPaint, - fillRule: this.currentLayerFillRule, - bounds: path.Bounds, - kind: GlyphLayerKind.Painted); + if (this.graphemeBuilder is not null) + { + this.graphemeBuilder.AddPath(path); + this.graphemeBuilder.AddLayer( + startIndex: this.layerStartIndex, + count: 1, + paint: this.currentLayerPaint, + fillRule: this.currentLayerFillRule, + bounds: path.Bounds, + kind: GlyphLayerKind.Painted); - this.graphemePathCount++; + this.graphemePathCount++; + } } this.Builder.Clear(); @@ -314,91 +509,398 @@ void IGlyphRenderer.EndLayer() } /// - /// Called by the font engine to emit a text decoration (underline, strikeout, or overline) - /// for the current glyph. Builds a filled rectangle path from the start/end positions and - /// thickness, then registers it as a layer. - /// Adjacent decorations are stitched together using the previous decoration details to - /// eliminate sub-pixel gaps caused by font metric rounding. + /// Called by the font engine to report a text decoration (underline, strikeout, or overline) + /// for the current glyph as the full, untrimmed line plus the intervals where the glyph's ink + /// crosses it. The line is not drawn immediately: it enters a per-type two-glyph window so the + /// previous glyph can be carved around its own ink and both neighbours' ink, letting a skip-ink + /// gap widen by the drawn thickness and reach across a glyph boundary. The middle glyph is + /// emitted once the following glyph arrives; the last is flushed in EndText. /// - void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) + /// The type of decoration (underline, strikeout, or overline). + /// The start position of the full decoration line. + /// The end position of the full decoration line. + /// The thickness of the decoration line in pixels. + /// The along-line intervals where the glyph's ink crosses the band. + void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections) { if (thickness == 0) { return; } - // Clamp the thickness to whole pixels. - thickness = MathF.Max(1F, (float)Math.Round(thickness)); - IGlyphRenderer renderer = this; + ref DecorationLane lane = ref this.LaneFor(textDecorations); + // Stacked colour-layer glyphs repeat the same decoration once per component; the window would + // otherwise treat the repeat as the next glyph. Ignore an exact repeat of the held line. + if (lane.Current.HasValue && lane.Current.Start == start && lane.Current.End == end) + { + return; + } + + // PendingDecoration takes a pooled copy of the ink; the font engine reuses its buffer per + // glyph. The composed builder transform is captured now because path text retargets the + // builder per glyph and this decoration is not emitted until the window advances. bool rotated = this.parameters.LayoutMode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; - Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F); + PendingDecoration incoming = new(start, end, thickness, intersections, this.parameters.TextRun, rotated, this.Builder.Transform); - start = ClampToPixel(start, (int)thickness, rotated); - end = ClampToPixel(end, (int)thickness, rotated); + // The held glyph is now flanked on both sides, so carve and emit it, then retire the glyph + // that leaves the window and dispose its pooled buffer. + if (lane.Current.HasValue) + { + try + { + this.CarveDecoration(textDecorations, in lane.Previous, in lane.Current, in incoming, ref lane); + } + catch + { + // Carving reaches user path and pen code; the incoming glyph never entered the + // window, so its pooled buffer has no other owner to return it. + incoming.Dispose(); + throw; + } + + lane.Previous.Dispose(); + lane.Previous = lane.Current; + } + + lane.Current = incoming; + } - // Sometimes the start and end points do not align properly leaving pixel sized gaps - // so we need to adjust them. Use any previous decoration to try and continue the line. - TextDecorationDetails? previous = textDecorations switch + /// + /// Emits the last held glyph of a decoration window, carved against its previous neighbour and + /// no following neighbour, emits any segment still accumulating, then clears the window. + /// + /// The decoration type of the window. + /// The window to flush. + private void FlushDecorationLane(TextDecorations textDecorations, ref DecorationLane lane) + { + if (lane.Current.HasValue) { - TextDecorations.Underline => this.previousUnderlineTextDecoration, - TextDecorations.Overline => this.previousOverlineTextDecoration, - TextDecorations.Strikeout => this.previousStrikeoutTextDecoration, - _ => null - }; + this.CarveDecoration(textDecorations, in lane.Previous, in lane.Current, in PendingDecoration.None, ref lane); + + // Default each slot with its dispose: a later throw from segment emission must not + // leave a disposed value in the lane, or a recovery release returns the same pooled + // buffer twice. + lane.Previous.Dispose(); + lane.Previous = default; + lane.Current.Dispose(); + lane.Current = default; + } - if (previous != null) + this.FlushPendingSegment(textDecorations, ref lane); + + lane.Previous = default; + lane.Current = default; + } + + /// + public void Dispose() + { + if (this.isDisposed) { - float prevThickness = previous.Value.Thickness; - Vector2 prevStart = previous.Value.Start; - Vector2 prevEnd = previous.Value.End; + return; + } + + this.isDisposed = true; + this.Dispose(true); + GC.SuppressFinalize(this); + } - // If the previous line is identical to the new one ignore it. - // This can happen when multiple glyph layers are used. - if (prevStart == start && prevEnd == end) + /// + /// Releases resources owned by this builder. The decoration windows hold pooled ink buffers + /// that only reach the pool through , so a render that + /// threw before then must return them here. + /// + /// to release managed resources. + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + this.ReleaseDecorationLanes(); + } + } + + /// + /// Returns the pooled ink buffers of every decoration window without emitting them, for + /// disposal paths where rendering never reached . + /// + private void ReleaseDecorationLanes() + { + ReleaseDecorationLane(ref this.underlineLane); + ReleaseDecorationLane(ref this.overlineLane); + ReleaseDecorationLane(ref this.strikeoutLane); + } + + /// + /// Returns one window's pooled ink buffers and clears its slots. + /// + /// The window to release. + private static void ReleaseDecorationLane(ref DecorationLane lane) + { + lane.Previous.Dispose(); + lane.Previous = default; + lane.Current.Dispose(); + lane.Current = default; + } + + /// + /// Carves the middle glyph's decoration line around its own ink and the ink of its previous and + /// next neighbours, each widened by that glyph's thickness, and emits the pieces that survive. + /// + /// The decoration type. + /// The glyph before the one being carved, if any. + /// The glyph being carved. + /// The glyph after the one being carved, if any. + /// The window owning the segment accumulator for this decoration type. + private void CarveDecoration( + TextDecorations textDecorations, + in PendingDecoration previous, + in PendingDecoration current, + in PendingDecoration next, + ref DecorationLane lane) + { + bool rotated = current.Rotated; + float lineStart = rotated ? Math.Min(current.Start.Y, current.End.Y) : Math.Min(current.Start.X, current.End.X); + float lineEnd = rotated ? Math.Max(current.Start.Y, current.End.Y) : Math.Max(current.Start.X, current.End.X); + + // Gather the widened ink gaps that fall within this glyph's extent: its own ink, plus the + // trailing reach of the previous glyph and the leading reach of the next, each haloed by + // that glyph's own thickness. + List<(float Start, float End)> gaps = this.decorationGaps; + gaps.Clear(); + AddGaps(gaps, in current, lineStart, lineEnd); + if (previous.HasValue) + { + AddGaps(gaps, in previous, lineStart, lineEnd); + } + + if (next.HasValue) + { + AddGaps(gaps, in next, lineStart, lineEnd); + } + + gaps.Sort(static (x, y) => x.Start.CompareTo(y.Start)); + + // Emit the solid pieces between the merged gaps. + float cursor = lineStart; + float gapStart = 0F; + float gapEnd = float.MinValue; + bool hasGap = false; + for (int i = 0; i < gaps.Count; i++) + { + (float start, float end) = gaps[i]; + if (hasGap && start <= gapEnd) { - return; + gapEnd = Math.Max(gapEnd, end); + continue; } - // Align the new line with the previous one if they are close enough. - // Use a 2 pixel threshold to account for anti-aliasing gaps. - if (rotated) + if (hasGap) { - if (thickness == prevThickness - && prevEnd.Y + 2 >= start.Y - && prevEnd.X == start.X) + this.EmitCarvedSegment(textDecorations, in current, rotated, cursor, Math.Max(cursor, gapStart), ref lane); + cursor = Math.Max(cursor, gapEnd); + } + + gapStart = start; + gapEnd = end; + hasGap = true; + } + + if (hasGap) + { + this.EmitCarvedSegment(textDecorations, in current, rotated, cursor, Math.Max(cursor, gapStart), ref lane); + cursor = Math.Max(cursor, gapEnd); + } + + this.EmitCarvedSegment(textDecorations, in current, rotated, cursor, lineEnd, ref lane); + + // Widens each ink interval of a glyph by its own thickness and clips it to the carved + // glyph's extent, so only the parts that reach into it open a gap. + static void AddGaps(List<(float Start, float End)> gaps, in PendingDecoration glyph, float lineStart, float lineEnd) + { + ReadOnlySpan ink = glyph.Ink; + float halo = glyph.Thickness; + for (int i = 0; i < ink.Length; i += 2) + { + float from = Math.Max(ink[i] - halo, lineStart); + float to = Math.Min(ink[i + 1] + halo, lineEnd); + if (to > from) { - start = prevEnd; + gaps.Add((from, to)); } } - else if (thickness == prevThickness - && prevEnd.Y == start.Y - && prevEnd.X + 2 >= start.X) + } + } + + /// + /// Accumulates a carved along-line piece towards emission. Contiguous pieces that share a + /// run, geometry, and transform accumulate into one segment so abutting glyph cells never + /// meet in a pair of anti-aliased edges, and the accumulator flushes whenever continuity + /// or styling breaks. A grapheme aggregate receives each merged segment when it flushes, + /// consistent with the carving window already emitting one glyph after the segment's owner. + /// + private void EmitCarvedSegment(TextDecorations textDecorations, in PendingDecoration current, bool rotated, float from, float to, ref DecorationLane lane) + { + if (to <= from) + { + return; + } + + float crossStart = rotated ? current.Start.X : current.Start.Y; + float crossEnd = rotated ? current.End.X : current.End.Y; + + // Adjacent cells are not bitwise contiguous: the engine derives a cell's end and the + // next cell's start through differently ordered advance sums, so the shared boundary + // drifts by a few ULPs. The tolerance sits far above that drift and far below anything + // visible; emission rounds along-line ends to whole pixels regardless. Real spacing + // (tracking, justification, new lines) exceeds it by orders of magnitude and flushes. + // + // Path-following decorations merge across the per-glyph placement transforms because + // the flushed band re-derives its geometry from the path; everything else needs the + // single shared transform the flushed rectangle is built under. + ref PendingSegment pending = ref lane.Pending; + bool transformCompatible = (!rotated && this.TextPath is not null) || + pending.Transform == current.Transform; + + if (pending.HasValue && + pending.Rotated == rotated && + MathF.Abs(pending.To - from) <= DecorationContiguityEpsilon && + pending.CrossStart == crossStart && + pending.CrossEnd == crossEnd && + pending.Thickness == current.Thickness && + ReferenceEquals(pending.Run, current.Run) && + transformCompatible) + { + pending.To = to; + return; + } + + this.FlushPendingSegment(textDecorations, ref lane); + + pending.HasValue = true; + pending.From = from; + pending.To = to; + pending.CrossStart = crossStart; + pending.CrossEnd = crossEnd; + pending.Thickness = current.Thickness; + pending.Rotated = rotated; + pending.Run = current.Run; + pending.Transform = current.Transform; + } + + /// + /// Emits the contiguous decoration segment a window has accumulated, if any, and clears it. + /// + /// The decoration type of the window. + /// The window whose accumulated segment should be emitted. + private void FlushPendingSegment(TextDecorations textDecorations, ref DecorationLane lane) + { + ref PendingSegment pending = ref lane.Pending; + if (!pending.HasValue) + { + return; + } + + if (!pending.Rotated && this.TextPath is IPath textPath) + { + this.EmitPathDecorationBand(textDecorations, in pending, textPath); + } + else + { + Vector2 segmentStart = pending.Rotated ? new Vector2(pending.CrossStart, pending.From) : new Vector2(pending.From, pending.CrossStart); + Vector2 segmentEnd = pending.Rotated ? new Vector2(pending.CrossEnd, pending.To) : new Vector2(pending.To, pending.CrossEnd); + this.EmitDecorationSegment(textDecorations, segmentStart, segmentEnd, pending.Thickness, pending.Rotated, pending.Run, pending.Transform); + } + + pending = default; + } + + /// + /// Emits an accumulated path-following decoration segment as one continuous band. The band + /// samples the path over the segment's along-path interval and offsets each sample along + /// the local normal, so the drawn line flows with the curve instead of approximating it + /// with one rotated rectangle per glyph cell. + /// + /// The decoration type. + /// The accumulated segment. + /// The path the text is laid out along. + private void EmitPathDecorationBand(TextDecorations textDecorations, in PendingSegment pending, IPath textPath) + { + // Same drawn thickness and CSS positioning as the straight-line rectangle: underline + // shifts down by half the thickness, overline up, strikeout stays centered. + float thickness = MathF.Max(1F, (float)Math.Round(pending.Thickness)); + float half = thickness * .5F; + + float offset = 0F; + if (textDecorations == TextDecorations.Overline) + { + offset = -half; + } + else if (textDecorations == TextDecorations.Underline) + { + offset = half; + } + + // Glyph placement maps a layout point (x, y) to path(x) + rotate(angle(x)) * (0, y), + // so sampling that frame along the interval yields the exact curve the glyph cells + // follow. A fixed step keeps chord error far below pixel scale at text curvatures. + const float sampleStep = 2F; + float crossTop = pending.CrossStart + offset - half; + float crossBottom = crossTop + thickness; + + int steps = Math.Max(1, (int)MathF.Ceiling((pending.To - pending.From) / sampleStep)); + PointF[] outline = new PointF[(steps + 1) * 2]; + for (int i = 0; i <= steps; i++) + { + float x = i == steps ? pending.To : pending.From + (i * sampleStep); + if (!textPath.TryGetPathPointAtDistanceUnbounded(x, out PathPoint pathPoint)) { - start = prevEnd; + return; } + + (float sin, float cos) = MathF.SinCos(GeometryUtilities.DegreeToRadian(pathPoint.Angle)); + Vector2 normal = new(-sin, cos); + Vector2 point = (Vector2)pathPoint.Point; + outline[i] = point + (normal * crossTop); + outline[^(i + 1)] = point + (normal * crossBottom); } - TextDecorationDetails current = new() - { - Start = start, - End = end, - Thickness = thickness - }; + // The sampled frames replace the per-glyph placement transforms, but the builder's + // default transform still applies on top, exactly as it does for glyph geometry. + this.decorationBuilder.Clear(); + this.decorationBuilder.SetTransform(this.baseTransform); + this.decorationBuilder.StartFigure(); + this.decorationBuilder.AddLines(outline); + this.decorationBuilder.CloseFigure(); + + IPath path = this.decorationBuilder.Build(); + this.decorationBuilder.Clear(); - switch (textDecorations) + if (path.Bounds.IsEmpty) { - case TextDecorations.Underline: - this.previousUnderlineTextDecoration = current; - break; - case TextDecorations.Strikeout: - this.previousStrikeoutTextDecoration = current; - break; - case TextDecorations.Overline: - this.previousOverlineTextDecoration = current; - break; + return; } + this.RegisterDecorationPath(textDecorations, path, outline[0], outline[steps], thickness, pending.Run); + } + + /// + /// Builds a filled rectangle path for one carved decoration segment, registers it as a + /// layer, and hands it to the drawing override. + /// The path is built on the dedicated decoration builder under the owning glyph's captured + /// transform: the window emits a glyph's segments after the shared builder has already been + /// retargeted to the next glyph, which rotates per glyph when text follows a path. + /// + private void EmitDecorationSegment(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness, bool rotated, TextRun? run, Matrix4x4 transform) + { + // Clamp the thickness to whole pixels. + thickness = MathF.Max(1F, (float)Math.Round(thickness)); + + Vector2 pad = rotated ? new Vector2(thickness * .5F, 0) : new Vector2(0, thickness * .5F); + + start = ClampToPixel(start, (int)thickness, rotated); + end = ClampToPixel(end, (int)thickness, rotated); + Vector2 a = start - pad; Vector2 b = start + pad; Vector2 c = end + pad; @@ -417,25 +919,42 @@ void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start offset = rotated ? new Vector2(-(thickness * .5F), 0) : new Vector2(0, thickness * .5F); } - // We clamp the start and end points to the pixel grid to avoid anti-aliasing - // when there is no transform. - renderer.BeginFigure(); - renderer.MoveTo(ClampToPixel(a + offset)); - renderer.LineTo(ClampToPixel(b + offset)); - renderer.LineTo(ClampToPixel(c + offset)); - renderer.LineTo(ClampToPixel(d + offset)); - renderer.EndFigure(); + // Snap the corners to whole pixels on the cross axis only, keeping the stroke's edges crisp; + // the along-line coordinates stay exact so skip-ink gap boundaries land symmetrically. + this.decorationBuilder.Clear(); + this.decorationBuilder.SetTransform(transform); + this.decorationBuilder.StartFigure(); + this.decorationBuilder.AddLine(SnapCross(a + offset, rotated), SnapCross(b + offset, rotated)); + this.decorationBuilder.AddLine(SnapCross(b + offset, rotated), SnapCross(c + offset, rotated)); + this.decorationBuilder.AddLine(SnapCross(c + offset, rotated), SnapCross(d + offset, rotated)); + this.decorationBuilder.CloseFigure(); - IPath path = this.Builder.Build(); + IPath path = this.decorationBuilder.Build(); // If the path is degenerate (e.g. zero width line) we just skip it // and return. This might happen when clamping moves the points. if (path.Bounds.IsEmpty) { - this.Builder.Clear(); + this.decorationBuilder.Clear(); return; } + this.decorationBuilder.Clear(); + this.RegisterDecorationPath(textDecorations, path, start, end, thickness, run); + } + + /// + /// Registers a finished decoration path with the running collections and hands it to the + /// drawing override. + /// + /// The decoration type. + /// The built decoration path. + /// The start position of the decoration line. + /// The end position of the decoration line. + /// The drawn thickness in whole pixels. + /// The text run styling the decoration. + private void RegisterDecorationPath(TextDecorations textDecorations, IPath path, Vector2 start, Vector2 end, float thickness, TextRun? run) + { this.CurrentPaths.Add(path); if (this.graphemeBuilder is not null) { @@ -453,10 +972,56 @@ void IGlyphRenderer.SetDecoration(TextDecorations textDecorations, Vector2 start this.graphemePathCount++; } - this.Builder.Clear(); + this.CurrentDecorationRun = run; this.SetDecoration(textDecorations, start, end, thickness); } + /// + /// Returns the carving window for the given decoration type by reference. + /// + /// The decoration type. + /// The window for the type. + private ref DecorationLane LaneFor(TextDecorations textDecorations) + { + if (textDecorations == TextDecorations.Underline) + { + return ref this.underlineLane; + } + + if (textDecorations == TextDecorations.Overline) + { + return ref this.overlineLane; + } + + return ref this.strikeoutLane; + } + + /// + /// Positions the current glyph along : the glyph's horizontal center + /// maps to the path distance and the glyph rotates to the path tangent at that point. + /// Aligned text can overflow the path on either side, so overflowing glyphs extrapolate + /// along the boundary tangents. + /// + /// The path the text is laid out along. + /// The font-metric bounding rectangle of the glyph. + private void ApplyPathTransform(IPath textPath, in FontRectangle bounds) + { + Vector2 half = new(bounds.Width * .5F, 0); + if (!textPath.TryGetPathPointAtDistanceUnbounded(bounds.Left + half.X, out PathPoint pathPoint)) + { + return; + } + + float angle = GeometryUtilities.DegreeToRadian(pathPoint.Angle); + + // Translate so the glyph's top-left aligns with the path point, + // then rotate around the path point to follow the tangent. + Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); + this.Builder.SetTransform( + Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) + * new Matrix4x4(Matrix3x2.CreateRotation(angle, (Vector2)pathPoint.Point))); + } + /// protected virtual void BeginText(in FontRectangle bounds) { @@ -517,7 +1082,9 @@ protected virtual void EndLayer() /// Subclasses override this to include decorations implied by rich-text pens /// (e.g. ). /// - /// A flags enum of the active text decorations. + /// + /// A flags enum of the active text decorations. + /// public virtual TextDecorations EnabledDecorations() => this.parameters.TextRun.TextDecorations; @@ -535,48 +1102,236 @@ public virtual void SetDecoration(TextDecorations textDecorations, Vector2 start } /// - /// Truncates a floating-point position to the nearest whole pixel toward negative infinity. + /// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and orientation + /// into account. On the cross axis, even-thickness lines snap to whole pixels and + /// odd-thickness lines snap to half pixels so the 1px-wide center row/column aligns with + /// physical pixels. The along-line coordinate is ROUNDED to the nearest pixel rather than + /// floored: rounding keeps abutting segments seamless (two anti-aliased edges composited + /// separately do not sum to opaque) and keeps short skip-ink pieces crisp, while its + /// zero-mean error avoids the systematic one-directional gap bias flooring produced. /// + /// The decoration endpoint to snap. + /// The decoration stroke thickness in whole pixels. + /// + /// when the glyph uses vertical layout, in which case the + /// perpendicular (snap) axis is X rather than Y. + /// + /// + /// The snapped endpoint. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Point ClampToPixel(PointF point) => Point.Truncate(point); + private static PointF ClampToPixel(PointF point, int thickness, bool rotated) + { + float half = (thickness & 1) == 0 ? 0F : .5F; + + return rotated + ? new PointF(MathF.Floor(point.X) + half, MathF.Round(point.Y)) + : new PointF(MathF.Round(point.X), MathF.Floor(point.Y) + half); + } /// - /// Snaps a decoration endpoint to the pixel grid, taking stroke thickness and - /// orientation into account. Even-thickness lines snap to whole pixels; odd-thickness - /// lines snap to half pixels so the stroke center lands on a pixel boundary. + /// Snaps a decoration corner to the pixel grid on the cross axis only, so the stroke's + /// long edges stay crisp while its ends keep their exact along-line positions. /// + /// The corner to snap. + /// + /// when the glyph uses vertical layout, in which case the + /// cross (snap) axis is X rather than Y. + /// + /// + /// The snapped corner. + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static PointF ClampToPixel(PointF point, int thickness, bool rotated) + private static PointF SnapCross(PointF point, bool rotated) + => rotated + ? new PointF(MathF.Floor(point.X), point.Y) + : new PointF(point.X, MathF.Floor(point.Y)); + + /// + /// One glyph's untrimmed decoration line and the ink it must skip, held in the carving window + /// until its neighbours are known. + /// + private readonly struct PendingDecoration : IDisposable { - // Even thickness: snap to whole pixels. - if ((thickness & 1) == 0) + /// + /// A pending decoration that is not present (an absent neighbour at a line end). + /// + public static readonly PendingDecoration None; + + /// + /// The pooled buffer holding the ink, owned by this instance and handed back by + /// . Null when the glyph has no ink. + /// + private readonly float[]? rented; + + /// + /// The number of valid entries in . + /// + private readonly int count; + + /// + /// Initializes a new instance of the struct, taking a pooled + /// copy of the ink so it survives after the font engine reuses its per-glyph buffer. + /// + /// The start of the full, untrimmed decoration line. + /// The end of the full, untrimmed decoration line. + /// The decoration thickness in pixels (drawn width and skip-ink halo). + /// The along-line ink intervals to copy, as [start, end] pairs. + /// The text run styling the glyph, captured while it was live. + /// Whether the glyph uses vertical layout. + /// The composed builder transform of the owning glyph, captured while it was live. + public PendingDecoration(Vector2 start, Vector2 end, float thickness, ReadOnlyMemory intersections, TextRun? run, bool rotated, Matrix4x4 transform) { - return Point.Truncate(point); + this.HasValue = true; + this.Start = start; + this.End = end; + this.Thickness = thickness; + this.Run = run; + this.Rotated = rotated; + this.Transform = transform; + + if (intersections.IsEmpty) + { + this.rented = null; + this.count = 0; + } + else + { + // Favor ArrayPool over MemoryAllocator here, as it's faster and the buffers are small and short-lived. + // The font engine reuses its own buffer per glyph, so we must take a copy to survive until the glyph is carved. + this.rented = ArrayPool.Shared.Rent(intersections.Length); + intersections.Span.CopyTo(this.rented); + this.count = intersections.Length; + } } - // Odd thickness: snap to half pixels along the perpendicular axis - // so the 1px-wide center row/column aligns with physical pixels. - if (rotated) + /// + /// Gets a value indicating whether this slot holds a glyph. + /// + public bool HasValue { get; } + + /// + /// Gets the start of the full, untrimmed decoration line. + /// + public Vector2 Start { get; } + + /// + /// Gets the end of the full, untrimmed decoration line. + /// + public Vector2 End { get; } + + /// + /// Gets the decoration thickness in pixels, used both as the drawn width and as the + /// skip-ink halo. + /// + public float Thickness { get; } + + /// + /// Gets the along-line ink intervals for this glyph, as [start, end] pairs. + /// + public ReadOnlySpan Ink => new(this.rented, 0, this.count); + + /// + /// Gets the text run styling this glyph, captured while it was live so the pen can still be + /// resolved when the glyph is carved a step later. + /// + public TextRun? Run { get; } + + /// + /// Gets a value indicating whether the glyph uses vertical layout. + /// + public bool Rotated { get; } + + /// + /// Gets the composed builder transform of the owning glyph. Carved segments are emitted + /// a window step later, after the shared builder has been retargeted to the next glyph, + /// so each glyph's decoration geometry must be built under this captured value. + /// + public Matrix4x4 Transform { get; } + + /// + /// Returns the pooled ink buffer this decoration owns, if any, to the shared pool. + /// + public void Dispose() { - return Point.Truncate(point) + new Vector2(.5F, 0); + if (this.rented != null) + { + ArrayPool.Shared.Return(this.rented); + } } - - return Point.Truncate(point) + new Vector2(0, .5F); } /// - /// Records the start, end, and thickness of a previously emitted decoration line - /// so that the next adjacent decoration can be stitched seamlessly. + /// A two-glyph carving window for one decoration type. /// - private struct TextDecorationDetails + private struct DecorationLane { - /// Gets or sets the start position of the decoration. - public Vector2 Start { get; set; } - - /// Gets or sets the end position of the decoration. - public Vector2 End { get; set; } + /// + /// The glyph before the one currently held. + /// + public PendingDecoration Previous; + + /// + /// The most recently reported glyph, awaiting its following neighbour before it is carved. + /// + public PendingDecoration Current; + + /// + /// The carved segment being extended across contiguous same-styled glyph cells; + /// emitted when continuity or styling breaks, or at end of text. + /// + public PendingSegment Pending; + } - /// Gets or sets the decoration thickness in pixels. - public float Thickness { get; internal set; } + /// + /// A carved decoration piece accumulating across glyphs that share a run, geometry, and + /// transform, held until a piece that cannot merge or the end of text flushes it. + /// + private struct PendingSegment + { + /// + /// Whether this slot holds a segment. + /// + public bool HasValue; + + /// + /// The along-line start of the accumulated segment. + /// + public float From; + + /// + /// The along-line end of the accumulated segment; extended as contiguous pieces merge. + /// + public float To; + + /// + /// The cross-axis coordinate of the decoration line at its start point. + /// + public float CrossStart; + + /// + /// The cross-axis coordinate of the decoration line at its end point. + /// + public float CrossEnd; + + /// + /// The decoration thickness in pixels. + /// + public float Thickness; + + /// + /// Whether the owning glyphs use vertical layout. + /// + public bool Rotated; + + /// + /// The text run styling the accumulated cells; a different run never merges. + /// + public TextRun? Run; + + /// + /// The composed builder transform of the owning glyphs; a different transform never merges. + /// + public Matrix4x4 Transform; } } diff --git a/src/ImageSharp.Drawing/Text/GlyphBuilder.cs b/src/ImageSharp.Drawing/Text/GlyphBuilder.cs index 378591ee3..8ee1b8622 100644 --- a/src/ImageSharp.Drawing/Text/GlyphBuilder.cs +++ b/src/ImageSharp.Drawing/Text/GlyphBuilder.cs @@ -23,6 +23,13 @@ public GlyphBuilder() /// /// Initializes a new instance of the class. /// - /// The origin. + /// The origin offset applied to all captured glyph geometry. public GlyphBuilder(Vector2 origin) => this.Builder.SetOrigin(origin); + + /// + /// Initializes a new instance of the class for text laid out + /// along a path. Each glyph is positioned on the path and rotated to its tangent. + /// + /// The path to lay the glyphs out along. + public GlyphBuilder(IPath textPath) => this.TextPath = textPath; } diff --git a/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs index 0011f932b..5973da22f 100644 --- a/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs +++ b/src/ImageSharp.Drawing/Text/GlyphLayerInfo.cs @@ -33,6 +33,8 @@ internal GlyphLayerInfo( this.Paint = paint; this.IntersectionRule = TextUtilities.MapFillRule(fillRule); + // Map the font composite mode to ImageSharp blend/composition modes once at + // construction so consumers can render the layer without re-mapping per use. CompositeMode compositeMode = paint?.CompositeMode ?? CompositeMode.SrcOver; this.PixelAlphaCompositionMode = TextUtilities.MapCompositionMode(compositeMode); this.PixelColorBlendingMode = TextUtilities.MapBlendingMode(compositeMode); @@ -40,6 +42,19 @@ internal GlyphLayerInfo( this.Kind = kind; } + /// + /// Initializes a new instance of the struct from + /// already-mapped rendering modes. Used by to copy a layer + /// without re-mapping the paint's composite mode. + /// + /// Start index (inclusive) of the layer's paths within the glyph's path list. + /// Number of paths in this layer. + /// The layer paint (null means use renderer default). + /// The pre-mapped fill rule. + /// The pre-mapped pixel alpha composition mode. + /// The pre-mapped pixel color blending mode. + /// Axis-aligned bounds of the layer geometry. + /// An optional semantic hint for the layer type. private GlyphLayerInfo( int startIndex, int count, @@ -100,6 +115,16 @@ private GlyphLayerInfo( /// public GlyphLayerKind Kind { get; } + /// + /// Returns a copy of with its cached bounds transformed by + /// . The path indices, paint, and rendering modes are + /// unchanged; the caller is responsible for transforming the paths themselves. + /// + /// The layer descriptor to transform. + /// The transform matrix to apply to the bounds. + /// + /// The transformed layer descriptor. + /// internal static GlyphLayerInfo Transform(in GlyphLayerInfo info, Matrix4x4 matrix) => new( info.StartIndex, diff --git a/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs index b628e75ea..452f00674 100644 --- a/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs +++ b/src/ImageSharp.Drawing/Text/GlyphPathCollection.cs @@ -12,9 +12,24 @@ namespace SixLabors.ImageSharp.Drawing.Text; /// public sealed class GlyphPathCollection { + /// + /// All paths emitted for the glyph in z-order; layer descriptors index into this list. + /// private readonly List paths; + + /// + /// Cached read-only wrapper for exposed via . + /// private readonly ReadOnlyCollection readOnlyPaths; + + /// + /// Layer descriptors referring to spans within . + /// private readonly List layers; + + /// + /// Cached read-only wrapper for exposed via . + /// private readonly ReadOnlyCollection readOnlyLayers; /// @@ -91,7 +106,9 @@ public GlyphPathCollection Transform(Matrix4x4 matrix) /// satisfy . Useful to project to monochrome. /// /// A filter deciding whether to keep a layer. - /// A new with the selected paths. + /// + /// A new with the selected paths. + /// public PathCollection ToPathCollection(Func? predicate = null) { List kept = []; @@ -117,7 +134,9 @@ public PathCollection ToPathCollection(Func? predicate = n /// Gets a view of a single layer's geometry. /// /// The zero-based layer index. - /// A path collection comprising only that layer's span. + /// + /// A path collection comprising only that layer's span. + /// public PathCollection GetLayerPaths(int layerIndex) { Guard.MustBeLessThan(layerIndex, this.layers.Count, nameof(layerIndex)); @@ -138,7 +157,14 @@ public PathCollection GetLayerPaths(int layerIndex) /// internal sealed class Builder { + /// + /// Paths accumulated so far, in z-order. + /// private readonly List paths = []; + + /// + /// Layer descriptors accumulated so far, each spanning a range of . + /// private readonly List layers = []; /// @@ -154,8 +180,8 @@ internal sealed class Builder /// Number of paths belonging to this layer. /// The paint for this layer (may be null for default). /// The fill rule for this layer. - /// Optional cached bounds for this layer. - /// Optional semantic kind (eg. Decoration). + /// Cached axis-aligned bounds for this layer. + /// Optional semantic kind (e.g. Decoration). /// /// Thrown if the specified span is out of range of the current path list. /// @@ -178,7 +204,9 @@ public void AddLayer( /// /// Builds the immutable . /// - /// The collection. + /// + /// The collection. + /// public GlyphPathCollection Build() => new(this.paths, this.layers); } } diff --git a/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs b/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs deleted file mode 100644 index 1f6144adb..000000000 --- a/src/ImageSharp.Drawing/Text/PathGlyphBuilder.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts; -using SixLabors.Fonts.Rendering; - -namespace SixLabors.ImageSharp.Drawing.Text; - -/// -/// A rendering surface that Fonts can use to generate shapes by following a path. -/// Each glyph is positioned along the path and rotated to match the path tangent -/// at the glyph's horizontal center. -/// -internal sealed class PathGlyphBuilder : GlyphBuilder -{ - /// - /// The path that glyphs are laid out along. Exposed as - /// to access the method for efficient - /// position + tangent queries. - /// - private readonly IPathInternals path; - - /// - /// Initializes a new instance of the class. - /// - /// The path to render the glyphs along. - public PathGlyphBuilder(IPath path) - { - if (path is IPathInternals internals) - { - this.path = internals; - } - else - { - // Wrap in ComplexPolygon to gain IPathInternals. - this.path = new ComplexPolygon(path); - } - } - - /// - protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) - { - // Translate + rotate the glyph to follow the path. Always returns true because - // path-based glyphs are never cached (each has a unique per-position transform). - this.TransformGlyph(in bounds); - return true; - } - - /// - /// Computes the translation + rotation matrix that places a glyph along the path. - /// The glyph's horizontal center is mapped to the path distance, and the glyph - /// is rotated to match the path tangent at that point. - /// - /// The font-metric bounding rectangle of the glyph. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void TransformGlyph(in FontRectangle bounds) - { - // Query the path at the glyph's horizontal center. - Vector2 half = new(bounds.Width * .5F, 0); - SegmentInfo pathPoint = this.path.PointAlongPath(bounds.Left + half.X); - - // Translate so the glyph's top-left aligns with the path point, - // then rotate around the path point to follow the tangent. - Vector2 translation = (Vector2)pathPoint.Point - bounds.Location - half + new Vector2(0, bounds.Top); - Matrix4x4 matrix = Matrix4x4.CreateTranslation(translation.X, translation.Y, 0) * new Matrix4x4(Matrix3x2.CreateRotation(pathPoint.Angle - MathF.PI, (Vector2)pathPoint.Point)); - - this.Builder.SetTransform(matrix); - } -} diff --git a/src/ImageSharp.Drawing/Text/TextBuilder.cs b/src/ImageSharp.Drawing/Text/TextBuilder.cs index 4597384b3..9f8a20b39 100644 --- a/src/ImageSharp.Drawing/Text/TextBuilder.cs +++ b/src/ImageSharp.Drawing/Text/TextBuilder.cs @@ -18,30 +18,74 @@ public static class TextBuilder /// /// The text to shape and render. /// The text rendering and layout options. - /// The combined for the rendered glyphs. + /// + /// The combined for the rendered glyphs. + /// public static IPathCollection GeneratePaths(string text, TextOptions textOptions) { - GlyphBuilder glyphBuilder = new(); + using GlyphBuilder glyphBuilder = new(); TextRenderer renderer = new(glyphBuilder); - renderer.RenderText(text, textOptions); + renderer.Render(text, textOptions); return glyphBuilder.Paths; } + /// + /// Generates the combined outline paths for positioned glyphs. + /// The result merges per-glyph outlines into a single suitable for filling or stroking as one unit. + /// + /// The glyph identifiers. + /// The absolute glyph origins in pixel units. + /// The glyph rendering options. + /// + /// The combined for the rendered glyphs. + /// + public static IPathCollection GeneratePaths(ReadOnlySpan glyphIds, ReadOnlySpan points, GlyphOptions glyphOptions) + { + using GlyphBuilder glyphBuilder = new(); + TextRenderer renderer = new(glyphBuilder); + + renderer.Render(glyphIds, points, glyphOptions); + + return glyphBuilder.Paths; + } + + /// + /// Generates per-glyph path data and metadata for positioned glyphs. + /// Each entry contains the combined outline paths for a glyph and associated metadata that enables intelligent fill or stroke decisions at the glyph level. + /// + /// The glyph identifiers. + /// The absolute glyph origins in pixel units. + /// The glyph rendering options. + /// + /// A read-only list of entries, one for each rendered glyph. + /// + public static IReadOnlyList GenerateGlyphs(ReadOnlySpan glyphIds, ReadOnlySpan points, GlyphOptions glyphOptions) + { + using GlyphBuilder glyphBuilder = new(); + TextRenderer renderer = new(glyphBuilder); + + renderer.Render(glyphIds, points, glyphOptions); + + return glyphBuilder.Glyphs; + } + /// /// Generates per-glyph path data and metadata for the rendered . /// Each entry contains the combined outline paths for a glyph and associated metadata that enables intelligent fill or stroke decisions at the glyph level. /// /// The text to shape and render. /// The text rendering and layout options. - /// A read-only list of entries, one for each rendered glyph. + /// + /// A read-only list of entries, one for each rendered glyph. + /// public static IReadOnlyList GenerateGlyphs(string text, TextOptions textOptions) { - GlyphBuilder glyphBuilder = new(); + using GlyphBuilder glyphBuilder = new(); TextRenderer renderer = new(glyphBuilder); - renderer.RenderText(text, textOptions); + renderer.Render(text, textOptions); return glyphBuilder.Glyphs; } @@ -54,14 +98,16 @@ public static IReadOnlyList GenerateGlyphs(string text, Tex /// The text to shape and render. /// The path that defines the text baseline. /// The text rendering and layout options. - /// The combined for the rendered glyphs. + /// + /// The combined for the rendered glyphs. + /// public static IPathCollection GeneratePaths(string text, IPath path, TextOptions textOptions) { (IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path); - PathGlyphBuilder glyphBuilder = new(transformed.Path); + using GlyphBuilder glyphBuilder = new(transformed.Path); TextRenderer renderer = new(glyphBuilder); - renderer.RenderText(text, transformed.TextOptions); + renderer.Render(text, transformed.TextOptions); return glyphBuilder.Paths; } @@ -74,18 +120,30 @@ public static IPathCollection GeneratePaths(string text, IPath path, TextOptions /// The text to shape and render. /// The path that defines the text baseline. /// The text rendering and layout options. - /// A read-only list of entries, one for each rendered glyph. + /// + /// A read-only list of entries, one for each rendered glyph. + /// public static IReadOnlyList GenerateGlyphs(string text, IPath path, TextOptions textOptions) { (IPath Path, TextOptions TextOptions) transformed = ConfigureOptions(textOptions, path); - PathGlyphBuilder glyphBuilder = new(transformed.Path); + using GlyphBuilder glyphBuilder = new(transformed.Path); TextRenderer renderer = new(glyphBuilder); - renderer.RenderText(text, transformed.TextOptions); + renderer.Render(text, transformed.TextOptions); return glyphBuilder.Glyphs; } + /// + /// Normalizes options for path-based layout by moving any origin offset from the + /// text options onto the path itself. + /// + /// The source text options. + /// The layout path. + /// + /// The (possibly translated) path and matching options with a zero origin. When the + /// origin is already zero the original instances are returned unchanged. + /// private static (IPath Path, TextOptions TextOptions) ConfigureOptions(TextOptions options, IPath path) { // When a path is specified we should explicitly follow that path diff --git a/src/ImageSharp.Drawing/Text/TextUtilities.cs b/src/ImageSharp.Drawing/Text/TextUtilities.cs index 1c59fcf81..78e11267a 100644 --- a/src/ImageSharp.Drawing/Text/TextUtilities.cs +++ b/src/ImageSharp.Drawing/Text/TextUtilities.cs @@ -6,8 +6,21 @@ namespace SixLabors.ImageSharp.Drawing.Text; +/// +/// Provides mapping helpers between SixLabors.Fonts rendering enums and their +/// ImageSharp.Drawing equivalents, plus cloning utilities that avoid allocating +/// new options instances when the requested rules already match. +/// internal static class TextUtilities { + /// + /// Maps a font to the equivalent . + /// + /// The font fill rule. + /// + /// The equivalent intersection rule. Unrecognized values map to + /// . + /// public static IntersectionRule MapFillRule(FillRule fillRule) => fillRule switch { @@ -16,6 +29,17 @@ public static IntersectionRule MapFillRule(FillRule fillRule) _ => IntersectionRule.NonZero, }; + /// + /// Maps a font to the equivalent Porter-Duff + /// . + /// + /// The font composite mode. + /// + /// The equivalent alpha composition mode. Separable blend modes (Plus, Screen, etc.) + /// carry no Porter-Duff alpha behavior of their own and map to + /// ; their color contribution is + /// handled by . + /// public static PixelAlphaCompositionMode MapCompositionMode(CompositeMode mode) => mode switch { @@ -34,6 +58,14 @@ public static PixelAlphaCompositionMode MapCompositionMode(CompositeMode mode) _ => PixelAlphaCompositionMode.SrcOver, }; + /// + /// Maps a font to the equivalent . + /// + /// The font composite mode. + /// + /// The equivalent color blending mode. Pure Porter-Duff modes and unsupported blend + /// modes map to . + /// public static PixelColorBlendingMode MapBlendingMode(CompositeMode mode) => mode switch { @@ -52,29 +84,50 @@ public static PixelColorBlendingMode MapBlendingMode(CompositeMode mode) _ => PixelColorBlendingMode.Normal }; + /// + /// Returns unchanged when its rules already match the + /// requested values; otherwise returns a deep clone with the requested rules applied. + /// This avoids allocating per glyph layer in the common case where a layer uses + /// the same modes as the surrounding text. + /// + /// The source drawing options. + /// The required intersection rule. + /// The required alpha composition mode. + /// The required color blending mode. + /// + /// The original instance when it already matches; otherwise a configured clone. + /// public static DrawingOptions CloneOrReturnForRules( this DrawingOptions drawingOptions, IntersectionRule intersectionRule, PixelAlphaCompositionMode compositionMode, PixelColorBlendingMode colorBlendingMode) { - if (drawingOptions.ShapeOptions.IntersectionRule == intersectionRule && + if (drawingOptions.IntersectionRule == intersectionRule && drawingOptions.GraphicsOptions.AlphaCompositionMode == compositionMode && drawingOptions.GraphicsOptions.ColorBlendingMode == colorBlendingMode) { return drawingOptions; } - ShapeOptions shapeOptions = drawingOptions.ShapeOptions.DeepClone(); - shapeOptions.IntersectionRule = intersectionRule; - GraphicsOptions graphicsOptions = drawingOptions.GraphicsOptions.DeepClone(); graphicsOptions.AlphaCompositionMode = compositionMode; graphicsOptions.ColorBlendingMode = colorBlendingMode; - return new DrawingOptions(graphicsOptions, shapeOptions, drawingOptions.Transform); + return new DrawingOptions(graphicsOptions, intersectionRule, drawingOptions.Transform, drawingOptions.TextContrast); } + /// + /// Returns unchanged when its blend modes already + /// match the requested values; otherwise returns a deep clone with the requested + /// modes applied. + /// + /// The source graphics options. + /// The required alpha composition mode. + /// The required color blending mode. + /// + /// The original instance when it already matches; otherwise a configured clone. + /// public static GraphicsOptions CloneOrReturnForRules( this GraphicsOptions graphicsOptions, PixelAlphaCompositionMode compositionMode, diff --git a/tests/Directory.Build.targets b/tests/Directory.Build.targets index 9cd3fdadd..68991f3ce 100644 --- a/tests/Directory.Build.targets +++ b/tests/Directory.Build.targets @@ -18,7 +18,7 @@ - + diff --git a/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextDecorated.cs b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextDecorated.cs new file mode 100644 index 000000000..9c8e7f1a3 --- /dev/null +++ b/tests/ImageSharp.Drawing.Benchmarks/Drawing/DrawTextDecorated.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.Fonts; +using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Benchmarks.Drawing; + +[MemoryDiagnoser] +public class DrawTextDecorated +{ + public const int Width = 800; + public const int Height = 800; + + private const string TextPhrase = "asdfghjkl123456789{}[]+$%?"; + + private Image image; + private Font font; + private IPath curve; + private string text; + private List textRuns; + + [GlobalSetup] + public void Setup() + { + this.image = new Image(Width, Height); + this.font = SystemFonts.CreateFont("Arial", 12); + this.text = string.Join(" ", Enumerable.Repeat(TextPhrase, 20)); + + // All three decoration lanes are active so the benchmark is dominated by decoration + // emission rather than glyph fills. + this.textRuns = + [ + new RichTextRun + { + Start = 0, + End = CodePoint.GetCodePointCount(this.text.AsSpan()), + TextDecorations = TextDecorations.Underline | TextDecorations.Overline | TextDecorations.Strikeout + } + ]; + + _ = Path.TryParseSvgPath("M80,400 C80,80 400,80 400,400 C400,720 720,720 720,400", out this.curve); + } + + [GlobalCleanup] + public void Cleanup() => this.image.Dispose(); + + [Benchmark(Baseline = true)] + public void Linear() + { + RichTextOptions textOptions = new(this.font) + { + WrappingLength = 780, + Origin = new PointF(10, 10), + TextRuns = this.textRuns + }; + + this.image.Mutate(x => x.Paint( + canvas => canvas.DrawText(textOptions, this.text, Brushes.Solid(Color.HotPink), pen: null))); + } + + [Benchmark] + public void OnPath() + { + RichTextOptions textOptions = new(this.font) + { + WrappingLength = this.curve.ComputeLength(), + TextRuns = this.textRuns + }; + + this.image.Mutate(x => x.Paint( + canvas => canvas.DrawText(textOptions, this.text, this.curve, Brushes.Solid(Color.HotPink), pen: null))); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/ConfigurationTests.cs b/tests/ImageSharp.Drawing.Tests/ConfigurationTests.cs index bd40a944c..297656536 100644 --- a/tests/ImageSharp.Drawing.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Drawing.Tests/ConfigurationTests.cs @@ -18,7 +18,7 @@ public class ConfigurationTests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 12; + private readonly int expectedDefaultConfigurationCount = 13; public ConfigurationTests() { diff --git a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj index 860284701..9f4d52b35 100644 --- a/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj +++ b/tests/ImageSharp.Drawing.Tests/ImageSharp.Drawing.Tests.csproj @@ -28,11 +28,6 @@ - - - - - @@ -58,13 +53,4 @@ - - - - - - - - - diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs index e66a65b1e..8c765979c 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_28_108.cs @@ -10,13 +10,14 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_28_108 { [Theory] - [InlineData(1F)] - [InlineData(1.5F)] - [InlineData(2F)] - [InlineData(3F)] - public void DrawingLineAtTopShouldDisplay(float stroke) + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1.5F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 2F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 3F)] + public void DrawingLineAtTopShouldDisplay(TestImageProvider provider, float stroke) + where TPixel : unmanaged, IPixel { - using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); + using Image image = provider.GetImage(); DrawingOptions options = CreateAliasedDrawingOptions(); image.Mutate(x => x.Paint( options, @@ -25,18 +26,21 @@ public void DrawingLineAtTopShouldDisplay(float stroke) new PointF(0, 0), new PointF(100, 0)))); + image.DebugSave(provider, $"stroke-{stroke}", appendSourceFileOrDescription: false); + IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 0)); - Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] - [InlineData(1F)] - [InlineData(1.5F)] - [InlineData(2F)] - [InlineData(3F)] - public void DrawingLineAtBottomShouldDisplay(float stroke) + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1.5F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 2F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 3F)] + public void DrawingLineAtBottomShouldDisplay(TestImageProvider provider, float stroke) + where TPixel : unmanaged, IPixel { - using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); + using Image image = provider.GetImage(); DrawingOptions options = CreateAliasedDrawingOptions(); image.Mutate(x => x.Paint( options, @@ -45,48 +49,56 @@ public void DrawingLineAtBottomShouldDisplay(float stroke) new PointF(0, 99), new PointF(100, 99)))); + image.DebugSave(provider, $"stroke-{stroke}", appendSourceFileOrDescription: false); + IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: i, y: 99)); - Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] - [InlineData(1F)] - [InlineData(1.5F)] - [InlineData(2F)] - [InlineData(3F)] - public void DrawingLineAtLeftShouldDisplay(float stroke) + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1.5F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 2F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 3F)] + public void DrawingLineAtLeftShouldDisplay(TestImageProvider provider, float stroke) + where TPixel : unmanaged, IPixel { - using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); + using Image image = provider.GetImage(); DrawingOptions options = CreateAliasedDrawingOptions(); image.Mutate(x => x.Paint( options, canvas => canvas.DrawLine( Pens.Solid(Color.Red, stroke), new PointF(0, 0), - new PointF(0, 99)))); + new PointF(0, 100)))); + + image.DebugSave(provider, $"stroke-{stroke}", appendSourceFileOrDescription: false); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 0, y: i)); - Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } [Theory] - [InlineData(1F)] - [InlineData(1.5F)] - [InlineData(2F)] - [InlineData(3F)] - public void DrawingLineAtRightShouldDisplay(float stroke) + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 1.5F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 2F)] + [WithSolidFilledImages(100, 100, nameof(Color.Black), PixelTypes.Rgba32, 3F)] + public void DrawingLineAtRightShouldDisplay(TestImageProvider provider, float stroke) + where TPixel : unmanaged, IPixel { - using Image image = new(Configuration.Default, 100, 100, Color.Black.ToPixel()); + using Image image = provider.GetImage(); DrawingOptions options = CreateAliasedDrawingOptions(); image.Mutate(x => x.Paint( options, canvas => canvas.DrawLine( Pens.Solid(Color.Red, stroke), new PointF(99, 0), - new PointF(99, 99)))); + new PointF(99, 100)))); + + image.DebugSave(provider, $"stroke-{stroke}", appendSourceFileOrDescription: false); IEnumerable<(int X, int Y)> locations = Enumerable.Range(0, 100).Select(i => (x: 99, y: i)); - Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); + Assert.All(locations, l => Assert.Equal(Color.Red.ToPixel(), image[l.X, l.Y])); } private static DrawingOptions CreateAliasedDrawingOptions() => diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs index 47d3fe181..eb825f964 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_330.cs @@ -16,8 +16,8 @@ public void OffsetTextOutlines(TestImageProvider provider) { FontFamily fontFamily = TestFontUtilities.GetFontFamily(TestFonts.OpenSans); - Font bibfont = fontFamily.CreateFont(600, FontStyle.Bold); - Font namefont = fontFamily.CreateFont(140, FontStyle.Bold); + Font bibfont = fontFamily.CreateFont(600, FontStyle.Regular); + Font namefont = fontFamily.CreateFont(140, FontStyle.Regular); provider.RunValidatingProcessorTest(p => p.Paint(canvas => { diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_397.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_397.cs index a222c8e45..d705a25e6 100644 --- a/tests/ImageSharp.Drawing.Tests/Issues/Issue_397.cs +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_397.cs @@ -10,54 +10,44 @@ namespace SixLabors.ImageSharp.Drawing.Tests.Issues; public class Issue_397 { [Theory] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)] + [WithBlankImage(240, 160, PixelTypes.Rgba32, ClipOperation.Intersection)] + [WithBlankImage(240, 160, PixelTypes.Rgba32, ClipOperation.Difference)] public void DrawTextWithIntersectingClip( TestImageProvider provider, - BooleanOperation operation) + ClipOperation operation) where TPixel : unmanaged, IPixel { PointF textOrigin = new(54, 78); PointF clipCenter = new(104, 70); - DrawingOptions clipOptions = CreateClipOptions(operation); Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18); // Expected output: // - Intersection shows only red text inside the moved star. // - Difference shows only red text outside the moved star. - // - Union and Xor can show a red star because the boolean-combined path includes the clip path, - // and DrawText fills that combined result with the text brush. provider.RunValidatingProcessorTest( - x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)), + x => x.Paint(canvas => DrawIssue397Sample(canvas, operation, clipCenter, textOrigin, font)), testOutputDetails: $"{operation}_IntersectingClip", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); } [Theory] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Intersection)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Union)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Difference)] - [WithBlankImage(240, 160, PixelTypes.Rgba32, BooleanOperation.Xor)] + [WithBlankImage(240, 160, PixelTypes.Rgba32, ClipOperation.Intersection)] + [WithBlankImage(240, 160, PixelTypes.Rgba32, ClipOperation.Difference)] public void DrawTextWithNonIntersectingClip( TestImageProvider provider, - BooleanOperation operation) + ClipOperation operation) where TPixel : unmanaged, IPixel { PointF textOrigin = new(54, 78); PointF clipCenter = new(192, 116); - DrawingOptions clipOptions = CreateClipOptions(operation); Font font = TestFontUtilities.GetFont("OpenSans-Regular.ttf", 18); // Expected output: // - Intersection shows no red text because the moved star and text do not overlap. // - Difference shows the full red text because the moved star removes nothing from it. - // - Union and Xor show both the full red text and a red star because disjoint Xor matches Union, - // and DrawText fills the boolean-combined result with the text brush. provider.RunValidatingProcessorTest( - x => x.Paint(canvas => DrawIssue397Sample(canvas, clipOptions, clipCenter, textOrigin, font)), + x => x.Paint(canvas => DrawIssue397Sample(canvas, operation, clipCenter, textOrigin, font)), testOutputDetails: $"{operation}_NonIntersectingClip", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); @@ -65,7 +55,7 @@ public void DrawTextWithNonIntersectingClip( private static void DrawIssue397Sample( DrawingCanvas canvas, - DrawingOptions clipOptions, + ClipOperation operation, PointF clipCenter, PointF textOrigin, Font font) @@ -82,7 +72,8 @@ private static void DrawIssue397Sample( // The blue outline marks the moved clipping path without adding a filled shape behind the text. canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath); - canvas.Save(clipOptions, clipPath); + canvas.Save(); + canvas.Clip(operation, clipPath); canvas.DrawText( textOptions, @@ -93,13 +84,4 @@ private static void DrawIssue397Sample( canvas.Restore(); canvas.Draw(Pens.Solid(Color.DarkBlue, 1F), clipPath); } - - private static DrawingOptions CreateClipOptions(BooleanOperation operation) - => new() - { - ShapeOptions = new() - { - BooleanOperation = operation - } - }; } diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_403.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_403.cs new file mode 100644 index 000000000..e0d31d86f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_403.cs @@ -0,0 +1,83 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +// https://github.com/SixLabors/ImageSharp.Drawing/issues/403 +// Paths containing very large coordinates overflowed the 24.8 fixed-point midpoint +// computation in the rasterizer's recursive segment subdivider, so the segment never +// shrank and the recursion overflowed the stack. +public class Issue_403 +{ + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, 5_000_000F)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, 500_000_000F)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, 1E20F)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, -5_000_000F)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, float.NaN)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, float.PositiveInfinity)] + public void DrawLineWithHugeCoordinateDoesNotOverflow(TestImageProvider provider, float extreme) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new(0, 0), + new(extreme, 1), + new(10, 10), + new(5, 5), + new(0, 0) + ]; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.Paint(canvas => + canvas.DrawLine(new SolidPen(Color.Black, 4F), points))); + + image.DebugSave(provider, $"extreme-{extreme}", appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, 5_000_000F)] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32, 1E20F)] + public void FillWithHugeCoordinateDoesNotOverflow(TestImageProvider provider, float extreme) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new(0, 0), + new(extreme, 1), + new(10, 10), + new(5, 5) + ]; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.Paint(canvas => + canvas.Fill(Brushes.Solid(Color.Black), new Polygon(points)))); + + image.DebugSave(provider, $"extreme-{extreme}", appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(100, 100, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawLineWithHugeYCoordinateDoesNotOverflow(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] points = + [ + new(0, 0), + new(1, 5_000_000), + new(10, 10), + new(5, 5), + new(0, 0) + ]; + + using Image image = provider.GetImage(); + image.Mutate(ctx => ctx.Paint(canvas => + canvas.DrawLine(new SolidPen(Color.Black, 4F), points))); + + image.DebugSave(provider, appendSourceFileOrDescription: false); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Issues/Issue_405.cs b/tests/ImageSharp.Drawing.Tests/Issues/Issue_405.cs new file mode 100644 index 000000000..ba0bdb0a6 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Issues/Issue_405.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Drawing.Tests.Issues; + +/// +/// Underlined text rendered with tracking must produce a continuous underline that spans the +/// letter spacing, matching browser behavior, rather than one segment per glyph. +/// See . +/// +public class Issue_405 +{ + [Theory] + [WithSolidFilledImages(800, 200, nameof(Color.White), PixelTypes.Rgba32)] + public void UnderlineWithTracking_SpansLetterSpacing(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + const string text = "Hello World!"; + + RichTextOptions options = new(font) + { + Origin = new PointF(20, 80), + Tracking = 2F, + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = text.Length, + TextDecorations = TextDecorations.Underline + } + ] + }; + + provider.RunValidatingProcessorTest( + c => c.Paint(canvas => canvas.DrawText(options, text, Brushes.Solid(Color.Black), null)), + comparer: ImageComparer.TolerantPercentage(0.002f)); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs index f4e0899f0..0d70cef18 100644 --- a/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs +++ b/tests/ImageSharp.Drawing.Tests/PolygonGeometry/PolygonClippingTests.cs @@ -109,8 +109,7 @@ public void TouchingButNotOverlapping() [Fact] public void ClippingRectanglesCreateCorrectNumberOfPoints() { - IEnumerable paths = new RectanglePolygon(10, 10, 40, 40) - .Clip(new RectanglePolygon(20, 0, 20, 20)) + IEnumerable paths = Clip(new RectanglePolygon(10, 10, 40, 40), new RectanglePolygon(20, 0, 20, 20)) .Flatten(); Assert.Single(paths); diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/FineAreaComputeShaderTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/FineAreaComputeShaderTests.cs new file mode 100644 index 000000000..8ee8e977c --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/FineAreaComputeShaderTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Text; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.PixelFormats; +using TextureFormat = SixLabors.ImageSharp.Drawing.Processing.Backends.Native.WGPUTextureFormat; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class FineAreaComputeShaderTests +{ + [Fact] + public void GetCode_CachesByTargetDescriptor() + { + byte[] associated = FineAreaComputeShader.GetCode(TextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit); + byte[] associatedAgain = FineAreaComputeShader.GetCode(TextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit); + byte[] unassociated = FineAreaComputeShader.GetCode(TextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit); + + Assert.Same(associated, associatedAgain); + Assert.NotSame(associated, unassociated); + } + + [Fact] + public void GetCode_AssociatedTargetPreservesAssociatedBackdropAndOutput() + { + string source = GetSource(TextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit); + + Assert.Contains("fn decode_target(color: vec4) -> vec4 {\n return decode_numeric(color);\n}", source); + Assert.Contains("fn encode_target(color: vec4) -> vec4 {\n return encode_numeric(color);\n}", source); + Assert.Contains("rgba[i] = decode_target(backdrop_raw);", source); + Assert.Contains("textureStore(output, vec2(coords), encode_target(rgba[i]));", source); + } + + [Fact] + public void GetCode_UnassociatedTargetAssociatesBackdropAndUnassociatesOutput() + { + string source = GetSource(TextureFormat.RGBA8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit); + + Assert.Contains("return premul_alpha(decode_numeric(color));", source); + Assert.Contains("let a_inv = select(0.0, 1.0 / color.a, color.a > 0.0);", source); + Assert.Contains("return encode_numeric(vec4(color.rgb * a_inv, color.a));", source); + } + + [Fact] + public void GetCode_SnormTargetDecodesAndEncodesNumericRange() + { + string source = GetSource(TextureFormat.RGBA8Snorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.SignedUnit); + + Assert.Contains("return (color + vec4(1.0)) * 0.5;", source); + Assert.Contains("return (color * 2.0) - vec4(1.0);", source); + } + + [Fact] + public void GetCode_HalfTargetUsesUnitValues() + { + string source = GetSource(TextureFormat.RGBA16Float, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit); + + Assert.Contains("fn decode_numeric(color: vec4) -> vec4 {\n return color;\n}", source); + Assert.Contains("fn encode_numeric(color: vec4) -> vec4 {\n return color;\n}", source); + } + + private static string GetSource( + TextureFormat textureFormat, + PixelAlphaRepresentation alphaRepresentation, + WebGPUTargetNumericEncoding numericEncoding) + { + byte[] code = FineAreaComputeShader.GetCode(textureFormat, alphaRepresentation, numericEncoding); + return Encoding.UTF8.GetString(code.AsSpan(0, code.Length - 1)); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/NativeTypeNameAttributeTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/NativeTypeNameAttributeTests.cs new file mode 100644 index 000000000..ac28adab2 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/NativeTypeNameAttributeTests.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class NativeTypeNameAttributeTests +{ + [Fact] + public void Constructor_StoresNativeName() + => Assert.Equal("WGPUBool", new NativeTypeNameAttribute("WGPUBool").Name); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUCallbackLifetimeTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUCallbackLifetimeTests.cs new file mode 100644 index 000000000..c0fc676de --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUCallbackLifetimeTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public unsafe class WebGPUCallbackLifetimeTests +{ + [Fact] + public void BufferMapCallback_RemainsRootedAndSuppressesManagedCallbackAfterDisposal() + { + StrongBox callbackCount = new(); + nint pointerAddress = CreateRetiredBufferMapCallback(callbackCount, out WeakReference callbackReference); + + // The test intentionally retains only the unmanaged function pointer and a weak reference. + // A collection here reproduces the native-timeout boundary where the managed owner has + // returned but WebGPU still owns one pending callback invocation. + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.True(callbackReference.IsAlive); + + delegate* unmanaged[Cdecl] pointer = + (delegate* unmanaged[Cdecl])pointerAddress; + + pointer(WGPUMapAsyncStatus.Success, default, null, null); + + Assert.Equal(0, callbackCount.Value); + + // The callback invocation retires native ownership. Once it returns, neither the managed + // operation nor native WebGPU owns the wrapper, so its self-root must be released. + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.False(callbackReference.IsAlive); + } + + [Fact] + public void Dispose_WaitsForAlreadyEnteredCallback() + { + using ManualResetEventSlim callbackEntered = new(false); + using ManualResetEventSlim releaseCallback = new(false); + using ManualResetEventSlim disposeCompleted = new(false); + WebGPUQueueWorkDoneCallback callback = WebGPUQueueWorkDoneCallback.From((_, _) => + { + callbackEntered.Set(); + releaseCallback.Wait(); + }); + + callback.RegisterInvocation(); + Thread callbackThread = new(() => callback.Pointer(WGPUQueueWorkDoneStatus.Success, default, null, null)) { IsBackground = true }; + Thread disposeThread = new(() => + { + callback.Dispose(); + disposeCompleted.Set(); + }) + { + IsBackground = true + }; + + callbackThread.Start(); + bool disposeThreadStarted = false; + + try + { + Assert.True(callbackEntered.Wait(TimeSpan.FromSeconds(5))); + + disposeThread.Start(); + disposeThreadStarted = true; + + // EnterInvocation holds the callback's lifetime monitor through the managed callback. + // Observing the disposal thread blocked on that monitor proves Dispose cannot return while + // the callback may still be accessing its owner's event or captured state. + Assert.True(SpinWait.SpinUntil( + () => (disposeThread.ThreadState & ThreadState.WaitSleepJoin) != 0, + TimeSpan.FromSeconds(5))); + + Assert.False(disposeCompleted.IsSet); + releaseCallback.Set(); + + Assert.True(callbackThread.Join(TimeSpan.FromSeconds(5))); + Assert.True(disposeThread.Join(TimeSpan.FromSeconds(5))); + Assert.True(disposeCompleted.IsSet); + } + finally + { + // Failed assertions must not leave either background thread holding the callback root. + releaseCallback.Set(); + _ = callbackThread.Join(TimeSpan.FromSeconds(5)); + + if (disposeThreadStarted) + { + _ = disposeThread.Join(TimeSpan.FromSeconds(5)); + } + else + { + callback.Dispose(); + } + } + } + + [Fact] + public void QueueWorkDoneCallback_SuppressesManagedCallbackAfterDisposal() + { + int callbackCount = 0; + WebGPUQueueWorkDoneCallback callback = WebGPUQueueWorkDoneCallback.From((_, _) => callbackCount++); + callback.RegisterInvocation(); + callback.Dispose(); + + callback.Pointer(WGPUQueueWorkDoneStatus.Success, default, null, null); + + Assert.Equal(0, callbackCount); + } + + [Fact] + public void RequestAdapterCallback_RoutesLateOwnedResultToAbandonmentCallback() + { + int callbackCount = 0; + WGPUAdapterImpl* abandonedAdapter = null; + WebGPURequestAdapterCallback callback = WebGPURequestAdapterCallback.From( + (_, _, _, _) => callbackCount++, + (_, adapter, _, _) => abandonedAdapter = adapter); + + callback.RegisterInvocation(); + callback.Dispose(); + + WGPUAdapterImpl* expectedAdapter = (WGPUAdapterImpl*)1; + callback.Pointer(WGPURequestAdapterStatus.Success, expectedAdapter, default, null, null); + + Assert.Equal(0, callbackCount); + Assert.Equal((nint)expectedAdapter, (nint)abandonedAdapter); + } + + [Fact] + public void RequestDeviceCallback_RoutesLateOwnedResultToAbandonmentCallback() + { + int callbackCount = 0; + WGPUDeviceImpl* abandonedDevice = null; + WebGPURequestDeviceCallback callback = WebGPURequestDeviceCallback.From( + (_, _, _, _) => callbackCount++, + (_, device, _, _) => abandonedDevice = device); + + callback.RegisterInvocation(); + callback.Dispose(); + + WGPUDeviceImpl* expectedDevice = (WGPUDeviceImpl*)1; + callback.Pointer(WGPURequestDeviceStatus.Success, expectedDevice, default, null, null); + + Assert.Equal(0, callbackCount); + Assert.Equal((nint)expectedDevice, (nint)abandonedDevice); + } + + /// + /// Creates a buffer-map callback whose managed owner has retired while one native invocation + /// remains outstanding. + /// + /// Receives any incorrect managed invocation after retirement. + /// Receives a weak reference used to verify native ownership keeps the thunk rooted. + /// The unmanaged callback pointer retained by the simulated native operation. + [MethodImpl(MethodImplOptions.NoInlining)] + private static nint CreateRetiredBufferMapCallback(StrongBox callbackCount, out WeakReference callbackReference) + { + WebGPUBufferMapCallback callback = WebGPUBufferMapCallback.From((_, _) => callbackCount.Value++); + callback.RegisterInvocation(); + callbackReference = new WeakReference(callback); + nint pointer = (nint)callback.Pointer; + callback.Dispose(); + return pointer; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDeviceContextTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDeviceContextTests.cs index d27504a5f..ef108de52 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDeviceContextTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDeviceContextTests.cs @@ -4,65 +4,58 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using FeatureName = SixLabors.ImageSharp.Drawing.Processing.Backends.Native.WGPUFeatureName; namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; public class WebGPUDeviceContextTests { [Fact] - public void Create_RejectsZeroHandles() + public void CompositeTargetDescriptors_MapSupportedPixelTypes() { - Assert.Throws(() => new WebGPUDeviceContext(0, 1)); - Assert.Throws(() => new WebGPUDeviceContext(1, 0)); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.SignedUnit, FeatureName.TextureFormatsTier1); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.SignedUnit, FeatureName.TextureFormatsTier1); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit, default); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit, default); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit, default); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit, default); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Unassociated, WebGPUTargetNumericEncoding.Unit, FeatureName.BGRA8UnormStorage); + AssertCompositeTargetDescriptor(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated, WebGPUTargetNumericEncoding.Unit, FeatureName.BGRA8UnormStorage); } - [WebGPUFact] - public void CreateCanvas_RejectsInvalidHandles_AndReadbackRejectsMismatchedFormat() + [Fact] + public void OffscreenRgba16FloatTarget_UsesUnitNumericEncoding() { - using WebGPUDeviceContext drawing = new(); - using WebGPURenderTarget target = drawing.CreateRenderTarget(8, 8); - using WebGPUHandle.HandleReference textureReference = target.TextureHandle.AcquireReference(); - using WebGPUHandle.HandleReference textureViewReference = target.TextureViewHandle.AcquireReference(); + WebGPUTargetDescriptor descriptor = WebGPUDrawingBackend.CreateOffscreenTargetDescriptor(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated); - Assert.Throws( - () => drawing.CreateCanvas(0, textureViewReference.Handle, target.Format, 8, 8)); + Assert.Equal(WebGPUTargetNumericEncoding.Unit, descriptor.NumericEncoding); + } - Assert.Throws( - () => drawing.CreateCanvas(textureReference.Handle, 0, target.Format, 8, 8)); + [Fact] + public void CompositeTargetDescriptors_RejectUnsupportedAssociatedLayouts() + { + Assert.False(WebGPUDrawingBackend.TryGetCompositeTargetDescriptor(out _, out _)); + Assert.False(WebGPUDrawingBackend.TryGetCompositeTargetDescriptor(out _, out _)); + } + [WebGPUFact] + public void RenderTarget_ReadbackRejectsMismatchedFormat() + { + using WebGPURenderTarget target = new(WebGPUTextureFormat.Rgba8Unorm, 8, 8); using Image destination = new(8, 8); Assert.Throws( () => target.ReadbackInto(destination.Frames.RootFrame.PixelBuffer.GetRegion())); } - [WebGPUFact] - public void CreateCanvas_WithExternalTexture_UsesGpuPath() - { - using WebGPUDeviceContext drawing = new(); - using WebGPURenderTarget target = drawing.CreateRenderTarget(32, 24); - using (DrawingCanvas canvas = drawing.CreateCanvas( - new DrawingOptions(), - target.TextureHandle, - target.TextureViewHandle, - target.Format, - 32, - 24)) - { - canvas.Fill(Brushes.Solid(Color.Red), new RectanglePolygon(0, 0, 32, 24)); - } - - using Image readback = target.ReadbackImage(); - Assert.NotEqual(default, readback[16, 12]); - } - [WebGPUFact] public void RenderTarget_CreateCanvas_RendersAndReadsBack() { - using WebGPUDeviceContext drawing = new(); - using WebGPURenderTarget target = drawing.CreateRenderTarget(18, 14); + using WebGPURenderTarget target = new(WebGPUTextureFormat.Rgba8Unorm, 18, 14); using (DrawingCanvas canvas = target.CreateCanvas()) { canvas.Fill(Brushes.Solid(Color.Green), new RectanglePolygon(0, 0, 18, 14)); @@ -88,6 +81,108 @@ public void RenderTarget_ReadbackImage_UsesTargetFormat() Assert.Equal(target.Height, typedReadback.Height); } + [WebGPUFact] + public void RenderTarget_AssociatedRgbaTarget_UsesAssociatedCanvasAndReadback() + => AssertAssociatedRenderTarget(WebGPUTextureFormat.Rgba8Unorm); + + [WebGPUFact] + public void RenderTarget_AssociatedBgraTarget_UsesAssociatedCanvasAndReadback() + => AssertAssociatedRenderTarget(WebGPUTextureFormat.Bgra8Unorm); + + [WebGPUFact] + public void RenderTarget_AssociatedSnormTarget_UsesAssociatedCanvasAndReadback() + { + using WebGPURenderTarget probe = new(WebGPUTextureFormat.Rgba8Unorm, 1, 1); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), probe.DeviceContext.DeviceHandle); + + if (!deviceState.HasFeature(FeatureName.TextureFormatsTier1)) + { + Assert.Throws(() => new WebGPURenderTarget(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, 8, 6)); + return; + } + + AssertAssociatedRenderTarget(WebGPUTextureFormat.Rgba8Snorm); + } + + [WebGPUFact] + public void RenderTarget_AssociatedHalfTarget_UsesAssociatedCanvasAndReadback() + => AssertAssociatedRenderTarget(WebGPUTextureFormat.Rgba16Float); + + [WebGPUFact] + public void RenderTarget_HalfTargets_InitializeToNativeDefault() + { + AssertRenderTargetInitializesToNativeDefault(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Unassociated); + AssertRenderTargetInitializesToNativeDefault(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated); + } + + [WebGPUFact] + public void RenderTarget_SnormTargets_InitializeToNativeDefault() + { + using WebGPURenderTarget probe = new(WebGPUTextureFormat.Rgba8Unorm, 1, 1); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), probe.DeviceContext.DeviceHandle); + + if (!deviceState.HasFeature(FeatureName.TextureFormatsTier1)) + { + Assert.Throws(() => new WebGPURenderTarget(WebGPUTextureFormat.Rgba8Snorm, 8, 6)); + return; + } + + AssertRenderTargetInitializesToNativeDefault(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Unassociated); + AssertRenderTargetInitializesToNativeDefault(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated); + } + + [WebGPUFact] + public void RenderTarget_AssociatedTarget_PreservesBackdropAcrossFlushes() + { + Color backdrop = Color.FromPixel(new Rgba32P(80, 40, 20, 128)); + Color foreground = Color.FromPixel(new Rgba32(20, 120, 200, 96)); + + using WebGPURenderTarget target = new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, 8, 6); + using (DrawingCanvas canvas = target.CreateCanvas()) + { + canvas.Fill(Brushes.Solid(backdrop), new RectanglePolygon(0, 0, target.Width, target.Height)); + } + + using (DrawingCanvas canvas = target.CreateCanvas()) + { + canvas.Fill(Brushes.Solid(foreground), new RectanglePolygon(0, 0, target.Width, target.Height)); + } + + using Image expected = new(target.Width, target.Height); + expected.Mutate(context => context.Paint(canvas => + { + canvas.Fill(Brushes.Solid(backdrop)); + canvas.Fill(Brushes.Solid(foreground)); + })); + + using Image actual = target.ReadbackImage(); + Rgba32P expectedPixel = expected[target.Width / 2, target.Height / 2]; + Rgba32P actualPixel = actual[target.Width / 2, target.Height / 2]; + + // CPU byte blending and WGSL floating-point blending can round an associated color channel + // to adjacent bytes. Alpha is not association-scaled and must remain exact. + Assert.InRange(Math.Abs(expectedPixel.R - actualPixel.R), 0, 1); + Assert.InRange(Math.Abs(expectedPixel.G - actualPixel.G), 0, 1); + Assert.InRange(Math.Abs(expectedPixel.B - actualPixel.B), 0, 1); + Assert.Equal(expectedPixel.A, actualPixel.A); + } + + [WebGPUFact] + public void RenderTarget_AssociatedImageBrush_IsNotAssociatedTwice() + { + Rgba32P sourcePixel = new(80, 40, 20, 128); + + using Image source = new(2, 2, sourcePixel); + using WebGPURenderTarget target = new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, 8, 6); + using (DrawingCanvas canvas = target.CreateCanvas()) + { + canvas.Fill(new ImageBrush(source), new RectanglePolygon(0, 0, target.Width, target.Height)); + } + + using Image actual = target.ReadbackImage(); + Assert.Equal(sourcePixel, actual[target.Width / 2, target.Height / 2]); + } + [WebGPUFact] public void RenderTarget_ReadbackInto_BufferRegion_WritesSubregion() { @@ -113,20 +208,104 @@ public void RenderTarget_ReadbackInto_BufferRegion_WritesSubregion() } [WebGPUFact] - public void Dispose_Context_DoesNotReleaseOwnedTargetHandles() + public void PresentationRenderer_TransfersEverySupportedTargetFormat() + { + using WebGPURenderTarget probe = new(WebGPUTextureFormat.Rgba8Unorm, 1, 1); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), probe.DeviceContext.DeviceHandle); + + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, copyToSurface: true); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, copyToSurface: false); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, copyToSurface: true); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, copyToSurface: false); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated, copyToSurface: true); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba16Float, PixelAlphaRepresentation.Associated, copyToSurface: false); + + if (deviceState.HasFeature(FeatureName.BGRA8UnormStorage)) + { + AssertPresentationTransfer(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated, copyToSurface: true); + AssertPresentationTransfer(WebGPUTextureFormat.Bgra8Unorm, PixelAlphaRepresentation.Associated, copyToSurface: false); + } + + if (deviceState.HasFeature(FeatureName.TextureFormatsTier1)) + { + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, copyToSurface: true); + AssertPresentationTransfer(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, copyToSurface: false); + } + } + + private static void AssertCompositeTargetDescriptor(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, WebGPUTargetNumericEncoding numericEncoding, FeatureName requiredFeature) + where TPixel : unmanaged, IPixel { - using WebGPUDeviceContext drawing = new(); - using WebGPURenderTarget target = drawing.CreateRenderTarget(12, 10); + Assert.True(WebGPUDrawingBackend.TryGetCompositeTargetDescriptor(out WebGPUTargetDescriptor descriptor, out FeatureName actualRequiredFeature)); + Assert.Equal(format, descriptor.Format); + Assert.Equal(alphaRepresentation, descriptor.AlphaRepresentation); + Assert.Equal(numericEncoding, descriptor.NumericEncoding); + Assert.Equal(requiredFeature, actualRequiredFeature); + } + + private static void AssertRenderTargetInitializesToNativeDefault(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation) + where TPixel : unmanaged, IPixel + { + using WebGPURenderTarget target = new(format, alphaRepresentation, 8, 6); + using Image readback = target.ReadbackImage(); - drawing.Dispose(); + // WebGPU and ImageSharp clean allocations both initialize native pixel storage to zero. + // Preserve those bits: unit formats interpret them as transparent black, while signed-unit + // formats interpret them as the midpoint of their logical range. + Assert.Equal(default, readback[target.Width / 2, target.Height / 2]); + } - using WebGPUDrawingBackend backend = new(); - using Image image = new(12, 10); + private static void AssertAssociatedRenderTarget(WebGPUTextureFormat format) + where TPixel : unmanaged, IPixel + { + Color color = Color.FromPixel(new Rgba32P(80, 40, 20, 128)); + + using Image expected = new(8, 6); + using (DrawingCanvas canvas = new(expected.Configuration, new DrawingOptions(), expected.Frames.RootFrame.PixelBuffer.GetRegion())) + { + canvas.Fill(Brushes.Solid(color), new RectanglePolygon(0, 0, expected.Width, expected.Height)); + } + + using WebGPURenderTarget target = new(format, PixelAlphaRepresentation.Associated, 8, 6); + Assert.Equal(PixelAlphaRepresentation.Associated, target.AlphaRepresentation); + + using (DrawingCanvas canvas = target.CreateCanvas()) + { + Assert.IsType>(canvas); + canvas.Fill(Brushes.Solid(color), new RectanglePolygon(0, 0, target.Width, target.Height)); + } + + using Image readback = target.ReadbackImage(); + Image typedReadback = Assert.IsType>(readback); + TPixel expectedPixel = expected[expected.Width / 2, expected.Height / 2]; + TPixel actualPixel = typedReadback[target.Width / 2, target.Height / 2]; + Assert.Equal(expectedPixel.ToRgba32(), actualPixel.ToRgba32()); + + Assert.Throws(target.ReadbackImage); + + using WebGPURenderTarget child = target.CreateRenderTarget(4, 3); + Assert.Equal(target.Format, child.Format); + Assert.Equal(target.AlphaRepresentation, child.AlphaRepresentation); + } + + private static void AssertPresentationTransfer(WebGPUTextureFormat format, PixelAlphaRepresentation alphaRepresentation, bool copyToSurface) + where TPixel : unmanaged, IPixel + { + using WebGPURenderTarget source = new(format, alphaRepresentation, 13, 9); + using WebGPURenderTarget destination = source.CreateRenderTarget(source.Width, source.Height); + using (DrawingCanvas canvas = source.CreateCanvas()) + { + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32(173, 41, 229, 137))), new RectanglePolygon(0, 0, source.Width, source.Height)); + canvas.Fill(Brushes.Solid(Color.FromPixel(new Rgba32(19, 211, 67, 83))), new RectanglePolygon(3, 2, 7, 5)); + } + + using (WebGPUPresentationRenderer renderer = new(WebGPURuntime.GetApi(), source.DeviceContext, source, copyToSurface)) + { + renderer.Present(destination.TextureHandle, destination.TextureViewHandle); + } - backend.ReadRegion( - Configuration.Default, - WebGPUCanvasFactory.CreateFrame(target.Bounds, target.Surface), - target.Bounds, - image.Frames.RootFrame.PixelBuffer.GetRegion()); + using Image expected = source.ReadbackImage(); + using Image actual = destination.ReadbackImage(); + ImageComparer.Exact.VerifySimilarity(expected, actual); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Diagnostics.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Diagnostics.cs new file mode 100644 index 000000000..a6c9e37bc --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Diagnostics.cs @@ -0,0 +1,278 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public partial class WebGPUDrawingBackendTests +{ + // A blank or near-uniform frame (the black-screen failure mode) has only a handful of distinct + // colours; a correctly rendered batch of thousands of random-coloured, blended lines has hundreds of + // thousands. This threshold cleanly separates a real render from a dropped-coverage failure without + // depending on how much background remains visible at high line density. + private const int RenderedContentDistinctColorFloor = 2000; + + // A moderate (100K-line) stroke batch whose scratch buffers fit under the device's per-buffer ceiling + // must render in a single pass. The bounding-box-diagonal segment estimate previously over-seeded the + // scratch buffers so badly that even this scene spilled over the binding limit and chunked; both fill + // and stroke estimates are now seeded from exact per-line tile spans, keeping the buffers realistic. + [WebGPUFact] + public void FillManyLines_ModerateScene_RendersSinglePass() + { + (int Distinct, bool Chunked, int Errors) result = RenderManyLines(600, 400, lineCount: 100_000, passes: 3); + Assert.Equal(0, result.Errors); + Assert.False(result.Chunked, "A 100K-line scene at 600x400 fits one binding and must not chunk."); + Assert.True(result.Distinct >= RenderedContentDistinctColorFloor, $"Frame is blank: {result.Distinct} distinct colours."); + } + + // Regression guard for the benchmark app's black screen: jumping straight to 100K lines at the app's + // window size (no ramp-up growing the retained scratch first) previously produced a fully transparent + // frame. The scene's ptcl buffer exceeds the device's 256 MiB maxBufferSize, so it cannot render in a + // single buffer; the chunking ceiling now respects maxBufferSize (not just the larger binding size), + // so the scene chunks and renders instead of failing buffer creation with an invalid-buffer error. + [WebGPUFact] + public void FillManyLines_AtWindowSize_RendersWithoutBlackScreen() + { + (int Distinct, bool Chunked, int Errors) result = RenderManyLines(1600, 1100, lineCount: 100_000, passes: 1); + Assert.Equal(0, result.Errors); + Assert.True(result.Distinct >= RenderedContentDistinctColorFloor, $"First flush is a black screen: {result.Distinct} distinct colours."); + } + + // Ordered execution retains one status record per successful chunk until its next Apply + // barrier. Constrain only this backend instance to WebGPU's baseline 128 MiB storage-binding + // limit so the test covers the multi-record path on adapters whose native limit is much larger. + [WebGPUFact] + public void FillManyLines_ChunkedOrderedRange_RendersThroughApplyBarrier() + { + const int width = 1600; + const int height = 1100; + (PointF Start, PointF End, Color Color, float Width)[] lines = CreateRandomLines(width, height, 100_000); + Brush background = Brushes.Solid(Color.ParseHex("#003366")); + + int gpuErrors = 0; + WebGPUEnvironment.UncapturedError = (_, _) => Interlocked.Increment(ref gpuErrors); + try + { + using WebGPURenderTarget target = new(WebGPUTextureFormat.Bgra8Unorm, width, height); + target.Backend.ScratchBufferBindingSizeLimit = 128U * 1024U * 1024U; + + using (DrawingCanvas canvas = target.CreateCanvas()) + { + canvas.Fill(background); + foreach ((PointF start, PointF end, Color color, float lineWidth) in lines) + { + canvas.DrawLine(new SolidPen(color, lineWidth), start, end); + } + + // Apply forces the oversized preceding range through the ordered transaction's + // shared status-and-pixel barrier. + canvas.Apply(new Rectangle(0, 0, width, height), context => context.Invert()); + } + + using Image readback = target.ReadbackImage().CloneAs(); + int distinct = CountDistinctColors(readback); + + Assert.Equal(0, gpuErrors); + Assert.True( + target.Backend.DiagnosticLastFlushUsedChunking, + "The ordered range must exercise multi-chunk status validation."); + Assert.True(distinct >= RenderedContentDistinctColorFloor, $"Ordered chunk output is blank: {distinct} distinct colours."); + } + finally + { + WebGPUEnvironment.UncapturedError = null; + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + public void SchedulingScratchGrowth_HandlesEachAllocatorUniformly(int allocatorIndex) + { + WebGPUSceneBumpSizes current = new(100, 100, 100, 100, 100, 100, 100, 100); + GpuSceneBumpAllocators actual = new() + { + Lines = allocatorIndex == 0 ? 101U : 100U, + Binning = allocatorIndex == 1 ? 101U : 100U, + PathRows = allocatorIndex == 2 ? 101U : 100U, + Tile = allocatorIndex == 3 ? 101U : 100U, + SegCounts = allocatorIndex == 4 ? 101U : 100U, + Segments = allocatorIndex == 5 ? 101U : 100U, + BlendSpill = allocatorIndex == 6 ? 101U : 100U, + Ptcl = allocatorIndex == 7 ? 101U : 100U + }; + + Assert.True(WebGPUSceneDispatch.RequiresScratchReallocation(in actual, current)); + WebGPUSceneBumpSizes grown = WebGPUSceneDispatch.GrowBumpSizes(current, in actual); + + Assert.Equal(allocatorIndex == 0, grown.Lines > current.Lines); + Assert.Equal(allocatorIndex == 1, grown.Binning > current.Binning); + Assert.Equal(allocatorIndex == 2, grown.PathRows > current.PathRows); + Assert.Equal(allocatorIndex == 3, grown.PathTiles > current.PathTiles); + Assert.Equal(allocatorIndex == 4, grown.SegCounts > current.SegCounts); + Assert.Equal(allocatorIndex == 5, grown.Segments > current.Segments); + Assert.Equal(allocatorIndex == 6, grown.BlendSpill > current.BlendSpill); + Assert.Equal(allocatorIndex == 7, grown.Ptcl > current.Ptcl); + } + + [Fact] + public void ChunkSchedulingScratchGrowth_IncludesBlendSpillOverflow() + { + WebGPUSceneBumpSizes submitted = new(100, 100, 100, 100, 100, 100, 100, 100); + GpuSceneBumpAllocators[] statuses = + [ + new GpuSceneBumpAllocators + { + Lines = 100, + Binning = 100, + PathRows = 100, + Tile = 100, + SegCounts = 100, + Segments = 100, + BlendSpill = 101, + Ptcl = 100 + } + ]; + WebGPUSceneBumpSizes[] submittedSizes = [submitted]; + uint[] chunkTileHeights = [1]; + + bool requiresGrowth = WebGPUSceneDispatch.ResolveChunkSchedulingStatuses( + statuses, + submittedSizes, + chunkTileHeights, + 4, + out WebGPUSceneBumpSizes grown); + + Assert.True(requiresGrowth); + Assert.True(grown.BlendSpill > submitted.BlendSpill); + } + + // A correctly seeded scene renders fully on its first flush. An under-seeded scratch estimate (for + // example counting only the stroke centerline instead of the emitted offset sides, joins and caps) + // overflows the first flush, drops coverage, and only converges after later flushes grow the buffers. + // So a single-pass render must be pixel-identical to a converged multi-pass render of the same scene. + [WebGPUFact] + public void FillManyLines_SinglePass_MatchesConvergedRender() + { + const int width = 600; + const int height = 400; + (PointF Start, PointF End, Color Color, float Width)[] lines = CreateRandomLines(width, height, 100_000); + Brush background = Brushes.Solid(Color.ParseHex("#003366")); + + using Image singlePass = RenderManyLinesToImage(width, height, background, lines, passes: 1); + using Image converged = RenderManyLinesToImage(width, height, background, lines, passes: 4); + + long differing = CountDifferingPixels(singlePass, converged); + long total = (long)width * height; + Assert.True( + differing <= total / 500, + $"Single-pass render differs from the converged render in {differing} of {total} pixels, indicating under-seeded scratch."); + } + + private static Image RenderManyLinesToImage(int width, int height, Brush background, (PointF Start, PointF End, Color Color, float Width)[] lines, int passes) + { + using WebGPURenderTarget target = new(WebGPUTextureFormat.Bgra8Unorm, width, height); + for (int pass = 0; pass < passes; pass++) + { + DrawLines(target, background, lines); + } + + return target.ReadbackImage().CloneAs(); + } + + private static long CountDifferingPixels(Image first, Image second) + { + long differing = 0; + for (int y = 0; y < first.Height; y++) + { + for (int x = 0; x < first.Width; x++) + { + if (!first[x, y].Equals(second[x, y])) + { + differing++; + } + } + } + + return differing; + } + + private static (int Distinct, bool Chunked, int Errors) RenderManyLines(int width, int height, int lineCount, int passes) + { + (PointF Start, PointF End, Color Color, float Width)[] lines = CreateRandomLines(width, height, lineCount); + Brush background = Brushes.Solid(Color.ParseHex("#003366")); + + int gpuErrors = 0; + WebGPUEnvironment.UncapturedError = (_, _) => Interlocked.Increment(ref gpuErrors); + try + { + using WebGPURenderTarget target = new(WebGPUTextureFormat.Bgra8Unorm, width, height); + for (int pass = 0; pass < passes; pass++) + { + DrawLines(target, background, lines); + } + + using Image readback = target.ReadbackImage().CloneAs(); + return (CountDistinctColors(readback), target.Backend.DiagnosticLastFlushUsedChunking, gpuErrors); + } + finally + { + WebGPUEnvironment.UncapturedError = null; + } + } + + private static (PointF Start, PointF End, Color Color, float Width)[] CreateRandomLines(int width, int height, int lineCount) + { + Random rng = new(0); + (PointF Start, PointF End, Color Color, float Width)[] lines = new (PointF, PointF, Color, float)[lineCount]; + for (int i = 0; i < lineCount; i++) + { + lines[i] = ( + new PointF((float)(rng.NextDouble() * width), (float)(rng.NextDouble() * height)), + new PointF((float)(rng.NextDouble() * width), (float)(rng.NextDouble() * height)), + Color.FromPixel(new Rgba32((byte)rng.Next(255), (byte)rng.Next(255), (byte)rng.Next(255), (byte)rng.Next(255))), + rng.Next(1, 10)); + } + + return lines; + } + + private static void DrawLines(WebGPURenderTarget target, Brush background, ReadOnlySpan<(PointF Start, PointF End, Color Color, float Width)> lines) + { + using DrawingCanvas canvas = target.CreateCanvas(); + canvas.Fill(background); + foreach ((PointF start, PointF end, Color color, float width) in lines) + { + canvas.DrawLine(new SolidPen(color, width), start, end); + } + } + + private static int CountDistinctColors(Image image) + { + HashSet colors = new(); + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + colors.Add(row[x].PackedValue); + } + } + }); + + return colors.Count; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Recolor.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Recolor.cs new file mode 100644 index 000000000..6642dcc61 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.Recolor.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using FeatureName = SixLabors.ImageSharp.Drawing.Processing.Backends.Native.WGPUFeatureName; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public partial class WebGPUDrawingBackendTests +{ + [WebGPUFact] + public void FillPath_WithRecolorBrush_UnmatchedKeyStillAppliesOuterClear() + { + Rgba32 background = Color.Green.ToPixel(); + Brush brush = new RecolorBrush(Color.Red, Color.Blue, 0.01F); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Clear + } + }; + + using Image defaultImage = new(8, 8, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, canvas => canvas.Fill(brush)); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(8, 8, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, canvas => canvas.Fill(brush), initialImage); + + // Recolor supplies the unchanged backdrop when the key does not match, but the brush + // still runs that overlay through the configured outer composition operation. + Assert.Equal(default, defaultImage[4, 4]); + Assert.Equal(defaultImage[4, 4], actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_TargetPreservesF32Precision() + { + Color targetColor = Color.FromScaledVector(new Vector4(0.4999F, 0F, 0F, 1F)); + Brush brush = new RecolorBrush(Color.Black, targetColor, 1F); + DrawingOptions drawingOptions = CreateRecolorSourceOptions(); + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Black)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using WebGPUDrawingBackend backend = new(); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, Draw); + + // 0.4999 * 255 rounds to 127. Transporting the color through binary16 first rounds it + // to 0.5, which incorrectly crosses the byte midpoint and produces 128. + Rgba32 expected = new(127, 0, 0, 255); + Assert.Equal(expected, defaultImage[4, 4]); + Assert.Equal(expected, actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_Rgba32PTargetUsesStoredAlphaForAssociation() + { + Color targetColor = Color.FromScaledVector(new Vector4(0.49F, 0F, 0F, 0.5F)); + Brush brush = new RecolorBrush(Color.Black, targetColor, 1F); + DrawingOptions drawingOptions = CreateRecolorSourceOptions(); + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Black)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using Image actual = RenderRecolorWithAssociatedWebGpuBackend(WebGPUTextureFormat.Rgba8Unorm, drawingOptions, Draw); + + // Rgba32P first stores alpha 0.5 as 128, then associates red with that stored alpha: + // round(0.49 * 128) = 63. Multiplying by the unquantized alpha would incorrectly yield 62. + Rgba32P expected = new(63, 0, 0, 128); + Assert.Equal(expected, defaultImage[4, 4]); + Assert.Equal(expected, actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_NormalizedByte4PTargetMatchesExactStorage() + { + using WebGPURenderTarget probe = new(WebGPUTextureFormat.Rgba8Unorm, 1, 1); + WebGPURuntime.DeviceSharedState deviceState = WebGPURuntime.GetOrCreateDeviceState(WebGPURuntime.GetApi(), probe.DeviceContext.DeviceHandle); + + if (!deviceState.HasFeature(FeatureName.TextureFormatsTier1)) + { + Assert.Throws(() => new WebGPURenderTarget(WebGPUTextureFormat.Rgba8Snorm, PixelAlphaRepresentation.Associated, 8, 8)); + return; + } + + Color targetColor = Color.FromScaledVector(new Vector4(0.37F, 0.61F, 0.19F, 0.503F)); + Brush brush = new RecolorBrush(Color.Black, targetColor, 1F); + DrawingOptions drawingOptions = CreateRecolorSourceOptions(); + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Black)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using Image actual = RenderRecolorWithAssociatedWebGpuBackend(WebGPUTextureFormat.Rgba8Snorm, drawingOptions, Draw); + + NormalizedByte4P expected = targetColor.ToPixel(); + Assert.Equal(expected, defaultImage[4, 4]); + Assert.Equal(expected, actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_RgbaHalfPTargetMatchesExactStorage() + { + Color targetColor = Color.FromScaledVector(new Vector4(0.37F, 0.61F, 0.19F, 0.503F)); + Brush brush = new RecolorBrush(Color.Black, targetColor, 1F); + DrawingOptions drawingOptions = CreateRecolorSourceOptions(); + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Black)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using Image actual = RenderRecolorWithAssociatedWebGpuBackend(WebGPUTextureFormat.Rgba16Float, drawingOptions, Draw); + + RgbaHalfP expected = targetColor.ToPixel(); + Assert.Equal(expected, defaultImage[4, 4]); + Assert.Equal(expected, actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_RebasesPartitionAuxiliaryOffsets() + { + const int width = 32; + const int height = 32; + Color secondTarget = Color.FromScaledVector(new Vector4(0F, 0.4999F, 0F, 1F)); + Brush firstBrush = new RecolorBrush(Color.Black, Color.Red, 1F); + Brush secondBrush = new RecolorBrush(Color.Black, secondTarget, 1F); + DrawingOptions drawingOptions = CreateRecolorSourceOptions(); + RectanglePolygon left = new(0, 0, width / 2, height); + RectanglePolygon right = new(width / 2, 0, width / 2, height); + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(firstBrush, left); + canvas.Fill(secondBrush, right); + } + + using Image defaultImage = new(width, height, Color.Black.ToPixel()); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using WebGPUDrawingBackend backend = new(); + using WebGPURenderTarget renderTarget = new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Unassociated, width, height); + Configuration configuration = Configuration.Default.Clone(); + configuration.MaxDegreeOfParallelism = 2; + configuration.SetDrawingBackend(backend); + + using Image initialImage = new(width, height, Color.Black.ToPixel()); + using (DrawingCanvas initialCanvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + new DrawingOptions(), + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + initialCanvas.DrawImage(initialImage, initialImage.Bounds, renderTarget.Bounds); + } + + // Two commands, two target tile rows, and a parallelism limit of two force one Recolor + // command into each encoder partition. The second command's local payload offset must be + // rebased past the first partition's payload when the encoded streams are concatenated. + using (DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + drawingOptions, + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + Draw(canvas); + } + + using Image actual = renderTarget.ReadbackImage(); + Rgba32 expectedSecond = new(0, 127, 0, 255); + + Assert.Equal(Color.Red.ToPixel(), defaultImage[8, 16]); + Assert.Equal(expectedSecond, defaultImage[24, 16]); + Assert.Equal(defaultImage[8, 16], actual[8, 16]); + Assert.Equal(defaultImage[24, 16], actual[24, 16]); + } + + private static DrawingOptions CreateRecolorSourceOptions() + => new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + private static Image RenderRecolorWithAssociatedWebGpuBackend(WebGPUTextureFormat format, DrawingOptions drawingOptions, Action drawAction) + where TPixel : unmanaged, IPixel + { + using WebGPUDrawingBackend backend = new(); + using WebGPURenderTarget renderTarget = new(format, PixelAlphaRepresentation.Associated, 8, 8); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using (DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + drawingOptions, + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + drawAction(canvas); + } + + return renderTarget.ReadbackImage(); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs index 2c12a30ed..ce675e7a5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUDrawingBackendTests.cs @@ -57,12 +57,13 @@ public void FillPath_WithWebGPUCoverageBackend_MatchesDefaultOutput(Test defaultImage.Width, defaultImage.Height, nativeSurfaceBackend, + WebGPUTextureFormat.Rgba8Unorm, drawingOptions, DrawAction, nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.05F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0012F); } @@ -104,7 +105,7 @@ public void FillPath_UncontainedGeometry_MatchesDefaultOutput(TestImageP nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.3F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -137,7 +138,7 @@ public void FillPath_AliasedWithThreshold_MatchesDefaultOutput(TestImage nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -174,10 +175,61 @@ void DrawAction(DrawingCanvas canvas) DrawAction); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } + [WebGPUTheory] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.None, WrapMode.None)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Repeat, WrapMode.Repeat)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Mirror, WrapMode.Repeat)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Repeat, WrapMode.Mirror)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Mirror, WrapMode.Mirror)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Clamp, WrapMode.Clamp)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Clamp, WrapMode.Repeat)] + [WithFile(TestImages.Png.Rainbow, PixelTypes.Rgba32, WrapMode.Mirror, WrapMode.Clamp)] + public void FillPath_WithImageBrushWrapModes_MatchesDefaultOutput(TestImageProvider provider, WrapMode wrapX, WrapMode wrapY) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + RectanglePolygon polygon = new(8F, 8F, 492F, 492F); + Brush clearBrush = Brushes.Solid(Color.White); + + // The rainbow is an opaque diagonal gradient (colour varies along both axes), so every mode is + // visibly distinct: Repeat tiles, Mirror reflects on each axis, Clamp stretches the edge colours, + // and None leaves transparency. A uniform, symmetric, or transparent-bordered source would make + // several modes indistinguishable. The region is inset 1px (sampling the interior) and is smaller + // than the target, so it repeats several times across the fill. + using Image foreground = provider.GetImage(); + Brush brush = new ImageBrush(foreground, new RectangleF(1, 1, foreground.Width - 2, foreground.Height - 2), new Point(20, 16), wrapX, wrapY); + void DrawAction(DrawingCanvas canvas) + { + canvas.Clear(clearBrush); + canvas.Fill(brush, polygon); + } + + using Image defaultImage = new(500, 500); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + // The GPU backend must match the CPU backend for every wrap mode on both axes, and both must + // match their committed reference outputs. + DebugSaveBackendPair(provider, $"{wrapX}-{wrapY}", defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, $"{wrapX}-{wrapY}", defaultImage, nativeSurfaceImage); + } + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(TestImageProvider provider) @@ -186,10 +238,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = true }, - ShapeOptions = new ShapeOptions - { - IntersectionRule = IntersectionRule.NonZero - } + IntersectionRule = IntersectionRule.NonZero }; PathBuilder pathBuilder = new(); @@ -236,7 +285,7 @@ public void FillPath_WithNonZeroNestedContours_MatchesDefaultOutput(Test // Non-zero winding semantics must still match on an interior point. Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -282,7 +331,7 @@ public void FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = false } + }; + + Rectangle sourceBounds = new(0, 0, 42, 30); + Rectangle targetBounds = new(0, 0, 96, 72); + + static void DrawSource(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Red), new RectanglePolygon(0, 0, 42, 30)); + canvas.Fill(Brushes.Solid(Color.Yellow), new RectanglePolygon(7, 6, 18, 12)); + } + + static void DrawTargetBeforeCopy(DrawingCanvas canvas) + => canvas.Fill(Brushes.Solid(Color.Blue), new RectanglePolygon(0, 0, 96, 72)); + + static void DrawTargetAfterCopy(DrawingCanvas canvas) + => canvas.Fill(Brushes.Solid(Color.Green), new RectanglePolygon(14, 10, 8, 6)); + + void DrawDefault(DrawingCanvas targetCanvas) + { + using Image sourceImage = new(sourceBounds.Width, sourceBounds.Height); + using (DrawingCanvas sourceCanvas = sourceImage.Frames.RootFrame.CreateCanvas(Configuration.Default, drawingOptions)) + { + DrawSource(sourceCanvas); + } + + DrawTargetBeforeCopy(targetCanvas); + + using (DrawingCanvas sourceCanvas = sourceImage.Frames.RootFrame.CreateCanvas(Configuration.Default, drawingOptions)) + { + targetCanvas.CopyPixelsFrom(sourceCanvas, sourceBounds, new Point(0, 0)); + } + + DrawTargetAfterCopy(targetCanvas); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawDefault); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using WebGPURenderTarget sourceRenderTarget = new(WebGPUTextureFormat.Bgra8Unorm, sourceBounds.Width, sourceBounds.Height); + using WebGPURenderTarget targetRenderTarget = sourceRenderTarget.CreateRenderTarget(targetBounds.Width, targetBounds.Height); + Configuration nativeSurfaceConfiguration = Configuration.Default.Clone(); + nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend); + + using (DrawingCanvas sourceCanvas = WebGPUCanvasFactory.CreateCanvas( + nativeSurfaceConfiguration, + drawingOptions, + nativeSurfaceBackend, + sourceRenderTarget.Bounds, + sourceRenderTarget.Surface, + sourceRenderTarget.Surface.TargetDescriptor)) + { + DrawSource(sourceCanvas); + } + + using (DrawingCanvas targetCanvas = WebGPUCanvasFactory.CreateCanvas( + nativeSurfaceConfiguration, + drawingOptions, + nativeSurfaceBackend, + targetRenderTarget.Bounds, + targetRenderTarget.Surface, + targetRenderTarget.Surface.TargetDescriptor)) + + using (DrawingCanvas sourceCanvas = WebGPUCanvasFactory.CreateCanvas( + nativeSurfaceConfiguration, + drawingOptions, + nativeSurfaceBackend, + sourceRenderTarget.Bounds, + sourceRenderTarget.Surface, + sourceRenderTarget.Surface.TargetDescriptor)) + { + DrawTargetBeforeCopy(targetCanvas); + targetCanvas.CopyPixelsFrom(sourceCanvas, sourceBounds, new Point(0, 0)); + DrawTargetAfterCopy(targetCanvas); + } + + using Image nativeSurfaceImage = targetRenderTarget.ReadbackImage(); + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0F); + } + [WebGPUTheory] [WithBlankImage(220, 160, PixelTypes.Rgba32)] public void Process_WithWebGPUBackend_MatchesDefaultOutput(TestImageProvider provider) @@ -557,30 +695,37 @@ void DrawAction(DrawingCanvas canvas) DrawAction); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0516F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.019F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); } [WebGPUTheory] - [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] - public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + [WithSolidFilledImages(420, 160, "White", PixelTypes.Rgba32)] + public void DrawText_WithDropShadowWriteBack_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel { - Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 60); RichTextOptions textOptions = new(font) { - Origin = new PointF(8, 8), - WrappingLength = 400 - }; - - DrawingOptions drawingOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true } + Origin = new PointF(24, 30) }; - string text = new('A', 200); + DrawingOptions drawingOptions = new(); + string text = "Shadow"; Brush brush = Brushes.Solid(Color.Black); - void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, null); + + // Content draws into an effect layer; on restore the canvas slots the tinted, blurred + // silhouette beneath the untouched content at the shadow offset, then composites text plus + // shadow onto the white background. + WebGPUDropShadowLayerEffect shadow = new(new Point(10, 10), 4F, Color.Firebrick); + + Rectangle region = new(0, 0, 420, 160); + void DrawAction(DrawingCanvas canvas) + { + canvas.SaveLayer(new GraphicsOptions(), region, shadow); + canvas.DrawText(textOptions, text, brush, null); + canvas.Restore(); + } using Image defaultImage = provider.GetImage(); RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); @@ -596,135 +741,441 @@ public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvi nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); - AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); } [WebGPUTheory] - [WithBlankImage(1200, 280, PixelTypes.Rgba32)] - public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) + [WithSolidFilledImages(420, 200, "White", PixelTypes.Rgba32)] + public void DrawText_WithBlurLayerEffect_MatchesDefaultOutput(TestImageProvider provider) where TPixel : unmanaged, IPixel { Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); - RichTextOptions textOptions = new(font) - { - Origin = new PointF(8, 8), - WrappingLength = 400 - }; - - DrawingOptions drawingOptions = new() - { - GraphicsOptions = new GraphicsOptions { Antialias = true } - }; - - DrawingOptions clearOptions = new() - { - GraphicsOptions = new GraphicsOptions - { - Antialias = false, - AlphaCompositionMode = PixelAlphaCompositionMode.Src, - ColorBlendingMode = PixelColorBlendingMode.Normal, - BlendPercentage = 1F - } - }; + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Black); - const int glyphCount = 200; - string text = new('A', glyphCount); - Brush drawBrush = Brushes.Solid(Color.HotPink); - Brush clearBrush = Brushes.Solid(Color.White); + // The layer isolates the blur: the text inside the effect layer softens on restore while + // the text outside it stays sharp. void DrawAction(DrawingCanvas canvas) { - canvas.Fill(clearBrush); - canvas.Flush(); - canvas.Save(drawingOptions); - canvas.DrawText(textOptions, text, drawBrush, null); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 20) }, "Sharp", brush, null); + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(0, 90, 420, 100), new WebGPUGaussianBlurLayerEffect(4F)); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 100) }, "Blurred", brush, null); canvas.Restore(); } using Image defaultImage = provider.GetImage(); - defaultImage.Mutate(c => c.Paint(clearOptions, DrawAction)); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); using WebGPUDrawingBackend nativeSurfaceBackend = new(); - using WebGPURenderTarget renderTarget = new(defaultImage.Width, defaultImage.Height); - Configuration nativeSurfaceConfiguration = Configuration.Default.Clone(); - nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend); - - using (DrawingCanvas nativeSurfaceCanvas = WebGPUCanvasFactory.CreateCanvas( - nativeSurfaceConfiguration, - clearOptions, - nativeSurfaceBackend, - renderTarget.Bounds, - renderTarget.Surface, - renderTarget.Format)) - { - DrawAction(nativeSurfaceCanvas); - } + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); - using Image nativeSurfaceImage = renderTarget.ReadbackImage(); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 2F); - AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); } - private static void RenderWithDefaultBackend(Image image, DrawingOptions options, CanvasAction drawAction) - where TPixel : unmanaged, IPixel => image.Mutate(c => c.Paint(options, drawAction)); - - private static IPath CreateLargeSceneDenseRectangleGridPath() + [WebGPUTheory] + [WithSolidFilledImages(420, 160, "White", PixelTypes.Rgba32)] + public void DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - const int gridSize = 260; - const int pitch = 2; - const int rectangleSize = 1; + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 54); + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Black); + // A polygon region confines the effect: the text crosses the triangle boundary, so the + // glyphs inside the triangle blur on restore while the rest stay sharp. PathBuilder pathBuilder = new(); - for (int y = 0; y < gridSize; y++) + pathBuilder.AddLine(210, 10, 400, 150); + pathBuilder.AddLine(400, 150, 20, 150); + pathBuilder.AddLine(20, 150, 210, 10); + pathBuilder.CloseAllFigures(); + IPath triangle = pathBuilder.Build(); + + void DrawAction(DrawingCanvas canvas) { - int top = y * pitch; - for (int x = 0; x < gridSize; x++) - { - pathBuilder.AddRectangle(x * pitch, top, rectangleSize, rectangleSize); - } + canvas.SaveLayer(new GraphicsOptions(), triangle, new BlurLayerEffect(4F)); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 40) }, "Blurred middle", brush, null); + canvas.Restore(); } - return pathBuilder.Build(); + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); } - private static Rectangle[] CreateClipReduceLayerBounds(int layerCount, Rectangle targetBounds) + [WebGPUTheory] + [WithSolidFilledImages(420, 160, "White", PixelTypes.Rgba32)] + public void DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel { - Rectangle[] layerBounds = new Rectangle[layerCount]; - for (int i = 0; i < layerCount; i++) + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 84); + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Gold); + + // The shadow hugs the glyphs' top and left inside edges, clipped to the content itself. + void DrawAction(DrawingCanvas canvas) { - int width = 18 + ((i * 7) % 22); - int height = 16 + ((i * 11) % 24); - int x = (i * 17) % Math.Max(1, targetBounds.Width - width + 1); - int y = ((i * 23) + ((i / 8) * 7)) % Math.Max(1, targetBounds.Height - height + 1); - layerBounds[i] = new Rectangle(x, y, width, height); + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(0, 0, 420, 160), new WebGPUInnerShadowLayerEffect(new Point(4, 4), 3F, Color.Black)); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 20) }, "Inset", brush, null); + canvas.Restore(); } - return layerBounds; - } + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); - private static IPath CreateClipReduceLayerLocalPath(int layerIndex, Rectangle layerBounds) - { - int insetX = 1 + (layerIndex % 4); - int insetY = 1 + ((layerIndex / 4) % 4); - int widthTrim = 1 + ((layerIndex * 3) % 5); - int heightTrim = 1 + ((layerIndex * 5) % 5); - int innerWidth = Math.Max(4, layerBounds.Width - insetX - widthTrim); - int innerHeight = Math.Max(4, layerBounds.Height - insetY - heightTrim); + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); - return (layerIndex & 1) == 0 - ? new RectanglePolygon(insetX, insetY, innerWidth, innerHeight) - : new EllipsePolygon( - insetX + (innerWidth / 2F), - insetY + (innerHeight / 2F), - innerWidth / 2F, - innerHeight / 2F); + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); } - private static SolidBrush CreateClipReduceLayerBrush(int layerIndex) - => Brushes.Solid((layerIndex & 3) switch - { - 0 => Color.Red.WithAlpha(0.55F), + [WebGPUTheory] + [WithSolidFilledImages(420, 160, "White", PixelTypes.Rgba32)] + public void DrawText_WithGlowLayerEffect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 72); + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Black); + + // The glow spreads evenly beneath the glyphs in all directions. + void DrawAction(DrawingCanvas canvas) + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(0, 0, 420, 160), new WebGPUGlowLayerEffect(6F, Color.Red)); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 30) }, "Glow", brush, null); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); + } + + [WebGPUTheory] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "blur")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "brightness")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "contrast")] + [WithFile(TestImages.Png.Ducky, PixelTypes.Rgba32, "drop-shadow")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "grayscale")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "hue-rotate")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "invert")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "opacity")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "sepia")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "saturate")] + [WithFile(TestImages.Jpeg.Baseline.Balloon, PixelTypes.Rgba32, "acrylic")] + public void DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput(TestImageProvider provider, string filter) + where TPixel : unmanaged, IPixel + { + // The CSS cases mirror the MDN backdrop-filter examples, with percentages as fractions and + // blur radii as Gaussian sigma, on the same balloon photograph MDN uses. Drop-shadow runs + // on the duck instead: the backdrop's silhouette needs transparency to cast into, and an + // opaque photograph has none. Acrylic is the non-CSS frosted-glass material. + BackdropLayerEffect effect = filter switch + { + "blur" => new WebGPUBackdropGaussianBlurLayerEffect(2F), + "brightness" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateBrightnessFilter(0.6F)), + "contrast" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateContrastFilter(0.4F)), + "drop-shadow" => new WebGPUBackdropDropShadowLayerEffect(new Point(4, 4), 5F, Color.Black.WithAlpha(.7F)), + "grayscale" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateGrayscaleBt709Filter(0.3F)), + "hue-rotate" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateHueFilter(120F)), + "invert" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateInvertFilter(0.7F)), + "opacity" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateOpacityFilter(0.2F)), + "sepia" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateSepiaFilter(0.9F)), + "saturate" => new WebGPUBackdropColorMatrixLayerEffect(KnownFilterMatrices.CreateSaturateFilter(0.8F)), + "acrylic" => new WebGPUBackdropAcrylicLayerEffect(2F, Color.PeachPuff.WithAlpha(0.35F)), + _ => throw new ArgumentOutOfRangeException(nameof(filter)), + }; + + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Black); + + using Image defaultImage = provider.GetImage(); + + // The layout scales with the source image so the balloon and duck images share one scene: + // a caption behind the panel, a rounded-rectangle panel through the IPath overload, and the + // filter's own name as the label rendered sharp above the filtered backdrop. + int width = defaultImage.Width; + int height = defaultImage.Height; + Font captionFont = TestFontUtilities.GetFont(TestFonts.OpenSans, height / 6F); + Font labelFont = TestFontUtilities.GetFont(TestFonts.OpenSans, height / 11F); + RectangleF panelBounds = new(width / 10F, height / 5F, width * 4F / 5F, height * 11F / 20F); + RoundedRectanglePolygon panel = new(panelBounds, panelBounds.Height / 10F); + + // The backdrop effect filters whatever is already on the canvas beneath the panel region - + // the photograph and the caption glyphs the panel overlaps - then the panel's label renders + // sharp above the filtered result. + void DrawAction(DrawingCanvas canvas) + { + canvas.DrawText(new RichTextOptions(captionFont) { Origin = new PointF(width / 20F, height / 15F) }, "Backdrop", brush, null); + canvas.SaveLayer(new GraphicsOptions(), panel, effect); + canvas.DrawText(new RichTextOptions(labelFont) { Origin = new PointF(panelBounds.X + 15F, panelBounds.Y + (panelBounds.Height / 2F)) }, filter, brush, null); + canvas.Restore(); + } + + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, filter, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.03F); + + // Reference outputs are rendered on one adapter; other conforming adapters round + // within one LSB across a small fraction of pixels. + AssertBackendPairReferenceOutputs(provider, filter, defaultImage, nativeSurfaceImage, 0.0043F); + } + + [WebGPUTheory] + [WithSolidFilledImages(420, 160, "White", PixelTypes.Rgba32)] + public void DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 72); + DrawingOptions drawingOptions = new(); + Brush brush = Brushes.Solid(Color.Red); + + // The hue rotation recolours the layer's text; content outside the layer would keep its + // original colour. + void DrawAction(DrawingCanvas canvas) + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(0, 0, 420, 160), new WebGPUColorMatrixLayerEffect(KnownFilterMatrices.CreateHueFilter(180F))); + canvas.DrawText(new RichTextOptions(font) { Origin = new PointF(24, 30) }, "Recoloured", brush, null); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); + } + + [WebGPUTheory] + [WithBasicTestPatternImages(420, 220, PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_UsesCoverageCache(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + string text = new('A', 200); + Brush brush = Brushes.Solid(Color.Black); + void DrawAction(DrawingCanvas canvas) => canvas.DrawText(textOptions, text, brush, null); + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(1200, 280, PixelTypes.Rgba32)] + public void DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(8, 8), + WrappingLength = 400 + }; + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + DrawingOptions clearOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src, + ColorBlendingMode = PixelColorBlendingMode.Normal, + BlendPercentage = 1F + } + }; + + const int glyphCount = 200; + string text = new('A', glyphCount); + Brush drawBrush = Brushes.Solid(Color.HotPink); + Brush clearBrush = Brushes.Solid(Color.White); + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(clearBrush); + canvas.Flush(); + canvas.Save(drawingOptions); + canvas.DrawText(textOptions, text, drawBrush, null); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + defaultImage.Mutate(c => c.Paint(clearOptions, DrawAction)); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using WebGPURenderTarget renderTarget = new(defaultImage.Width, defaultImage.Height); + Configuration nativeSurfaceConfiguration = Configuration.Default.Clone(); + nativeSurfaceConfiguration.SetDrawingBackend(nativeSurfaceBackend); + + using (DrawingCanvas nativeSurfaceCanvas = WebGPUCanvasFactory.CreateCanvas( + nativeSurfaceConfiguration, + clearOptions, + nativeSurfaceBackend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + DrawAction(nativeSurfaceCanvas); + } + + using Image nativeSurfaceImage = renderTarget.ReadbackImage(); + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + private static void RenderWithDefaultBackend(Image image, DrawingOptions options, CanvasAction drawAction) + where TPixel : unmanaged, IPixel => image.Mutate(c => c.Paint(options, drawAction)); + + private static IPath CreateLargeSceneDenseRectangleGridPath() + { + const int gridSize = 260; + const int pitch = 2; + const int rectangleSize = 1; + + PathBuilder pathBuilder = new(); + for (int y = 0; y < gridSize; y++) + { + int top = y * pitch; + for (int x = 0; x < gridSize; x++) + { + pathBuilder.AddRectangle(x * pitch, top, rectangleSize, rectangleSize); + } + } + + return pathBuilder.Build(); + } + + private static Rectangle[] CreateClipReduceLayerBounds(int layerCount, Rectangle targetBounds) + { + Rectangle[] layerBounds = new Rectangle[layerCount]; + for (int i = 0; i < layerCount; i++) + { + int width = 18 + ((i * 7) % 22); + int height = 16 + ((i * 11) % 24); + int x = (i * 17) % Math.Max(1, targetBounds.Width - width + 1); + int y = ((i * 23) + ((i / 8) * 7)) % Math.Max(1, targetBounds.Height - height + 1); + layerBounds[i] = new Rectangle(x, y, width, height); + } + + return layerBounds; + } + + private static IPath CreateClipReduceLayerLocalPath(int layerIndex, Rectangle layerBounds) + { + int insetX = 1 + (layerIndex % 4); + int insetY = 1 + ((layerIndex / 4) % 4); + int widthTrim = 1 + ((layerIndex * 3) % 5); + int heightTrim = 1 + ((layerIndex * 5) % 5); + int innerWidth = Math.Max(4, layerBounds.Width - insetX - widthTrim); + int innerHeight = Math.Max(4, layerBounds.Height - insetY - heightTrim); + + return (layerIndex & 1) == 0 + ? new RectanglePolygon(insetX, insetY, innerWidth, innerHeight) + : new EllipsePolygon( + insetX + (innerWidth / 2F), + insetY + (innerHeight / 2F), + innerWidth / 2F, + innerHeight / 2F); + } + + private static SolidBrush CreateClipReduceLayerBrush(int layerIndex) + => Brushes.Solid((layerIndex & 3) switch + { + 0 => Color.Red.WithAlpha(0.55F), 1 => Color.CornflowerBlue.WithAlpha(0.5F), 2 => Color.LimeGreen.WithAlpha(0.45F), _ => Color.Goldenrod.WithAlpha(0.5F) @@ -786,16 +1237,15 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( if (initialImage is not null) { - using (DrawingCanvas initialCanvas = WebGPUCanvasFactory.CreateCanvas( + using DrawingCanvas initialCanvas = WebGPUCanvasFactory.CreateCanvas( configuration, new DrawingOptions(), backend, renderTarget.Bounds, renderTarget.Surface, - renderTarget.Format)) - { - initialCanvas.DrawImage(initialImage, initialImage.Bounds, targetBounds); - } + renderTarget.Surface.TargetDescriptor); + + initialCanvas.DrawImage(initialImage, initialImage.Bounds, targetBounds); } using (DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( @@ -804,7 +1254,47 @@ private static Image RenderWithNativeSurfaceWebGpuBackend( backend, renderTarget.Bounds, renderTarget.Surface, - renderTarget.Format)) + renderTarget.Surface.TargetDescriptor)) + { + drawAction(canvas); + } + + return renderTarget.ReadbackImage(); + } + + private static Image RenderWithNativeSurfaceWebGpuBackend( + int width, + int height, + WebGPUDrawingBackend backend, + WebGPUTextureFormat format, + DrawingOptions options, + Action drawAction, + Image initialImage) + where TPixel : unmanaged, IPixel + { + using WebGPURenderTarget renderTarget = new(format, width, height); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + Rectangle targetBounds = new(0, 0, width, height); + + using (DrawingCanvas initialCanvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + new DrawingOptions(), + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + initialCanvas.DrawImage(initialImage, initialImage.Bounds, targetBounds); + } + + using (DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + options, + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) { drawAction(canvas); } @@ -919,7 +1409,146 @@ public void DrawPath_Stroke_MatchesDefaultOutput(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = CreateAvaloniaOptions(IntersectionRule.EvenOdd, true); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.Black)); + DrawThumb(canvas, new PointF(642F, 335F)); + DrawThumb(canvas, new PointF(875F, 359F)); + DrawThumb(canvas, new PointF(875F, 383F)); + DrawThumb(canvas, new PointF(875F, 407F)); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarityInRegion(defaultImage, nativeSurfaceImage, new Rectangle(638, 331, 265, 104), 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + + static void DrawThumb(DrawingCanvas canvas, PointF origin) + { + const float thumbSize = 24F; + const float thumbBorderThickness = 5F; + + RectanglePolygon thumbClip = new(origin.X, origin.Y, thumbSize, thumbSize); + _ = canvas.Save(CreateAvaloniaOptions(IntersectionRule.EvenOdd, false)); + canvas.Clip(thumbClip); + + RectangleF thumbBorderRectangle = new(origin.X + 2.5F, origin.Y + 2.5F, 19F, 19F); + RoundedRectanglePolygon thumbBorder = new(thumbBorderRectangle, 9.5F); + EllipsePolygon thumbEllipse = new(new PointF(origin.X + 12F, origin.Y + 12F), new SizeF(thumbSize, thumbSize)); + + DrawAvaloniaFill(canvas, Brushes.Solid(Color.Transparent), thumbBorder); + DrawAvaloniaStroke(canvas, Pens.Solid(Color.White, thumbBorderThickness), thumbBorder); + DrawAvaloniaFill(canvas, Brushes.Solid(Color.Transparent), thumbEllipse); + DrawAvaloniaStroke(canvas, Pens.Solid(Color.White, 1F), thumbEllipse); + + canvas.Restore(); + } + + static void DrawAvaloniaFill(DrawingCanvas canvas, Brush brush, IPath path) + { + _ = canvas.Save(CreateAvaloniaOptions(IntersectionRule.EvenOdd, true)); + canvas.Fill(brush, path); + canvas.Restore(); + } + + static void DrawAvaloniaStroke(DrawingCanvas canvas, Pen pen, IPath path) + { + _ = canvas.Save(CreateAvaloniaOptions(IntersectionRule.EvenOdd, true)); + canvas.Draw(pen, path); + canvas.Restore(); + } + + static DrawingOptions CreateAvaloniaOptions(IntersectionRule fillRule, bool antialias) + => new() + { + GraphicsOptions = new GraphicsOptions { Antialias = antialias }, + IntersectionRule = fillRule + }; + } + + [WebGPUTheory] + [WithSolidFilledImages(300, 220, "Gray", PixelTypes.Rgba32)] + public void BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true }, + IntersectionRule = IntersectionRule.EvenOdd + }; + + const float sigma = 4F; + + void DrawThumbWithShadow(DrawingCanvas canvas, PointF center) + { + EllipsePolygon contentPath = new(center, new SizeF(24F, 24F)); + EllipsePolygon shadowPath = new(new PointF(center.X, center.Y + 3F), new SizeF(24F, 24F)); + Rectangle layerBounds = new( + (int)(center.X - 12F - 16F), + (int)(center.Y - 12F - 16F), + 24 + 32, + 24 + 32 + 6); + + // Blurred outer box shadow: difference clip -> layer -> fill -> gaussian blur -> composite. + _ = canvas.Save(drawingOptions); + canvas.Clip(ClipOperation.Difference, contentPath); + _ = canvas.SaveLayer(new GraphicsOptions(), layerBounds); + canvas.Fill(Brushes.Solid(Color.Black.WithAlpha(0.55F)), shadowPath); + canvas.Apply(layerBounds, ctx => ctx.GaussianBlur(sigma)); + canvas.Restore(); + canvas.Restore(); + + canvas.Fill(Brushes.Solid(Color.White), contentPath); + canvas.Fill(Brushes.Solid(Color.Red), new EllipsePolygon(center, new SizeF(8F, 8F))); + } + + void DrawAction(DrawingCanvas canvas) + { + DrawThumbWithShadow(canvas, new PointF(70F, 60F)); + DrawThumbWithShadow(canvas, new PointF(150F, 60F)); + DrawThumbWithShadow(canvas, new PointF(230F, 60F)); + DrawThumbWithShadow(canvas, new PointF(110F, 150F)); + DrawThumbWithShadow(canvas, new PointF(190F, 150F)); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -961,7 +1590,7 @@ public void DrawPath_PointStroke_LineCap_MatchesDefaultOutput(TestImageP float referenceTolerance = lineCap == LineCap.Square ? 0.0016F : 0.0003F; DebugSaveBackendPair(provider, $"{lineCap}", defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.03F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, $"{lineCap}", defaultImage, nativeSurfaceImage, referenceTolerance); } @@ -1030,7 +1659,7 @@ public void DrawPath_Stroke_LineJoin_MatchesDefaultOutput(TestImageProvi defaultComparisonImage, nativeSurfaceComparisonImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.015F); AssertBackendPairReferenceOutputs( provider, $"{lineJoin}", @@ -1099,7 +1728,7 @@ public void DrawPath_Stroke_LineCap_MatchesDefaultOutput(TestImageProvid defaultComparisonImage, nativeSurfaceComparisonImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0103F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs( provider, $"{lineCap}", @@ -1163,10 +1792,7 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = true }, - ShapeOptions = new ShapeOptions - { - IntersectionRule = IntersectionRule.EvenOdd - } + IntersectionRule = IntersectionRule.EvenOdd }; PathBuilder pathBuilder = new(); @@ -1212,7 +1838,7 @@ public void FillPath_EvenOddRule_MatchesDefaultOutput(TestImageProvider< // EvenOdd with same winding inner contour should create a hole at center. Assert.Equal(defaultImage[128, 128], nativeSurfaceImage[128, 128]); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.5F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1245,7 +1871,7 @@ public void FillPath_LargeTileCount_MatchesDefaultOutput(TestImageProvid nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1277,10 +1903,259 @@ public void FillPath_LargeScene_UsesLargePathScan_AndMatchesDefaultOutput context.Invert()); + + if (offsetReadIntersectsEarlierWrite) + { + // The second write is disjoint, but its non-zero write-back offset moves the source + // read onto the first write. The planner must therefore preserve a separate barrier. + canvas.Apply( + new Rectangle(8, 8, 20, 20), + context => context.Invert(), + new GraphicsOptions(), + new Point(42, 0)); + } + else + { + canvas.Apply(new Rectangle(50, 8, 20, 20), context => context.Invert()); + } + + using DrawingBackendScene scene = canvas.CreateScene(); + WebGPUDrawingBackendScene webGPUScene = Assert.IsType(scene); + ReadOnlySpan operations = webGPUScene.EncodedScene.OrderedOperations; + int firstApplyIndex = 0; + + while (operations[firstApplyIndex].Kind != WebGPUSceneOperationKind.Apply) + { + firstApplyIndex++; + } + + Assert.Equal(expectedFirstGroupCount, operations[firstApplyIndex].ApplyGroupCount); + Assert.Equal(0, operations[firstApplyIndex].ApplyIndex); + Assert.True(operations[firstApplyIndex].PendingStatusCapacity > 0); + + WebGPUSceneOperation secondApply = operations[firstApplyIndex + 1]; + Assert.Equal(WebGPUSceneOperationKind.Apply, secondApply.Kind); + Assert.Equal(1, secondApply.ApplyIndex); + Assert.Equal(offsetReadIntersectsEarlierWrite ? 1 : 0, secondApply.ApplyGroupCount); + } + + [WebGPUTheory] + [WithBlankImage(128, 96, PixelTypes.Rgba32)] + public void ConsecutiveIndependentApplies_ShareReadbackAndMatchDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(8, 12, 40, 36)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(76, 44, 36, 40)); + + // These disjoint source/write rectangles are encoded as one two-image barrier while + // their processors and draw ranges remain in their original order. + canvas.Apply(new Rectangle(8, 12, 40, 36), context => context.Invert()); + canvas.Apply(new Rectangle(76, 44, 36, 40), context => context.Invert()); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image webGPUImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + backend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, webGPUImage); + AssertBackendPairSimilarity(defaultImage, webGPUImage, 0.005F); + } + + [WebGPUTheory] + [WithBlankImage(96, 64, PixelTypes.Rgba32)] + public void ConsecutiveClippedApplies_ShareReadbackAndMatchDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(0, 8, 16, 24)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(80, 8, 16, 24)); + + // Both reads extend beyond opposite target edges. Their packed rows retain the + // destination offsets needed to reconstruct the full processor images. + canvas.Apply(new Rectangle(-8, 8, 24, 24), context => context.Invert()); + canvas.Apply(new Rectangle(80, 8, 24, 24), context => context.Invert()); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image webGPUImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + backend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, webGPUImage); + AssertBackendPairSimilarity(defaultImage, webGPUImage, 0.005F); + } + + [WebGPUFact] + public void OrderedPlan_SplitsApplyGroupsAtRenderAndLayerBoundaries() + { + using WebGPUDrawingBackend backend = new(); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using WebGPURenderTarget renderTarget = new(96, 64); + using DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + new DrawingOptions(), + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor); + + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Apply(new Rectangle(4, 4, 16, 16), context => context.Invert()); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(28, 4, 16, 16)); + canvas.Apply(new Rectangle(52, 4, 16, 16), context => context.Invert()); + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(4, 28, 40, 28)); + canvas.Apply(new Rectangle(4, 28, 16, 16), context => context.Invert()); + canvas.Restore(); + + using DrawingBackendScene scene = canvas.CreateScene(); + WebGPUDrawingBackendScene webGPUScene = Assert.IsType(scene); + ReadOnlySpan operations = webGPUScene.EncodedScene.OrderedOperations; + int applyCount = 0; + bool sawBeginLayer = false; + bool sawEndLayer = false; + + for (int i = 0; i < operations.Length; i++) + { + WebGPUSceneOperation operation = operations[i]; + sawBeginLayer |= operation.Kind == WebGPUSceneOperationKind.BeginLayer; + sawEndLayer |= operation.Kind == WebGPUSceneOperationKind.EndLayer; + + if (operation.Kind == WebGPUSceneOperationKind.Apply) + { + Assert.Equal(1, operation.ApplyGroupCount); + applyCount++; + } + } + + Assert.Equal(3, applyCount); + Assert.True(sawBeginLayer); + Assert.True(sawEndLayer); + } + + [WebGPUTheory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void ConsecutiveIndependentApplies_ProcessorExceptionCommitsEarlierResults(int throwingApplyIndex) + { + using WebGPUDrawingBackend backend = new(); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + InvalidOperationException expectedException = new("Expected processor failure."); + + using WebGPURenderTarget renderTarget = new(128, 64); + DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + drawingOptions, + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor); + + canvas.Fill(Brushes.Solid(Color.White)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(8, 8, 24, 24)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(56, 8, 24, 24)); + canvas.Fill(Brushes.Solid(Color.Lime), new Rectangle(96, 8, 24, 24)); + + Rectangle[] applyRectangles = + [ + new Rectangle(8, 8, 24, 24), + new Rectangle(56, 8, 24, 24), + new Rectangle(96, 8, 24, 24) + ]; + + for (int i = 0; i < applyRectangles.Length; i++) + { + int applyIndex = i; + canvas.Apply( + applyRectangles[i], + context => + { + if (applyIndex == throwingApplyIndex) + { + throw expectedException; + } + + context.Invert(); + }); + } + + InvalidOperationException exception = Assert.Throws(canvas.Dispose); + Assert.Same(expectedException, exception); + + using Image image = renderTarget.ReadbackImage(); + Rgba32[] originalColors = + [ + Color.Red.ToPixel(), + Color.Blue.ToPixel(), + Color.Lime.ToPixel() + ]; + Rgba32[] invertedColors = + [ + Color.Cyan.ToPixel(), + Color.Yellow.ToPixel(), + Color.Magenta.ToPixel() + ]; + + for (int i = 0; i < applyRectangles.Length; i++) + { + Rgba32 expectedColor = i < throwingApplyIndex ? invertedColors[i] : originalColors[i]; + Assert.Equal(expectedColor, image[applyRectangles[i].X + 8, applyRectangles[i].Y + 8]); + } + + Assert.Equal(Color.White.ToPixel(), image[44, 44]); + } + [WebGPUTheory] [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] public void SaveLayer_ManyLayers_UsesClipReduce_AndMatchesDefaultOutput(TestImageProvider provider) @@ -1317,8 +2192,11 @@ void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); - AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0006F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + + // Reference outputs are rendered on one adapter; other conforming adapters round + // within one LSB across a small fraction of pixels. + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0045F); } [WebGPUTheory] @@ -1357,14 +2235,14 @@ void DrawAction(DrawingCanvas canvas) nativeSurfaceBackend, renderTarget.Bounds, renderTarget.Surface, - renderTarget.Format)) + renderTarget.Surface.TargetDescriptor)) { DrawAction(canvas); } using Image nativeSurfaceImage = renderTarget.ReadbackImage(); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1431,7 +2309,7 @@ static void DrawRetainedFlow(DrawingCanvas canvas, DrawingBackendScene retainedS nativeSurfaceBackend, sceneRenderTarget.Bounds, sceneRenderTarget.Surface, - sceneRenderTarget.Format); + sceneRenderTarget.Surface.TargetDescriptor); // Create the scene through the WebGPU backend so the test covers retained encoding and replay. DrawRetainedScene(nativeSceneCanvas); @@ -1444,14 +2322,14 @@ static void DrawRetainedFlow(DrawingCanvas canvas, DrawingBackendScene retainedS nativeSurfaceBackend, renderTarget.Bounds, renderTarget.Surface, - renderTarget.Format)) + renderTarget.Surface.TargetDescriptor)) { DrawRetainedFlow(nativeCanvas, nativeScene); } using Image nativeSurfaceImage = renderTarget.ReadbackImage(); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1513,7 +2391,7 @@ static void DrawRetainedFlow(DrawingCanvas canvas, DrawingBackendScene retainedS nativeSurfaceBackend, sceneRenderTarget.Bounds, sceneRenderTarget.Surface, - sceneRenderTarget.Format); + sceneRenderTarget.Surface.TargetDescriptor); // Create the scene through the WebGPU backend so retained layer commands are encoded. DrawRetainedScene(nativeSceneCanvas); @@ -1526,14 +2404,14 @@ static void DrawRetainedFlow(DrawingCanvas canvas, DrawingBackendScene retainedS nativeSurfaceBackend, renderTarget.Bounds, renderTarget.Surface, - renderTarget.Format)) + renderTarget.Surface.TargetDescriptor)) { DrawRetainedFlow(nativeCanvas, nativeScene); } using Image nativeSurfaceImage = renderTarget.ReadbackImage(); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1573,7 +2451,7 @@ public void FillPath_WithLinearGradientBrush_MatchesDefaultOutput(TestIm // MacOS on CI has some outliers with this test, so using a slightly higher tolerance here to avoid noise. DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.03F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.045F); AssertBackendPairReferenceOutputs( provider, null, @@ -1616,10 +2494,119 @@ public void FillPath_WithLinearGradientBrush_Repeat_MatchesDefaultOutput nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.02F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } + [WebGPUFact] + public void FillPath_WithZeroLengthLinearGradient_MatchesDefaultEndColor() + { + Rgba32 background = new(17, 43, 89, 255); + PointF endpoint = new(8, 8); + Brush brush = new LinearGradientBrush( + endpoint, + endpoint, + GradientRepetitionMode.None, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 16, 16)); + + using Image defaultImage = new(16, 16, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(16, 16, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(16, 16, backend, drawingOptions, DrawAction, initialImage); + + // A zero-length linear axis is defined at t=1, so the end stop fills every pixel. + Rgba32 expected = Color.Blue.ToPixel(); + Assert.Equal(expected, defaultImage[0, 0]); + Assert.Equal(defaultImage[0, 0], actual[0, 0]); + } + + [WebGPUFact] + public void FillPath_WithDontFillGradients_ComposesTransparentOutsideGradient() + { + Rgba32 background = new(17, 43, 89, 255); + (Brush Brush, Point Sample)[] cases = + [ + ( + new LinearGradientBrush( + new PointF(4, 8), + new PointF(12, 8), + GradientRepetitionMode.DontFill, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)), + new Point(0, 8)), + ( + new RadialGradientBrush( + new PointF(8, 8), + 3F, + GradientRepetitionMode.DontFill, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)), + new Point(0, 0)), + ( + new EllipticGradientBrush( + new PointF(8, 8), + new PointF(12, 8), + 0.5F, + GradientRepetitionMode.DontFill, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)), + new Point(0, 0)), + ( + new SweepGradientBrush( + new PointF(8.5F, 8.5F), + 0F, + 90F, + GradientRepetitionMode.DontFill, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)), + new Point(8, 15)) + ]; + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + static void DrawAction(DrawingCanvas canvas, Brush brush) => canvas.Fill(brush, new RectanglePolygon(0, 0, 16, 16)); + + using WebGPUDrawingBackend backend = new(); + foreach ((Brush brush, Point sample) in cases) + { + using Image defaultImage = new(16, 16, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, canvas => DrawAction(canvas, brush)); + + using Image initialImage = new(16, 16, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend( + 16, + 16, + backend, + drawingOptions, + canvas => DrawAction(canvas, brush), + initialImage); + + // DontFill returns a transparent brush sample outside the gradient. Src still + // composes that sample, replacing a fully covered backdrop with transparency. + Assert.Equal(default, defaultImage[sample.X, sample.Y]); + Assert.Equal(defaultImage[sample.X, sample.Y], actual[sample.X, sample.Y]); + } + } + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput(TestImageProvider provider) @@ -1654,7 +2641,7 @@ public void FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 32)); + + using Image defaultImage = new(32, 32, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(32, 32, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(32, 32, backend, drawingOptions, DrawAction, initialImage); + + // Radius1 == 0 makes the conical evaluator swap its circles. The restored parameter + // remains outside the finite cone, so DontFill supplies a transparent Src sample. + Assert.Equal(default, defaultImage[0, 0]); + Assert.Equal(defaultImage[0, 0], actual[0, 0]); + Assert.NotEqual(background, actual[17, 16]); + } + + [WebGPUTheory] + [InlineData(GradientRepetitionMode.Repeat)] + [InlineData(GradientRepetitionMode.Reflect)] + public void FillPath_WithSwappedRadialRepetition_MatchesDefaultOutput(GradientRepetitionMode repetitionMode) + { + Rgba32 background = new(17, 43, 89, 255); + Brush brush = new RadialGradientBrush( + new PointF(16, 16), + 10F, + new PointF(18, 16), + 0F, + repetitionMode, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Lime)); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 32)); + + using Image defaultImage = new(32, 32, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(32, 32, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(32, 32, backend, drawingOptions, DrawAction, initialImage); + + // The sampled point lies before the original start circle after the shader's + // canonical circle swap. Repetition must therefore see the restored negative t. + Rgba32 expected = Color.Red.ToPixel(); + Assert.Equal(expected, defaultImage[0, 0]); + Assert.Equal(defaultImage[0, 0], actual[0, 0]); + } + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(TestImageProvider provider) @@ -1733,10 +2796,72 @@ public void FillPath_WithEllipticGradientBrush_MatchesDefaultOutput(Test nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.035F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.014F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } + [WebGPUTheory] + [InlineData(GradientRepetitionMode.None, true)] + [InlineData(GradientRepetitionMode.Repeat, true)] + [InlineData(GradientRepetitionMode.Reflect, true)] + [InlineData(GradientRepetitionMode.DontFill, true)] + [InlineData(GradientRepetitionMode.None, false)] + [InlineData(GradientRepetitionMode.Repeat, false)] + [InlineData(GradientRepetitionMode.Reflect, false)] + [InlineData(GradientRepetitionMode.DontFill, false)] + public void FillPath_WithDegenerateEllipticGradient_MatchesDefaultOutput( + GradientRepetitionMode repetitionMode, + bool zeroReferenceAxis) + { + Rgba32 background = new(17, 43, 89, 255); + PointF center = new(3.5F, 3.5F); + PointF referenceAxisEnd = zeroReferenceAxis ? center : new PointF(5.5F, 5.5F); + float axisRatio = zeroReferenceAxis ? 1F : 0F; + Brush brush = new EllipticGradientBrush( + center, + referenceAxisEnd, + axisRatio, + repetitionMode, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Lime)); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 8, 8)); + + using Image defaultImage = new(8, 8, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(8, 8, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, DrawAction, initialImage); + + Rgba32 transparent = default; + Rgba32 lastStop = Color.Lime.ToPixel(); + bool fillsOutsideCollapsedAxes = repetitionMode != GradientRepetitionMode.DontFill; + + for (int y = 0; y < 8; y++) + { + for (int x = 0; x < 8; x++) + { + // The CPU equations produce NaN where a zero-radius division has a zero + // numerator. With a point ellipse that is either local axis; with a line + // ellipse it is the collapsed secondary axis, which is x == y here. + bool isUndefined = zeroReferenceAxis ? x == 3 || y == 3 : x == y; + Rgba32 expected = !isUndefined && fillsOutsideCollapsedAxes ? lastStop : transparent; + + Assert.Equal(expected, defaultImage[x, y]); + Assert.Equal(defaultImage[x, y], actual[x, y]); + } + } + } + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestImageProvider provider) @@ -1774,7 +2899,7 @@ public void FillPath_WithSweepGradientBrush_MatchesDefaultOutput(TestIma nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.0304F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.061F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -1818,7 +2943,7 @@ public void FillPath_WithSweepGradientBrush_PartialArc_MatchesDefaultOutput canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 32)); + + using Image defaultImage = new(32, 32, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(32, 32, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(32, 32, backend, drawingOptions, DrawAction, initialImage); + + // A quarter-turn direction lies outside this real but extremely small sweep. + // Comparing the epsilon in turns would misclassify the interval as a full circle. + Assert.Equal(background, defaultImage[16, 8]); + Assert.Equal(defaultImage[16, 8], actual[16, 8]); + Assert.NotEqual(background, actual[16, 16]); + } + + [WebGPUFact] + public void FillPath_WithSweepDontFillAtCenter_MatchesDefaultFirstColor() + { + Rgba32 background = new(17, 43, 89, 255); + Brush brush = new SweepGradientBrush( + new PointF(8.5F, 8.5F), + 90F, + 180F, + GradientRepetitionMode.DontFill, + new ColorStop(0, Color.Red), + new ColorStop(1, Color.Blue)); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 16, 16)); + + using Image defaultImage = new(16, 16, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(16, 16, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(16, 16, backend, drawingOptions, DrawAction, initialImage); + + // The center has no angle and is defined as t=0 rather than being derived + // from the configured angular interval. + Rgba32 expected = Color.Red.ToPixel(); + Assert.Equal(expected, defaultImage[8, 8]); + Assert.Equal(defaultImage[8, 8], actual[8, 8]); + } + [WebGPUTheory] [WithBasicTestPatternImages(384, 256, PixelTypes.Rgba32)] public void FillPath_WithPathGradientBrush_MatchesDefaultOutput(TestImageProvider provider) @@ -1850,21 +3046,265 @@ public void FillPath_WithPathGradientBrush_MatchesDefaultOutput(TestImag new PointF(10, 82) ], [ - Color.Red, - Color.Gold, - Color.LimeGreen, - Color.DeepSkyBlue, - Color.BlueViolet - ], - Color.White); + Color.Red, + Color.Gold, + Color.LimeGreen, + Color.DeepSkyBlue, + Color.BlueViolet + ], + Color.White); + + void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(persistedBrush, persistedShape); + canvas.Flush(); + + using DrawingCanvas regionCanvas = canvas.CreateRegion(region); + regionCanvas.Fill(brush, localPolygon); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = provider.GetImage(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.013F); + } + + [WebGPUFact] + public void FillPath_WithTriangularPathGradientOnEdgeExtension_MatchesDefaultOutput() + { + Rgba32 background = new(17, 43, 89, 255); + Brush brush = new PathGradientBrush( + [ + new PointF(0.5F, 0.5F), + new PointF(2.5F, 0.5F), + new PointF(0.5F, 2.5F) + ], + [ + Color.Red, + Color.Lime, + Color.Blue + ]); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 4, 3)); + + using Image defaultImage = new(4, 3, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(4, 3, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(4, 3, backend, drawingOptions, DrawAction, initialImage); + + // The CPU sign-product test accepts this extension of the triangle's horizontal edge. + // Preserve that contract exactly instead of substituting a conventional inside test. + Rgba32 expected = Color.Lime.ToPixel(); + Assert.Equal(expected, defaultImage[3, 0]); + Assert.Equal(defaultImage[3, 0], actual[3, 0]); + } + + [WebGPUFact] + public void FillPath_WithNearParallelPathGradientEdge_MatchesDefaultNoIntersection() + { + Rgba32 background = new(17, 43, 89, 255); + Brush brush = new PathGradientBrush( + [ + new PointF(12.5F, 8.4999875F), + new PointF(14.5F, 8.5000125F), + new PointF(-16F, 8.5F), + new PointF(4F, 8.5F), + new PointF(4F, 8.5F), + new PointF(5F, 8.5F) + ], + [ + Color.Lime, + Color.Lime, + Color.Lime, + Color.Lime, + Color.Lime, + Color.Lime + ], + Color.Lime); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 16)); + + using Image defaultImage = new(32, 16, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(32, 16, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(32, 16, backend, drawingOptions, DrawAction, initialImage); + + // The ray is 20 pixels long and the first edge rises by 0.000025 pixels, so their + // cross product is about 0.0005. The CPU rejects that value inside its +/-0.001 + // parallel window; the former shader window of +/-0.000001 incorrectly accepted it. + Rgba32 expected = Color.Transparent.ToPixel(); + Assert.Equal(expected, defaultImage[8, 8]); + Assert.Equal(defaultImage[8, 8], actual[8, 8]); + } + + [WebGPUFact] + public void FillPath_WithPathGradientIntersectionJustBehindSample_MatchesDefaultOutput() + { + Rgba32 background = new(17, 43, 89, 255); + Brush brush = new PathGradientBrush( + [ + new PointF(1F, 0.5001F), + new PointF(7F, 0.5001F), + new PointF(7F, 7F), + new PointF(1F, 7F) + ], + [ + Color.Lime, + Color.Lime, + Color.Lime, + Color.Lime + ], + Color.Lime); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 8, 8)); + + using Image defaultImage = new(8, 8, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(8, 8, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, DrawAction, initialImage); + + // The sample center is 0.0001 pixels above the first edge. Its ray intersection + // parameter is about -0.000023, inside the CPU's strict (-0.001, 1.001) window. + Rgba32 expected = Color.Lime.ToPixel(); + Assert.Equal(expected, defaultImage[4, 0]); + Assert.Equal(defaultImage[4, 0], actual[4, 0]); + } + + [WebGPUFact] + public void FillPath_WithNegativePathGradientEdgeParameter_UsesDistanceMagnitude() + { + Rgba32 background = new(17, 43, 89, 255); + Color halfRed = Color.FromScaledVector(new Vector4(0.5F, 0F, 0F, 1F)); + Brush brush = new PathGradientBrush( + [ + new PointF(10F, 10F), + new PointF(20F, 10F), + new PointF(-4.5F, -10F), + new PointF(2F, 27F), + new PointF(3.5F, -2F), + new PointF(11.045F, 7F) + ], + [ + halfRed, + Color.Red, + halfRed, + halfRed, + halfRed, + halfRed + ], + halfRed); + + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + AlphaCompositionMode = PixelAlphaCompositionMode.Src + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 32)); + + using Image defaultImage = new(32, 32, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(32, 32, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(32, 32, backend, drawingOptions, DrawAction, initialImage); + + // These vertices place the first-edge intersection at u=-0.00075, inside the CPU + // endpoint tolerance. CPU edge interpolation measures distance and therefore uses + // |u|; signed extrapolation lands on the opposite side of the Rgba32 rounding boundary. + Rgba32 expected = new(128, 0, 0, 255); + Assert.Equal(expected, defaultImage[8, 8]); + Assert.Equal(defaultImage[8, 8], actual[8, 8]); + } + + [WebGPUTheory] + [WithSolidFilledImages(320, 200, "White", PixelTypes.Rgba32)] + public void FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions { Antialias = true } + }; + + Brush radialBrush = new RadialGradientBrush( + new PointF(82, 100), + 68F, + GradientRepetitionMode.None, + new ColorStop(0F, Color.Orange.WithAlpha(0.95F)), + new ColorStop(1F, Color.MediumVioletRed.WithAlpha(0.25F))); + + PointF[] pathGradientPoints = + [ + new PointF(164, 20), + new PointF(306, 20), + new PointF(306, 180), + new PointF(164, 180) + ]; + Brush pathGradientBrush = new PathGradientBrush( + pathGradientPoints, + [ + Color.CornflowerBlue.WithAlpha(0.9F), + Color.Gold.WithAlpha(0.2F), + Color.LimeGreen.WithAlpha(0.75F), + Color.BlueViolet.WithAlpha(0.35F) + ], + Color.DeepPink.WithAlpha(0.6F)); + + // Unequal RGB and alpha values make straight-alpha interpolation visibly diverge from + // CSS Color 4 associated-alpha interpolation. Axis-aligned bounds remove curved-edge + // coverage noise while the two brushes cover the ramp and direct shader interpolation paths. void DrawAction(DrawingCanvas canvas) { - canvas.Fill(persistedBrush, persistedShape); - canvas.Flush(); - - using DrawingCanvas regionCanvas = canvas.CreateRegion(region); - regionCanvas.Fill(brush, localPolygon); + canvas.Fill(radialBrush, new RectanglePolygon(14, 20, 136, 160)); + canvas.Fill(pathGradientBrush, new RectanglePolygon(164, 20, 142, 160)); } using Image defaultImage = provider.GetImage(); @@ -1881,7 +3321,63 @@ void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.01F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.032F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUFact] + public void FillPath_WithSubEpsilonAlpha_RgbaHalfTargetPreservesColorAndAlpha() + { + const float sourceAlpha = 0.047F; + const float smallestPackedBlendPercentage = 1F / 65535F; + + Color color = Color.FromScaledVector(new Vector4(0.8F, 0.4F, 0.2F, sourceAlpha)); + PointF[] points = + [ + new PointF(0, 0), + new PointF(32, 0), + new PointF(32, 32), + new PointF(0, 32) + ]; + + Brush brush = new PathGradientBrush(points, [color, color, color, color], color); + DrawingOptions drawingOptions = new() + { + GraphicsOptions = new GraphicsOptions + { + Antialias = false, + BlendPercentage = smallestPackedBlendPercentage + } + }; + + void DrawAction(DrawingCanvas canvas) => canvas.Fill(brush, new RectanglePolygon(0, 0, 32, 32)); + + using Image defaultImage = new(32, 32); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceInitialImage = new(32, 32); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + WebGPUTextureFormat.Rgba16Float, + drawingOptions, + DrawAction, + nativeSurfaceInitialImage); + + RgbaHalf expected = RgbaHalf.FromScaledVector4(new Vector4(0.8F, 0.4F, 0.2F, sourceAlpha * smallestPackedBlendPercentage)); + + RgbaHalf defaultPixel = defaultImage[16, 16]; + RgbaHalf nativeSurfacePixel = nativeSurfaceImage[16, 16]; + + // The smallest nonzero packed blend value reduces alpha to a binary16 subnormal. The + // source alpha keeps that result away from a binary16 rounding boundary because WGSL + // permits conversion to round in either direction. Both backends must therefore produce + // the same exact pixel while preserving the nonzero alpha. + Assert.Equal(expected, defaultPixel); + Assert.Equal(expected, nativeSurfacePixel); + Assert.NotEqual((Half)0F, nativeSurfacePixel.A); } [WebGPUTheory] @@ -1913,7 +3409,7 @@ public void FillPath_WithPatternBrush_MatchesDefaultOutput(TestImageProv nativeSurfaceInitialImage); DebugSaveBackendPair(provider, "Horizontal", defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.045F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, "Horizontal", defaultImage, nativeSurfaceImage); } @@ -1983,6 +3479,97 @@ public void FillPath_WithRecolorBrush_MatchesDefaultOutput(TestImageProv AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } + [WebGPUFact] + public void FillPath_WithRecolorBrush_PreservesFullPrecisionSourceKey() + { + Rgba32 background = new(128, 0, 0, 255); + Color source = Color.FromScaledVector(new Vector4(128F / 255F, 0F, 0F, 1F)); + + // The exact Rgba32 component differs from its nearest binary16 value by about 7.7e-6. + // This threshold accepts the exact f32 key but rejects that prematurely rounded key. + Brush brush = new RecolorBrush(source, Color.Blue, 1e-12F); + DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + using Image defaultImage = new(8, 8, background); + RenderWithDefaultBackend(defaultImage, drawingOptions, canvas => canvas.Fill(brush)); + + using WebGPUDrawingBackend backend = new(); + using Image initialImage = new(8, 8, background); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, canvas => canvas.Fill(brush), initialImage); + + Assert.Equal(Color.Blue.ToPixel(), defaultImage[4, 4]); + Assert.Equal(defaultImage[4, 4], actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_ObservesPriorDrawInTargetStorage() + { + Color firstColor = Color.FromScaledVector(new Vector4(0.5F, 0F, 0F, 1F)); + Color source = Color.FromPixel(new Rgba32(128, 0, 0, 255)); + + // Rgba32 stores 0.5 as 128 / 255. The narrow threshold distinguishes that stored value + // from the unquantized 0.5 retained between commands by the staged GPU pipeline. + Brush brush = new RecolorBrush(source, Color.Blue, 1e-8F); + DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(firstColor)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using WebGPUDrawingBackend backend = new(); + using Image actual = RenderWithNativeSurfaceWebGpuBackend(8, 8, backend, drawingOptions, Draw); + + Assert.Equal(Color.Blue.ToPixel(), defaultImage[4, 4]); + Assert.Equal(defaultImage[4, 4], actual[4, 4]); + } + + [WebGPUFact] + public void FillPath_WithRecolorBrush_AssociatedTargetObservesStoredComponents() + { + Color firstColor = Color.FromScaledVector(new Vector4(1F, 0F, 0F, 0.5F)); + Color source = Color.FromScaledVector(new Vector4(1F, 0F, 0F, 128F / 255F)); + + // The source becomes (128 / 255, 0, 0, 128 / 255) in associated space. The first + // fill remains (0.5, 0, 0, 0.5) until its match-only Rgba32P storage round-trip. + Brush brush = new RecolorBrush(source, Color.Blue, 1e-8F); + DrawingOptions drawingOptions = new() { GraphicsOptions = new GraphicsOptions { Antialias = false } }; + + void Draw(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(firstColor)); + canvas.Fill(brush); + } + + using Image defaultImage = new(8, 8); + RenderWithDefaultBackend(defaultImage, drawingOptions, Draw); + + using WebGPUDrawingBackend backend = new(); + using WebGPURenderTarget renderTarget = new(WebGPUTextureFormat.Rgba8Unorm, PixelAlphaRepresentation.Associated, 8, 8); + Configuration configuration = Configuration.Default.Clone(); + configuration.SetDrawingBackend(backend); + + using (DrawingCanvas canvas = WebGPUCanvasFactory.CreateCanvas( + configuration, + drawingOptions, + backend, + renderTarget.Bounds, + renderTarget.Surface, + renderTarget.Surface.TargetDescriptor)) + { + Draw(canvas); + } + + using Image actual = renderTarget.ReadbackImage(); + + Assert.Equal(Color.Blue.ToPixel(), defaultImage[4, 4]); + Assert.Equal(defaultImage[4, 4], actual[4, 4]); + } + [WebGPUTheory] [WithSolidFilledImages(256, 256, "White", PixelTypes.Rgba32)] public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput(TestImageProvider provider) @@ -2018,7 +3605,7 @@ public void FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput initialImage = new(32, 32); + using Image actual = RenderWithNativeSurfaceWebGpuBackend( + 32, + 32, + backend, + WebGPUTextureFormat.Rgba16Float, + drawingOptions, + DrawAction, + initialImage); + + // The untouched part of an isolated layer must remain binary16 transparent black. + Assert.Equal(Vector4.Zero, actual[0, 0].ToScaledVector4()); + Assert.True(actual[16, 16].ToScaledVector4().W > 0F); + } + [WebGPUTheory] [WithSolidFilledImages(128, 128, "White", PixelTypes.Rgba32)] public void SaveLayer_HalfOpacity_MatchesDefaultOutput(TestImageProvider provider) @@ -2358,7 +3974,7 @@ void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0767F); } @@ -2399,7 +4015,7 @@ static void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -2440,7 +4056,7 @@ static void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -2475,7 +4091,187 @@ static void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + IPath clipPath = new EllipsePolygon(new PointF(30, 40), new SizeF(70, 60)); + + canvas.Save(); + canvas.Clip(clipPath); + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(-16, 8, 88, 80)); + canvas.Fill(Brushes.Solid(Color.Black), new Rectangle(0, 20, 56, 42)); + canvas.Apply(new Rectangle(-16, 8, 88, 80), x => x.GaussianBlur(6F)); + canvas.Restore(); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.007F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Expected output: an inverted cyan layer rectangle with an inverted yellow center square. + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(42, 42, 24, 24)); + canvas.Apply(new Rectangle(20, 20, 80, 80), x => x.Invert()); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Expected output: only the bounded layer area is inverted; the oversized fill and Apply rects do not affect the white background. + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(30, 30, 50, 50)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(0, 0, 120, 120)); + canvas.Apply(new Rectangle(0, 0, 120, 120), x => x.Invert()); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Expected output: a red outer layer with the bounded nested layer inverted from blue to yellow. + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(0, 0, 120, 120)); + + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(30, 30, 50, 50)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(30, 30, 50, 50)); + canvas.Apply(new Rectangle(30, 30, 50, 50), x => x.Invert()); + canvas.Restore(); + + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); + } + + [WebGPUTheory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DrawingOptions drawingOptions = new(); + + static void DrawAction(DrawingCanvas canvas) + { + canvas.Fill(Brushes.Solid(Color.White)); + + // Expected output: the red layer is inverted to cyan, then composited over white as a 50% opacity pale cyan rectangle. + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5F }, new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(20, 20, 80, 80)); + canvas.Apply(new Rectangle(20, 20, 80, 80), x => x.Invert()); + canvas.Restore(); + } + + using Image defaultImage = provider.GetImage(); + RenderWithDefaultBackend(defaultImage, drawingOptions, DrawAction); + + using WebGPUDrawingBackend nativeSurfaceBackend = new(); + using Image nativeSurfaceImage = RenderWithNativeSurfaceWebGpuBackend( + defaultImage.Width, + defaultImage.Height, + nativeSurfaceBackend, + drawingOptions, + DrawAction); + + DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.088F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -2514,7 +4310,7 @@ static void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.005F); AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage); } @@ -2536,7 +4332,8 @@ static void DrawAction(DrawingCanvas canvas) }; IPath rootClip = new EllipsePolygon(new PointF(160, 110), new SizeF(252, 164)); - _ = canvas.Save(rootOptions, rootClip); + _ = canvas.Save(rootOptions); + canvas.Clip(ClipOperation.Difference, rootClip); using (DrawingCanvas outerRegion = canvas.CreateRegion(new Rectangle(30, 24, 240, 156))) { @@ -2548,7 +4345,8 @@ static void DrawAction(DrawingCanvas canvas) Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78))) }; - _ = outerRegion.Save(outerOptions, new RectanglePolygon(18, 14, 204, 128)); + _ = outerRegion.Save(outerOptions); + outerRegion.Clip(ClipOperation.Difference, new RectanglePolygon(18, 14, 204, 128)); outerRegion.Fill(Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F)), new Rectangle(16, 16, 208, 124)); @@ -2561,7 +4359,8 @@ static void DrawAction(DrawingCanvas canvas) Transform = new Matrix4x4(Matrix3x2.CreateSkew(0.18F, 0F)) }; - _ = innerRegion.Save(innerOptions, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); + _ = innerRegion.Save(innerOptions); + innerRegion.Clip(ClipOperation.Difference, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); innerRegion.Fill(Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F)), new Rectangle(0, 0, 132, 82)); innerRegion.DrawLine( @@ -2601,7 +4400,10 @@ static void DrawAction(DrawingCanvas canvas) nativeSurfaceInitialImage); DebugSaveBackendPair(provider, null, defaultImage, nativeSurfaceImage); - AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 1F); - AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0005F); + AssertBackendPairSimilarity(defaultImage, nativeSurfaceImage, 0.108F); + + // Reference outputs are rendered on one adapter; other conforming adapters differ by + // rounding and antialiased edge coverage on a small fraction of pixels. + AssertBackendPairReferenceOutputs(provider, null, defaultImage, nativeSurfaceImage, 0.0098F); } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUEnvironmentErrorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUEnvironmentErrorTests.cs new file mode 100644 index 000000000..62ad33148 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUEnvironmentErrorTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUEnvironmentErrorTests +{ + [Fact] + public void EveryErrorValue_HasADedicatedExceptionMessage() + { + string fallback = WebGPURuntime.CreateEnvironmentExceptionMessage((WebGPUEnvironmentError)int.MaxValue); + + foreach (WebGPUEnvironmentError error in Enum.GetValues()) + { + if (error == WebGPUEnvironmentError.Success) + { + continue; + } + + string message = WebGPURuntime.CreateEnvironmentExceptionMessage(error); + + Assert.False(string.IsNullOrWhiteSpace(message)); + Assert.NotEqual(fallback, message); + } + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUErrorTypeMapperTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUErrorTypeMapperTests.cs new file mode 100644 index 000000000..412d009ba --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUErrorTypeMapperTests.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUErrorTypeMapperTests +{ + [Fact] + public void ToPublic_MapsEveryNativeClassification() + { + Assert.Equal(WebGPUErrorType.NoError, WebGPUErrorTypeMapper.ToPublic(WGPUErrorType.NoError)); + Assert.Equal(WebGPUErrorType.Validation, WebGPUErrorTypeMapper.ToPublic(WGPUErrorType.Validation)); + Assert.Equal(WebGPUErrorType.OutOfMemory, WebGPUErrorTypeMapper.ToPublic(WGPUErrorType.OutOfMemory)); + Assert.Equal(WebGPUErrorType.Internal, WebGPUErrorTypeMapper.ToPublic(WGPUErrorType.Internal)); + Assert.Equal(WebGPUErrorType.Unknown, WebGPUErrorTypeMapper.ToPublic(WGPUErrorType.Unknown)); + Assert.Equal(WebGPUErrorType.Unknown, WebGPUErrorTypeMapper.ToPublic((WGPUErrorType)int.MaxValue)); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUExternalSurfaceTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUExternalSurfaceTests.cs new file mode 100644 index 000000000..0688b2a8a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUExternalSurfaceTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.Attributes; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public static partial class WebGPUExternalSurfaceTests +{ + [WebGPUFact] + public static void Win32HostCreatesNativePresentationSurface() + { + if (!TestEnvironment.IsWindows) + { + return; + } + + nint module = GetModuleHandle(null); + + // A hidden window exercises wgpuInstanceCreateSurface and surface configuration without + // opening UI or requiring interaction from the test runner. + nint window = CreateWindowEx(0, "STATIC", null, 0, 0, 0, 16, 16, 0, 0, module, 0); + Assert.NotEqual(nint.Zero, window); + + try + { + using WebGPUExternalSurface surface = new(WebGPUSurfaceHost.Win32(window, module), new Size(16, 16)); + + // Hosts report a zero drawable size while minimized. Acquisition must pause without + // touching the still-configured native surface until a later nonzero resize. + surface.Resize(Size.Empty); + Assert.False(surface.TryAcquireFrame(out _)); + } + finally + { + DestroyWindow(window); + } + } + + [LibraryImport("kernel32", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)] + private static partial nint GetModuleHandle(string? moduleName); + + [LibraryImport("user32", EntryPoint = "CreateWindowExW", StringMarshalling = StringMarshalling.Utf16)] + private static partial nint CreateWindowEx(uint extendedStyle, string className, string? windowName, uint style, int x, int y, int width, int height, nint parent, nint menu, nint instance, nint parameter); + + [LibraryImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool DestroyWindow(nint window); +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderDiagnosticTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderDiagnosticTests.cs new file mode 100644 index 000000000..5905f1be4 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderDiagnosticTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUShaderDiagnosticTests +{ + [Fact] + public void Constructor_StoresAllValues() + { + WebGPUShaderDiagnostic diagnostic = new(WebGPUShaderDiagnosticSeverity.Error, "bad shader", 4, 7); + + Assert.Equal(WebGPUShaderDiagnosticSeverity.Error, diagnostic.Severity); + Assert.Equal("bad shader", diagnostic.Message); + Assert.Equal(4, diagnostic.Line); + Assert.Equal(7, diagnostic.Column); + } + + [Fact] + public void CompilationException_ExposesMessageAndDiagnostics() + { + WebGPUShaderDiagnostic[] diagnostics = + [ + new(WebGPUShaderDiagnosticSeverity.Warning, "first", 1, 2), + new(WebGPUShaderDiagnosticSeverity.Error, "second", 3, 4), + ]; + + WebGPUShaderCompilationException exception = new("compilation failed", diagnostics); + + Assert.Equal("compilation failed", exception.Message); + Assert.Equal(2, exception.Diagnostics.Count); + Assert.Equal("second", exception.Diagnostics[1].Message); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderSourceValidatorTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderSourceValidatorTests.cs new file mode 100644 index 000000000..b44bda0ee --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUShaderSourceValidatorTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUShaderSourceValidatorTests +{ + [Fact] + public void FullWgslControlFlowAndUniformReferencesAreAccepted() + { + const string source = """ + struct SamplePair { + offset: vec2, + weight: f32, + } + + const pairs = array( + SamplePair(vec2(-1.0, 0.0), 0.25), + SamplePair(vec2(1.0, 0.0), 0.25), + ); + + fn weighted_sample(position: vec2) -> vec4 { + var color = layer_sample(position) * imagesharp_uniforms.center_weight; + + for (var i = 0u; i < 2u; i++) { + color += layer_sample(position + pairs[i].offset) * pairs[i].weight; + } + + var remaining = 1u; + while (remaining > 0u) { + remaining--; + } + + return color; + } + + fn layer_effect(position: vec2) -> vec4 { + return weighted_sample(position); + } + """; + + WebGPUShaderSourceValidator.Validate(source, "source"); + } + + [Fact] + public void NestedCommentsDoNotDeclareFrameworkConstructs() + { + const string source = """ + /* + @group(0) @binding(0) + @fragment fn fs_main() {} + fn layer_effect(position: vec2) -> vec4 {} + /* let imagesharp_private = 1.0; */ + */ + // @compute fn imagesharp_compute() {} + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position); + } + """; + + WebGPUShaderSourceValidator.Validate(source, "source"); + } + + [Theory] + [InlineData("@group(0) var user_data: vec4;")] + [InlineData("@binding(0) var user_texture: texture_2d;")] + [InlineData("@vertex fn user_vertex() -> @builtin(position) vec4 { return vec4(); }")] + [InlineData("@fragment fn user_fragment() -> @location(0) vec4 { return vec4(); }")] + [InlineData("@compute @workgroup_size(1) fn user_compute() {}")] + public void FrameworkOwnedAttributesAreRejected(string declaration) + { + string source = $$""" + {{declaration}} + + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position); + } + """; + + ArgumentException exception = Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + + Assert.Equal("source", exception.ParamName); + } + + [Theory] + [InlineData("fn imagesharp_helper(position: vec2) -> vec4 { return layer_sample(position); }")] + [InlineData("alias imagesharp_scalar = f32;")] + [InlineData("struct imagesharp_state { value: f32, }")] + [InlineData("const imagesharp_value = 1.0;")] + [InlineData("var imagesharp_state: f32;")] + [InlineData("var imagesharp_uniforms: f32;")] + [InlineData("fn helper(imagesharp_value: f32) -> f32 { return imagesharp_value; }")] + [InlineData("struct State { imagesharp_value: f32, }")] + public void FrameworkPrefixedDeclarationsAreRejected(string declaration) + { + string source = $$""" + {{declaration}} + + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position); + } + """; + + ArgumentException exception = Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + + Assert.Equal("source", exception.ParamName); + } + + [Fact] + public void FrameworkPrefixedLocalDeclarationIsRejected() + { + const string source = """ + fn layer_effect(position: vec2) -> vec4 { + let imagesharp_value = 0.5; + return layer_sample(position) * imagesharp_value; + } + """; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Theory] + [InlineData("fn layer_load(position: vec2) -> vec4 { return vec4(0.0); }")] + [InlineData("fn layer_load_unassociated(position: vec2) -> vec4 { return vec4(0.0); }")] + [InlineData("fn layer_sample(position: vec2) -> vec4 { return vec4(0.0); }")] + [InlineData("fn vs_main() {}")] + [InlineData("fn fs_main() {}")] + [InlineData("struct ImageSharpFramework { value: f32, }")] + [InlineData("struct ImageSharpUniforms { value: f32, }")] + public void FrameworkOwnedDeclarationsAreRejected(string declaration) + { + string source = $$""" + {{declaration}} + + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position); + } + """; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Theory] + [InlineData("let values = imagesharp_uniforms;")] + [InlineData("let values = &imagesharp_uniforms;")] + [InlineData("consume(imagesharp_uniforms);")] + [InlineData("let value = imagesharp_uniforms[0];")] + public void UniformBindingMustBeReadThroughADirectField(string statement) + { + string source = $$""" + fn layer_effect(position: vec2) -> vec4 { + {{statement}} + return layer_sample(position); + } + """; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Fact] + public void UniformFieldMayFollowWhitespaceAndNestedComments() + { + const string source = """ + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position) * imagesharp_uniforms /* outer /* nested */ */ .opacity; + } + """; + + WebGPUShaderSourceValidator.Validate(source, "source"); + } + + [Fact] + public void FrameworkPrivateIdentifierReferenceIsRejected() + { + const string source = "fn layer_effect(position: vec2) -> vec4 { return imagesharp_layer_load_scaled(vec2(position)); }"; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Theory] + [InlineData("vs_main")] + [InlineData("fs_main")] + [InlineData("ImageSharpFramework")] + [InlineData("ImageSharpUniforms")] + public void FrameworkPrivateUnprefixedIdentifierReferenceIsRejected(string identifier) + { + string source = $$""" + fn layer_effect(position: vec2) -> vec4 { + let forbidden = {{identifier}}; + return layer_sample(position); + } + """; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Fact] + public void NullCharacterIsRejected() + { + const string source = "fn layer_effect(position: vec2) -> vec4 { return layer_sample(position); }\0"; + + ArgumentException exception = Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + + Assert.Equal("source", exception.ParamName); + } + + [Fact] + public void MissingLayerEffectDeclarationIsRejected() + { + const string source = "fn helper(position: vec2) -> vec4 { return layer_sample(position); }"; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } + + [Fact] + public void DuplicateLayerEffectDeclarationIsRejected() + { + const string source = """ + fn layer_effect(position: vec2) -> vec4 { + return layer_sample(position); + } + + fn layer_effect(position: vec2) -> vec4 { + return layer_load(vec2(position)); + } + """; + + Assert.Throws( + () => WebGPUShaderSourceValidator.Validate(source, "source")); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs index 4e59e4131..bcc0f44ce 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUTextureFormatMapperTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Silk.NET.WebGPU; using SixLabors.ImageSharp.Drawing.Processing.Backends; +using SixLabors.ImageSharp.Drawing.Processing.Backends.Native; namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; @@ -11,20 +11,20 @@ public class WebGPUTextureFormatMapperTests [Fact] public void Mapper_UsesExplicitMappings_ForAllSupportedFormats() { - (WebGPUTextureFormat Drawing, TextureFormat Silk)[] mappings = + (WebGPUTextureFormat Drawing, WGPUTextureFormat Native)[] mappings = [ - (WebGPUTextureFormat.Rgba8Unorm, TextureFormat.Rgba8Unorm), - (WebGPUTextureFormat.Rgba8Snorm, TextureFormat.Rgba8Snorm), - (WebGPUTextureFormat.Bgra8Unorm, TextureFormat.Bgra8Unorm), - (WebGPUTextureFormat.Rgba16Float, TextureFormat.Rgba16float) + (WebGPUTextureFormat.Rgba8Unorm, WGPUTextureFormat.RGBA8Unorm), + (WebGPUTextureFormat.Rgba8Snorm, WGPUTextureFormat.RGBA8Snorm), + (WebGPUTextureFormat.Bgra8Unorm, WGPUTextureFormat.BGRA8Unorm), + (WebGPUTextureFormat.Rgba16Float, WGPUTextureFormat.RGBA16Float) ]; Assert.Equal(Enum.GetValues().Length, mappings.Length); - foreach ((WebGPUTextureFormat drawing, TextureFormat silk) in mappings) + foreach ((WebGPUTextureFormat drawing, WGPUTextureFormat native) in mappings) { - Assert.Equal(silk, WebGPUTextureFormatMapper.ToNative(drawing)); - Assert.Equal(drawing, WebGPUTextureFormatMapper.FromNative(silk)); + Assert.Equal(native, WebGPUTextureFormatMapper.ToNative(drawing)); + Assert.Equal(drawing, WebGPUTextureFormatMapper.FromNative(native)); } } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUWindowOptionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUWindowOptionsTests.cs new file mode 100644 index 000000000..e43994d83 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/Backends/WebGPUWindowOptionsTests.cs @@ -0,0 +1,28 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing.Backends; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing.Backends; + +public class WebGPUWindowOptionsTests +{ + [Fact] + public void Defaults_MatchDocumentedInitialConfiguration() + { + WebGPUWindowOptions options = new(); + + Assert.Equal("ImageSharp.Drawing WebGPU", options.Title); + Assert.Equal(new Size(1280, 720), options.Size); + Assert.Equal(new Point(50, 50), options.Position); + Assert.True(options.IsVisible); + Assert.Equal(0, options.FramesPerSecond); + Assert.Equal(0, options.UpdatesPerSecond); + Assert.False(options.IsEventDriven); + Assert.Equal(WebGPUWindowState.Normal, options.WindowState); + Assert.Equal(WebGPUWindowBorder.Resizable, options.WindowBorder); + Assert.False(options.IsTopMost); + Assert.Equal(WebGPUPresentMode.Fifo, options.PresentMode); + Assert.Equal(WebGPUTextureFormat.Rgba8Unorm, options.Format); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/BrushesTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/BrushesTests.cs new file mode 100644 index 000000000..3b4cb925d --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/BrushesTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class BrushesTests +{ + public static TheoryData PatternFactoryNames { get; } = new() + { + nameof(Brushes.Horizontal), + nameof(Brushes.Min), + nameof(Brushes.Vertical), + nameof(Brushes.ForwardDiagonal), + nameof(Brushes.BackwardDiagonal), + nameof(Brushes.Cross), + nameof(Brushes.DiagonalCross), + }; + + [Fact] + public void Solid_CreatesBrushWithColor() + { + SolidBrush brush = Brushes.Solid(Color.Red); + + Assert.Equal(Color.Red, brush.Color); + } + + [Theory] + [MemberData(nameof(PatternFactoryNames))] + public void PatternFactories_SingleColor_UseTransparentBackground(string name) + { + PatternBrush brush = InvokePatternFactory(name, Color.Red, null); + + Assert.Contains(Color.Red, brush.Pattern.Data); + Assert.Contains(Color.Transparent, brush.Pattern.Data); + } + + [Theory] + [MemberData(nameof(PatternFactoryNames))] + public void PatternFactories_TwoColors_UseBothColors(string name) + { + PatternBrush brush = InvokePatternFactory(name, Color.Red, Color.Blue); + + Assert.Contains(Color.Red, brush.Pattern.Data); + Assert.Contains(Color.Blue, brush.Pattern.Data); + Assert.DoesNotContain(Color.Transparent, brush.Pattern.Data); + } + + private static PatternBrush InvokePatternFactory(string name, Color foreColor, Color? backColor) + => (name, backColor) switch + { + (nameof(Brushes.Horizontal), null) => Brushes.Horizontal(foreColor), + (nameof(Brushes.Horizontal), _) => Brushes.Horizontal(foreColor, backColor.Value), + (nameof(Brushes.Min), null) => Brushes.Min(foreColor), + (nameof(Brushes.Min), _) => Brushes.Min(foreColor, backColor.Value), + (nameof(Brushes.Vertical), null) => Brushes.Vertical(foreColor), + (nameof(Brushes.Vertical), _) => Brushes.Vertical(foreColor, backColor.Value), + (nameof(Brushes.ForwardDiagonal), null) => Brushes.ForwardDiagonal(foreColor), + (nameof(Brushes.ForwardDiagonal), _) => Brushes.ForwardDiagonal(foreColor, backColor.Value), + (nameof(Brushes.BackwardDiagonal), null) => Brushes.BackwardDiagonal(foreColor), + (nameof(Brushes.BackwardDiagonal), _) => Brushes.BackwardDiagonal(foreColor, backColor.Value), + (nameof(Brushes.Cross), null) => Brushes.Cross(foreColor), + (nameof(Brushes.Cross), _) => Brushes.Cross(foreColor, backColor.Value), + (nameof(Brushes.DiagonalCross), null) => Brushes.DiagonalCross(foreColor), + (nameof(Brushes.DiagonalCross), _) => Brushes.DiagonalCross(foreColor, backColor.Value), + _ => throw new ArgumentOutOfRangeException(nameof(name)), + }; +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DefaultDrawingBackendWorkerStatePoolTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DefaultDrawingBackendWorkerStatePoolTests.cs new file mode 100644 index 000000000..dfa57ce89 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DefaultDrawingBackendWorkerStatePoolTests.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +/// +/// Verifies the lifetime behavior of the CPU backend's pooled worker states: rendering +/// retains a bounded set of allocator buffers across flushes, and the Gen2 idle trim +/// returns every buffer to the allocator once rendering stops. +/// +public class DefaultDrawingBackendWorkerStatePoolTests +{ + /// + /// The idle trim rides Gen2 collections observed by a finalizer sentinel, so the + /// observation is only deterministic when nothing else drives the GC or rents worker + /// states. The test therefore runs in a dedicated process via + /// , matching how ImageSharp hardens its pool trim tests. + /// + [Fact] + public void WorkerStatePool_ReturnsAllBuffersToAllocator_WhenIdleAcrossGen2Collections() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + TestMemoryAllocator allocator = new(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + using (Image image = new(configuration, 128, 128)) + { + image.Mutate(context => context.Paint( + canvas => canvas.Fill(Brushes.Solid(Color.Red), new RectanglePolygon(new RectangleF(8, 8, 64, 64))))); + } + + // The first collection consumes the rent flag set by the flush above; the second + // observes an idle Gen2 and trims the pool, disposing the retained worker states. + // Extra iterations keep the test robust if the finalizer thread lags. + for (int i = 0; i < 5 && CountOutstanding(allocator) != 0; i++) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + + Assert.Equal(0, CountOutstanding(allocator)); + } + } + + /// + /// Counts allocations that have not been returned, matching allocation and return + /// entries by buffer identity so double returns cannot mask a leak. + /// + /// The tracking allocator used by the test. + /// The number of live allocations. + private static int CountOutstanding(TestMemoryAllocator allocator) + { + Dictionary outstanding = []; + foreach (TestMemoryAllocator.AllocationRequest request in allocator.AllocationLog) + { + outstanding[request.HashCodeOfBuffer] = outstanding.TryGetValue(request.HashCodeOfBuffer, out int count) ? count + 1 : 1; + } + + int live = allocator.AllocationLog.Count; + foreach (TestMemoryAllocator.ReturnRequest request in allocator.ReturnLog) + { + if (outstanding.TryGetValue(request.HashCodeOfBuffer, out int count) && count > 0) + { + outstanding[request.HashCodeOfBuffer] = count - 1; + live--; + } + } + + return live; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs index 260fd0a91..dc36f66aa 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasBatcherTests.cs @@ -7,11 +7,73 @@ using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; public class DrawingCanvasBatcherTests { + [Fact] + public void Flush_LayerEffect_RetainsEffectIdentity() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(configuration, 40, 40); + + LayerEffect effect = new BlurLayerEffect(2F); + using (DrawingCanvas canvas = image.Frames.RootFrame.CreateCanvas(configuration, new DrawingOptions())) + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(4, 6, 18, 12), effect); + canvas.Fill(Brushes.Solid(Color.Red), new RectanglePolygon(4, 6, 18, 12)); + canvas.Restore(); + canvas.Flush(); + } + + CompositionCommand command = GetSingleApplyCommand(backend); + Assert.Same(effect, command.ApplyEffect); + } + + [Fact] + public void Flush_BackdropLayerEffect_RetainsEffectIdentity() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(configuration, 40, 40); + + LayerEffect effect = new BackdropBlurLayerEffect(2F); + using (DrawingCanvas canvas = image.Frames.RootFrame.CreateCanvas(configuration, new DrawingOptions())) + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(4, 6, 18, 12), effect); + canvas.Restore(); + canvas.Flush(); + } + + CompositionCommand command = GetSingleApplyCommand(backend); + Assert.Same(effect, command.ApplyEffect); + } + + [Fact] + public void Flush_DirectApply_RemainsActionOnly() + { + Configuration configuration = new(); + CapturingBackend backend = new(); + configuration.SetDrawingBackend(backend); + using Image image = new(configuration, 40, 40); + + Action operation = _ => { }; + using (DrawingCanvas canvas = image.Frames.RootFrame.CreateCanvas(configuration, new DrawingOptions())) + { + canvas.Apply(new Rectangle(4, 6, 18, 12), operation); + canvas.Flush(); + } + + CompositionCommand command = GetSingleApplyCommand(backend); + Assert.Same(operation, command.ApplyOperation); + Assert.Null(command.ApplyEffect); + } + [Fact] public void Flush_SamePathDifferentBrushes_UsesSingleCoverageDefinition() { @@ -198,6 +260,15 @@ public void Flush_MiterStroke_PreparesStrokePathSceneCommand() Assert.NotNull(command.Command.Pen); } + private static CompositionCommand GetSingleApplyCommand(CapturingBackend backend) + { + PathCompositionSceneCommand prepared = Assert.Single( + backend.PreparedCommands.OfType(), + command => command.Command.Kind == CompositionCommandKind.Apply); + + return prepared.Command; + } + private sealed class CapturingBackend : IDrawingBackend { public List Definitions { get; } = []; @@ -212,7 +283,6 @@ private sealed class CapturingBackend : IDrawingBackend Rectangle.Empty, IntersectionRule.NonZero, RasterizationMode.Aliased, - RasterizerSamplingOrigin.PixelBoundary, 0.5f), default, []); @@ -234,12 +304,14 @@ public DrawingBackendScene CreateScene( } CompositionCommand command = pathCommand.Command; - IPath sourcePath = command.SourcePath; - if (sourcePath is null) + if (command.Kind is not (CompositionCommandKind.FillLayer or CompositionCommandKind.Apply)) { + // Layer boundaries share the retained composition wrapper but carry no + // rasterizable path, so they cannot contribute a coverage definition. continue; } + IPath sourcePath = command.SourcePath; RasterizerOptions rasterizerOptions = command.RasterizerOptions; CoverageDefinitionKey key = new(command); @@ -279,6 +351,15 @@ public void RenderScene( { } + public void CopyPixels( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame target, + Rectangle sourceRectangle, + Point targetPoint) + where TPixel : unmanaged, IPixel + => throw new NotSupportedException(); + public void ReadRegion( Configuration configuration, ICanvasFrame target, @@ -316,7 +397,6 @@ public CapturedCoverageDefinition( private readonly Rectangle interest; private readonly IntersectionRule intersectionRule; private readonly RasterizationMode rasterizationMode; - private readonly RasterizerSamplingOrigin samplingOrigin; private readonly int antialiasThresholdBits; public CoverageDefinitionKey(CompositionCommand command) @@ -325,7 +405,6 @@ public CoverageDefinitionKey(CompositionCommand command) this.interest = command.RasterizerOptions.Interest; this.intersectionRule = command.RasterizerOptions.IntersectionRule; this.rasterizationMode = command.RasterizerOptions.RasterizationMode; - this.samplingOrigin = command.RasterizerOptions.SamplingOrigin; this.antialiasThresholdBits = BitConverter.SingleToInt32Bits(command.RasterizerOptions.AntialiasThreshold); } @@ -334,7 +413,6 @@ public bool Equals(CoverageDefinitionKey other) this.interest.Equals(other.interest) && this.intersectionRule == other.intersectionRule && this.rasterizationMode == other.rasterizationMode && - this.samplingOrigin == other.samplingOrigin && this.antialiasThresholdBits == other.antialiasThresholdBits; public override bool Equals(object obj) @@ -346,7 +424,6 @@ public override int GetHashCode() this.interest, (int)this.intersectionRule, (int)this.rasterizationMode, - (int)this.samplingOrigin, this.antialiasThresholdBits); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs index 85dee3e16..e042ad516 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Clear.cs @@ -46,7 +46,8 @@ public void Clear_WithClipPath_MatchesReference(TestImageProvider(TestImag canvas.Clear(Brushes.Solid(Color.White)); canvas.Fill(Brushes.Solid(Color.LightGray.WithAlpha(0.45F)), new Rectangle(18, 16, 324, 208)); - _ = canvas.Save(transformedOptions, clipPath); + _ = canvas.Save(transformedOptions); + canvas.Clip(ClipOperation.Difference, clipPath); + canvas.DrawImage( foreground, new Rectangle(10, 8, 234, 180), @@ -105,6 +107,7 @@ public void DrawImage_WithClipPathAndTransform_MatchesReference(TestImag } target.DebugSave(provider, appendSourceFileOrDescription: false); + target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs index 3b0a369c6..3b9ee9e7a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.PathRules.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -19,12 +20,12 @@ public void Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference( DrawingOptions evenOddOptions = new() { - ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + IntersectionRule = IntersectionRule.EvenOdd }; DrawingOptions nonZeroOptions = new() { - ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } + IntersectionRule = IntersectionRule.NonZero }; using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) @@ -50,6 +51,102 @@ public void Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference( target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } + [Theory] + [WithBlankImage(180, 180, PixelTypes.Rgba32)] + public void Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + IPath path = CreatePentagramPath(new PointF(90, 90), 70F); + IPath generalClipPath = CreateFivePointRectanglePath(new Rectangle(0, 0, 100, 180)); + IPath rectangleClipPath = new RectanglePolygon(0, 0, 100, 180); + + DrawingOptions evenOddOptions = new() + { + IntersectionRule = IntersectionRule.EvenOdd + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + + _ = canvas.Save(evenOddOptions); + canvas.Clip(generalClipPath); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), path); + canvas.Restore(); + } + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + + _ = canvas.Save(evenOddOptions); + canvas.Clip(rectangleClipPath); + canvas.Fill(Brushes.Solid(Color.DeepPink.WithAlpha(0.85F)), path); + canvas.Restore(); + } + + expected.DebugSave(provider, "expected-general-clip", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "actual-rect-clip", appendSourceFileOrDescription: false); + + ImageComparer.TolerantPercentage(0.005F).VerifySimilarity(expected, actual); + expected.CompareToReferenceOutput(provider, "expected-general-clip", appendSourceFileOrDescription: false); + actual.CompareToReferenceOutput(provider, "actual-rect-clip", appendSourceFileOrDescription: false); + } + + [Theory] + [WithBlankImage(96, 64, PixelTypes.Rgba32)] + public void Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + IPath firstClip = new RectanglePolygon(20, 12, 34, 30); + IPath secondClip = new RectanglePolygon(38, 28, 34, 24); + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Clip(ClipOperation.Difference, firstClip); + canvas.Clip(ClipOperation.Difference, secondClip); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(4, 4, 88, 56)); + } + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.Clip(ClipOperation.Difference, firstClip, secondClip); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(4, 4, 88, 56)); + } + + expected.DebugSave(provider, "expected-sequential-difference-clips", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "actual-multiple-difference-clips", appendSourceFileOrDescription: false); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + expected.CompareToReferenceOutput(provider, "expected-sequential-difference-clips", appendSourceFileOrDescription: false); + actual.CompareToReferenceOutput(provider, "actual-multiple-difference-clips", appendSourceFileOrDescription: false); + } + + /// + /// Creates a rectangle path that is equivalent to but intentionally + /// has five vertices so the rectangle fast path does not recognize it. + /// + /// The rectangle to create. + /// The rectangle path. + private static IPath CreateFivePointRectanglePath(Rectangle rectangle) + { + PathBuilder builder = new(); + builder.AddLine(rectangle.Left, rectangle.Top, rectangle.Right, rectangle.Top); + builder.AddLine(rectangle.Right, rectangle.Top, rectangle.Right, rectangle.Top + (rectangle.Height / 2F)); + builder.AddLine(rectangle.Right, rectangle.Top + (rectangle.Height / 2F), rectangle.Right, rectangle.Bottom); + builder.AddLine(rectangle.Right, rectangle.Bottom, rectangle.Left, rectangle.Bottom); + builder.AddLine(rectangle.Left, rectangle.Bottom, rectangle.Left, rectangle.Top); + builder.CloseAllFigures(); + + return builder.Build(); + } + private static IPath CreatePentagramPath(PointF center, float radius) { PointF[] points = new PointF[5]; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs index 1f186b304..be164a0f9 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Process.cs @@ -2,9 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.Drawing.Processing.Backends; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities; -using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -62,39 +60,6 @@ public void Process_Path_MatchesReference(TestImageProvider prov target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); } - [Theory] - [WithBlankImage(220, 160, PixelTypes.Rgba32)] - public void Process_NoCpuFrame_UsesBackendReadback_MatchesReference(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - using Image target = provider.GetImage(); - IPath blurPath = CreateBlurEllipsePath(); - IPath pixelatePath = CreatePixelateTrianglePath(); - - MemoryCanvasFrame proxyFrame = new(target.Frames.RootFrame.PixelBuffer.GetRegion()); - MirroringCpuReadbackTestBackend mirroringBackend = new(proxyFrame, target); - - NativeSurface nativeSurface = new UnsupportedNativeSurface(); - Configuration configuration = provider.Configuration.Clone(); - configuration.SetDrawingBackend(mirroringBackend); - - using (DrawingCanvas canvas = new( - configuration, - new DrawingOptions(), - new NativeCanvasFrame(target.Bounds, nativeSurface))) - { - DrawProcessScenario(canvas); - canvas.Apply(blurPath, ctx => ctx.GaussianBlur(6F)); - canvas.Apply(pixelatePath, ctx => ctx.Pixelate(10)); - canvas.Flush(); - } - - Assert.True(mirroringBackend.ReadbackCallCount > 0); - Assert.Same(configuration, mirroringBackend.LastReadbackConfiguration); - target.DebugSave(provider, appendSourceFileOrDescription: false); - target.CompareToReferenceOutput(provider, appendSourceFileOrDescription: false); - } - [Fact] public void Process_UsesCanvasConfigurationForOperationContext() { @@ -157,84 +122,4 @@ private static IPath CreatePixelateTrianglePath() pathBuilder.CloseAllFigures(); return pathBuilder.Build(); } - - private sealed class UnsupportedNativeSurface : NativeSurface - { - } - - /// - /// Test backend that mirrors composition output into a CPU frame and optionally serves readback - /// from a backing image for process-path tests. - /// - private sealed class MirroringCpuReadbackTestBackend : IDrawingBackend - where TPixel : unmanaged, IPixel - { - private readonly ICanvasFrame proxyFrame; - private readonly Image? readbackSource; - - public MirroringCpuReadbackTestBackend(ICanvasFrame proxyFrame, Image? readbackSource = null) - { - this.proxyFrame = proxyFrame; - this.readbackSource = readbackSource; - } - - public int ReadbackCallCount { get; private set; } - - public Configuration? LastReadbackConfiguration { get; private set; } - - public DrawingBackendScene CreateScene( - Configuration configuration, - Rectangle targetBounds, - DrawingCommandBatch commandBatch, - IReadOnlyList? ownedResources = null) - => DefaultDrawingBackend.Instance.CreateScene(configuration, targetBounds, commandBatch, ownedResources); - - public void RenderScene( - Configuration configuration, - ICanvasFrame target, - DrawingBackendScene scene) - where TTargetPixel : unmanaged, IPixel - { - if (this.proxyFrame is not ICanvasFrame typedProxyFrame) - { - throw new NotSupportedException("Mirroring test backend pixel format mismatch."); - } - - DefaultDrawingBackend.Instance.RenderScene(configuration, typedProxyFrame, scene); - } - - public void ReadRegion( - Configuration configuration, - ICanvasFrame target, - Rectangle sourceRectangle, - Buffer2DRegion destination) - where TTargetPixel : unmanaged, IPixel - { - this.LastReadbackConfiguration = configuration; - - if (this.readbackSource is null) - { - throw new NotSupportedException(); - } - - this.ReadbackCallCount++; - - Rectangle clipped = Rectangle.Intersect(this.readbackSource.Bounds, sourceRectangle); - if (clipped.Width <= 0 || clipped.Height <= 0) - { - throw new ArgumentException("The requested readback rectangle does not intersect the target bounds.", nameof(sourceRectangle)); - } - - using Image cropped = this.readbackSource.Clone(ctx => ctx.Crop(clipped)); - using Image converted = cropped.CloneAs(); - Buffer2D source = converted.Frames.RootFrame.PixelBuffer; - int copyWidth = Math.Min(source.Width, destination.Width); - int copyHeight = Math.Min(source.Height, destination.Height); - - for (int y = 0; y < copyHeight; y++) - { - source.DangerousGetRowSpan(y).Slice(0, copyWidth).CopyTo(destination.DangerousGetRowSpan(y)); - } - } - } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs index a7c3b78d4..e28cdd07a 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.RegionAndState.cs @@ -16,19 +16,27 @@ public void CreateRegion_LocalCoordinates_MatchesReference(TestImageProv where TPixel : unmanaged, IPixel { using Image target = provider.GetImage(); + + // Expected output from the source operations: + // - The child canvas is clipped to absolute region (40,24)-(180,120), but commands use + // child-local coordinates. + // - The sea-green fill is local (10,8)-(90,54), so it lands at absolute (50,32)-(130,78). + // - The dark-blue 5px rectangle stroke is centered on local (0,0)-(140,96) and clipped by + // the region, leaving only the inner half of the stroke visible on the region boundary. + // - The orange-red 4px line runs local (0,95)->(139,0), landing on the region diagonal from + // absolute near (40,119) to (179,24), clipped to the child region. using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) { canvas.Clear(Brushes.Solid(Color.White)); - using (DrawingCanvas regionCanvas = canvas.CreateRegion(new Rectangle(40, 24, 140, 96))) - { - regionCanvas.Fill(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(10, 8, 80, 46)); - regionCanvas.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 140, 96)); - regionCanvas.DrawLine( - Pens.Solid(Color.OrangeRed, 4), - new PointF(0, 95), - new PointF(139, 0)); - } + using DrawingCanvas regionCanvas = canvas.CreateRegion(new Rectangle(40, 24, 140, 96)); + + regionCanvas.Fill(Brushes.Solid(Color.LightSeaGreen.WithAlpha(0.8F)), new Rectangle(10, 8, 80, 46)); + regionCanvas.Draw(Pens.Solid(Color.DarkBlue, 5), new Rectangle(0, 0, 140, 96)); + regionCanvas.DrawLine( + Pens.Solid(Color.OrangeRed, 4), + new PointF(0, 95), + new PointF(139, 0)); } target.DebugSave(provider, appendSourceFileOrDescription: false); @@ -47,7 +55,14 @@ public void SaveRestore_ClipPath_MatchesReference(TestImageProvider(TestImageProvider< { canvas.Clear(Brushes.Solid(Color.White)); - int firstSaveCount = canvas.Save(firstOptions, new RectanglePolygon(20, 20, 144, 104)); + // Expected output from the first saved state: + // - Save pushes the translated state, then Clip applies rectangle (20,20)-(164,124), transformed by + // translation to absolute (40,32)-(184,136). + // - The sky-blue fill is local (0,0)-(120,84), transformed to (20,12)-(140,96). + // - Difference leaves the translated fill only in the top strip y=12..32 and left strip + // x=20..40 outside the translated clip rectangle. + int firstSaveCount = canvas.Save(firstOptions); + canvas.Clip(ClipOperation.Difference, new RectanglePolygon(20, 20, 144, 104)); + canvas.Fill(Brushes.Solid(Color.SkyBlue.WithAlpha(0.8F)), new Rectangle(0, 0, 120, 84)); - _ = canvas.Save(secondOptions, new EllipsePolygon(new PointF(112, 80), new SizeF(130, 90))); + // Expected output from the second saved state: + // - Save pushes the rotated state, then Clip applies ellipse (47,35)-(177,125), transformed by + // the same rotation used for the purple stroke. + // - The purple 6px rectangle stroke is centered on (34,26)-(186,134). The ellipse is inside + // that border, so Difference leaves the rotated rectangular stroke visually uncut by the oval. + _ = canvas.Save(secondOptions); + canvas.Clip(ClipOperation.Difference, new EllipsePolygon(new PointF(112, 80), new SizeF(130, 90))); + canvas.Draw(Pens.Solid(Color.MediumPurple, 6), new Rectangle(34, 26, 152, 108)); + // RestoreTo(firstSaveCount) returns to the translated rectangle Difference clip. The + // orange-red 5px polyline is transformed to (20,112)->(96,30)->(188,104). + // The visible stroke pieces are centered on (20,112)->(40,90.4), the small top V + // (94.1,32)->(96,30)->(98.5,32), and (184,100.8)->(188,104). canvas.RestoreTo(firstSaveCount); canvas.DrawLine( Pens.Solid(Color.OrangeRed, 5), @@ -97,6 +131,8 @@ public void RestoreTo_MultipleStates_MatchesReference(TestImageProvider< new PointF(76, 18), new PointF(168, 92)); + // Restoring to root removes all saved transforms and clips before drawing the + // un-clipped gold rectangle and final dark border. canvas.RestoreTo(1); canvas.Fill(Brushes.Solid(Color.Gold.WithAlpha(0.7F)), new Rectangle(156, 106, 48, 34)); canvas.Draw(Pens.Solid(Color.DarkSlateGray, 4), new Rectangle(8, 8, 208, 144)); @@ -112,6 +148,38 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference where TPixel : unmanaged, IPixel { using Image target = provider.GetImage(); + + // This test checks the exact shape produced by each nested state: + // 1. The root canvas starts as solid white, then gets an unrotated ghost-white rectangle + // at (12,12)-(308,208). + // 2. The root saved state translates later drawing by (6,4) and uses Difference against + // the translated oval. The first outer-region fill and dark-blue rectangle stroke are + // therefore clipped to the outer region and have that oval removed. + // 3. Saving the outer region with a new explicit clip replaces the root oval clip for that + // state. The active shape becomes the outer rotation around local (120,78), with + // Difference against the rotated rectangle (18,14)-(222,142). + // 4. The purple fill is local rectangle (16,16)-(224,140). Because the active Difference + // rectangle fully covers its vertical span and starts/stops two pixels inside its + // horizontal edges, the only surviving purple geometry is local strip (16,16)-(18,140) + // and local strip (222,16)-(224,140), both rotated and clipped to the outer region. + // 5. The inner region is created inside the outer rotated state. Its yellow clear uses the + // inner canvas bounds, local rectangle (0,0)-(132,82). Difference against the outer + // clip rectangle removes local (18,14)-(132,82), leaving the rotated yellow L made from + // local strip (0,0)-(18,82) plus local strip (18,0)-(132,14), clipped to the inner target. + // 6. Saving the inner region with a new explicit clip replaces the outer rectangle clip for + // that state. The active shape becomes the inner skew transform with Difference against + // the skewed oval centered at (66,41), size (102,58). + // 7. The green fill is local rectangle (0,0)-(132,82) with that skewed oval removed. The + // red polyline (0,80)->(66,0)->(132,74) is clipped by the same skewed oval. + // 8. Restoring the inner state returns to the outer rotated Difference state. The black + // dash-dot rectangle is local stroke (4,4)-(128,78); the visible stroke is the rotated + // top edge and left edge that survive Difference against local rectangle (18,14)-(222,142), + // clipped to the inner target. + // 9. Restoring the outer state returns to the root translated oval-Difference state. The + // orange rectangle (8,112)-(98,142) and black diagonal (8,8)->(232,148) are translated, + // clipped to the outer region, and have the root oval removed. + // 10. Restoring the root state removes all nested clips and transforms. The final dark + // border (8,8)-(312,212) and grey dashed line (20,200)->(300,20) are root-local output. DrawingOptions rootOptions = new() { Transform = Matrix4x4.CreateTranslation(6F, 4F, 0) @@ -124,7 +192,8 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference canvas.Clear(Brushes.Solid(Color.White)); canvas.Fill(Brushes.Solid(Color.GhostWhite.WithAlpha(0.85F)), new Rectangle(12, 12, 296, 196)); - _ = canvas.Save(rootOptions, rootClip); + _ = canvas.Save(rootOptions); + canvas.Clip(ClipOperation.Difference, rootClip); using (DrawingCanvas outerRegion = canvas.CreateRegion(new Rectangle(30, 24, 240, 156))) { @@ -136,7 +205,8 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference Transform = new Matrix4x4(Matrix3x2.CreateRotation(0.18F, new Vector2(120, 78))) }; - _ = outerRegion.Save(outerOptions, new RectanglePolygon(18, 14, 204, 128)); + _ = outerRegion.Save(outerOptions); + outerRegion.Clip(ClipOperation.Difference, new RectanglePolygon(18, 14, 204, 128)); outerRegion.Fill(Brushes.Solid(Color.MediumPurple.WithAlpha(0.35F)), new Rectangle(16, 16, 208, 124)); @@ -149,7 +219,8 @@ public void CreateRegion_NestedRegionsAndStateIsolation_MatchesReference Transform = new Matrix4x4(Matrix3x2.CreateSkew(0.18F, 0F)) }; - _ = innerRegion.Save(innerOptions, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); + _ = innerRegion.Save(innerOptions); + innerRegion.Clip(ClipOperation.Difference, new EllipsePolygon(new PointF(66, 41), new SizeF(102, 58))); innerRegion.Fill(Brushes.Solid(Color.SeaGreen.WithAlpha(0.55F)), new Rectangle(0, 0, 132, 82)); innerRegion.DrawLine( diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs index ecb8e20bc..e0feb36e3 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveCount.cs @@ -43,7 +43,9 @@ public void SaveWithOptions_IncrementsSaveCount() using Image target = new(64, 64); using DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions()); - int count = canvas.Save(new DrawingOptions(), new RectanglePolygon(0, 0, 32, 32)); + int count = canvas.Save(new DrawingOptions()); + canvas.Clip(new RectanglePolygon(0, 0, 32, 32)); + Assert.Equal(2, count); Assert.Equal(2, canvas.SaveCount); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs index 66c7f4f03..684a66f22 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.SaveLayer.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -269,4 +270,134 @@ public void SaveLayer_MixedSaveAndSaveLayer_WorksCorrectly() Rgba32 pixel = target[32, 32]; Assert.Equal(new Rgba32(0, 128, 0, 255), pixel); } + + [Theory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_ProcessesLayerTarget(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(Brushes.Solid(Color.White)); + + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(42, 42, 24, 24)); + canvas.Apply(new Rectangle(20, 20, 80, 80), x => x.Invert()); + canvas.Restore(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + Assert.Equal(Color.Cyan.ToPixel(), target[24, 24]); + Assert.Equal(Color.Yellow.ToPixel(), target[48, 48]); + Assert.Equal(Color.White.ToPixel(), target[8, 8]); + } + + [Theory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_RespectsLayerBounds(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(Brushes.Solid(Color.White)); + + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(30, 30, 50, 50)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(0, 0, 120, 120)); + canvas.Apply(new Rectangle(0, 0, 120, 120), x => x.Invert()); + canvas.Restore(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + Assert.Equal(Color.Cyan.ToPixel(), target[34, 34]); + Assert.Equal(Color.White.ToPixel(), target[24, 24]); + Assert.Equal(Color.White.ToPixel(), target[84, 84]); + } + + [Theory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_ProcessesNestedLayer(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(Brushes.Solid(Color.White)); + + canvas.SaveLayer(); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(0, 0, 120, 120)); + + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(30, 30, 50, 50)); + canvas.Fill(Brushes.Solid(Color.Blue), new Rectangle(30, 30, 50, 50)); + canvas.Apply(new Rectangle(30, 30, 50, 50), x => x.Invert()); + canvas.Restore(); + + canvas.Restore(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + Assert.Equal(Color.Red.ToPixel(), target[20, 20]); + Assert.Equal(Color.Yellow.ToPixel(), target[40, 40]); + Assert.Equal(Color.Red.ToPixel(), target[100, 100]); + } + + [Theory] + [WithBlankImage(120, 120, PixelTypes.Rgba32)] + public void SaveLayer_Apply_CompositesLayerOpacityAfterProcessing(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Fill(Brushes.Solid(Color.White)); + + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.5F }, new Rectangle(20, 20, 80, 80)); + canvas.Fill(Brushes.Solid(Color.Red), new Rectangle(20, 20, 80, 80)); + canvas.Apply(new Rectangle(20, 20, 80, 80), x => x.Invert()); + canvas.Restore(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + Rgba32 blended = target[40, 40].ToRgba32(); + Assert.InRange(blended.R, 126, 129); + Assert.Equal(255, blended.G); + Assert.Equal(255, blended.B); + Assert.Equal(255, blended.A); + } + + [Theory] + [WithSolidFilledImages(240, 160, nameof(Color.White), PixelTypes.Rgba32)] + public void SaveLayer_BlurEffect_OutputReachesBeyondContentBounds(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + // The box-shadow pattern: a blur effect layer whose bounds equal the shadow geometry + // exactly, relying on the canvas to expand the effect region by the blur's reach. + Rectangle content = new(60, 40, 120, 80); + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.SaveLayer(new GraphicsOptions(), content, new BlurLayerEffect(6F)); + canvas.Fill(Brushes.Solid(Color.Black), new RectanglePolygon((RectangleF)content)); + canvas.Restore(); + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + + // Blurred ink must spill outside the content rectangle, and the boundary column must + // no longer be a hard edge: without reach expansion the output clamps to the content + // bounds, leaving pure white one pixel outside and pure black one pixel inside. + Rgba32 outside = target[content.Right + 4, content.Top + (content.Height / 2)].ToRgba32(); + Assert.True(outside.R < 250, $"Expected blurred ink beyond the content edge but found {outside}."); + + Rgba32 edge = target[content.Right - 1, content.Top + (content.Height / 2)].ToRgba32(); + Assert.True(edge.R > 5, $"Expected the content edge to feather but found {edge}."); + } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs index 02735a04d..5bc794a31 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.StrokeOptions.cs @@ -24,7 +24,7 @@ public void Draw_SelfIntersectingStroke_MatchesReference(TestImageProvid DrawingOptions evenOddOptions = new() { - ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.EvenOdd } + IntersectionRule = IntersectionRule.EvenOdd }; using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs index 989744f94..36daefa5c 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingCanvasTests.Text.cs @@ -8,6 +8,7 @@ using SixLabors.ImageSharp.Drawing.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Drawing.Text; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Drawing.Tests.Processing; @@ -52,6 +53,290 @@ public void DrawGlyphs_EmojiFont_MatchesReference(TestImageProvider(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + // Inter Light is the exact font the Avalonia sample renders (it ships Inter as its embedded UI + // font). Inter draws glyphs such as 'A' and 't' with overlapping contours, so if the glyph fill + // applies the wrong winding those overlaps render as holes. This isolates the fill issue at the + // library level (CPU canvas), independent of the Avalonia/WebGPU backends where it was observed. + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + + RichTextOptions textOptions = new(font) { Origin = new PointF(16, 16) }; + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + // No reference comparison yet: the bug being isolated is still present, so DebugSave the output for + // inspection. Promote to CompareToReferenceOutput once the overlap-hole fill issue is fixed. + target.DebugSave(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawGlyphById_Inter_OverlappingContours_NoHoles(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + // The string-based DrawText path renders Inter cleanly; the Avalonia sample instead renders glyph + // by glyph through DrawText(glyphId, RichGlyphOptions, ...) (the RenderGlyph path). This test drives + // that exact path so we can see whether the holes originate there rather than in the shared fill. + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + + using (DrawingCanvas canvas = CreateCanvas(provider, target, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + + float penX = 16F; + const float baselineY = 130F; + foreach (char c in text) + { + if (!font.FontMetrics.TryGetGlyphMetrics( + new CodePoint(c), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)) + { + continue; + } + + RichGlyphOptions glyphOptions = new() + { + Font = font, + Origin = new Vector2(penX, baselineY) + }; + + canvas.DrawText(metrics.GlyphId, glyphOptions, Brushes.Solid(Color.Black), pen: null); + + penX += metrics.AdvanceWidth * font.Size / metrics.UnitsPerEm; + } + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawGlyphById_Inter_EvenOddCanvasState_NoHoles(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image target = provider.GetImage(); + + // The Avalonia backend wraps its glyph loop in PushDrawingState(), whose IntersectionRule defaults to + // EvenOdd. The previous glyph-id test uses a default (NonZero) canvas state and renders cleanly; + // this one replicates the sample's EvenOdd state to confirm the glyph fill's forced non-zero winding + // holds even when the canvas requests even-odd. If holes appear here, the force has a gap. + DrawingOptions options = new() + { + IntersectionRule = IntersectionRule.EvenOdd + }; + + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + + using (DrawingCanvas canvas = CreateCanvas(provider, target, options)) + { + canvas.Clear(Brushes.Solid(Color.White)); + + float penX = 16F; + const float baselineY = 130F; + foreach (char c in text) + { + if (!font.FontMetrics.TryGetGlyphMetrics( + new CodePoint(c), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)) + { + continue; + } + + RichGlyphOptions glyphOptions = new() + { + Font = font, + Origin = new Vector2(penX, baselineY) + }; + + canvas.DrawText(metrics.GlyphId, glyphOptions, Brushes.Solid(Color.Black), pen: null); + + penX += metrics.AdvanceWidth * font.Size / metrics.UnitsPerEm; + } + } + + target.DebugSave(provider, appendSourceFileOrDescription: false); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.Black), PixelTypes.Rgba32)] + public void DrawGlyphById_Inter_EvenOddCanvasState_MatchesNonZeroCanvasState(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + PointF origin = new(16F, 130F); + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + DrawGlyphs(canvas, text, font, origin); + } + + DrawingOptions evenOddOptions = new() + { + IntersectionRule = IntersectionRule.EvenOdd + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, evenOddOptions)) + { + DrawGlyphs(canvas, text, font, origin); + } + + expected.DebugSave(provider, "expected", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "actual", appendSourceFileOrDescription: false); + + ImageComparer.TolerantPercentage(0.005F).VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.Black), PixelTypes.Rgba32)] + public void DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + PointF origin = new(16F, 130F); + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + DrawGlyphs(canvas, text, font, origin); + } + + DrawingOptions clipOptions = new() + { + IntersectionRule = IntersectionRule.NonZero + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + _ = canvas.Save(clipOptions); + canvas.Clip(new RectanglePolygon(0, 0, 420, 180)); + DrawGlyphs(canvas, text, font, origin); + canvas.Restore(); + } + + expected.DebugSave(provider, "expected-unclipped", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "actual-clipped", appendSourceFileOrDescription: false); + + ImageComparer.TolerantPercentage(0.0027F).VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.Black), PixelTypes.Rgba32)] + public void DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + PointF origin = new(16F, 130F); + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + DrawGlyphs(canvas, text, font, origin); + } + + DrawingOptions evenOddClipOptions = new() + { + IntersectionRule = IntersectionRule.EvenOdd + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + _ = canvas.Save(evenOddClipOptions); + canvas.Clip(new RectanglePolygon(0, 0, 420, 180)); + DrawGlyphs(canvas, text, font, origin); + canvas.Restore(); + } + + expected.DebugSave(provider, "expected-unclipped", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "actual-clipped", appendSourceFileOrDescription: false); + + ImageComparer.TolerantPercentage(0.0027F).VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(420, 180, nameof(Color.Black), PixelTypes.Rgba32)] + public void DrawPositionedGlyphs_Inter_MatchesGlyphByIdLoop(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.InterLight, 32); + const string text = "Avalonia Test"; + PointF origin = new(16F, 130F); + ushort[] glyphIds = new ushort[text.Length]; + Vector2[] origins = new Vector2[text.Length]; + float penX = origin.X; + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + for (int i = 0; i < text.Length; i++) + { + if (!font.FontMetrics.TryGetGlyphMetrics( + new CodePoint(text[i]), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)) + { + continue; + } + + RichGlyphOptions options = new() + { + Font = font, + Origin = new Vector2(penX, origin.Y) + }; + + glyphIds[i] = metrics.GlyphId; + origins[i] = options.Origin; + canvas.DrawText(metrics.GlyphId, options, Brushes.Solid(Color.White), pen: null); + + penX += metrics.AdvanceWidth * font.Size / metrics.UnitsPerEm; + } + } + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + RichGlyphOptions options = new() { Font = font }; + canvas.DrawText(glyphIds, origins, options, Brushes.Solid(Color.White), pen: null); + } + + expected.DebugSave(provider, "per-glyph-loop", appendSourceFileOrDescription: false); + actual.DebugSave(provider, "batched-positioned-glyphs", appendSourceFileOrDescription: false); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + [Theory] [WithBlankImage(760, 320, PixelTypes.Rgba32)] public void DrawText_Multiline_WithLineMetricsGuides_MatchesReference(TestImageProvider provider) @@ -354,4 +639,201 @@ public void DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + string text = BuildNumberedLines(60); + + // Caller-supplied visible bounds always win, so an effectively unbounded rectangle + // renders every line and provides the unculled reference. + RichTextOptions unculledOptions = new(font) + { + Origin = new Vector2(8, 8), + VisibleBounds = new FontRectangle(-1e6F, -1e6F, 2e6F, 2e6F) + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(unculledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + RichTextOptions culledOptions = new(font) { Origin = new Vector2(8, 8) }; + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(culledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(240, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawText_TranslatedTransform_CulledOutputMatchesUnculled(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + string text = BuildNumberedLines(60); + + // The translation scrolls lines 240 to 360 of the text into view, so the culling band + // must follow the transform; a band left at the image rectangle would cull every + // visible line and produce a blank image. + DrawingOptions scrolled = new() + { + Transform = Matrix4x4.CreateTranslation(0, -240, 0) + }; + + RichTextOptions unculledOptions = new(font) + { + Origin = new Vector2(8, 8), + VisibleBounds = new FontRectangle(-1e6F, -1e6F, 2e6F, 2e6F) + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, scrolled)) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(unculledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + RichTextOptions culledOptions = new(font) { Origin = new Vector2(8, 8) }; + using (DrawingCanvas canvas = CreateCanvas(provider, actual, scrolled)) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(culledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + // Culling changes the run origin the batched glyph geometry is keyed against, and + // run-relative caching computes vertex positions as (absolute - origin) at bake time + // plus origin at raster time. Floating point addition is not associative, so the two + // renders' vertices differ by ULPs, and the rasterizer's sub-pixel grid snap can + // amplify an ULP into a single least-significant-bit coverage step on an occasional + // antialiased edge pixel. + ImageComparer.TolerantPercentage(0.005F).VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(240, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawText_RotatedTransform_CulledOutputMatchesUnculled(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image expected = provider.GetImage(); + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + string text = BuildNumberedLines(60); + + // Rotation moves text space away from device space, so culling must stand down; the + // rotation about the image center swings lines from below the straight-line band into + // view and any band applied regardless would drop them. + DrawingOptions rotated = new() + { + Transform = + Matrix4x4.CreateTranslation(-120, -60, 0) * + Matrix4x4.CreateRotationZ(MathF.PI / 2F) * + Matrix4x4.CreateTranslation(120, 60, 0) + }; + + RichTextOptions unculledOptions = new(font) + { + Origin = new Vector2(8, 8), + VisibleBounds = new FontRectangle(-1e6F, -1e6F, 2e6F, 2e6F) + }; + + using (DrawingCanvas canvas = CreateCanvas(provider, expected, rotated)) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(unculledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + RichTextOptions culledOptions = new(font) { Origin = new Vector2(8, 8) }; + using (DrawingCanvas canvas = CreateCanvas(provider, actual, rotated)) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(culledOptions, text, Brushes.Solid(Color.Black), pen: null); + } + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + + [Theory] + [WithSolidFilledImages(240, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawText_TextBlock_OffscreenLines_CulledOutputMatchesUnculled(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image actual = provider.GetImage(); + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + string text = BuildNumberedLines(60); + TextBlock block = new(text, new TextOptions(font)); + + // On a target tall enough for every line nothing is culled, so its top band is the + // unculled reference for the small target where most lines lie below the bottom edge. + using Image tall = new(provider.Configuration, actual.Width, 2200); + using (DrawingCanvas canvas = CreateCanvas(provider, tall, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(block, new PointF(8, 8), -1, Brushes.Solid(Color.Black), pen: null); + } + + using (DrawingCanvas canvas = CreateCanvas(provider, actual, new DrawingOptions())) + { + canvas.Clear(Brushes.Solid(Color.White)); + canvas.DrawText(block, new PointF(8, 8), -1, Brushes.Solid(Color.Black), pen: null); + } + + using Image expected = tall.Clone(ctx => ctx.Crop(new Rectangle(0, 0, actual.Width, actual.Height))); + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + + /// + /// Builds multi-line text whose numbered lines make any culled-but-visible line an exact + /// pixel difference. + /// + /// The number of lines. + /// The text. + private static string BuildNumberedLines(int count) + => string.Join('\n', Enumerable.Range(0, count).Select(static i => $"line {i}")); + + /// + /// Draws text through the glyph-id API used by the glyph regression tests. + /// + /// The canvas under test. + /// The text to draw. + /// The font used to resolve glyph ids and advances. + /// The baseline origin in the current canvas coordinate space. + private static void DrawGlyphs(DrawingCanvas canvas, string text, Font font, PointF origin) + where TPixel : unmanaged, IPixel + { + float penX = origin.X; + for (int i = 0; i < text.Length; i++) + { + if (!font.FontMetrics.TryGetGlyphMetrics( + new CodePoint(text[i]), + TextAttributes.None, + TextDecorations.None, + LayoutMode.HorizontalTopBottom, + ColorFontSupport.None, + out FontGlyphMetrics metrics)) + { + continue; + } + + RichGlyphOptions options = new() + { + Font = font, + Origin = new Vector2(penX, origin.Y) + }; + + canvas.DrawText(metrics.GlyphId, options, Brushes.Solid(Color.White), pen: null); + + penX += metrics.AdvanceWidth * font.Size / metrics.UnitsPerEm; + } + } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/DrawingClipStateTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/DrawingClipStateTests.cs new file mode 100644 index 000000000..2b0dca2b9 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/DrawingClipStateTests.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Drawing.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class DrawingClipStateTests +{ + private static DrawingClipDescriptor Rectangle(float x, float y, float width, float height) + => DrawingClipDescriptor.CreateRectangle( + new RectangleF(x, y, width, height), + ClipOperation.Intersection, + DrawingClipEdgeMode.Hard, + 0.5F); + + [Fact] + public void Empty_HasNoClips() + { + DrawingClipState state = DrawingClipState.Empty; + + Assert.Equal(0, state.Count); + Assert.False(state.HasClips); + Assert.False(state.TryGetConservativeBounds(Point.Empty, out _)); + Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); + } + + [Fact] + public void FromPaths_EmptyList_ReturnsEmpty() + { + DrawingClipState state = DrawingClipState.FromPaths([], ClipOperation.Intersection, DrawingClipEdgeMode.Hard, 0.5F); + + Assert.False(state.HasClips); + } + + [Fact] + public void FromPaths_MultiplePaths_FormOneOperand() + { + IPath[] paths = + [ + new RectanglePolygon(0, 0, 10, 10), + new RectanglePolygon(20, 0, 10, 10), + ]; + + DrawingClipState state = DrawingClipState.FromPaths(paths, ClipOperation.Intersection, DrawingClipEdgeMode.Antialiased, 0.5F); + + Assert.Equal(1, state.Count); + Assert.Equal(2, state.GetDescriptor(0).Paths.Count); + } + + [Fact] + public void AppendDescriptor_GrowsThroughInlineSlotsAndOverflow() + { + DrawingClipState state = DrawingClipState.Empty; + for (int i = 0; i < 6; i++) + { + state = state.Append(Rectangle(i * 10, 0, 10, 10)); + + Assert.Equal(i + 1, state.Count); + for (int j = 0; j <= i; j++) + { + Assert.Equal(new RectangleF(j * 10, 0, 10, 10), state.GetDescriptor(j).Rectangle); + } + } + } + + [Theory] + [InlineData(1, 1)] + [InlineData(1, 2)] + [InlineData(2, 2)] + [InlineData(3, 1)] + [InlineData(2, 3)] + [InlineData(4, 2)] + public void AppendState_PreservesStackOrder(int firstCount, int secondCount) + { + DrawingClipState first = DrawingClipState.Empty; + for (int i = 0; i < firstCount; i++) + { + first = first.Append(Rectangle(i * 10, 0, 10, 10)); + } + + DrawingClipState second = DrawingClipState.Empty; + for (int i = 0; i < secondCount; i++) + { + second = second.Append(Rectangle((firstCount + i) * 10, 0, 10, 10)); + } + + DrawingClipState combined = first.Append(second); + + Assert.Equal(firstCount + secondCount, combined.Count); + for (int i = 0; i < combined.Count; i++) + { + Assert.Equal(new RectangleF(i * 10, 0, 10, 10), combined.GetDescriptor(i).Rectangle); + } + } + + [Fact] + public void AppendState_WithEmptyOperands_ReturnsOtherOperand() + { + DrawingClipState populated = DrawingClipState.Empty.Append(Rectangle(0, 0, 10, 10)); + + Assert.Equal(1, DrawingClipState.Empty.Append(populated).Count); + Assert.Equal(1, populated.Append(DrawingClipState.Empty).Count); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + public void Translate_ShiftsEveryDescriptor(int count) + { + DrawingClipState state = DrawingClipState.Empty; + for (int i = 0; i < count; i++) + { + state = state.Append(Rectangle(i * 10, 0, 10, 10)); + } + + DrawingClipState translated = state.Translate(new Vector2(5, 7)); + + Assert.Equal(count, translated.Count); + for (int i = 0; i < count; i++) + { + Assert.Equal(new RectangleF((i * 10) + 5, 7, 10, 10), translated.GetDescriptor(i).Rectangle); + } + } + + [Fact] + public void Translate_ZeroOffset_ReturnsUnchangedState() + { + DrawingClipState state = DrawingClipState.Empty.Append(Rectangle(1, 2, 3, 4)); + DrawingClipState translated = state.Translate(Vector2.Zero); + + Assert.Equal(state.GetDescriptor(0).Rectangle, translated.GetDescriptor(0).Rectangle); + } + + [Theory] + [InlineData(1)] + [InlineData(4)] + [InlineData(5)] + public void Transform_TranslationMatrix_ShiftsEveryDescriptor(int count) + { + DrawingClipState state = DrawingClipState.Empty; + for (int i = 0; i < count; i++) + { + state = state.Append(Rectangle(i * 10, 0, 10, 10)); + } + + DrawingClipState transformed = state.Transform(Matrix4x4.CreateTranslation(5, 7, 0)); + + Assert.Equal(count, transformed.Count); + for (int i = 0; i < count; i++) + { + Assert.Equal(new RectangleF((i * 10) + 5, 7, 10, 10), transformed.GetDescriptor(i).Rectangle); + } + } + + [Fact] + public void Transform_Identity_ReturnsUnchangedState() + { + DrawingClipState state = DrawingClipState.Empty.Append(Rectangle(1, 2, 3, 4)); + DrawingClipState transformed = state.Transform(Matrix4x4.Identity); + + Assert.Equal(state.GetDescriptor(0).Rectangle, transformed.GetDescriptor(0).Rectangle); + } + + [Fact] + public void TryGetConservativeBounds_IntersectsIntersectionDescriptors() + { + DrawingClipState state = DrawingClipState.Empty + .Append(Rectangle(0, 0, 20, 20)) + .Append(Rectangle(10, 10, 20, 20)); + + Assert.True(state.TryGetConservativeBounds(new Point(100, 200), out Rectangle bounds)); + Assert.Equal(new Rectangle(110, 210, 10, 10), bounds); + } + + [Fact] + public void TryGetConservativeBounds_IgnoresDifferenceDescriptors() + { + DrawingClipDescriptor difference = DrawingClipDescriptor.CreateRectangle( + new RectangleF(0, 0, 10, 10), + ClipOperation.Difference, + DrawingClipEdgeMode.Hard, + 0.5F); + DrawingClipState state = DrawingClipState.Empty.Append(difference); + + Assert.False(state.TryGetConservativeBounds(Point.Empty, out _)); + } + + [Fact] + public void TryGetTargetBoundsClip_PixelAlignedSingleRectangle_ReturnsOffsetBounds() + { + DrawingClipState state = DrawingClipState.Empty.Append(Rectangle(10, 20, 30, 40)); + + Assert.True(state.TryGetTargetBoundsClip(new Point(1, 2), out Rectangle bounds)); + Assert.Equal(new Rectangle(11, 22, 30, 40), bounds); + } + + [Fact] + public void TryGetTargetBoundsClip_FractionalRectangle_ReturnsFalse() + { + DrawingClipState state = DrawingClipState.Empty.Append(Rectangle(10.5F, 20, 30, 40)); + + Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); + } + + [Fact] + public void TryGetTargetBoundsClip_MultipleDescriptors_ReturnsFalse() + { + DrawingClipState state = DrawingClipState.Empty + .Append(Rectangle(0, 0, 10, 10)) + .Append(Rectangle(0, 0, 10, 10)); + + Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); + } + + [Fact] + public void TryGetTargetBoundsClip_DifferenceOperation_ReturnsFalse() + { + DrawingClipDescriptor difference = DrawingClipDescriptor.CreateRectangle( + new RectangleF(0, 0, 10, 10), + ClipOperation.Difference, + DrawingClipEdgeMode.Hard, + 0.5F); + DrawingClipState state = DrawingClipState.Empty.Append(difference); + + Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); + } + + [Fact] + public void TryGetTargetBoundsClip_SingleRectangleIntegerRegion_ReturnsOffsetBounds() + { + DrawingClipDescriptor region = DrawingClipDescriptor.CreateIntegerRegion( + [new Rectangle(10, 20, 30, 40)], + ClipOperation.Intersection, + 0.5F); + DrawingClipState state = DrawingClipState.Empty.Append(region); + + Assert.True(state.TryGetTargetBoundsClip(new Point(1, 2), out Rectangle bounds)); + Assert.Equal(new Rectangle(11, 22, 30, 40), bounds); + } + + [Fact] + public void TryGetTargetBoundsClip_MultiRectangleIntegerRegion_ReturnsFalse() + { + DrawingClipDescriptor region = DrawingClipDescriptor.CreateIntegerRegion( + [new Rectangle(0, 0, 10, 10), new Rectangle(20, 0, 10, 10)], + ClipOperation.Intersection, + 0.5F); + DrawingClipState state = DrawingClipState.Empty.Append(region); + + Assert.False(state.TryGetTargetBoundsClip(Point.Empty, out _)); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AssociatedAlpha.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AssociatedAlpha.cs new file mode 100644 index 000000000..04e52554f --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.AssociatedAlpha.cs @@ -0,0 +1,802 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public partial class ProcessWithDrawingCanvasTests +{ + /// + /// Verifies every Drawing blend and composition selection against paired associated and + /// unassociated destinations. + /// + /// The color blending mode. + /// The alpha composition mode. + [Theory] + [MemberData(nameof(BlendingsModes))] + public void SolidBrushPreservesAlphaRepresentationForAllModeCombinations( + PixelColorBlendingMode blending, + PixelAlphaCompositionMode composition) + { + Rgba32 source = new(211, 47, 139, 149); + Color unassociatedSource = Color.FromPixel(source); + Color associatedSource = ToAssociatedColor(unassociatedSource); + DrawingOptions options = CreateBlendOptions(blending, composition); + + AssertBrushAssociationSimilarityForAllFormats(Brushes.Solid(unassociatedSource), Brushes.Solid(associatedSource), options); + } + + /// + /// Verifies linear gradients use CSS Color 4 associated-alpha interpolation. + /// + [Fact] + public void LinearGradientUsesCssColor4AlphaInterpolation() + => AssertGradientAssociationSimilarity(GradientKind.Linear, GradientRepetitionMode.Repeat, false); + + /// + /// Verifies radial gradients use CSS Color 4 associated-alpha interpolation. + /// + [Fact] + public void RadialGradientUsesCssColor4AlphaInterpolation() + => AssertGradientAssociationSimilarity(GradientKind.Radial, GradientRepetitionMode.Reflect, false); + + /// + /// Verifies elliptic gradients use CSS Color 4 associated-alpha interpolation. + /// + [Fact] + public void EllipticGradientUsesCssColor4AlphaInterpolation() + => AssertGradientAssociationSimilarity(GradientKind.Elliptic, GradientRepetitionMode.DontFill, false); + + /// + /// Verifies sweep gradients use CSS Color 4 associated-alpha interpolation. + /// + [Fact] + public void SweepGradientUsesCssColor4AlphaInterpolation() + => AssertGradientAssociationSimilarity(GradientKind.Sweep, GradientRepetitionMode.None, false); + + /// + /// Verifies equal gradient stops use the same representation-independent result as the + /// general interpolation path. + /// + [Fact] + public void EqualGradientStopsPreserveAlphaRepresentation() + => AssertGradientAssociationSimilarity(GradientKind.Linear, GradientRepetitionMode.None, true); + + /// + /// Verifies the midpoint oracle defined by CSS Color 4: interpolate associated components, + /// then unassociate only when storing into a straight-alpha destination. + /// + [Fact] + public void LinearGradientMidpointMatchesCssColor4() + { + ColorStop[] stops = + [ + new(0F, Color.FromPixel(new Rgba32(255, 0, 0, 255))), + new(1F, Color.FromPixel(new Rgba32(0, 0, 255, 0))) + ]; + LinearGradientBrush brush = new(new PointF(0, 0), new PointF(1, 0), GradientRepetitionMode.None, stops); + using Image unassociated = new(1, 1); + using Image associated = new(1, 1); + + unassociated.Mutate(context => context.Paint(canvas => canvas.Fill(brush))); + associated.Mutate(context => context.Paint(canvas => canvas.Fill(brush))); + + // At the sole pixel center t is exactly 0.5. Associated interpolation between opaque + // red (1, 0, 0, 1) and transparent blue (0, 0, 0, 0) yields (0.5, 0, 0, 0.5). + // The straight destination unassociates that value to (1, 0, 0, 0.5). + Assert.Equal(new Rgba32(255, 0, 0, 128), unassociated[0, 0]); + Assert.Equal(new Rgba32P(128, 0, 0, 128), associated[0, 0]); + } + + /// + /// Verifies associated destinations compare associated color components. + /// + [Fact] + public void RecolorBrushAssociatedDestinationUsesAssociatedComponents() + { + Rgba32 background = new(0, 255, 0, 26); + Color source = Color.FromPixel(new Rgba32(255, 0, 0, 26)); + Color target = Color.FromPixel(new Rgba32(0, 0, 255, 211)); + const float threshold = 0.1F; + RecolorBrush brush = new(source, target, threshold); + DrawingOptions options = CreateBlendOptions(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src); + using Image actual = RenderRecolor(background, brush, options); + Rgba32P backgroundPixel = Color.FromPixel(background).ToPixel(); + Rgba32P targetPixel = target.ToPixel(); + float scaledThreshold = threshold * 4F; + float distance = Vector4.DistanceSquared(backgroundPixel.ToScaledVector4(), source.ToScaledVector4(PixelAlphaRepresentation.Associated)); + float amount = (scaledThreshold - distance) / scaledThreshold; + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options.GraphicsOptions); + Rgba32P expected = blender.Blend(backgroundPixel, targetPixel, amount); + + // The straight RGB distance is two, but multiplying both colors by their low alpha + // reduces the native associated distance enough for the pixel to be recolored. + Assert.Equal(expected, actual[4, 4]); + Assert.NotEqual(backgroundPixel, actual[4, 4]); + } + + /// + /// Verifies unassociated destinations compare unassociated color components. + /// + [Fact] + public void RecolorBrushUnassociatedDestinationUsesUnassociatedComponents() + { + Rgba32 background = new(0, 255, 0, 26); + Color source = Color.FromPixel(new Rgba32(255, 0, 0, 26)); + Color target = Color.FromPixel(new Rgba32(0, 0, 255, 211)); + RecolorBrush brush = new(source, target, 0.1F); + DrawingOptions options = CreateBlendOptions(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src); + using Image actual = RenderRecolor(background, brush, options); + + // The native straight RGB distance is two, which exceeds the scaled threshold of 0.4. + Assert.Equal(background, actual[4, 4]); + } + + /// + /// Verifies a high-precision source key is not quantized through the destination pixel + /// format before comparison. + /// + [Fact] + public void RecolorBrushSourceKeyIsNotQuantizedThroughDestinationFormat() + { + Color source = Color.FromScaledVector(new Vector4(0.5F, 0F, 0F, 1F)); + Rgba32 quantizedSource = source.ToPixel(); + const float threshold = 0.0000005F; + RecolorBrush brush = new(source, Color.Lime, threshold); + DrawingOptions options = CreateBlendOptions(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src); + using Image actual = RenderRecolor(quantizedSource, brush, options); + float directDistance = Vector4.DistanceSquared(quantizedSource.ToScaledVector4(), source.ToScaledVector4(PixelAlphaRepresentation.Unassociated)); + + // Quantizing 0.5 to Rgba32 produces 128/255. That pixel would be an exact match if the + // renderer first converted its key to Rgba32, while the direct Color distance remains + // just outside this deliberately narrow threshold. + Assert.Equal(new Rgba32(128, 0, 0, 255), quantizedSource); + Assert.True(directDistance > threshold * 4F); + Assert.Equal(quantizedSource, actual[4, 4]); + } + + /// + /// Verifies Recolor uses the selected color blending and alpha composition modes for its + /// replacement blend in every supported alpha representation. + /// + [Fact] + public void RecolorBrushUsesConfiguredBlenderForAllFormats() + { + Rgba32 source = new(61, 137, 223, 255); + Rgba32 target = new(229, 83, 37, 109); + DrawingOptions options = CreateBlendOptions(PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcOver); + + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + AssertRecolorUsesConfiguredBlender(source, target, options); + + AssertRecolorUsesConfiguredBlender(source, target, options); + } + + /// + /// Verifies pattern colors and destinations preserve their alpha representation. + /// + [Fact] + public void PatternBrushPreservesAlphaRepresentation() + { + Color foreground = Color.FromPixel(new Rgba32(227, 41, 113, 149)); + Color background = Color.FromPixel(new Rgba32(29, 191, 73, 67)); + bool[,] pattern = { { true, false }, { false, true } }; + PatternBrush unassociatedBrush = new(foreground, background, pattern); + PatternBrush associatedBrush = new(ToAssociatedColor(foreground), ToAssociatedColor(background), pattern); + + AssertBrushAssociationSimilarityForAllFormats(unassociatedBrush, associatedBrush, new DrawingOptions()); + } + + /// + /// Verifies image brushes preserve source and destination alpha representation for all CPU format pairs. + /// + [Fact] + public void ImageBrushPreservesAlphaRepresentation() + { + AssertImageBrushAssociationSimilarity(); + AssertImageBrushAssociationSimilarity(); + AssertImageBrushAssociationSimilarity(); + AssertImageBrushAssociationSimilarity(); + AssertImageBrushAssociationSimilarity(); + AssertImageBrushAssociationSimilarity(); + } + + /// + /// Verifies image drawing preserves source and destination alpha representation for all CPU format pairs. + /// + [Fact] + public void DrawImagePreservesAlphaRepresentation() + { + AssertDrawImageAssociationSimilarity(); + AssertDrawImageAssociationSimilarity(); + AssertDrawImageAssociationSimilarity(); + AssertDrawImageAssociationSimilarity(); + AssertDrawImageAssociationSimilarity(); + AssertDrawImageAssociationSimilarity(); + } + + /// + /// Verifies clipped drawing preserves associated destination storage. + /// + [Fact] + public void ClipPreservesAlphaRepresentation() + { + EllipsePolygon clip = new(new PointF(24, 24), new SizeF(34, 28)); + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(231, 47, 113, 149))); + + AssertCanvasSceneAssociationSimilarity(canvas => + { + canvas.Save(); + canvas.Clip(ClipOperation.Intersection, clip); + canvas.Fill(brush, new RectanglePolygon(4, 6, 40, 36)); + canvas.Restore(); + }); + } + + /// + /// Verifies layer isolation and restore preserve associated destination storage. + /// + [Fact] + public void SaveLayerPreservesAlphaRepresentation() + { + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(37, 211, 89, 173))); + + AssertCanvasSceneAssociationSimilarity(canvas => + { + canvas.SaveLayer(new GraphicsOptions { BlendPercentage = 0.55F }, new Rectangle(6, 8, 36, 32)); + canvas.Fill(brush, new EllipsePolygon(new PointF(25, 23), new SizeF(30, 26))); + canvas.Restore(); + }); + } + + /// + /// Verifies glyph coverage and brush blending preserve associated destination storage. + /// + [Fact] + public void TextPreservesAlphaRepresentation() + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24F); + RichTextOptions options = new(font) { Origin = new PointF(3, 8) }; + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(227, 61, 149, 181))); + + AssertCanvasSceneAssociationSimilarity(canvas => canvas.DrawText(options, "Alpha", brush, pen: null)); + } + + /// + /// Verifies processing a queued canvas region preserves associated destination storage. + /// + [Fact] + public void ApplyPreservesAlphaRepresentation() + { + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(41, 193, 227, 157))); + + AssertCanvasSceneAssociationSimilarity(canvas => + { + canvas.Fill(brush, new EllipsePolygon(new PointF(24, 24), new SizeF(32, 30))); + canvas.Apply(new Rectangle(8, 8, 32, 32), context => context.Invert()); + }); + } + + /// + /// Verifies backdrop filtering preserves associated destination storage. + /// + [Fact] + public void BackdropLayerEffectPreservesAlphaRepresentation() + { + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(217, 79, 31, 163))); + BackdropLayerEffect effect = new BackdropAcrylicLayerEffect(1F, Color.FromPixel(new Rgba32(73, 149, 239, 91))); + + AssertCanvasSceneAssociationSimilarity(canvas => + { + canvas.Fill(brush, new EllipsePolygon(new PointF(20, 20), new SizeF(30, 28))); + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(12, 10, 30, 32), effect); + canvas.Restore(); + }); + } + + /// + /// Verifies foreground layer effects preserve associated destination storage. + /// + [Fact] + public void LayerEffectPreservesAlphaRepresentation() + { + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(237, 173, 43, 187))); + + // Keep this comparison to one native quantization boundary. The separate blurred test + // covers the multi-pass processor, whose TPixel intermediate necessarily quantizes + // associated and unassociated 8-bit storage at different points. + LayerEffect effect = new DropShadowLayerEffect(new Point(3, 2), 0F, Color.FromPixel(new Rgba32(31, 59, 211, 109))); + + AssertCanvasSceneAssociationSimilarity(canvas => + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(6, 6, 36, 34), effect); + canvas.Fill(brush, new RoundedRectanglePolygon(new RectangleF(12, 12, 24, 20), 5F)); + canvas.Restore(); + }); + } + + /// + /// Verifies a blurred foreground layer effect preserves native channel layout and associated + /// storage while producing identical alpha in associated and unassociated destinations. + /// + [Fact] + public void BlurredLayerEffectPreservesAssociationAndChannelLayout() + { + Brush brush = Brushes.Solid(Color.FromPixel(new Rgba32(237, 173, 43, 187))); + LayerEffect effect = new DropShadowLayerEffect(new Point(3, 2), 1F, Color.FromPixel(new Rgba32(31, 59, 211, 109))); + CanvasAction draw = canvas => + { + canvas.SaveLayer(new GraphicsOptions(), new Rectangle(6, 6, 36, 34), effect); + canvas.Fill(brush, new RoundedRectanglePolygon(new RectangleF(12, 12, 24, 20), 5F)); + canvas.Restore(); + }; + + using Image rgba = RenderCanvasScene(draw); + using Image bgra = RenderCanvasScene(draw); + using Image rgbaAssociated = RenderCanvasScene(draw); + using Image bgraAssociated = RenderCanvasScene(draw); + + // Gaussian blur stores its horizontal pass in the native TPixel before the vertical pass. + // Associated and unassociated 8-bit formats therefore cross different quantization + // lattices more than once. ImageSharp's processor tests verify the floating-point + // association math; this integration test compares each native representation exactly + // and requires alpha, which does not depend on RGB association, to remain identical. + for (int y = 0; y < rgba.Height; y++) + { + for (int x = 0; x < rgba.Width; x++) + { + Rgba32 rgbaPixel = rgba[x, y]; + Bgra32 bgraPixel = bgra[x, y]; + Rgba32P rgbaAssociatedPixel = rgbaAssociated[x, y]; + Bgra32P bgraAssociatedPixel = bgraAssociated[x, y]; + + Assert.Equal(rgbaPixel, new Rgba32(bgraPixel.R, bgraPixel.G, bgraPixel.B, bgraPixel.A)); + Assert.Equal(rgbaAssociatedPixel, new Rgba32P(bgraAssociatedPixel.R, bgraAssociatedPixel.G, bgraAssociatedPixel.B, bgraAssociatedPixel.A)); + Assert.Equal(rgbaPixel.A, rgbaAssociatedPixel.A); + Assert.Equal(bgraPixel.A, bgraAssociatedPixel.A); + } + } + + AssertAssociatedStorage(rgbaAssociated); + AssertAssociatedStorage(bgraAssociated); + } + + /// + /// Creates equivalent gradient brushes from associated and unassociated color inputs and + /// verifies their output across every paired destination format. + /// + /// The gradient family to render. + /// The repetition mode to exercise. + /// Whether both stops describe the same color. + private static void AssertGradientAssociationSimilarity(GradientKind kind, GradientRepetitionMode repetitionMode, bool equalStops) + { + Rgba32[] colors = equalStops + ? [new(197, 71, 149, 137), new(197, 71, 149, 137)] + : [new(239, 37, 19, 211), new(17, 227, 83, 0), new(43, 79, 241, 73)]; + + ColorStop[] unassociatedStops = new ColorStop[colors.Length]; + ColorStop[] associatedStops = new ColorStop[colors.Length]; + + for (int i = 0; i < colors.Length; i++) + { + float ratio = (float)i / (colors.Length - 1); + unassociatedStops[i] = new ColorStop(ratio, Color.FromPixel(colors[i])); + associatedStops[i] = new ColorStop(ratio, ToAssociatedColor(unassociatedStops[i].Color)); + } + + Brush unassociatedBrush = CreateGradient(kind, repetitionMode, unassociatedStops); + Brush associatedBrush = CreateGradient(kind, repetitionMode, associatedStops); + DrawingOptions options = CreateBlendOptions(PixelColorBlendingMode.Normal, PixelAlphaCompositionMode.Src); + + // Src composition isolates gradient interpolation and destination encoding from an + // additional source-over quantization boundary in the canvas blender. + AssertBrushAssociationSimilarityForAllFormats(unassociatedBrush, associatedBrush, options); + AssertBrushDestinationAssociationSimilarityForAllFormats(unassociatedBrush, options); + AssertBrushDestinationAssociationSimilarityForAllFormats(associatedBrush, options); + } + + /// + /// Creates one gradient family for the shared alpha-representation test matrix. + /// + /// The gradient family. + /// The repetition mode. + /// The color stops. + /// The configured gradient brush. + private static Brush CreateGradient(GradientKind kind, GradientRepetitionMode repetitionMode, ColorStop[] colorStops) + => kind switch + { + GradientKind.Linear => new LinearGradientBrush(new PointF(-4, 2), new PointF(12, 6), repetitionMode, colorStops), + GradientKind.Radial => new RadialGradientBrush(new PointF(4, 4), 5F, repetitionMode, colorStops), + GradientKind.Elliptic => new EllipticGradientBrush(new PointF(4, 4), new PointF(9, 4), 0.6F, repetitionMode, colorStops), + _ => new SweepGradientBrush(new PointF(4, 4), -45F, 315F, repetitionMode, colorStops) + }; + + /// + /// Verifies one brush against every associated and unassociated pixel-format pair. + /// + /// The brush created from unassociated colors. + /// The brush created from associated colors. + /// The drawing options. + private static void AssertBrushAssociationSimilarityForAllFormats(Brush unassociatedBrush, Brush associatedBrush, DrawingOptions options) + { + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + AssertBrushAssociationSimilarity(unassociatedBrush, associatedBrush, options); + } + + /// + /// Verifies one logical brush through both input-color representations and both destination + /// representations for a pixel-format pair. + /// + /// The unassociated destination format. + /// The associated destination format. + /// The brush created from unassociated colors. + /// The brush created from associated colors. + /// The drawing options. + private static void AssertBrushAssociationSimilarity(Brush unassociatedBrush, Brush associatedBrush, DrawingOptions options) + where TUnassociated : unmanaged, IPixel + where TAssociated : unmanaged, IPixel + { + using Image unassociatedDestination = RenderBrush(unassociatedBrush, options); + using Image associatedInput = RenderBrush(associatedBrush, options); + using Image associatedDestination = RenderBrush(unassociatedBrush, options); + using Image associatedInputAndDestination = RenderBrush(associatedBrush, options); + + AssertAssociationSimilarity(unassociatedDestination, associatedInput); + AssertAssociationSimilarity(associatedDestination, associatedInputAndDestination); + + AssertAssociatedStorage(associatedDestination); + AssertAssociatedStorage(associatedInputAndDestination); + } + + /// + /// Compares one brush directly across every associated and unassociated destination pair. + /// + /// The brush to render. + /// The drawing options. + private static void AssertBrushDestinationAssociationSimilarityForAllFormats(Brush brush, DrawingOptions options) + { + AssertBrushDestinationAssociationSimilarity(brush, options); + AssertBrushDestinationAssociationSimilarity(brush, options); + AssertBrushDestinationAssociationSimilarity(brush, options); + AssertBrushDestinationAssociationSimilarity(brush, options); + AssertBrushDestinationAssociationSimilarity(brush, options); + AssertBrushDestinationAssociationSimilarity(brush, options); + } + + /// + /// Compares one brush directly across an associated and unassociated destination pair. + /// + /// The unassociated destination format. + /// The associated destination format. + /// The brush to render. + /// The drawing options. + private static void AssertBrushDestinationAssociationSimilarity(Brush brush, DrawingOptions options) + where TUnassociated : unmanaged, IPixel + where TAssociated : unmanaged, IPixel + { + using Image unassociated = RenderBrush(brush, options); + using Image associated = RenderBrush(brush, options); + + AssertAssociationSimilarity(unassociated, associated); + AssertAssociatedStorage(associated); + } + + /// + /// Renders a brush over the same logical translucent background in the requested pixel format. + /// + /// The destination pixel format. + /// The brush to render. + /// The drawing options. + /// The rendered image. + private static Image RenderBrush(Brush brush, DrawingOptions options) + where TPixel : unmanaged, IPixel + { + Rgba32 background = new(31, 87, 143, 113); + Image image = new(8, 8, Color.FromPixel(background).ToPixel()); + + image.Mutate(context => context.Paint(options, canvas => canvas.Fill(brush))); + + return image; + } + + /// + /// Renders a recolor brush over alternating matching and non-matching pixels. + /// + /// The destination pixel format. + /// The logical matching color. + /// The recolor brush. + /// The drawing options. + /// The rendered image. + private static Image RenderRecolor(Rgba32 source, RecolorBrush brush, DrawingOptions options) + where TPixel : unmanaged, IPixel + { + Image image = new(8, 8); + Rgba32 other = new(19, 211, 101, 197); + + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = Color.FromPixel(((x + y) & 1) == 0 ? source : other).ToPixel(); + } + } + }); + + image.Mutate(context => context.Paint(options, canvas => canvas.Fill(brush))); + + return image; + } + + /// + /// Compares Recolor output with an independently selected pixel blender for one format. + /// + /// The destination pixel format. + /// The logical destination and matching color. + /// The logical replacement color. + /// The drawing options selecting the blender. + private static void AssertRecolorUsesConfiguredBlender(Rgba32 source, Rgba32 target, DrawingOptions options) + where TPixel : unmanaged, IPixel + { + RecolorBrush brush = new(Color.FromPixel(source), Color.FromPixel(target), 0.01F); + using Image actual = RenderRecolor(source, brush, options); + TPixel sourcePixel = Color.FromPixel(source).ToPixel(); + TPixel targetPixel = Color.FromPixel(target).ToPixel(); + PixelBlender blender = PixelOperations.Instance.GetPixelBlender(options.GraphicsOptions); + + // An exact key match has replacement amount one. Full shape coverage then applies the + // same configured blender a second time with the DrawingOptions blend percentage. + TPixel overlay = blender.Blend(sourcePixel, targetPixel, 1F); + TPixel expected = blender.Blend(sourcePixel, overlay, options.GraphicsOptions.BlendPercentage); + + Assert.Equal(expected, actual[4, 4]); + } + + /// + /// Verifies all four image-brush source and destination association combinations for one format pair. + /// + /// The unassociated source and destination format. + /// The associated source and destination format. + private static void AssertImageBrushAssociationSimilarity() + where TUnassociated : unmanaged, IPixel + where TAssociated : unmanaged, IPixel + { + using Image unassociatedSource = CreateAssociationSource(); + using Image associatedSource = CreateAssociationSource(); + using Image unassociatedSourceAndDestination = RenderImageBrush(unassociatedSource); + using Image associatedSourceUnassociatedDestination = RenderImageBrush(associatedSource); + using Image unassociatedSourceAssociatedDestination = RenderImageBrush(unassociatedSource); + using Image associatedSourceAndDestination = RenderImageBrush(associatedSource); + + AssertAssociationSimilarity(unassociatedSourceAndDestination, associatedSourceUnassociatedDestination); + AssertAssociationSimilarity(unassociatedSourceAssociatedDestination, associatedSourceAndDestination); + AssertAssociatedStorage(unassociatedSourceAssociatedDestination); + AssertAssociatedStorage(associatedSourceAndDestination); + } + + /// + /// Verifies all four image-drawing source and destination association combinations for one format pair. + /// + /// The unassociated source and destination format. + /// The associated source and destination format. + private static void AssertDrawImageAssociationSimilarity() + where TUnassociated : unmanaged, IPixel + where TAssociated : unmanaged, IPixel + { + using Image unassociatedSource = CreateAssociationSource(); + using Image associatedSource = CreateAssociationSource(); + using Image normalizedAssociatedSource = associatedSource.CloneAs(); + using Image unassociatedSourceAndDestination = RenderDrawImage(unassociatedSource); + using Image associatedSourceUnassociatedDestination = RenderDrawImage(associatedSource); + using Image unassociatedSourceAssociatedDestination = RenderDrawImage(unassociatedSource); + using Image associatedSourceAndDestination = RenderDrawImage(associatedSource); + + // DrawImage normalizes a source whose format differs from its destination before resampling it. + // Verify that representation conversion independently so a source-conversion failure cannot be mistaken for a resampling failure. + AssertAssociationSimilarity(unassociatedSource, normalizedAssociatedSource); + AssertAssociationSimilarity(unassociatedSourceAndDestination, associatedSourceUnassociatedDestination); + AssertAssociationSimilarity(unassociatedSourceAssociatedDestination, associatedSourceAndDestination); + AssertAssociatedStorage(unassociatedSourceAssociatedDestination); + AssertAssociatedStorage(associatedSourceAndDestination); + } + + /// + /// Creates a translucent source image in the requested representation. + /// + /// The source pixel format. + /// The populated source image. + private static Image CreateAssociationSource() + where TPixel : unmanaged, IPixel + { + Rgba32[] colors = [new(239, 37, 19, 211), new(17, 227, 83, 0), new(43, 79, 241, 73), new(191, 113, 47, 157)]; + Image image = new(4, 4); + + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = Color.FromPixel(colors[(x + y) % colors.Length]).ToPixel(); + } + } + }); + + return image; + } + + /// + /// Renders an image brush into the requested destination representation. + /// + /// The source pixel format. + /// The destination pixel format. + /// The source image. + /// The rendered destination. + private static Image RenderImageBrush(Image source) + where TSource : unmanaged, IPixel + where TDestination : unmanaged, IPixel + { + Image destination = new(8, 8, Color.FromPixel(new Rgba32(31, 87, 143, 113)).ToPixel()); + ImageBrush brush = new(source, WrapMode.Mirror, WrapMode.Repeat); + + destination.Mutate(context => context.Paint(canvas => canvas.Fill(brush))); + + return destination; + } + + /// + /// Draws an image into the requested destination representation. + /// + /// The source pixel format. + /// The destination pixel format. + /// The source image. + /// The rendered destination. + private static Image RenderDrawImage(Image source) + where TSource : unmanaged, IPixel + where TDestination : unmanaged, IPixel + { + Image destination = new(8, 8, Color.FromPixel(new Rgba32(31, 87, 143, 113)).ToPixel()); + + destination.Mutate(context => context.Paint(canvas => canvas.DrawImage(source, source.Bounds, destination.Bounds))); + + return destination; + } + + /// + /// Verifies one canvas scene against the two direct 8-bit associated destination layouts. + /// + /// The scene to render. + private static void AssertCanvasSceneAssociationSimilarity(CanvasAction draw) + { + using Image rgba = RenderCanvasScene(draw); + using Image rgbaAssociated = RenderCanvasScene(draw); + using Image bgra = RenderCanvasScene(draw); + using Image bgraAssociated = RenderCanvasScene(draw); + + AssertAssociationSimilarity(rgba, rgbaAssociated); + AssertAssociationSimilarity(bgra, bgraAssociated); + AssertAssociatedStorage(rgbaAssociated); + AssertAssociatedStorage(bgraAssociated); + } + + /// + /// Renders one canvas scene into the requested destination representation. + /// + /// The destination pixel format. + /// The scene to render. + /// The rendered image. + private static Image RenderCanvasScene(CanvasAction draw) + where TPixel : unmanaged, IPixel + { + TPixel background = Color.FromPixel(new Rgba32(29, 83, 137, 107)).ToPixel(); + Image image = new(48, 48, background); + + image.Mutate(context => context.Paint(draw)); + + return image; + } + + /// + /// Verifies that crossing an 8-bit association boundary changes the canonical associated + /// color by at most one byte while preserving alpha exactly. + /// + /// The expected image pixel format. + /// The actual image pixel format. + /// The expected image. + /// The actual image. + private static void AssertAssociationSimilarity(Image expected, Image actual) + where TExpected : unmanaged, IPixel + where TActual : unmanaged, IPixel + { + expected.ProcessPixelRows(actual, (expectedAccessor, actualAccessor) => + { + for (int y = 0; y < expectedAccessor.Height; y++) + { + Span expectedRow = expectedAccessor.GetRowSpan(y); + Span actualRow = actualAccessor.GetRowSpan(y); + + for (int x = 0; x < expectedRow.Length; x++) + { + Rgba32P expectedValue = Color.FromPixel(expectedRow[x]).ToPixel(); + Rgba32P actualValue = Color.FromPixel(actualRow[x]).ToPixel(); + + // Unassociation divides component quantization error by alpha, so comparing + // straight values would manufacture larger differences at low alpha. The + // canonical associated byte representation retains the strict one-byte + // component limit without floating-point normalization error. + int redDifference = Math.Abs(expectedValue.R - actualValue.R); + int greenDifference = Math.Abs(expectedValue.G - actualValue.G); + int blueDifference = Math.Abs(expectedValue.B - actualValue.B); + + string failure = $"{typeof(TExpected).Name} -> {typeof(TActual).Name} at ({x}, {y}): expected {expectedValue}, actual {actualValue}."; + Assert.True(redDifference <= 1, failure); + Assert.True(greenDifference <= 1, failure); + Assert.True(blueDifference <= 1, failure); + Assert.Equal(expectedValue.A, actualValue.A); + } + } + }); + } + + /// + /// Creates an associated color without introducing an intermediate pixel quantization. + /// + /// The unassociated color. + /// The same color expressed with associated components. + private static Color ToAssociatedColor(Color color) + => Color.FromScaledVector(color.ToScaledVector4(PixelAlphaRepresentation.Associated), PixelAlphaRepresentation.Associated); + + /// + /// Verifies that every stored associated color component is bounded by its alpha component. + /// + /// The associated pixel format. + /// The associated image. + private static void AssertAssociatedStorage(Image image) + where TPixel : unmanaged, IPixel + => image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + TPixel pixel = row[x]; + Vector4 value = pixel.ToScaledVector4(); + Assert.True(value.X <= value.W && value.Y <= value.W && value.Z <= value.W, $"{typeof(TPixel).Name} at ({x}, {y}) stored {value}."); + } + } + }); + + /// + /// Identifies the gradient family used by the shared representation test. + /// + private enum GradientKind + { + Linear, + Radial, + Elliptic, + Sweep + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs index 617954080..ba146c657 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Clip.cs @@ -57,11 +57,11 @@ public void ClipIssue250VerticalHorizontalCountShouldMatch() Path vertical = new(new LinearLineSegment(new PointF(26, 384), new PointF(26, 163))); Path horizontal = new(new LinearLineSegment(new PointF(26, 163), new PointF(176, 163))); - IPath reverse = vertical.Clip(clip); - int verticalCount = vertical.Clip(reverse).Flatten().Select(x => x.Points).Count(); + IPath reverse = vertical.Clip(BooleanOperation.Difference, clip); + int verticalCount = vertical.Clip(BooleanOperation.Difference, reverse).Flatten().Select(x => x.Points).Count(); - reverse = horizontal.Clip(clip); - int horizontalCount = horizontal.Clip(reverse).Flatten().Select(x => x.Points).Count(); + reverse = horizontal.Clip(BooleanOperation.Difference, clip); + int horizontalCount = horizontal.Clip(BooleanOperation.Difference, reverse).Flatten().Select(x => x.Points).Count(); Assert.Equal(verticalCount, horizontalCount); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs index 1e4c251b4..17bd31b32 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.GradientBrushes.cs @@ -113,7 +113,7 @@ public void SweepGradientBrush_Transform_TranslationMovesCenter() Matrix4x4 matrix = Matrix4x4.CreateTranslation(50F, 30F, 0F); - SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix)); + SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix, default, default)); Assert.Equal(150F, transformed.Center.X, 0.01F); Assert.Equal(130F, transformed.Center.Y, 0.01F); @@ -135,7 +135,7 @@ public void SweepGradientBrush_Transform_RotationRotatesAngles() // which corresponds to counter-clockwise on the design grid. Matrix4x4 matrix = Matrix4x4.CreateRotationZ(MathF.PI / 2F); - SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix)); + SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix, default, default)); // The 90-degree sweep should be preserved. float sweep = transformed.EndAngleDegrees - transformed.StartAngleDegrees; @@ -156,7 +156,7 @@ public void SweepGradientBrush_Transform_ReflectionFlipsSweepDirection() // Reflect across Y axis (negative determinant). Matrix4x4 matrix = Matrix4x4.CreateScale(-1F, 1F, 1F); - SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix)); + SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix, default, default)); // Reflection should flip the sweep direction: positive 90 becomes negative 90. float sweep = transformed.EndAngleDegrees - transformed.StartAngleDegrees; @@ -179,7 +179,7 @@ public void SweepGradientBrush_Transform_FullSweepPreserved() Matrix4x4.CreateScale(2F) * Matrix4x4.CreateTranslation(10F, 20F, 0F); - SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix)); + SweepGradientBrush transformed = Assert.IsType(brush.Transform(matrix, default, default)); // Full sweep should remain a full 360 degrees. float sweep = MathF.Abs(transformed.EndAngleDegrees - transformed.StartAngleDegrees); @@ -202,7 +202,7 @@ public void RadialGradientBrush_Transform_UsesAverageScaleForRadii() Matrix4x4.CreateScale(2F, 4F, 1F) * Matrix4x4.CreateTranslation(5F, 7F, 0F); - RadialGradientBrush transformed = Assert.IsType(brush.Transform(matrix)); + RadialGradientBrush transformed = Assert.IsType(brush.Transform(matrix, default, default)); Assert.Equal(PointF.Transform(brush.Center0, matrix), transformed.Center0); Assert.Equal(PointF.Transform(brush.Center1.Value, matrix), transformed.Center1.Value); @@ -274,6 +274,78 @@ public void FillEllipticGradientBrushAxisParallelEllipsesWithDifferentRatio(TestImageProvider provider, float degrees) + where TPixel : unmanaged, IPixel + { + // The elliptic gradient must be rotation-covariant: sampling a rotated ellipse at the + // rotated location of a point must produce the same value the axis-aligned ellipse + // produces at the original point. A mirrored (sign-flipped) rotation breaks this for + // every angle that is not a multiple of 90 degrees. + const float majorRadius = 60F; + const float axisRatio = 0.4F; + PointF center = new(100, 100); + float radians = GeometryUtilities.DegreeToRadian(degrees); + float cos = MathF.Cos(radians); + float sin = MathF.Sin(radians); + + EllipticGradientBrush axisAlignedBrush = new( + center, + new PointF(center.X + majorRadius, center.Y), + axisRatio, + GradientRepetitionMode.None, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Black)); + + EllipticGradientBrush rotatedBrush = new( + center, + new PointF(center.X + (majorRadius * cos), center.Y + (majorRadius * sin)), + axisRatio, + GradientRepetitionMode.None, + new ColorStop(0, Color.Yellow), + new ColorStop(1, Color.Black)); + + using Image axisAlignedImage = provider.GetImage(); + using Image rotatedImage = provider.GetImage(); + axisAlignedImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(axisAlignedBrush))); + rotatedImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(rotatedBrush))); + + // Sample fractions along both ellipse axes. Rotated sample points land between pixel + // centers, so allow a small per-channel tolerance for the sub-pixel rounding; the + // mirror bug produces differences an order of magnitude larger at 30/45/120 degrees. + const int channelTolerance = 24; + foreach (float fraction in new[] { 0.25F, 0.5F, 0.75F }) + { + // Major axis: axis-aligned sample at (f * a, 0); rotated sample at the rotated point. + AssertRotatedSampleMatches(fraction * majorRadius, 0F); + + // Minor axis: axis-aligned sample at (0, f * b); rotated sample at the rotated point. + AssertRotatedSampleMatches(0F, fraction * majorRadius * axisRatio); + } + + void AssertRotatedSampleMatches(float localX, float localY) + { + Point axisAlignedPoint = new( + (int)MathF.Round(center.X + localX), + (int)MathF.Round(center.Y + localY)); + Point rotatedPoint = new( + (int)MathF.Round(center.X + (localX * cos) - (localY * sin)), + (int)MathF.Round(center.Y + (localX * sin) + (localY * cos))); + + Rgba32 expected = axisAlignedImage[axisAlignedPoint.X, axisAlignedPoint.Y].ToRgba32(); + Rgba32 actual = rotatedImage[rotatedPoint.X, rotatedPoint.Y].ToRgba32(); + + Assert.True( + Math.Abs(expected.R - actual.R) <= channelTolerance && + Math.Abs(expected.G - actual.G) <= channelTolerance && + Math.Abs(expected.B - actual.B) <= channelTolerance, + $"Rotated elliptic gradient diverged at local ({localX}, {localY}) for {degrees} degrees: expected {expected}, actual {actual}."); + } + } + [Theory] [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.1, 0)] [WithBlankImage(200, 200, PixelTypes.Rgba32, 0.4, 0)] diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs index d2eb74483..f4471fe60 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.PathGradientBrushes.cs @@ -46,9 +46,31 @@ public void FillPathGradientBrushFillTriangleWithDifferentColors(TestIma image.DebugSave(provider, appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); }); + [Theory] + [WithSolidFilledImages(200, 200, 224, 232, 240, PixelTypes.Rgba32)] + public void FillPathGradientBrushTrianglePreservesAlphaRepresentation(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] points = [new(100, 0), new(200, 200), new(0, 200)]; + Rgba32[] colors = [new(255, 0, 0, 211), new(0, 255, 0, 0), new(0, 0, 255, 73)]; + + AssertPathGradientAlphaRepresentation(provider, points, colors, null); + } + + [Theory] + [WithSolidFilledImages(200, 200, 224, 232, 240, PixelTypes.Rgba32)] + public void FillPathGradientBrushGeneralCasePreservesAlphaRepresentation(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + PointF[] points = [new(0, 0), new(200, 0), new(200, 200), new(0, 200)]; + Rgba32[] colors = [new(255, 64, 0, 223), new(0, 255, 64, 0), new(64, 0, 255, 79), new(191, 128, 64, 161)]; + + AssertPathGradientAlphaRepresentation(provider, points, colors, new Rgba32(128, 191, 255, 117)); + } + [Theory] [WithBlankImage(20, 20, PixelTypes.HalfSingle)] - public void FillPathGradientBrushFillTriangleWithGreyscale(TestImageProvider provider) + public void FillPathGradientBrushFillTriangleWithHalfSingleRedChannel(TestImageProvider provider) where TPixel : unmanaged, IPixel => provider.VerifyOperation( ImageComparer.TolerantPercentage(0.02f), @@ -56,9 +78,11 @@ public void FillPathGradientBrushFillTriangleWithGreyscale(TestImageProv { PointF[] points = [new(10, 0), new(20, 20), new(0, 20)]; - Color c1 = Color.FromPixel(new HalfSingle(-1)); + // HalfSingle represents DXGI_FORMAT_R16_FLOAT, so its single component is red. + // Use the finite binary16 endpoints to cover the complete scaled [0, 1] range. + Color c1 = Color.FromPixel(new HalfSingle((float)Half.MinValue)); Color c2 = Color.FromPixel(new HalfSingle(0)); - Color c3 = Color.FromPixel(new HalfSingle(1)); + Color c3 = Color.FromPixel(new HalfSingle((float)Half.MaxValue)); Color[] colors = [c1, c2, c3]; @@ -198,4 +222,52 @@ public void FillPathGradientBrushFillComplex(TestImageProvider p }, appendSourceFileOrDescription: false, appendPixelTypeToFileName: false); + + private static void AssertPathGradientAlphaRepresentation(TestImageProvider provider, PointF[] points, Rgba32[] colors, Rgba32? centerColor) + where TPixel : unmanaged, IPixel + { + Color[] unassociatedColors = new Color[colors.Length]; + Color[] associatedColors = new Color[colors.Length]; + + for (int i = 0; i < colors.Length; i++) + { + unassociatedColors[i] = Color.FromPixel(colors[i]); + associatedColors[i] = ToAssociatedColor(unassociatedColors[i]); + } + + PathGradientBrush unassociatedBrush; + PathGradientBrush associatedBrush; + + if (centerColor.HasValue) + { + Color unassociatedCenterColor = Color.FromPixel(centerColor.Value); + unassociatedBrush = new PathGradientBrush(points, unassociatedColors, unassociatedCenterColor); + associatedBrush = new PathGradientBrush(points, associatedColors, ToAssociatedColor(unassociatedCenterColor)); + } + else + { + unassociatedBrush = new PathGradientBrush(points, unassociatedColors); + associatedBrush = new PathGradientBrush(points, associatedColors); + } + + Rgba32 background = new(224, 232, 240, 255); + Rgba32P associatedBackground = Rgba32P.FromRgba32(background); + using Image unassociatedImage = provider.GetImage(); + using Image associatedInputImage = provider.GetImage(); + using Image unassociatedInputAssociatedDestinationImage = new(unassociatedImage.Width, unassociatedImage.Height, associatedBackground); + using Image associatedDestinationImage = new(unassociatedImage.Width, unassociatedImage.Height, associatedBackground); + + unassociatedImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(unassociatedBrush))); + associatedInputImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(associatedBrush))); + unassociatedInputAssociatedDestinationImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(unassociatedBrush))); + associatedDestinationImage.Mutate(ctx => ctx.Paint(canvas => canvas.Fill(associatedBrush))); + + unassociatedImage.DebugSave(provider, "unassociated-input-rgba32", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + associatedInputImage.DebugSave(provider, "associated-input-rgba32", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + unassociatedInputAssociatedDestinationImage.DebugSave(provider, "unassociated-input-rgba32p", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + associatedDestinationImage.DebugSave(provider, "associated-input-rgba32p", appendPixelTypeToFileName: false, appendSourceFileOrDescription: false); + + AssertAssociationSimilarity(unassociatedImage, associatedInputImage); + AssertAssociationSimilarity(unassociatedInputAssociatedDestinationImage, associatedDestinationImage); + } } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs index 4894c3ed3..ae0a77916 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Polygons.cs @@ -152,7 +152,7 @@ public void FillPolygon_Complex(TestImageProvider provider, bool DrawingOptions options = new() { - ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + IntersectionRule = intersectionRule }; provider.RunValidatingProcessorTest( @@ -200,7 +200,7 @@ public void FillPolygon_StarCircle(TestImageProvider provider) { EllipsePolygon circle = new(32, 32, 30); StarPolygon star = new(32, 32, 7, 10, 27); - IPath shape = circle.Clip(star); + IPath shape = circle.Clip(BooleanOperation.Difference, star); provider.RunValidatingProcessorTest( c => c.Paint(canvas => canvas.Fill(Brushes.Solid(Color.White), shape)), @@ -220,12 +220,10 @@ public void FillPolygon_StarCircle_AllOperations(TestImageProvider provi StarPolygon star = new(64, 64, 5, 24, 64); // See http://www.angusj.com/clipper2/Docs/Units/Clipper/Types/ClipType.htm for reference. - ShapeOptions shapeOptions = new() { BooleanOperation = operation }; - IPath shape = star.Clip(shapeOptions, circle); - DrawingOptions options = new() { ShapeOptions = shapeOptions }; + IPath shape = star.Clip(operation, circle); provider.RunValidatingProcessorTest( - c => c.Paint(options, canvas => + c => c.Paint(canvas => { canvas.Fill(Brushes.Solid(Color.DeepPink), circle); canvas.Fill(Brushes.Solid(Color.LightGray), star); @@ -349,7 +347,7 @@ public void FillPolygon_EllipsePolygon(TestImageProvider provide Color color = Color.Azure; DrawingOptions options = new() { - ShapeOptions = new ShapeOptions { IntersectionRule = intersectionRule } + IntersectionRule = intersectionRule }; provider.RunValidatingProcessorTest( @@ -379,7 +377,7 @@ public void FillPolygon_IntersectionRules_OddEven(TestImageProvider(TestImageProvider(TestImageProvider provider, bool overlap ? new Vector2(130, 40) : new Vector2(93, 85), new Vector2(65, 137))); - IPath clipped = simplePath.Clip(hole1); + IPath clipped = simplePath.Clip(BooleanOperation.Difference, hole1); Color color = Color.White; if (transparent) @@ -352,7 +352,7 @@ public void FillComplexPolygon_SolidFill(TestImageProvider provi overlap ? new Vector2(130, 40) : new Vector2(93, 85), new Vector2(65, 137))); - IPath clipped = simplePath.Clip(hole1); + IPath clipped = simplePath.Clip(BooleanOperation.Difference, hole1); Color color = Color.HotPink; if (transparent) diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs index e1dc13259..25b493791 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Robustness.cs @@ -31,7 +31,7 @@ public void CompareToSkiaResults_StarCircle(TestImageProvider provider) { EllipsePolygon circle = new(32, 32, 30); StarPolygon star = new(32, 32, 7, 10, 27); - IPath shape = circle.Clip(star); + IPath shape = circle.Clip(BooleanOperation.Difference, star); CompareToSkiaResultsImpl(provider, shape); } diff --git a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs index c2cf7a303..2b3deccd5 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/ProcessWithDrawingCanvasTests.Text.cs @@ -850,7 +850,8 @@ public void CanFillTextVertical(TestImageProvider provider) FallbackFontFamilies = [fallback.Family], WrappingLength = 300, LayoutMode = LayoutMode.VerticalLeftRight, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + TextRuns = [ + new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } ] }; @@ -879,13 +880,15 @@ public void CanFillTextVerticalMixed(TestImageProvider provider) FallbackFontFamilies = [fallback.Family], WrappingLength = 400, LayoutMode = LayoutMode.VerticalMixedLeftRight, - TextRuns = [new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } + LineSpacing = 1.4F, + TextRuns = [ + new RichTextRun() { Start = 0, End = text.GetGraphemeCount(), TextDecorations = TextDecorations.Underline | TextDecorations.Strikeout | TextDecorations.Overline } ] }; IPathCollection glyphs = TextBuilder.GeneratePaths(text, textOptions); - DrawingOptions options = new() { ShapeOptions = new ShapeOptions { IntersectionRule = IntersectionRule.NonZero } }; + DrawingOptions options = new() { IntersectionRule = IntersectionRule.NonZero }; provider.RunValidatingProcessorTest( c => @@ -1088,6 +1091,127 @@ public void DrawText_EmojiGrid_NotoColorEmoji(TestImageProvider appendSourceFileOrDescription: false); } + [Theory] + [WithSolidFilledImages(320, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawTextUnderlineSkipsInk(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image auto = RenderDecoratedText(provider, "jogging", TextDecorationSkipInk.Auto); + using Image none = RenderDecoratedText(provider, "jogging", TextDecorationSkipInk.None); + + auto.DebugSave(provider, "auto", appendSourceFileOrDescription: false); + none.DebugSave(provider, "none", appendSourceFileOrDescription: false); + auto.CompareToReferenceOutput(TextDrawingComparer, provider, "auto", appendSourceFileOrDescription: false); + none.CompareToReferenceOutput(TextDrawingComparer, provider, "none", appendSourceFileOrDescription: false); + + // Descenders cross the underline band, so skipping must change the output. + Assert.Throws(() => ImageComparer.Exact.VerifySimilarity(auto, none)); + } + + [Theory] + [WithSolidFilledImages(320, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawTextUnderlineWithoutInkDoesNotSkip(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image auto = RenderDecoratedText(provider, "HOME", TextDecorationSkipInk.Auto); + using Image none = RenderDecoratedText(provider, "HOME", TextDecorationSkipInk.None); + + auto.DebugSave(provider, appendSourceFileOrDescription: false); + auto.CompareToReferenceOutput(TextDrawingComparer, provider, appendSourceFileOrDescription: false); + + // No ink reaches the underline band, so skipping must be a no-op. + ImageComparer.Exact.VerifySimilarity(auto, none); + } + + [Theory] + [WithSolidFilledImages(320, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawTextStrikethroughIgnoresSkipInk(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image auto = RenderDecoratedText(provider, "jogging", TextDecorationSkipInk.Auto, TextDecorations.Strikeout); + using Image none = RenderDecoratedText(provider, "jogging", TextDecorationSkipInk.None, TextDecorations.Strikeout); + + auto.DebugSave(provider, appendSourceFileOrDescription: false); + auto.CompareToReferenceOutput(TextDrawingComparer, provider, appendSourceFileOrDescription: false); + + // Strikethrough crosses ink by definition and never skips. + ImageComparer.Exact.VerifySimilarity(auto, none); + } + + [Theory] + [WithSolidFilledImages(320, 120, nameof(Color.White), PixelTypes.Rgba32)] + public void DrawTextUnderlinePenSkipsInk(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // A thick underline pen must still be broken by skip-ink where the descenders cross it. + // The gap is sized from the pen's stroke width, so it clears the width that is drawn. + using Image auto = RenderPennedUnderline(provider, "jogging", TextDecorationSkipInk.Auto, 8F); + using Image none = RenderPennedUnderline(provider, "jogging", TextDecorationSkipInk.None, 8F); + + auto.DebugSave(provider, "auto", appendSourceFileOrDescription: false); + none.DebugSave(provider, "none", appendSourceFileOrDescription: false); + + // Descenders cross the thick underline, so skipping ink must change the output. + Assert.Throws(() => ImageComparer.Exact.VerifySimilarity(auto, none)); + } + + private static Image RenderDecoratedText( + TestImageProvider provider, + string text, + TextDecorationSkipInk skipInk, + TextDecorations decorations = TextDecorations.Underline) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(10, 20), + TextDecorationSkipInk = skipInk, + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = text.Length, + TextDecorations = decorations + } + ] + }; + + Image image = provider.GetImage(); + image.Mutate(context => context.Paint(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null))); + return image; + } + + private static Image RenderPennedUnderline( + TestImageProvider provider, + string text, + TextDecorationSkipInk skipInk, + float penWidth) + where TPixel : unmanaged, IPixel + { + Font font = CreateFont(TestFonts.OpenSans, 48); + RichTextOptions textOptions = new(font) + { + Origin = new PointF(10, 20), + TextDecorationSkipInk = skipInk, + TextRuns = + [ + new RichTextRun + { + Start = 0, + End = text.Length, + TextDecorations = TextDecorations.Underline, + UnderlinePen = Pens.Solid(Color.Black, penWidth) + } + ] + }; + + Image image = provider.GetImage(); + image.Mutate(context => context.Paint(canvas => canvas.DrawText(textOptions, text, Brushes.Solid(Color.Black), pen: null))); + return image; + } + private static RichTextOptions CreateTextOptionsAt(Font font, PointF origin) => new(font) { Origin = origin }; diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs index 68b28cdd6..33ca58c27 100644 --- a/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Processing/RasterizerDefaultsExtensionsTests.cs @@ -62,6 +62,15 @@ public void RenderScene( { } + public void CopyPixels( + Configuration configuration, + ICanvasFrame source, + ICanvasFrame target, + Rectangle sourceRectangle, + Point targetPoint) + where TPixel : unmanaged, IPixel + => throw new NotSupportedException(); + public void ReadRegion( Configuration configuration, ICanvasFrame target, diff --git a/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs b/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs new file mode 100644 index 000000000..519b394b9 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Processing/RichTextGlyphRendererTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts; +using SixLabors.Fonts.Rendering; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Processing.Processors.Text; + +namespace SixLabors.ImageSharp.Drawing.Tests.Processing; + +public class RichTextGlyphRendererTests +{ + [Fact] + public void SetDecoration_ContiguousRun_EmitsSingleDecorationOperation() + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + const string text = "lllll"; + + int plainCount = CountOperations(font, text, runs: null); + int underlinedCount = CountOperations( + font, + text, + runs: + [ + new RichTextRun { Start = 0, End = text.Length, TextDecorations = TextDecorations.Underline } + ]); + + // Contiguous cells styled by one run merge into a single decoration operation; + // per-glyph emission would add one operation per glyph. + Assert.Equal(1, underlinedCount - plainCount); + } + + [Fact] + public void SetDecoration_RunBoundary_FlushesSegment() + { + Font font = TestFontUtilities.GetFont(TestFonts.OpenSans, 24); + const string text = "llllll"; + + int plainCount = CountOperations(font, text, runs: null); + int underlinedCount = CountOperations( + font, + text, + runs: + [ + new RichTextRun { Start = 0, End = 3, TextDecorations = TextDecorations.Underline, UnderlinePen = Pens.Solid(Color.Red, 2) }, + new RichTextRun { Start = 3, End = text.Length, TextDecorations = TextDecorations.Underline, UnderlinePen = Pens.Solid(Color.Blue, 2) } + ]); + + // A run boundary is a styling boundary: cells accumulate per run and flush where the + // pen changes, so each run contributes exactly one decoration operation. + Assert.Equal(2, underlinedCount - plainCount); + } + + private static int CountOperations(Font font, string text, List? runs) + { + RichTextOptions options = new(font); + if (runs is not null) + { + options.TextRuns = [.. runs]; + } + + List operations = []; + using RichTextGlyphRenderer renderer = new( + new DrawingOptions(), + path: null, + pen: null, + brush: Brushes.Solid(Color.Black), + new DrawingTextCache(), + operations); + + TextRenderer.RenderTo(renderer, text, options); + + // Dispose clears the caller-owned operation list, so count before leaving scope. + return operations.Count; + } +} diff --git a/tests/ImageSharp.Drawing.Tests/RegionTests.cs b/tests/ImageSharp.Drawing.Tests/RegionTests.cs new file mode 100644 index 000000000..2492e514e --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/RegionTests.cs @@ -0,0 +1,265 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Tests; + +public class RegionTests +{ + [Fact] + public void DefaultConstructor_CreatesEmptyRegion() + { + Region region = new(); + + Assert.True(region.IsEmpty); + Assert.Equal(Rectangle.Empty, region.Bounds); + Assert.Empty(region.Rectangles); + Assert.False(region.Contains(0, 0)); + Assert.False(region.Intersects(new Rectangle(0, 0, 10, 10))); + } + + [Fact] + public void RectangleConstructor_ContainsRectangle() + { + Region region = new(new Rectangle(10, 20, 30, 40)); + + Assert.False(region.IsEmpty); + Assert.Equal(new Rectangle(10, 20, 30, 40), region.Bounds); + Rectangle single = Assert.Single(region.Rectangles); + Assert.Equal(new Rectangle(10, 20, 30, 40), single); + } + + [Fact] + public void CopyConstructor_CopiesAreaAndIsIndependent() + { + Region source = new(new Rectangle(0, 0, 10, 10)); + Region copy = new(source); + + Assert.Equal(source.Rectangles, copy.Rectangles); + + copy.Add(new Rectangle(20, 0, 10, 10)); + + Assert.Single(source.Rectangles); + Assert.Equal(2, copy.Rectangles.Count); + } + + [Theory] + [InlineData(0, 0)] + [InlineData(-5, 3)] + [InlineData(3, -5)] + public void Add_NonPositiveRectangle_DoesNotChangeRegion(int width, int height) + { + Region region = new(); + region.Add(new Rectangle(10, 10, width, height)); + + Assert.True(region.IsEmpty); + Assert.Equal(Rectangle.Empty, region.Bounds); + } + + [Fact] + public void Add_StackedRectanglesWithSameWidth_MergeIntoOne() + { + Region region = new(new Rectangle(0, 0, 10, 5)); + region.Add(new Rectangle(0, 5, 10, 5)); + + Rectangle single = Assert.Single(region.Rectangles); + Assert.Equal(new Rectangle(0, 0, 10, 10), single); + } + + [Fact] + public void Add_OverlappingRectangles_NormalizesIntoBands() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Add(new Rectangle(5, 5, 10, 10)); + + Assert.Equal(Rectangle.FromLTRB(0, 0, 15, 15), region.Bounds); + Assert.Equal( + new[] + { + Rectangle.FromLTRB(0, 0, 10, 5), + Rectangle.FromLTRB(0, 5, 15, 10), + Rectangle.FromLTRB(5, 10, 15, 15), + }, + region.Rectangles); + } + + [Fact] + public void Add_DisjointRectangles_PreservesIslands() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Add(new Rectangle(100, 100, 10, 10)); + + Assert.Equal(2, region.Rectangles.Count); + Assert.Equal(Rectangle.FromLTRB(0, 0, 110, 110), region.Bounds); + Assert.True(region.Contains(5, 5)); + Assert.True(region.Contains(105, 105)); + Assert.False(region.Contains(50, 50)); + } + + [Fact] + public void Contains_IsInclusiveOfLeftTopAndExclusiveOfRightBottom() + { + Region region = new(new Rectangle(10, 10, 10, 10)); + + Assert.True(region.Contains(new Point(10, 10))); + Assert.True(region.Contains(19, 19)); + Assert.False(region.Contains(20, 10)); + Assert.False(region.Contains(10, 20)); + Assert.False(region.Contains(9, 10)); + Assert.False(region.Contains(10, 9)); + } + + [Fact] + public void Intersects_TouchingEdges_DoNotIntersect() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + + Assert.True(region.Intersects(new Rectangle(9, 9, 10, 10))); + Assert.False(region.Intersects(new Rectangle(10, 0, 10, 10))); + Assert.False(region.Intersects(new Rectangle(0, 10, 10, 10))); + Assert.False(region.Intersects(new Rectangle(0, 0, 0, 10))); + } + + [Fact] + public void IntersectRectangle_ClipsRegion() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Add(new Rectangle(100, 100, 10, 10)); + + bool result = region.Intersect(new Rectangle(5, 5, 20, 20)); + + Assert.True(result); + Rectangle single = Assert.Single(region.Rectangles); + Assert.Equal(Rectangle.FromLTRB(5, 5, 10, 10), single); + Assert.Equal(Rectangle.FromLTRB(5, 5, 10, 10), region.Bounds); + } + + [Fact] + public void IntersectRectangle_Disjoint_ClearsRegionAndReturnsFalse() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + + bool result = region.Intersect(new Rectangle(50, 50, 10, 10)); + + Assert.False(result); + Assert.True(region.IsEmpty); + } + + [Fact] + public void IntersectRectangle_EmptyRectangle_ClearsRegionAndReturnsFalse() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + + bool result = region.Intersect(Rectangle.Empty); + + Assert.False(result); + Assert.True(region.IsEmpty); + } + + [Fact] + public void IntersectRegion_KeepsOnlySharedArea() + { + Region first = new(new Rectangle(0, 0, 10, 10)); + first.Add(new Rectangle(20, 0, 10, 10)); + + Region second = new(new Rectangle(5, 0, 20, 10)); + + bool result = first.Intersect(second); + + Assert.True(result); + Assert.Equal( + new[] + { + Rectangle.FromLTRB(5, 0, 10, 10), + Rectangle.FromLTRB(20, 0, 25, 10), + }, + first.Rectangles); + } + + [Fact] + public void IntersectRegion_Disjoint_ClearsRegionAndReturnsFalse() + { + Region first = new(new Rectangle(0, 0, 10, 10)); + Region second = new(new Rectangle(50, 50, 10, 10)); + + bool result = first.Intersect(second); + + Assert.False(result); + Assert.True(first.IsEmpty); + } + + [Fact] + public void IntersectRegion_WithEmpty_ClearsRegionAndReturnsFalse() + { + Region first = new(new Rectangle(0, 0, 10, 10)); + + bool result = first.Intersect(new Region()); + + Assert.False(result); + Assert.True(first.IsEmpty); + } + + [Fact] + public void Clear_RemovesAllArea() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Clear(); + + Assert.True(region.IsEmpty); + Assert.Equal(Rectangle.Empty, region.Bounds); + Assert.Empty(region.Rectangles); + } + + [Fact] + public void ToPath_EmptyRegion_HasEmptyBounds() + { + Region region = new(); + IPath path = region.ToPath(); + + Assert.Equal(0, path.Bounds.Width * path.Bounds.Height); + } + + [Fact] + public void ToPath_SingleRectangle_MatchesRectangleBounds() + { + Region region = new(new Rectangle(10, 20, 30, 40)); + IPath path = region.ToPath(); + + Assert.Equal(new RectangleF(10, 20, 30, 40), path.Bounds); + } + + [Fact] + public void ToPath_MultipleRectangles_MatchesRegionBounds() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Add(new Rectangle(5, 5, 10, 10)); + + IPath path = region.ToPath(); + + Assert.Equal((RectangleF)region.Bounds, path.Bounds); + } + + [Fact] + public void ToPath_IsCachedUntilTheRegionChanges() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + + IPath first = region.ToPath(); + IPath second = region.ToPath(); + Assert.Same(first, second); + + region.Add(new Rectangle(20, 0, 10, 10)); + IPath third = region.ToPath(); + Assert.NotSame(first, third); + } + + [Fact] + public void ToPath_DisjointIslands_ProducesFigurePerIsland() + { + Region region = new(new Rectangle(0, 0, 10, 10)); + region.Add(new Rectangle(100, 0, 10, 10)); + + IPath path = region.ToPath(); + + Assert.Equal(Rectangle.FromLTRB(0, 0, 110, 10), (Rectangle)path.Bounds); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs index 84deee496..f48d6c6a7 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/ComplexPolygonTests.cs @@ -67,57 +67,52 @@ public void ToLinearGeometry_WithScale_PreservesContourMetadata() } [Fact] - public void PointAlongPath_AtStart_ReturnsFirstPoint() + public void TryGetPathPointAtDistance_AtStart_ReturnsFirstPoint() { ComplexPolygon complex = new(CreateSquare(0, 0, 10)); - IPathInternals internals = complex; - SegmentInfo info = internals.PointAlongPath(0); + Assert.True(complex.TryGetPathPointAtDistance(0, out PathPoint info)); Assert.Equal(0, info.Point.X, 1F); Assert.Equal(0, info.Point.Y, 1F); } [Fact] - public void PointAlongPath_MidSegment_ReturnsInterpolatedPoint() + public void TryGetPathPointAtDistance_MidSegment_ReturnsInterpolatedPoint() { // First segment goes from (0,0) to (10,0), length 10. ComplexPolygon complex = new(CreateSquare(0, 0, 10)); - IPathInternals internals = complex; - SegmentInfo info = internals.PointAlongPath(5); + Assert.True(complex.TryGetPathPointAtDistance(5, out PathPoint info)); Assert.Equal(5, info.Point.X, 1F); Assert.Equal(0, info.Point.Y, 1F); } [Fact] - public void PointAlongPath_WrapsAroundTotalLength() + public void TryGetPathPointAtDistance_WrapsAroundTotalLength() { // Perimeter is 40, so distance 45 should wrap to 5. ComplexPolygon complex = new(CreateSquare(0, 0, 10)); - IPathInternals internals = complex; - SegmentInfo atFive = internals.PointAlongPath(5); - SegmentInfo wrapped = internals.PointAlongPath(45); + Assert.True(complex.TryGetPathPointAtDistance(5, out PathPoint atFive)); + Assert.True(complex.TryGetPathPointAtDistance(45, out PathPoint wrapped)); Assert.Equal(atFive.Point.X, wrapped.Point.X, 1F); Assert.Equal(atFive.Point.Y, wrapped.Point.Y, 1F); } [Fact] - public void PointAlongPath_MultipleSubPaths_TraversesSecondPath() + public void TryGetPathPointAtDistance_MultipleSubPaths_TraversesSecondPath() { // Two separate squares; first has perimeter 40, second has perimeter 20. Polygon first = CreateSquare(0, 0, 10); Polygon second = CreateSquare(50, 50, 5); ComplexPolygon complex = new(first, second); - IPathInternals internals = complex; - // Distance 42 = 40 (first path perimeter) + 2 into second path. // Second path first segment goes from (50,50) to (55,50), so 2 units in -> (52,50). - SegmentInfo info = internals.PointAlongPath(42); + Assert.True(complex.TryGetPathPointAtDistance(42, out PathPoint info)); Assert.Equal(52, info.Point.X, 1F); Assert.Equal(50, info.Point.Y, 1F); diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs index 0da338dc4..c3357cabc 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/InternalPathTests.cs @@ -58,14 +58,6 @@ public void Bounds() Assert.Equal(5, path.Bounds.Bottom); } - private static InternalPath Create(PointF location, SizeF size, bool closed = true) - { - LinearLineSegment seg1 = new(location, location + new PointF(size.Width, 0)); - LinearLineSegment seg2 = new(location + new PointF(size.Width, size.Height), location + new PointF(0, size.Height)); - - return new InternalPath([seg1, seg2], closed); - } - public static TheoryData PointInPolygonTheoryData { get; } = new() { @@ -82,23 +74,4 @@ private static InternalPath Create(PointF location, SizeF size, bool closed = tr false }, }; - - private const float HalfPi = (float)(Math.PI / 2); - private const float Pi = (float)Math.PI; - - [Theory] - [InlineData(0, 50, 50, Pi)] - [InlineData(100, 150, 50, Pi)] - [InlineData(200, 250, 50, -HalfPi)] - [InlineData(259, 250, 109, -HalfPi)] - [InlineData(261, 249, 110, 0)] - [InlineData(620, 150, 50, Pi)] // wrap about end of path - public void PointOnPath(float distance, float expectedX, float expectedY, float expectedAngle) - { - InternalPath shape = Create(new PointF(50, 50), new Size(200, 60)); - SegmentInfo point = shape.PointAlongPath(distance); - Assert.Equal(expectedX, point.Point.X, 4F); - Assert.Equal(expectedY, point.Point.Y, 4F); - Assert.Equal(expectedAngle, point.Angle, 4F); - } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/LinearGeometryTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/LinearGeometryTests.cs new file mode 100644 index 000000000..20c8f9187 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/LinearGeometryTests.cs @@ -0,0 +1,183 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; + +public class LinearGeometryTests +{ + private static LinearGeometry CreateLPolyline() + => LinearGeometry.CreateOpenPolyline( + [ + new PointF(0, 0), + new PointF(10, 0), + new PointF(10, 10), + ]); + + private static LinearGeometry CreateClosedSquare() + { + PointF[] points = + [ + new PointF(0, 0), + new PointF(10, 0), + new PointF(10, 10), + new PointF(0, 10), + ]; + RectangleF bounds = RectangleF.FromLTRB(0, 0, 10, 10); + + return new LinearGeometry( + new LinearGeometryInfo + { + Bounds = bounds, + ContourCount = 1, + PointCount = points.Length, + SegmentCount = points.Length, + }, + [new LinearContour + { + PointStart = 0, + PointCount = points.Length, + Bounds = bounds, + SegmentStart = 0, + SegmentCount = points.Length, + IsClosed = true, + } + ], + points); + } + + [Fact] + public void CreateOpenPolyline_PopulatesInfoAndContours() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.Equal(1, geometry.Info.ContourCount); + Assert.Equal(3, geometry.Info.PointCount); + Assert.Equal(2, geometry.Info.SegmentCount); + Assert.Equal(RectangleF.FromLTRB(0, 0, 10, 10), geometry.Info.Bounds); + + LinearContour contour = Assert.Single(geometry.Contours); + Assert.False(contour.IsClosed); + Assert.Equal(3, geometry.Points.Count); + } + + [Fact] + public void CreateOpenPolyline_AppliesScale() + { + LinearGeometry geometry = LinearGeometry.CreateOpenPolyline( + [new PointF(1, 2), new PointF(3, 4)], + new Vector2(2, 10)); + + Assert.Equal(new PointF(2, 20), geometry.Points[0]); + Assert.Equal(new PointF(6, 40), geometry.Points[1]); + } + + [Fact] + public void CreateOpenPolyline_FewerThanTwoPoints_Throws() + => Assert.Throws(() => LinearGeometry.CreateOpenPolyline([new PointF(0, 0)])); + + [Fact] + public void GetSegments_YieldsEachSegmentInOrder() + { + LinearGeometry geometry = CreateLPolyline(); + List<(PointF Start, PointF End)> segments = []; + + SegmentEnumerator enumerator = geometry.GetSegments(); + while (enumerator.MoveNext()) + { + segments.Add((enumerator.Current.Start, enumerator.Current.End)); + } + + Assert.Equal( + [ + (new PointF(0, 0), new PointF(10, 0)), + (new PointF(10, 0), new PointF(10, 10)), + ], + segments); + } + + [Fact] + public void ComputeLength_SumsSegmentLengths() + => Assert.Equal(20F, CreateLPolyline().ComputeLength()); + + [Fact] + public void ComputeArea_OpenRunWithThreePoints_UsesShoelaceOfPointRun() + => Assert.Equal(50F, CreateLPolyline().ComputeArea()); + + [Fact] + public void ComputeArea_ClosedSquare_ReturnsEnclosedArea() + => Assert.Equal(100F, CreateClosedSquare().ComputeArea()); + + [Fact] + public void Contains_OpenContour_NeverContains() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.False(geometry.Contains(new PointF(9, 1), IntersectionRule.NonZero)); + Assert.False(geometry.Contains(new PointF(9, 1), IntersectionRule.EvenOdd)); + } + + [Theory] + [InlineData(5, 5, true)] + [InlineData(0, 0, true)] + [InlineData(15, 5, false)] + [InlineData(-1, 5, false)] + public void Contains_ClosedContour_UsesWinding(float x, float y, bool expected) + { + LinearGeometry geometry = CreateClosedSquare(); + + Assert.Equal(expected, geometry.Contains(new PointF(x, y), IntersectionRule.NonZero)); + Assert.Equal(expected, geometry.Contains(new PointF(x, y), IntersectionRule.EvenOdd)); + } + + [Theory] + [InlineData(0, 0, 0)] + [InlineData(5, 5, 0)] + [InlineData(15, 10, 5)] + [InlineData(20, 10, 10)] + public void TryGetPathPointAtDistance_WithinLength_ReturnsPointOnPolyline(float distance, float x, float y) + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.True(geometry.TryGetPathPointAtDistance(distance, out PathPoint pathPoint)); + Assert.Equal(new PointF(x, y), pathPoint.Point); + } + + [Fact] + public void TryGetPathPointAtDistance_BeyondOpenPolyline_ReturnsFalse() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.False(geometry.TryGetPathPointAtDistance(25, out _)); + Assert.False(geometry.TryGetPathPointAtDistance(-1, out _)); + Assert.False(geometry.TryGetPathPointAtDistance(float.NaN, out _)); + } + + [Fact] + public void TryGetPathPointAtDistanceUnbounded_ExtrapolatesAlongEndTangent() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.True(geometry.TryGetPathPointAtDistanceUnbounded(25, out PathPoint pathPoint)); + Assert.Equal(new PointF(10, 15), pathPoint.Point); + } + + [Fact] + public void TryGetPathPointAtDistanceUnbounded_NegativeDistance_ExtrapolatesBeforeStart() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.True(geometry.TryGetPathPointAtDistanceUnbounded(-5, out PathPoint pathPoint)); + Assert.Equal(new PointF(-5, 0), pathPoint.Point); + } + + [Fact] + public void TryGetSegment_ReturnsSubPathBetweenDistances() + { + LinearGeometry geometry = CreateLPolyline(); + + Assert.True(geometry.TryGetSegment(5, 15, false, out IPath segment)); + Assert.Equal(RectangleF.FromLTRB(5, 0, 10, 5), segment.Bounds); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/NormalizePathExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/NormalizePathExtensionsTests.cs new file mode 100644 index 000000000..d5c78140a --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/NormalizePathExtensionsTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; + +public class NormalizePathExtensionsTests +{ + [Fact] + public void Normalize_SimpleRectangle_PreservesBounds() + { + IPath path = new RectanglePolygon(10, 20, 30, 40); + + IPath normalized = path.Normalize(); + + Assert.Equal(path.Bounds, normalized.Bounds); + } + + [Fact] + public void Normalize_SelfIntersectingBowtie_KeepsOnlyThePositiveWindingLobe() + { + // The bowtie crosses itself at (50, 50), giving its two lobes opposite winding. + // Normalization fills using positive winding, so only the positively wound lobe + // survives; reversing the point order keeps the opposite lobe. + PointF[] points = + [ + new PointF(0, 0), + new PointF(100, 100), + new PointF(100, 0), + new PointF(0, 100), + ]; + + IPath normalized = new Polygon(points).Normalize(); + Assert.Equal(RectangleF.FromLTRB(0, 0, 50, 100), normalized.Bounds); + + Array.Reverse(points); + IPath reversed = new Polygon(points).Normalize(); + Assert.Equal(RectangleF.FromLTRB(50, 0, 100, 100), reversed.Bounds); + } + + [Fact] + public void Normalize_OverlappingContours_MergesIntoOneArea() + { + IPath combined = new ComplexPolygon( + new RectanglePolygon(0, 0, 60, 60), + new RectanglePolygon(40, 0, 60, 60)); + + IPath normalized = combined.Normalize(); + + Assert.Equal(RectangleF.FromLTRB(0, 0, 100, 60), normalized.Bounds); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/PathTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/PathTests.cs index 32eec9a95..018d3deee 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/PathTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/PathTests.cs @@ -76,8 +76,6 @@ public void EmptyPath_ToLinearGeometry_ReturnsEmptyGeometry() Assert.Equal(0, identity.Info.ContourCount); Assert.Equal(0, identity.Info.PointCount); Assert.Equal(0, identity.Info.SegmentCount); - Assert.Equal(0, identity.Info.NonHorizontalSegmentCountPixelBoundary); - Assert.Equal(0, identity.Info.NonHorizontalSegmentCountPixelCenter); Assert.Empty(identity.Contours); Assert.Empty(identity.Points); @@ -146,6 +144,45 @@ public void PathCollection_Bounds_ReturnsEmptyForEmptyCollection() Assert.Equal(RectangleF.Empty, collection.Bounds); } + [Theory] + [InlineData(-30, -30, 0, 0, 1, 0)] // extrapolates backward along the first segment + [InlineData(0, 0, 0, 0, 1, 0)] + [InlineData(150, 100, 50, 90, 0, 1)] + [InlineData(230, 100, 130, 90, 0, 1)] // extrapolates forward along the last segment + public void PointOnPathUnbounded(float distance, float expectedX, float expectedY, float expectedAngle, float expectedTangentX, float expectedTangentY) + { + IPath path = new Path(new LinearLineSegment(new PointF(0, 0), new PointF(100, 0), new PointF(100, 100))); + + Assert.True(path.TryGetPathPointAtDistanceUnbounded(distance, out PathPoint point)); + Assert.Equal(expectedX, point.Point.X, 4F); + Assert.Equal(expectedY, point.Point.Y, 4F); + Assert.Equal(expectedTangentX, point.Tangent.X, 4F); + Assert.Equal(expectedTangentY, point.Tangent.Y, 4F); + Assert.Equal(expectedAngle, point.Angle, 4F); + } + + [Theory] + [InlineData(-30)] + [InlineData(230)] + public void PointOnPathUnbounded_StrictVariantRejectsOutOfRangeDistances(float distance) + { + IPath path = new Path(new LinearLineSegment(new PointF(0, 0), new PointF(100, 0), new PointF(100, 100))); + + Assert.False(path.TryGetPathPointAtDistance(distance, out _)); + Assert.True(path.TryGetPathPointAtDistanceUnbounded(distance, out _)); + } + + [Theory] + [InlineData(float.NaN)] + [InlineData(float.PositiveInfinity)] + [InlineData(float.NegativeInfinity)] + public void PointOnPathUnbounded_RejectsNonFiniteDistances(float distance) + { + IPath path = new Path(new LinearLineSegment(new PointF(0, 0), new PointF(100, 0))); + + Assert.False(path.TryGetPathPointAtDistanceUnbounded(distance, out _)); + } + [Fact] public void PathCollection_Transform_TransformsEachPath() { diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs index 83684b45a..be6b62559 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/RectangleTests.cs @@ -169,22 +169,21 @@ public void Center() Assert.Equal(new PointF(150, 80), shape.Center); } - private const float HalfPi = (float)(Math.PI / 2); - private const float Pi = (float)Math.PI; - [Theory] - [InlineData(0, 50, 50, Pi)] - [InlineData(100, 150, 50, Pi)] - [InlineData(200, 250, 50, -HalfPi)] - [InlineData(259, 250, 109, -HalfPi)] - [InlineData(261, 249, 110, 0)] - [InlineData(620, 150, 50, Pi)] // wrap about end of path - public void PointOnPath(float distance, float expectedX, float expectedY, float expectedAngle) + [InlineData(0, 50, 50, 0, 1, 0)] + [InlineData(100, 150, 50, 0, 1, 0)] + [InlineData(200, 250, 50, 90F, 0, 1)] + [InlineData(259, 250, 109, 90F, 0, 1)] + [InlineData(261, 249, 110, 180F, -1, 0)] + [InlineData(620, 150, 50, 0, 1, 0)] // wrap about end of path + public void PointOnPath(float distance, float expectedX, float expectedY, float expectedAngle, float expectedTangentX, float expectedTangentY) { RectanglePolygon shape = new(50, 50, 200, 60); - SegmentInfo point = ((IPathInternals)shape).PointAlongPath(distance); + Assert.True(shape.TryGetPathPointAtDistance(distance, out PathPoint point)); Assert.Equal(expectedX, point.Point.X); Assert.Equal(expectedY, point.Point.Y); + Assert.Equal(expectedTangentX, point.Tangent.X); + Assert.Equal(expectedTangentY, point.Tangent.Y); Assert.Equal(expectedAngle, point.Angle); } } diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/RoundedRectanglePolygonTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/RoundedRectanglePolygonTests.cs new file mode 100644 index 000000000..5f1c5d528 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/Shapes/RoundedRectanglePolygonTests.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Drawing.Tests.Shapes; + +public class RoundedRectanglePolygonTests +{ + [Fact] + public void SeparateCornerRadiiIsCorrect() + { + RoundedRectanglePolygon polygon = new( + new RectangleF(10, 20, 100, 80), + new SizeF(10, 2), + new SizeF(20, 4), + new SizeF(30, 6), + new SizeF(40, 8)); + + Assert.Equal(8, polygon.LineSegments.Count); + + ApproximateFloatComparer comparer = new(1e-4F); + AssertSegment(polygon.LineSegments[0], new PointF(20, 20), new PointF(90, 20), comparer); + AssertSegment(polygon.LineSegments[1], new PointF(90, 20), new PointF(110, 24), comparer); + AssertSegment(polygon.LineSegments[2], new PointF(110, 24), new PointF(110, 94), comparer); + AssertSegment(polygon.LineSegments[3], new PointF(110, 94), new PointF(80, 100), comparer); + AssertSegment(polygon.LineSegments[4], new PointF(80, 100), new PointF(50, 100), comparer); + AssertSegment(polygon.LineSegments[5], new PointF(50, 100), new PointF(10, 92), comparer); + AssertSegment(polygon.LineSegments[6], new PointF(10, 92), new PointF(10, 22), comparer); + AssertSegment(polygon.LineSegments[7], new PointF(10, 22), new PointF(20, 20), comparer); + } + + private static void AssertSegment(ILineSegment segment, PointF start, PointF end, ApproximateFloatComparer comparer) + { + Assert.Equal(start, segment.StartPoint, comparer); + Assert.Equal(end, segment.EndPoint, comparer); + } +} diff --git a/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs b/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs index 6a8899cdc..3dd2f2746 100644 --- a/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/Shapes/TextBuilderTests.cs @@ -192,7 +192,7 @@ public void TextUtilities_CloneOrReturnForRules_ReturnsDrawingOptionsWhenRulesMa DrawingOptions options = new(); DrawingOptions result = options.CloneOrReturnForRules( - options.ShapeOptions.IntersectionRule, + options.IntersectionRule, options.GraphicsOptions.AlphaCompositionMode, options.GraphicsOptions.ColorBlendingMode); @@ -213,13 +213,12 @@ public void TextUtilities_CloneOrReturnForRules_ClonesDrawingOptionsWhenRulesDif PixelColorBlendingMode.Multiply); Assert.NotSame(options, result); - Assert.NotSame(options.ShapeOptions, result.ShapeOptions); Assert.NotSame(options.GraphicsOptions, result.GraphicsOptions); - Assert.Equal(IntersectionRule.EvenOdd, result.ShapeOptions.IntersectionRule); + Assert.Equal(IntersectionRule.EvenOdd, result.IntersectionRule); Assert.Equal(PixelAlphaCompositionMode.SrcIn, result.GraphicsOptions.AlphaCompositionMode); Assert.Equal(PixelColorBlendingMode.Multiply, result.GraphicsOptions.ColorBlendingMode); Assert.Equal(options.Transform, result.Transform); - Assert.Equal(IntersectionRule.NonZero, options.ShapeOptions.IntersectionRule); + Assert.Equal(IntersectionRule.NonZero, options.IntersectionRule); Assert.Equal(PixelAlphaCompositionMode.SrcOver, options.GraphicsOptions.AlphaCompositionMode); Assert.Equal(PixelColorBlendingMode.Normal, options.GraphicsOptions.ColorBlendingMode); } diff --git a/tests/ImageSharp.Drawing.Tests/TestFonts.cs b/tests/ImageSharp.Drawing.Tests/TestFonts.cs index 0b688d090..434cce98c 100644 --- a/tests/ImageSharp.Drawing.Tests/TestFonts.cs +++ b/tests/ImageSharp.Drawing.Tests/TestFonts.cs @@ -11,6 +11,11 @@ public static class TestFonts public const string OpenSans = "OpenSans-Regular.ttf"; + // Inter Light is the weight Avalonia's ControlCatalog renders body text with (the font the sample + // shows holes in). Inter draws several glyphs (e.g. 'A', 't') with overlapping contours, so it is a + // good repro for glyph-fill winding/overlap holes. SIL OFL licensed, taken from Avalonia.Fonts.Inter. + public const string InterLight = "Inter-Light.ttf"; + public const string SixLaborsSampleAB = "SixLaborsSampleAB.woff"; public const string TwemojiMozilla = "TwemojiMozilla.ttf"; diff --git a/tests/ImageSharp.Drawing.Tests/TestFonts/Inter-Light.ttf b/tests/ImageSharp.Drawing.Tests/TestFonts/Inter-Light.ttf new file mode 100644 index 000000000..c3aad90d1 --- /dev/null +++ b/tests/ImageSharp.Drawing.Tests/TestFonts/Inter-Light.ttf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c44ff7a5fde4816f94fc1e5e232b021a812a411339ddd08b0a475ca6e21db810 +size 310420 diff --git a/tests/ImageSharp.Drawing.Tests/TestImages.cs b/tests/ImageSharp.Drawing.Tests/TestImages.cs index 4643e201f..3a4f3870e 100644 --- a/tests/ImageSharp.Drawing.Tests/TestImages.cs +++ b/tests/ImageSharp.Drawing.Tests/TestImages.cs @@ -145,6 +145,7 @@ public static class Bad public const string Hiyamugi = "Jpg/baseline/Hiyamugi.jpg"; public const string Snake = "Jpg/baseline/Snake.jpg"; public const string Lake = "Jpg/baseline/Lake.jpg"; + public const string Balloon = "Jpg/baseline/balloon.jpg"; public const string Jpeg400 = "Jpg/baseline/jpeg400jfif.jpg"; public const string Jpeg420Exif = "Jpg/baseline/jpeg420exif.jpg"; public const string Jpeg444 = "Jpg/baseline/jpeg444.jpg"; diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/PixelTypes.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/PixelTypes.cs index a2f3fecda..76450c22a 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/PixelTypes.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/PixelTypes.cs @@ -60,6 +60,8 @@ public enum PixelTypes L8 = 1 << 23, + RgbaHalf = 1 << 24, + // TODO: Add multi-flag entries by rules defined in PackedPixelConverterHelper // "All" is handled as a separate, individual case instead of using bitwise OR diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs index e27d5ec79..9b61dc965 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -16,13 +16,13 @@ public class TestImageProviderTests public static readonly TheoryData BasicData = new() { TestImageProvider.Blank(10, 20), - TestImageProvider.Blank(10, 20), + TestImageProvider.Blank(10, 20), }; public static readonly TheoryData FileData = new() { TestImageProvider.File(TestImages.Bmp.Car), - TestImageProvider.File(TestImages.Bmp.F) + TestImageProvider.File(TestImages.Bmp.F) }; public TestImageProviderTests(ITestOutputHelper output) => this.Output = output; diff --git a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs index 212cf6fe9..215d18cf6 100644 --- a/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs +++ b/tests/ImageSharp.Drawing.Tests/TestUtilities/Tests/TestUtilityExtensionsTests.cs @@ -63,12 +63,14 @@ public void IsEquivalentTo_WhenTrue(TestImageProvider provider, [InlineData(PixelTypes.Rgba32, typeof(Rgba32))] [InlineData(PixelTypes.Argb32, typeof(Argb32))] [InlineData(PixelTypes.HalfVector4, typeof(HalfVector4))] + [InlineData(PixelTypes.RgbaHalf, typeof(RgbaHalf))] public void ToType(PixelTypes pt, Type expectedType) => Assert.Equal(pt.GetClrType(), expectedType); [Theory] [InlineData(typeof(Rgba32), PixelTypes.Rgba32)] [InlineData(typeof(Argb32), PixelTypes.Argb32)] + [InlineData(typeof(RgbaHalf), PixelTypes.RgbaHalf)] public void GetPixelType(Type clrType, PixelTypes expectedPixelType) => Assert.Equal(expectedPixelType, clrType.GetPixelType()); diff --git a/tests/Images/Input/Jpg/baseline/balloon.jpg b/tests/Images/Input/Jpg/baseline/balloon.jpg new file mode 100644 index 000000000..d2fe14634 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/balloon.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:291db9000bc51d52878951893de6f043ae0748c1085d29c25273d492c09c8212 +size 10308 diff --git a/tests/Images/Input/Png/rainbow.png b/tests/Images/Input/Png/rainbow.png new file mode 100644 index 000000000..1ee6a28de --- /dev/null +++ b/tests/Images/Input/Png/rainbow.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc47aad128b8f103beb17252b198e1a4b2bb9310d3eb4aca984fc872aeea1d87 +size 5743 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png index 7e84536ba..84d1c06e3 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_RegionAndPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ac188df8fa9947f63d7af26ba3a184a727c843c054d25d0d9d65171d6e53052 -size 3665 +oid sha256:b255f0a986d5b33773a7af23cdb8411f3a2fbab02b64ae7cd29cbe4ff748b3fc +size 3632 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png index e4ead0f65..7aff2bf2a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clear_WithClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fd069e33c1e597c3de2c4b8635c47b9901c2b7e820b573dcd045020896371410 -size 10954 +oid sha256:218011a820dab6702a1ca7bd0eefec1f35b430370732657d5ab32b481b36d3b7 +size 11000 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_actual-multiple-difference-clips.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_actual-multiple-difference-clips.png new file mode 100644 index 000000000..658eda133 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_actual-multiple-difference-clips.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7710bdb6818017717191a0004d5b1eef844ca776a79e0b65e011e87faef44650 +size 117 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_expected-sequential-difference-clips.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_expected-sequential-difference-clips.png new file mode 100644 index 000000000..658eda133 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Clip_DifferenceWithMultiplePaths_MatchesSequentialDifferenceClips_Rgba32_expected-sequential-difference-clips.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7710bdb6818017717191a0004d5b1eef844ca776a79e0b65e011e87faef44650 +size 117 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png index 5751b85c8..8a65dd1ee 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_LocalCoordinates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f76d22a318eade6c4bdaba12d58fa5569338cc89e180fbec67f0f4f6a5c196a6 -size 2003 +oid sha256:9974591a296eb82efabdbaaf58fdba66554414e0da9ac3b889965e02798a2b65 +size 1853 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png index dbf64bf12..23996dd7b 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:394d7cc4cf1ce07d0c4439d0e8c5d10287fb8e1357c625ce7d53d02478cc9360 -size 12316 +oid sha256:b4a91e81f7708c71c8d1f3c20b2718b9f036c088b90e3890e2078bddacdf4de3 +size 8643 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png new file mode 100644 index 000000000..2150fe514 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0eda5bdf7d312b442906e3ad5d057dd1d9bd4ead99045309290e38107ab39b5 +size 1696 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png new file mode 100644 index 000000000..8077402c9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipEvenOddRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18d46e825adbe1ddc7789612513871b18583742d7cc1c5f9aa8b8dccb475e8b5 +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png new file mode 100644 index 000000000..2150fe514 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_actual-clipped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0eda5bdf7d312b442906e3ad5d057dd1d9bd4ead99045309290e38107ab39b5 +size 1696 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png new file mode 100644 index 000000000..8077402c9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphById_SubjectNonZero_ClipNonZeroRect_DoesNotChangeGlyph_Rgba32_expected-unclipped.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18d46e825adbe1ddc7789612513871b18583742d7cc1c5f9aa8b8dccb475e8b5 +size 1686 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png index 8b00505fb..eb52f831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_ColrV1-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcb628b16154433d9c4bbaa7052d3a22bcc3a112bdfa045894a7cae74ce85b27 -size 10941 +oid sha256:4b9563a979fd3797426abf4e745e90fc0e9e58ff24afefb9ec00c33b55fa8a57 +size 11114 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png index 8b00505fb..eb52f831a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawGlyphs_EmojiFont_MatchesReference_Rgba32_Svg-draw-glyphs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcb628b16154433d9c4bbaa7052d3a22bcc3a112bdfa045894a7cae74ce85b27 -size 10941 +oid sha256:4b9563a979fd3797426abf4e745e90fc0e9e58ff24afefb9ec00c33b55fa8a57 +size 11114 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png index 1dc458c0d..0f7212f1d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithClipPathAndTransform_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14d2b67399c1f82b825369023b3758ef8cb39d6c201bfbf24e1211f910988ea7 -size 11079 +oid sha256:4600a4582640f739dcc7ef2533ca266e6303ba8d8e2fe08d935ee6b2645fb327 +size 11083 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png index e836e72ea..e66defb7a 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawImage_WithSourceClippingAndScaling_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:431a0e81f68c1052900a104702e139051df38cf2aace11df421e695dae7a1679 -size 627 +oid sha256:8fb851ffd000cdc80e5ba290bd2d95a658446a736548808ed873e6f9412d4a0e +size 661 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png index 39b2d195a..d493cfce4 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPieHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6923af9c419384b639d09a32a2b549b419120e15b47341c809ca512a09cf29e3 -size 4305 +oid sha256:5b0d3bbd305cac719874d45b11de59bd50f8fd2775f3a294f69b9001f41e12fc +size 4263 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png index 586ad8c5f..d02b21d8f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawPrimitiveHelpers_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aa7d53f508b0c8d76e5b6733f284504def4d1a5dbc03ffe6dfe1a2f4a5744fd -size 9160 +oid sha256:dd3d2a1795424ff40abf616e55b34cb5365a77fba90b6c0442b957312ac10dc2 +size 9092 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png index c5ab0cec9..0c2805844 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_AlongPathWithOrigin_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c66e111393532a429b7c9d5b80364b14b58102bba2051eda82e41e891912dc1 -size 11051 +oid sha256:c0b92fd87faa72e92dc89b2d9a97a71fde2c96c1b388e8bfd338e55fc182a358 +size 10861 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png index 029e0c2b6..35d519994 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_FillAndStroke_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c2f94b818c408caad040ab8877d33a3c321de2d725a819d99a739df5569151c -size 21340 +oid sha256:45fa8aa1a7e6bc272875a9fc746ca5d1781e6065cd6016a3114aacfac4945213 +size 19999 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png index 7c736f1a4..40af29788 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_LineLayoutsAlongDifferentPaths_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82e824b35766cb30fbe1bf530fee4cbfa382a3f1bdd8894be3e2c99e36f809c6 -size 31193 +oid sha256:c9666d4c79f8cf6b126d838aa6bc61c02f7bb3da4f69cda60837ebf3edeb08c5 +size 29517 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png index c57dec1df..18ef8c476 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_Multiline_WithLineMetricsGuides_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95c016e02dbaaace8933bcc9fec9949205924bfc4d6e87302ad9d2ed037097a2 -size 25708 +oid sha256:b0ddaef53d274d98eadf70eec0e725f2cc7b7d54f284e88b0e37386811a2b37d +size 37416 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png index 355f3e943..28a8ddc78 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_PenOnly_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a33299ed85cf430e010aa6e84ca97692e352d6fca9682d21324071bab729dc1f -size 3214 +oid sha256:cabe7445ea3b518be4c35417bc9da9c248cc617f8ed0c2e7d683a71e219b9fe3 +size 3210 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png index ebb6d0a10..33858ae37 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_TextBlockAlongPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d86305060146c0c471993b5b40a92f1c45ac5a5308ed3da164de47c1ca1f6b -size 23283 +oid sha256:eff0d0d3162042cc45e534f3f1ed33233cf035726b1d6cffcf618665695e2dd0 +size 20912 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png index 00b33934c..65d24be66 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/DrawText_WithWrappingAlignmentAndLineSpacing_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96d24b93a9d31736e3f1407f18c04ed2b122a806caa2d29a7b1738be2708cc23 -size 45452 +oid sha256:3ca3fa74665c9b52b624e4177b3700a285263454ef159f73a8f93e0c0ad6b06f +size 37626 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png index baf2db608..36b08246c 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3ea391f2f80df10ba127ac6c68f58def48f93506f424c55973da551e25b8008 -size 3561 +oid sha256:2a1ef3c89b12b76890cd3052ae80aa31be5fb15741198cfd7424d94da507aa8f +size 3545 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_SelfIntersectingStroke_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_SelfIntersectingStroke_MatchesReference_Rgba32.png index 91d799817..c09df8a63 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_SelfIntersectingStroke_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_SelfIntersectingStroke_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:098df255c38dfb4c5677e4566cdfc2e512e3ca488326d68352f9dc79ce40c5a5 -size 9481 +oid sha256:799f9018b86de15a864eeec4deb0fdd884cf79a4d755fb1d752079f72635a1a7 +size 8811 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png index 361afe25b..550e824f4 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Draw_WithPatternAndGradientPens_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5064b994cba9dee461bb310a2d0e1544d61b547605d6061980697d4ab8ea477f -size 9781 +oid sha256:fd51fce4f7bfd405d9f5965e6d97b0a87b1eb1986c8b4663586ddb418ee1dde1 +size 8512 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png index 8ef00c610..a9e6b8a90 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc897fe86398bc7892a8d0f3e0e9d9457d169914f79c76cc87492532612b9961 -size 2677 +oid sha256:4208226859ac23fb3671840563d2c58892f06282e4d3ce69f823d9e715dd0230 +size 2700 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png index 34c0b52e6..79892945e 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddVsNonZero_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:607b7584b4b29ea80f54eabcccec3daf272bb8c1afff232d2942cbc7e8b79d65 -size 8341 +oid sha256:ba3dec5e3295839d6123de0639083b6cb657d237d5d5efc40d9b873ccff9aeeb +size 8200 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_actual-rect-clip.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_actual-rect-clip.png new file mode 100644 index 000000000..b286d6baf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_actual-rect-clip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3115ff7dd813df2e266c880c2a052a53c58afe82266cf14a977ff100a06d75c5 +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_expected-general-clip.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_expected-general-clip.png new file mode 100644 index 000000000..b286d6baf --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_SelfIntersectingPath_EvenOddWithRectangleClip_MatchesGeneralClip_Rgba32_expected-general-clip.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3115ff7dd813df2e266c880c2a052a53c58afe82266cf14a977ff100a06d75c5 +size 1558 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png index 0d3417582..ea9559b4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Fill_WithGradientAndPatternBrushes_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61d473ea6d0e639a3d87888f85157e39c1b4d87c499705021eea9c5c4ef8dd9a -size 18913 +oid sha256:4bd803e978fb02bfd75b7eb85f7ef555f1ae39171100a3b7098c31104849d94b +size 20859 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_UsesBackendReadback_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_UsesBackendReadback_MatchesReference_Rgba32.png deleted file mode 100644 index c7a7ea6b8..000000000 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_NoCpuFrame_UsesBackendReadback_MatchesReference_Rgba32.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:82af0b7a5959132f9750349f330e11c2a8ed1c0a843a37386d838ac7a48c25d8 -size 12958 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png index 78ae5641b..5061a0b0d 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_PathBuilder_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b12f3fb162e53c8917a620ffe9817683a0ab55b2fc92bbafd1f82194ce2eb098 -size 12983 +oid sha256:79746f0adf5db701238ebe0910228cdb73fd666b51addd7f46b4019aba78f7e6 +size 13049 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png index c7a7ea6b8..7769792b3 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/Process_Path_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82af0b7a5959132f9750349f330e11c2a8ed1c0a843a37386d838ac7a48c25d8 -size 12958 +oid sha256:cba5e51955bc1fcc91f7a4d5a5b2e094653f8706879cdf8bf34d05492e4a2c22 +size 13041 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png index 2fac7049a..8ae873830 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RestoreTo_MultipleStates_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9d17b7b41436034a596f7e31ba50ec01e18daf83e1660568ef88876699e5e781 -size 4926 +oid sha256:12ef9380471fc081d5f9ea3ec24a2967cc9657c6e5051118fb6eac592f51a817 +size 3928 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png index 74ce8b7d2..d1a2a81b5 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/RoundedRectanglePolygon_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc617c86052a45238e9140590a8518b7d90cc314a968293a348f3aa993d6387b -size 7438 +oid sha256:d83614918b91c5c4face452817c6387e1ee8ed39376e87e934ce54682e5c14b8 +size 5131 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_Rgba32.png new file mode 100644 index 000000000..6b8503694 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:138f557a498e38acb370b5771ebff5e8bfff3d6c554ddf365247906cea65a484 +size 105 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png index 54f99100d..7f1dace5f 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/SaveRestore_ClipPath_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b73361e5260c971194675c81629a95a6fd9b0a3d6f08e21d31775859ea5ef739 -size 1363 +oid sha256:4a38a20b9157c08258488e462bc42a08f03707f536afc3339f5dd044b9a9351a +size 1325 diff --git a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png index 8b15070e0..d9be1f2c4 100644 --- a/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png +++ b/tests/Images/ReferenceOutput/Drawing/DrawingCanvasTests/TextMeasuring_RenderedMetrics_MatchesReference_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3730fb89404438de5955cccd6b73027d346fd5fbe3e1ee393bedc98a2daf44f -size 37123 +oid sha256:5e8f1daad7615b839878d8af547a42dda0cc6c51430a0284cff238f209e52fdc +size 26268 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png index 5273f1f6d..466bb80d8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:317b38b7018133add4f9c8e522760db6525dc97f248e322d62b879dcca45c7e4 -size 209 +oid sha256:d2c0c2bf0974e0b16c752b4f2333fac027fb17aacf8e977af1615ac55981163a +size 1281 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png index 138bd9cb5..725d3a8ac 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c5667457953ecc19659b3d36dccc7dc36ca6588e78aa3e6dc1c20a1a52a8860 -size 2584 +oid sha256:622ea2ff1ee2bc0c0d02b0476b737b8e58ee36e517e72d33c6f3859a0fba3c75 +size 2758 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png index 4a700ea8d..54549cafd 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d89bacd7e62b3c64a8431aa10b0d68ee2d89e4818de59e5924fb98a3d08ceef -size 2683 +oid sha256:b2151084ed364dcfef9126dd4b9abd7ace20c24ac160cca53363637d80287e74 +size 3001 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png index 8354afc18..1c79c4224 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e9dfb0f36776c2c5295b14f073953133ed727ab6a2f6cff114d7174d1b54c7d -size 2712 +oid sha256:3eb49d25447c51adfb0b501acb24bcb3c27a1591235f72b50426c74a2dde9fb1 +size 2999 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png index 54cb65651..ac9336d3e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aef4851463386427415ba24bda02e1208f4c6467b3e0fd926ff72b68d5b75758 -size 2580 +oid sha256:006196c78bda4924ad2ab138b488174a66279e7b34906d2321123127004828b1 +size 2755 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png index 8e613bfcf..00c739c89 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b3fc5f7be155a5fe783c334d640b80af008aa30a6892577199be1c303b059e6 -size 2699 +oid sha256:d67bcd3836b78e2891d0e68edabc66dbb75cbafbe35290b9353cf302e612059a +size 3007 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png index eea6af608..bfa4d2d19 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab992c8aaebb206fbf57dbd9194aaed8329d6891d452e6caddbdb4bf1e0dc8a3 -size 2587 +oid sha256:583b580e8f58cf77d05efc9fbc6e70d77214ff0a57110a9f56eb97a556637e76 +size 2600 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png index 8c2b99005..e21d70fcb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7de72d4b860eb88953c35ba4af1e874b947149b071b1c91c5d4a49ebb4fd93d -size 2699 +oid sha256:e62455f819d0fb5bbd4eff69bf6e7277e930c240211b58ae8847ae6494347ecc +size 3111 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png index 51a806d76..1c9d761f1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20d8f62866e45385ef76fa960e9d811e158d90df8c147f122d64b7d161aacf4d -size 2597 +oid sha256:ce7ad37381d5f762c2a48816761a382e0c1832f72bfad9707318b92cec84cfdf +size 2759 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png index df6499401..81b77f574 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b840f5a28985ddca910283d62d3b10dd553c97377b32ac29aebabcf1736c854 -size 2695 +oid sha256:d2c720c0aff4dfa74857c7067337145ee51712e26b4756460bc5a2d79398d4de +size 3342 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png index ef8a7a8eb..8ccab8eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-DestIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c260557ffb5b106aac505d35719f88c27875d3e0e0cda9dd9cab2bb53efe729 -size 1508 +oid sha256:cd9cc2ae43ae470f10fda30277cbdb5a4f898a2077669667a5d3b414387789af +size 1520 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png index 824ee391b..11dae27af 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6dc94909e233b8e4ff73890de29e0226cdb9d92e8451b960a5fc225c4d197fdb -size 1605 +oid sha256:aa6275580cede68f1ffa5028bae4afa1e80cc52f5bae9c936148c48430cb1843 +size 1961 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png index e7b64e4bb..966056234 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-SrcOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f6af26e5b52f347c085c09dfd62db189f561e6973213ddfb2228ffba88a17d0 -size 2193 +oid sha256:b96c2b7b4446ed819b49072cf47f6a5db5f806367398eae4a82eb6ff3f2eedee +size 2760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png index 2c67a06c2..c15bd36de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendSemiTransparentRedEllipse_composition-Src_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e9a3f41b8cec22976f3fcb2e0473a3d46b189f8a3982d182b632a8f51d444d4 -size 2702 +oid sha256:e04d490a7578c7ff13c4e037bd26ba24c3c8ffb76be75a811944695c3f667d82 +size 3175 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Clear_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestAtop_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-DestIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png index 9d5057939..8b95235f3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcIn_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ccd94917276d7eca47647b598a6dbe9cdb9eba6e57b88598fa3613b66e521f -size 168 +oid sha256:2406c59d9f60ff4b51ff5e71f5c5878d33f8a2c0fbf45758408c8cc2735d9cb1 +size 1304 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-SrcOut_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Add.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Darken.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-HardLight.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Lighten.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Multiply.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Normal.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Overlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Screen.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png index 17360edc9..73fd689de 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/BlendingsDarkBlueRectBlendHotPinkRectBlendTransparentEllipse_composition-Src_blending-Subtract.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb77cbc6fd92eb1ad3be8080bb1f340872aaa37ab9c1cf977ea05d3ab0337b10 -size 312 +oid sha256:df334c9adb9c9be67dae4379639902c0e34b52d15b12e7a0f7d1c57020699c10 +size 2117 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png index 5db1bf7f1..e1b76c676 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid100x100_(0,0,0,255)_RichText-Path-(spiral).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83e49ad1f300392a2f2362a30adb6fb235cd0f8e235bc43ef2145c52f2b1b1d9 -size 4163 +oid sha256:c6d120d307790eec54397bc818462668df94025c4d5d9bb4713e263f1c0b8b0f +size 4012 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png index c8fcd2ab1..2570b4eba 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid120x120_(0,0,0,255)_RichText-Path-(triangle).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88bbc7800a9969ff9342c7540f350cdcefc4e13c7e8fccabf1ee054f2703cb8f -size 4124 +oid sha256:659faf60f57b436d3a7f8851869cdf6c972779cd38f80ad1068fed8ff14c1e40 +size 4122 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png index b54aeacfb..d7921e097 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawRichTextAlongPathHorizontal_Solid350x350_(0,0,0,255)_RichText-Path-(circle).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49851db29c931c3570989f11adff93d1930f4f1354a8e519d29f91f5fcae8f4c -size 5258 +oid sha256:dcab42cff919a8739e097127db58c70fe6dc80dfb02a3b46f714e1298d853c6c +size 5178 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png index 35ce1c32c..29a7e772a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank100x100_type-spiral.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a6931fb1ea3a9e0965d255106cafb2c87f11bccc6b6af82d8c4bfd44b1bd211 -size 5300 +oid sha256:73d63e3ad2afd0d1e7186a2304b95cc98623b97ce874e1e75649aeeb608946c5 +size 5311 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png index 40d43d551..707399ad2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank120x120_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8bbd454ab2cbc654edb2db73aa3832c51dda344d63b5c70e5d68f8e49652952e -size 4393 +oid sha256:a039bf64e2449c129d967977874f081826d6694fa523a94d49d00194bcf76113 +size 4443 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png index 1db1a6f97..07a8bf39b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathHorizontal_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eff7a53d587a4e93c75fa2415935468d20d060530ab29a698d402139839dd9e -size 9408 +oid sha256:7d55fe15a80a6391fb95626684ec485e8a9c8d5e65a2fdf6ac2250e9969fe26c +size 9525 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png index 9d0a1b5d5..d266ded4a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank250x250_type-triangle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0c9d29ecc7d5e20795f3ff7f444b354aaa0eab87800100e2a7b78b2438afd38 -size 5182 +oid sha256:f85f62fbe30119c52db614b634539ab0c2cd67796726d72092056200044c49c9 +size 5135 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png index 59803e3c8..8f607887c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextAlongPathVertical_Rgba32_Blank350x350_type-circle.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e16001c728efa248c29d42e179aa558b48f02d950e0bc9c0123bd6be31880a45 -size 7388 +oid sha256:057662f3c567cf3c36d6d001a42ea4b400a0a409c4e8bf26558de3ddce6d54d3 +size 7330 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png index 7866fd7d7..45d7f7ee6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical2_Rgba32_Blank48x935.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a757d3569e5b8a8722f940148febe01347ba0cdb9ed74ed518e46d1525c6862a -size 5018 +oid sha256:8c722cd07c3ff50d269fe9f14b8a7ce3443a2ee1dc22796d5e691ec0f8338ab8 +size 4986 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png index 2ce4c4d04..fb2688850 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed2_Rgba32_Blank48x839.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73d153851e3af63efd643b7394bd9b7ec8038bcdbd2cf840f237722d1cbf7fda -size 4939 +oid sha256:73b7471840d14e27b6892d3f84be6d1b3867981afda1524862cd3351cfce71a5 +size 4896 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png index ad44e8869..8de44377f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:258ec0147568222dbb8b31c9983af4710b1087156041463806f5642d5ce5df57 -size 14398 +oid sha256:d4ab45d1241c6cd534bca395e0ef9f093444246ece8c4e2d649da87618986641 +size 14297 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png index ee3000dee..0e6c7a306 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanDrawTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b9030cb4f27a6d5ce5413661dc67ceb869dba2a34ecaf9c007946a83d9c1282 -size 13249 +oid sha256:f8e88227709ea57a720a0c5012b0eb76dc8880113b9b4f3052089e98d310e4f8 +size 13394 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png index 9f4815fb2..9a20cef7d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVerticalMixed_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b4ce6602d0cb30fc4be9231733846585154fa4a7f758a2f16a1a56ecb769cf1d -size 11113 +oid sha256:dde20574ec8bd7beba3d8e3629657e0c3ae029ae7318473b6729413d391a6e45 +size 14326 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png index 57b32fdfb..846429ef3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanFillTextVertical_Rgba32_Blank500x400.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e33fb0e2d48a2f36331cff72a2da12a1755238bf966ca7ff3fca7e15245cdf8 -size 4497 +oid sha256:d91ae45bd6e142e6efa60bbe9948f282dcb4791eed2ed1d1fb9132656de10d2e +size 4530 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png index 0c362455f..3501a4a38 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRenderTextOutOfBoundsIssue301.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f88ac63b47ee66823b5fc6d930c27f50dd1432b4539c28305ca69ef961572683 -size 1133 +oid sha256:4dba65ee80dfd10d3b37a4a87ebf390ecd928a405b9aece928c09b668627bedb +size 1125 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png index c1e16f70b..4a3500044 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fcc1cd944ae8c269d1e0762059030ae2272bd69b2d17bc85fdd904ed7f6ff99f -size 1953 +oid sha256:daf19dffcccfb9986db2445108455a2c77fb8ccb58c87d73b92341ad8c1610e2 +size 1927 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png index 6f01de5e4..226bebee8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateFilledFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:281e9bf4b1d155d41922dec436d03b656cffaf4edd70ffbb267cf8eec89386b1 -size 1723 +oid sha256:8270e48ece66038c0133ff5c94f02bb915a0c15100998bc5de04214b627950cb +size 1708 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png index faca38cad..e0ce9ef88 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(32)-A(75)-STR(1)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1c09326e00d6f95c5458fdfbbaac1d49eab98ba91b660b3a595c55bf6c709c9 -size 2591 +oid sha256:331de24b598458cbbaf4c2a6797b6ecb683d0a3d4031609c3918ef57c22a16a4 +size 2578 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png index 26760cf37..bd0a41a9f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/CanRotateOutlineFont_Issue175_Solid300x200_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(40)-A(90)-STR(2)-Quic).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac954ddc1a1319578bf0f81256fcbcb5add52253f3d21064b5959acf1142308d -size 2498 +oid sha256:d98e65df71d0172bf7ed0c94af41171ed968188cb984f31543e8b4fcb802f6b9 +size 2497 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png index 580577b02..3b7e3bc1b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/ClipConstrainsOperationToClipBounds.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7a381cf9d9a6999f30c9198cc7dbe3797ba3a066605c2f4c3562cea7664d9f6 -size 31167 +oid sha256:9e501499026e1a62a33b048b0541e1c51d69891189947c65c0faa176b2eef411 +size 29760 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png index 5e95d2191..a8b7e434b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:142b5e03b7004c1f1c52c4b05485e02068860348e1d90cbc695f21b94b665273 -size 15606 +oid sha256:85efe3c0ea93bb31176a4c29d7e4d893ba460efed56e32ea72fce1c02371ffdf +size 4381 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png index b247ec2f4..b3cf47e13 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:660c7a03c5c95eabb890afa65aac828ed6aa47c03f5114705bc780df91cc58f1 -size 4643 +oid sha256:527e9370db4fc11519499c58a771b3db3aa0d03a1960b2439437e9a1db5a654a +size 8182 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png index 26659267c..8e47a82c4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85f35c20c79ccaff7ecf3c99f220e05e2cebb165e20c4c78be92e637ca2369fd -size 4643 +oid sha256:e48ba2874190eea6b8b33fbfd206935285c9262ffccbc66a3de297c7f8193d80 +size 7405 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png index 86bf1cf36..fb78c559d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d689c43e5c9e6ea28c396d48ba9cabc71e30143449fcd1481901759de97e9df -size 4640 +oid sha256:30f7a5d02ae237e246068acdcce6ed5002a5f2753ca812df8be942da8b92a222 +size 4650 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png index 86bf1cf36..fb78c559d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawBeziers_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d689c43e5c9e6ea28c396d48ba9cabc71e30143449fcd1481901759de97e9df -size 4640 +oid sha256:30f7a5d02ae237e246068acdcce6ed5002a5f2753ca812df8be942da8b92a222 +size 4650 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png index 0f201d656..716eea640 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2f26de15a1f4337969df4bfe7efcb572230721ad21010d49bbbd60deec27323 -size 6769 +oid sha256:729e68e28c8a6bacbc7ce8436a775915fde0963ae8be065c6651676047b4808b +size 6944 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png index fc9bf5add..ffc794189 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Dashed.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54522fd2a0609ee2c428960ad620b5bcc120be6b3a951e95a6e446700efa176f -size 8188 +oid sha256:d816c725383d44f9b2deef3c07d1bec82bc24359c6a2b7b9c4ed98be2f2ef272 +size 8223 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png index 2e99abfef..9f57f9e14 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Overlap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e44d5e58c0f41c7ac9cd7b936db456b73249e608a76d61c146ff0b40d24addd -size 7054 +oid sha256:5e3523d31ec0dd3b89b425d37b3f158869c1b574735a241aebd67421a8983261 +size 7219 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png index d66e51c99..059bd4b98 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawComplexPolygon__Transparent.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2185e16f6c55bcd2f7bdd016ea2563983b53b896218ce0b4e72feeb7e18156ed -size 6119 +oid sha256:a785b0bb8795eb5d23ccb37e8976dcf5ed5cb5f92af8a5a4cab68eb3fc6ac365 +size 6203 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png index 368f44ff6..28a9c62b7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 +oid sha256:33d05521a734a9645eda292deffea29a6e5583aded9cead27c4336681b76d3df size 90 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png index 368f44ff6..6cd79f23b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(1)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1f38021d5659c8e5ce22d31d85bdc90a141d4cbc5aa5cae18ff7dd403961935 -size 90 +oid sha256:a2a405e64d39be85b3475f615f06eeef94c6f3f2ed577765a6fb557d3d1b4a07 +size 83 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png index b213ccca7..12324c3f4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b -size 92 +oid sha256:f10fc35770b62695c92570d6de2a464f00988aad37cc35b7aee0cf97f4a2cec4 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png index b213ccca7..6053918ea 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLinesInvalidPoints_Rgba32_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da58a2cbefb47348fa0563b6d2bc1fd81697c7a388d13be988d4aa84be480d8b -size 92 +oid sha256:f22b03e2ea6fcd6ad41d4509e53866d2fe6a7e8b2ee1f799bcdf73167973d32d +size 97 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png index 6c637567f..effaddfb6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDotDot_Rgba32_Black_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2829e5dfc6f4853b5768fd4915111f0562c33bf5f34e341265b2bfd77bd0cdac -size 996 +oid sha256:fe39f7f19f0ecac81e57094946db235c78f6c54a11946e014dcfd95940de8c74 +size 997 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png index 083087091..ff159bcbb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_DashDot_Rgba32_Yellow_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04a606b11f631e90ba9be606ca63cad163f0cb9bf6749b58e9d3709ccedd54de -size 2405 +oid sha256:866c456f117c27429200ec127a2e806bb4015bda44fa781ee493016e3adfb93e +size 1020 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png index 547b9debe..eaf97614e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dash_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11b66faac1f2735dcd623b41c91b45c0077004b52789500f826a2e47f9ef0a55 -size 1072 +oid sha256:6940b1313031e529227c47d46a53468468557d0bedb9545082f370b92e018a65 +size 1080 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png index 27f4564db..d84fddeac 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Dot_Rgba32_LightGreen_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5809a59acabe48b8cbb66dd3636fe1575856d461e511c78632ef9551c2d3c14 -size 903 +oid sha256:ea45e05097cb5fc0a2b570c13f2fd1cf32b7beaccb703db4f8b71e1edf5c81ef +size 901 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png index 37b789988..82bf4d058 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapButt_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0b7729976799cbfe26feb64d15c7947f45d3696489c9e903bf5e4db6581d141e -size 2812 +oid sha256:13cb2dc240f3b4daec5183d02f7236536c483574cb2f699e75580e1d11cf7ad2 +size 2791 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png index d9d2e1b47..98a506bc9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapRound_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afc4d28bee0241c3aa3c031608a28938105a856d0edec2ba27c5787348aa1b7d -size 4217 +oid sha256:584f3e2fe45ad352be6f8836145d9c04de16d4135dfcf28e5b6ab944f9146e1c +size 4230 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png index 9f3ab80a6..f5a4d1c7c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_EndCapSquare_Rgba32_Yellow_A(1)_T(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fd7146b76f927bdff762e4f9fb641f76c0251362cdbccb7587b23c6f37d5591 -size 3105 +oid sha256:a2539a412cc6506af072535e0cd160f220097acf864c087eeb6432cb097fbbfb +size 3171 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png index 544f82da8..e324acfb6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleMiter_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6db36e204d5449f8e0f4bf0126dcb3517107a86c27516d439fa89e1acbe92618 -size 3851 +oid sha256:a4cb44d478e8cfb45b1f650fdb3c1ba2ce6493a7402a9cc01dee7eb77ee8abbe +size 3067 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png index 4336d5440..0333820d2 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleRound_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ff9d084f5403e816192f9ac5dcad2fbf9a2e223da7888a84978a922853e6902 -size 3873 +oid sha256:bca90dbde5f0c63eb6cedc9b67b1a1f7b4598a2adcbc2b40df0ac42090d2213f +size 3840 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png index 0a161bb6b..e996f9dbf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_JointStyleSquare_Rgba32_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1f81085a6d596848b70aa0b8dbe131aceca5d778912a6e0349a76ec2abe3428 -size 3857 +oid sha256:6999b29f6fba324f44c5c39a493a509f4492488edd5329878a9b3ddf4693b0a1 +size 3825 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png index c8c1d931d..9399d3e42 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:972e185cfd4eea0b39a4dc149a1d762133fcaaef09fdd36e05f29d0a6aff938c -size 3641 +oid sha256:a609157d1bca13d104b6c24e1fa3b986b461411c829accdc1a435b5aca738655 +size 3642 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png index 9212dd414..43879630b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9d81af055fc4b1218ca5968c5e386d864ec8eaa0518ffc7c55544fc8e96d9e1 -size 3077 +oid sha256:71375fde7e5f3b556ada4858831115b09906d10c03eec828dadfaf392540549f +size 3081 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png index d87520a15..03ab88bc1 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f665c137029b53c147a84a635a01a62e024257b45130d929323ff5f36bd89265 -size 3273 +oid sha256:3520c806fc6ad61f57f1e79a9e4318d87c7d31e09008ed383f0f0b4633e113d8 +size 4253 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png index 7f7e70b75..9293df6ca 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawLines_Simple_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:442300c5be415a8750ea5c417cea575bbc96073aaf805237a08f2f56f4b80a6f -size 1061 +oid sha256:5f2161b3d1ce4bafc8dc583aba65b1683637f5617e106bd75d9fbe3f012b2227 +size 1056 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png index 0bda1081a..40e0145ee 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_359.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88071bec9ae4522829edecd1708695e635a64be8ea8fcead219c2ccebc7ea690 -size 1758 +oid sha256:d1ea814eb776d5f6224ad8b0042ca9658789d490063865d1ecfd70253120862c +size 1726 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png index 01a7ef30e..924d9133e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingAddArc_360.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd6a3395d5f5f5be1472fb24bbcee5d2875bda2a7ec7d39e7c2a1cb4c27433c9 -size 1697 +oid sha256:98a37d928a6298859666d0d570ef3786ae22a467dd351c18e7693dcf07805fb5 +size 1673 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png index 7bb6c13cb..196ab480b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c0c45c56dc401cc336599dc41faa0d9328a90f3e7175cc4ebd377b6e5a8fb0f8 -size 1562 +oid sha256:9144e3eadb065da3c353bd3d2bd7d48b3723b4307441c50cee8ba481cf343aee +size 1510 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png index 67db28ca0..b75f537fe 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathCircleUsingArcTo_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87aca77294e8381ebe2a4cacbeef12723bb692c9c34d448ce9f4fc96cc4eef4b -size 4887 +oid sha256:a6426fa279985a51b6ad06f2e26d3fa06b66fe13372510137193aced4947723c +size 1496 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png index 114c8d1cf..3d5f4f536 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathClippedOnTop.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8df1fac9d4eb6c875dae5c2e4bbdda70b7067fdfa8b4745b884f645f0574fb6 -size 204 +oid sha256:ea292371c8329e2364453016c6a89b18f7385b858e1b9acdf58660e215c994d9 +size 210 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png index 6f598f18d..cceb10a3a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPathExtendingOffEdgeOfImageShouldNotBeCropped.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b06531c1bf7a096b75e4a80fcc5022158a11dabc5d96dca99b75de7dabda386 -size 5520 +oid sha256:e094b2c57ec46d60b60d7f9a772755a82c40f2e7c8f99c578b89ce35e6d7f238 +size 5493 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png index fc7549df2..bcf0c8925 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A150_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f5ccf61088696cc13a3d322ee34be9dc1ecffac92d655c8a4bb3dc8ea259dbf -size 8046 +oid sha256:27bc906153430b09b38ce85b9903ced5944a1a5dca5eaf1c0086f8d4d00171b7 +size 8032 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png index a596daf15..83c3fd706 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_HotPink_A255_T5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc26ee7c34e23d16d575e02e4e8c5b414cbc1733ab7547207641a42da7247a3c -size 15608 +oid sha256:40f8b71d69bde2346d4634ad8de7c3922a8883517f905c989d4167cd209e355b +size 15588 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png index 5d096af75..669933b74 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_Red_A255_T3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a3069cba2914fd9240388e6e84ba869f9f38a4a553b60f9892c112cae401907c -size 15408 +oid sha256:baa261af1ab99e68d21cfaec8f16383ff29ce2dff9775c40fee5bf80428de8bf +size 15428 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png index c339a8cf0..8530e239d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T1.5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fa9cc8ea61478654737251f1c33125115f38ba6f2804f8167762b508f0a3836 -size 7582 +oid sha256:9c7cb7d6f904e55a7c6b81856b76e715a0a43675ebb135b0ba30f4a6f3222571 +size 7579 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png index a3664c887..639b30c67 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPath_White_A255_T15.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ad4f83017c447f8c2bbc0cca2e28d3caf53058f9378257c5c5b69d1d2eaa4d4 -size 8116 +oid sha256:90298cafeec606095e5e6d0d150d3bcdf73cfd0b04d57eea87712e803954e6db +size 8076 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png index 0632d49bb..4934be331 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygonRectangular_Transformed_Rgba32_BasicTestPattern100x100.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74d9e27ef56c1783e335739185abc8163f7930f20d84605099045bd2ac1cbd0a -size 601 +oid sha256:fd2e2ecc4aaa1a97a393fee3c66fcaf03bf834b167c913e8c204b3ab75d4a241 +size 559 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png index 06eeccc08..64e3208f4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Bgr24_Yellow_A(1)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3cbd4a43a23893223eb6115e899b5a327aeee373b2545086707d3ea4f8eaac3d -size 4840 +oid sha256:86ed964b614b50054124aa737266eb074c6d370fc6cb7e3bc0022b3e8524a488 +size 4658 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png index f6ffed973..b16938882 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(0.6)_T(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dca30a379135739dfee32200cbe8d9893070ca0e0c915c0dddb2f34dba0e9b6b -size 4910 +oid sha256:b29fc24a266f9d208dd54d05df2bfc490ee1bf3293b5c4614d8ffa427bf89a15 +size 4851 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png index 514af8dc1..d236bd658 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(2.5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed2ece186260d83016ddf0c98cbf9460b2301c25fe37835dfdadd8f55c993a7a -size 5839 +oid sha256:9defda56009f1cab547c9022e2d162d0ba4b9bdd2794908f0eb0abb6dcb1688d +size 5950 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png index 254581e31..b2a7d6600 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Rgba32_White_A(1)_T(5)_NoAntialias.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9eeec58daf1701fc6f900187892619d2598c9a2f064ec17123d3acdd7634c780 -size 1229 +oid sha256:bd9436d93cb5d53304fd37cbc64b956906217c8a54a283a3fee90f8300fe611c +size 1226 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png index 057c4f7b7..932a275ad 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawPolygon_Transformed_Rgba32_BasicTestPattern250x350.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:122a38b0fe0aef338a8cf79a0eb689813734fef7778ce3990f0de4179dca638f -size 8790 +oid sha256:840afc19dacd26157aff8c5b99f37cda865e2f03b0997e1e80050dbc72d978e8 +size 8784 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png index 1d51563d3..b2b2942b7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x200_(0,0,0,255)_RichText-Arabic-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:efcfec120e4b66fa14261ac779d8c2a75bb5a817a2feac80bacae081861f4e6a -size 3106 +oid sha256:203744697d038fd8764d6063792868f1c1948a41b78aa279eec59bc9e16c9a35 +size 3095 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png index 77b80d1f8..ea5335ca6 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextArabic_Solid500x300_(0,0,0,255)_RichText-Arabic-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:337d37aea806e5bd6cc1f4719173b0fbad28a9de3e853f48b6f17e9d355c4e1e -size 3920 +oid sha256:e5af6ab8085f9c2abea85bc556ded1ae927ce46005a6b4455b188147b207ab1f +size 3974 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png index 47a7c53f6..621c76c86 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x200_(0,0,0,255)_RichText-Rainbow-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ad1ceecc6021c051e3ef97bc7e65644bba9ec0768da9875bb8897fe54201c4d -size 8845 +oid sha256:000c4701adfb688247e5e6dfe826e23dbc677932e8e2bd2a5f5025703310573a +size 5983 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png index 27fde42ac..e919531da 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichTextRainbow_Solid500x300_(0,0,0,255)_RichText-Rainbow-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21c84de6bc3f85f1f1751c8b25622d4796a8bd94fa706a7aec47752ca217e284 -size 11428 +oid sha256:54ac67ccc86da32f48ce97ca349a8bba7f31ad03a4af2dd27ae9275380ea478a +size 7560 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png index d1954f866..06015eb88 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x200_(0,0,0,255)_RichText-F(32).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cc8d66c35bc9782cb9cfb106355620e694cb930d2ce280a67c5a2800b013028 -size 9148 +oid sha256:b43babafdbdf2b0ba6e38d1f8df33f4ca5a20084a10d2a5899a91c02a0574604 +size 8887 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png index af0ca8347..e1fc4089a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawRichText_Solid500x300_(0,0,0,255)_RichText-F(40).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d91b10b7a9276a2cbc61704f6c25d94d11e7c3371e9259f72cd5b5be992278f2 -size 11843 +oid sha256:edd76e5a7d69abf54f78efea0fe65b85e67d6db11358c67c9264efaf465ea06c +size 11855 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png new file mode 100644 index 000000000..b505e9a10 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextStrikethroughIgnoresSkipInk_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3eae426348b062957c574806ad0c7086cb6111047a0736ad08bfe1c68141ee0e +size 2275 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png new file mode 100644 index 000000000..037838b5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_auto.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fe614161403bce9db3906f89889a763efe598c6f5b32b6c1de497bd60f84689 +size 2314 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png new file mode 100644 index 000000000..cd9abac7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineSkipsInk_Rgba32_none.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d6d8e876244992089268bfffad199323db2bd932dd06d972d59150455fe3b1f +size 2280 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineWithoutInkDoesNotSkip_Rgba32.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineWithoutInkDoesNotSkip_Rgba32.png new file mode 100644 index 000000000..2fbddfc41 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawTextUnderlineWithoutInkDoesNotSkip_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:030e16e4c6f4b6ac800d9fc66eed7d03a82a0f7674ca28645efb821a8b409471 +size 1303 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png index 20a41191f..edc2f3b08 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/DrawText_EmojiGrid_NotoColorEmoji.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:522cbe7a037065fbab55539265658a27f669940059ab7aab30db0c77e8177c4d -size 728604 +oid sha256:eaf160b7ef8d87f813b2b92313c803c531ed39e5bf5d3b6d73730ce70d0b2aa8 +size 704710 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png index 2fa4a68da..41f8dfdce 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:96979d9458c0723f35aa088d2e346289289389766a83c947fe7f3a2916d1c410 -size 10033 +oid sha256:f0c3737056cc0d5a38aaad979bbdfdbb4b5ec89cbcdd69480a5b7902a689b1a4 +size 9872 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png index 7a0b90cfc..64825c995 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/EmojiFontRendering_Rgba32_Solid1276x336_(255,255,255,255)_ColorFontsEnabled-True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72d19a8122803bc7f40c8b5d85a296280a0f5531c2eb1b13efe7f86aa4e982f4 -size 25515 +oid sha256:125775d23ff3092883e28d7ab6bb6090aaf6e83916bd7b7344e5d76373e70584 +size 18105 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png index 62d287aa3..778e56241 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FallbackFontRendering_Rgba32_Solid400x200_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02ac1d24e53661c1fce8d69413abf6bfc1142d5602a580532c4f39080075207d -size 1804 +oid sha256:cd20e84490d3090662a2114bfcbe9c80ca0df3c690d99a60c90aecd63ddb5d78 +size 1789 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png index 06c04c71c..f46c6626c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e2dc06fdec0dd48bd1e4e29420bf25e094fcf98263634e8efb0b11f85f5892b -size 932 +oid sha256:329ef9404b72f69cfb97be34f221654c6584dcf0c804d0c199ae09f36b50c3a1 +size 928 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png index 90d85f0fb..629c6b3e3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.10_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a742a4e1ff6786d4435a3abfae5f3de97b8409434a7fc7c7d8c567cdf523c7b8 -size 796 +oid sha256:5d83deabb4cadd61a49d02bd197927a9f2227a5ed9c2d79aae24b1c4ec2b3429 +size 797 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png index 82fe59ac1..c4c6afdd7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97cf0529ee30218df153d180864938918f9ef118cdae35f6d0d347e088b41776 -size 1445 +oid sha256:5a210e868c7a893e4e2e841c3b41a2c63b38d21494c1d81b648b816f5682172d +size 1363 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png index a3f902e34..156cfa703 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.40_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6aa7cda377b908fb4c4cd9ae1b8e2dfc493d82f6771a1ad3e074657c8c431c0f -size 1329 +oid sha256:87a081132c5f67445bf0050f3b4489ae9ebe5a83b054fdfba345d9e467c772e3 +size 1390 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png index d96dd5716..2a4962f21 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_30deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd10694b16b98783c3aa16ac9e50501fff4c5c9a5dd9a78ee91dd2af8c949f8c -size 1687 +oid sha256:b916fa2b242ecde3a539879c8f173b64e8c3f9b3fe499304ee664cddde1490e5 +size 1721 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png index 322d69db7..7c485fc97 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillEllipticGradientBrushRotatedEllipsesWithDifferentRatio_0.80_AT_45deg.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:405686e70c0f341f219496dd0b34e7ad5373ac43eb38358dee9a5e59d67cc77a -size 1674 +oid sha256:6500df9520de82e28fae146a2d6ff048ab70213b989ceffe18a54a3693c7c0ab +size 1690 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png index a3e06f7a9..6ec54cbbf 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathArcToAlternates.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bd23bc44fc7d54c1f77be3e0fa13d9abfe0839d0cfb3aa6dfc31a88f518242f -size 1795 +oid sha256:07e3ae648be8c7b49bf02fbe55eca3dfe9e2ac9443c915c25c5abbaa8df55c51 +size 1775 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png index 0ab0ff081..60891375d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathCanvasArcs.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2bd470d283cff870e291ab85e6d95289f408087ac231a7a6d6d5b868d718681 -size 1659 +oid sha256:51e94e42cdd8b8206f1f1feeea8dae41312e997c986c5a55219a084715eac85c +size 1652 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel.png new file mode 100644 index 000000000..8d4a81f75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel_HalfSingle_Blank20x20.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel_HalfSingle_Blank20x20.png new file mode 100644 index 000000000..8d4a81f75 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FillPathGradientBrushFillTriangleWithHalfSingleRedChannel_HalfSingle_Blank20x20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f38baa9ef4e4fa2484a2036f2b03f0aed4f2d82b5ffd42cba01643a12552621c +size 240 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index c923025b6..424b0b440 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36b706cada79f2c4d3cbbf7f361827201181c9b2629730f59c479bfc6771b707 -size 17267 +oid sha256:6872a229f42638b130921a7f56f4960351cbc8fcc5abeece89925f1654700a0a +size 17499 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 537c3c0c6..6435a53b7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8516205e56719c706abfb9703fed2dd6bf4e80662ecd12d0211461d29bbd5065 -size 798 +oid sha256:acb6d7261e226a71372c60c8bf4c4c12dcbdf3e69a90880bad96a5e3b38bc8a0 +size 790 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index 3f56b6710..2110478cb 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPenPatterned_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f16c7ffff13d3997c1e5371f38a71a5fb5b1085ac9868e4bad41312494b9cab -size 16822 +oid sha256:34b8c497bd9cde44db04478c6deb92dbabcae2e14acdb3e05aa4c0bc265b61b6 +size 16965 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png index bb7c1b711..2b32f6930 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid1100x200_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aed4070e23f4d58e17101ef2210ba6a7796f9be5a10b4c0448939e144960a93c -size 15227 +oid sha256:22fcacdc7995676fed257d92af1c1967efcb22bbf7f560ac5bcf8fc77751553d +size 15065 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 801784a2e..dfef8823f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid200x150_(255,255,255,255)_pen_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1086e1b2d276ed69dd63bc30017907b78e032b9dd0ccb28a4d51db7413ead817 -size 814 +oid sha256:5d15066bbd3c490f258ae5be853974d11178888cfee73acd66e8d0ed36160f4f +size 789 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png index b7ce49703..8838f564f 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectlyWithAPen_Solid900x150_(255,255,255,255)_pen_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8afc25b0d7888836d28095d97e068749ed33eda3bb707e197cd234d700e76a1a -size 15460 +oid sha256:b183072d61d42409bd9c1d5cae22a96a3eed6c159bbc2b5222d655d02d39afe6 +size 15386 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png index 0afba1cf9..b95bb51c3 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_LargeText.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37e581157826a3e1d9ef4b925dc808fad438b363732054f896b74b8e6b4d4e41 -size 241409 +oid sha256:46bc54948a8ce95c97da3eb909a6d2d3a1840d5c0efc5a2d5f665ba9257d400c +size 130784 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png index b3a44c6bb..55eed1ce9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid1100x200_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(150,50).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4b534ea97b96425c0af83a75e004bd1f3ae35be1ca1382899877bf488c48b07 -size 11120 +oid sha256:9bcb292dde40781a191846cdeb8a6ce1ff771a4e86d2614c02d8d3d0c6cc032e +size 10972 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png index 8455e5fcd..6b1a4acd4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid200x150_(255,255,255,255)_SixLaborsSampleAB.woff-50-ABAB-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5fb5bb7a898eea05c2564f82b05527b91591af97997016ddaff2027576a5fc2e -size 698 +oid sha256:bd492d0fc04d091802534f1d0dae491328aed1f04bed0164b6de8aec2864929d +size 693 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png index d8dfab133..7c9634752 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid20x50_(255,255,255,255)_OpenSans-Regular.ttf-50-i-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9a8cb42bef151d899fe30a4799d4a4734a933ac93d7dbff531686babec342a4 +oid sha256:ee6d51df9002b3b11f6e64d2e61872bca07483bc897b4d727da3705df5a4b8cc size 147 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png index 8c96c3d54..6e0f5fe13 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid400x45_(255,255,255,255)_OpenSans-Regular.ttf-20-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e75f18e1de464d87aa48ce53db604a36740d4190eb447bc18a98689106692a05 -size 3701 +oid sha256:cd6df4330dfbabd7474c98774a58e67ccafbe9b59d49f115814ae7580677e666 +size 3649 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png index ac0923150..b6e648e01 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_Solid900x150_(255,255,255,255)_OpenSans-Regular.ttf-50-Sphi-(0,0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534a1c455b321836444ea0dc41a372bb9b083a69fa28e610458bcfc369665224 -size 10611 +oid sha256:15c50a278cb765d97662f1c4cb38ad70c30eccee33f59dc0b3a2a303db9164a1 +size 10361 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png index af45f33cb..5761f761b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2783552269c0c37d60fb9b032c59c923fdf143a098a8b3d6f0bd3ac9e0f4c57 -size 2903 +oid sha256:0695afbff866e73ead0df2c71d48c6a78925470d2c3ef79470fadea3e022e459 +size 2932 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png index ad1dbb37f..d3f97c96c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1.5_linecount_3_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3fd9d0b03b86a56c7d89081ec35a9e84b5f5579f764b0de9731017904d66003 -size 24808 +oid sha256:56b4b12320241768e60102389a7c5bc2480bdaee3c5743b5dcb5fd82ef55d851 +size 29443 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png index 37c7af7b3..230522e23 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:091c39c7d46d0125c1149a7ef0ca9e01c604b5e31e8fa50ad78a82086dcce977 -size 2984 +oid sha256:bfc831ad8f6aea39b2cf42ef6974cf417d8cfd40d0bd8d4889791098ef85d041 +size 2945 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png index 2e10f81fa..30cc5a54d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_1_linecount_5_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0aef3b2a6cc7cf6fecdc880adb61cefc3d34c0a16e27f5e189916e1f9c053e9a -size 35398 +oid sha256:a336b86687d75001d8ca85d7d9a760f8ed848efcb0b47f7b2ff661dcdbca44e6 +size 47470 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png index 64f10d86c..e4d169808 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_False.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d88a5d5136531056d70c2d2f115bddc9089f2fea51fdd2bf06cfa9a8a6982c7 -size 2890 +oid sha256:847f9cc666ec6ba93bc2395748edea1f7b75a67aa2ff7265f2e145ee93e5e6bc +size 2910 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png index 6ec0c11d8..37c4036c5 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithLineSpacing_linespacing_2_linecount_2_wrap_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eaa94bf2957e7363a97a0c90736950f39b6961321fe89633ad5eaee1fe859e4 -size 45421 +oid sha256:313497ac05e4f145fdb64d81be5261e4f4a449037335d846c7b262ec7a20fae8 +size 20300 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png index 2871d4c02..83bfb512d 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54536f8aaae1fc48d7b2273509b613c02ac5cc0541c846011dac5c2f376512b9 -size 43912 +oid sha256:5209ac432c48e91ee049e4e288df60f29fbd1e9837ced9ea29e9d9764b6b1881 +size 13694 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png index 935282e14..e9054f7e7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(45)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c83d07e7a7a089c2884e77b751bf3a09d85fc6af1659eb4835ee548137f4898b -size 1094 +oid sha256:218381b5777a1ea57a1288de407ee0bebda6c0352d47edb0218e567f724da9c1 +size 1091 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png index 8cc06c98f..0de8fe7d7 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(45)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fdec81f93a8d50fe454e05b8eee302c4539b365df6fafe0b35cea370ca356a32 -size 5234 +oid sha256:07d50fa6126f183117f143fdff8dfef1ca02a2a9ed798f31c230a41d475ecccf +size 4982 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png index a1084dd6d..b3ae8fad4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithRotationApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(45)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f15a9a114c67c2329e27417f41f1a2d10a70e38b8d8be95cf2972a51400c01d8 -size 229 +oid sha256:ca5fd285a2214a08f363a2a3ec040cdb81bacc2d55dad2b92f5393ef88ae7d8a +size 226 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png index d2f168927..171669a87 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid1100x1100_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(0,10)-Sphi-(550,550).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6481dfb2ee66c6124a32153f00d70f7de46827800de03efdf445adf75bccf408 -size 14704 +oid sha256:482fadf09985e54978cfddcc811059efe160be0e24f4fffc5efa3136999f0049 +size 12830 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png index 20e62ed09..9c8174833 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid200x200_(255,255,255,255)_F(SixLaborsSampleAB.woff)-S(50)-A(10,0)-ABAB-(100,100).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d26e529bfd790ed177a7ff3a31e5a293c22bd40290f008de03146b2353d93be -size 2731 +oid sha256:0e8cb85e04fddb4f66f7a4a2f3f91da554c081c870822d20f12bd59fcb865206 +size 997 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png index 861bdabd1..dfefed36e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid400x400_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(20)-A(0,-10)-Sphi-(200,200).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8784b5efc02286f904c97b07051a2de3f3febb7835f089d14f11232ffe7c0ee9 -size 5026 +oid sha256:be8bc6a30334f2c06acf4c1b1f5be6feb8307b4d6ed9da0e87f739bcd97fcdf6 +size 4849 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png index b32058491..a15d84060 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/FontShapesAreRenderedCorrectly_WithSkewApplied_Solid50x50_(255,255,255,255)_F(OpenSans-Regular.ttf)-S(50)-A(-12,0)-i-(25,25).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:509e8691cd700301a8c0b7542f55a7069ab9d05db7f52f881f8518d8e5f8a0a4 -size 293 +oid sha256:6d357dbffb47720eba47c1904fcb5c1d071d4be44774db3b9fab754c86d9c093 +size 292 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/HatchPatternBrushes_RenderGallery.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/HatchPatternBrushes_RenderGallery.png index 524782e14..adb2defdc 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/HatchPatternBrushes_RenderGallery.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/HatchPatternBrushes_RenderGallery.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95b45a768fcb7a4d2e0fd8e436b74cdb35118fd47c1a337ac6967e881c3bcd42 -size 4872 +oid sha256:62a6918bc67e7191b34f9b0cc7698b1dedcc173e0a03d8d0525279405007d291 +size 10980 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png index f60224e5b..17f1d3682 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(10).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2a3c896ab200989bbb653d2d029563bc97616065061f0662f14e3cb8de6e173 -size 80946 +oid sha256:404969ab16837a682ca55e98939bed6de72e85c2499f0214c0081467db71575e +size 80943 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png index c4120d34f..04d8d1ec9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(3).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa58e9264435bd56bde8d9713eedd977fe89c120ef216707c02f2a0007741158 -size 18171 +oid sha256:880190f21912c51e88b997ec04e0820fc048268df6c2333399d521607310b775 +size 18194 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png index fc21e4642..748039568 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_LinesScaled_Scale(5).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b739646c2aa4b745aefb87471a36cbebe4566f8470ed0c0647ae9841776bfd8 -size 34740 +oid sha256:5d063f016936b11440f7b2d1db4a5bb66b0f38dbb37115fdf5827a539fe87787 +size 34730 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png index 1c882755d..df716f826 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(0).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:06e80d62763ea95029865b0365929a2c94bfaf6635a1fab978eb660823bff3df -size 4490 +oid sha256:cadc4b69bfcd9374f441611e9c7164d5d871b254ad5f9b3ad10ba2b3b88d8a5f +size 4473 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png index d521ee2e2..010cfadc8 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/LargeGeoJson_Mississippi_Lines_PixelOffset(5500).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0e56b6e6c96867e78434bb511706e5ef10d2441845ad41655d79a4021e94852 -size 41034 +oid sha256:466cc101cd71a1e8ea13ab405012773d8e507f96ef0ac9b1bf9d3119d087d073 +size 41030 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png index f9ace84f6..5c0b0f85e 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/PathAndTextDrawingMatch_Rgba32_Solid1000x1000_(255,255,255,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e0766a586c335eb362211407cbc261e5c1bab28515f15b162ff91738627eede -size 36780 +oid sha256:005e2d553f3841bfaae9f5263a0a3eb74c2d97cad7e58678e6f4383d5b9c9d63 +size 36235 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png index 8ee9484f2..8991c564c 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank100x100_type-arrows.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7b52e2255f8f1a7b72bb6a6937580d87cddd3b339a7894f5ec3f052b8485c64 -size 407 +oid sha256:bcb2bd1764b18c6572a11e4712690321e929cd5c8407e907da93c75604e2e45e +size 424 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png index eeba35cb1..f87631f72 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x50_type-wave.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fbc6b1d8c43ddbdf4358e84bc1d163b8e1c4dc0b7ba3b5f47ad5a8f4b861cf7 -size 679 +oid sha256:42cf6e22abc4704a88820c9841f5be8b6d606c4b509ece4427545922b8b66c0b +size 612 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png index e55a958f8..6fa81d28b 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank110x70_type-zag.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b93e2240bad4293d5ea0520ecccc15104650961cee116c4091120f2ed3b2cd0e -size 564 +oid sha256:c4fa78b477da68330eca8148e9259660c99f0568c0543e3e050d6cb696a3a891 +size 536 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png index 1b4c9caad..5a73450e4 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-bumpy.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc4965a857d93d6c092d9eeebc63f2ab5e1fe9dcaa12a1fb91dc14049315fe1c -size 4868 +oid sha256:ca78f4d28f84517ba314a2287699401559855fa2db0ce201eda3cba2a993a66e +size 4883 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png index 6d2deae35..0f80e4d2a 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-chopped_oval.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c96d1280248da68c20cbb501d38fdcf0d88fb516dad5ca3df942ca91a51955b2 -size 2835 +oid sha256:09c7976b47a421005aba5440b8a1c0de9f3c086dae25fc28398af282d4ea4a07 +size 2819 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png index 2d27668f2..fa73bacd9 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_big.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:512354b7fee3b606a3b78f394e290772d395893f3011bbe6df02c93f0f02ac44 -size 2528 +oid sha256:577da1878a30515d65e071b7d595b6bb0af3818e564d034ac55b0cbb4ada7656 +size 2523 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png index 2bd77ed68..31be59c88 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/SvgPathRenderSvgPath_Rgba32_Blank500x400_type-pie_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f45a4565ee4b1210982411d43795c603db2509438823f2658a90d16108a42c53 -size 4847 +oid sha256:be6e7a562b928e1b3abc4bf8ef19c2bd8f385b3a2394d49dedd6e171c42542af +size 4902 diff --git a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png index a162e8a97..8dfe44199 100644 --- a/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png +++ b/tests/Images/ReferenceOutput/Drawing/ProcessWithDrawingCanvasTests/TextPositioningIsRobust_OpenSans-Regular.ttf.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5458a51422b2f9f126263260c59934934c24b36dfd810e246479f5b542de5c1 -size 188042 +oid sha256:949834f3ee2376514418763db33d2d81f5f3ddda52f2a27e6dcc10c9e980002b +size 258674 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..d4237742e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10068ba45692e023d4d5a809380c0ae99e2336d87828703a35887bb4f9685522 +size 1137 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..0a220acb7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/BlurredBoxShadow_DifferenceClip_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b52cb6e0a5d10e888db9445b68e16cde9fb4b1c4b9db506dfe8ac41432d703c +size 1095 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png index 715c0798c..f11733333 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3550e8fc5b0f537d834ae76cd82cdfc9b9c774ad8ed8726271df7d8df196a5e8 -size 32251 +oid sha256:5e840cf50433f85bf9f9f04046d114b232fd9888c290a3581c8eb0b627acdc68 +size 31558 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png index 3492bdb2f..b0a75b741 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CanApplyPerspectiveTransform_StarWarsCrawl_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d7ff4c13edacc3e7231bf722f6da11c3727cf77c3cca903a113447edd6f0c9f -size 32253 +oid sha256:9b54d12359554df5814d5f6abcc9a58fc7b704b6188dede8a94d563a86e1d7bd +size 31579 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..530de5598 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c9f245e420e381c945f1185ddaad705a88e130a58f8a45d7e3c908c3bfed4f58 +size 1323 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..edf4edfc5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/ColorPickerSliderThumb_AvaloniaTemplate_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:328316e65eeb6a781fb4c65bbe73d7077867d531f4c7dc449faeb706f98b3837 +size 1310 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_Default.png index dbf64bf12..23996dd7b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:394d7cc4cf1ce07d0c4439d0e8c5d10287fb8e1357c625ce7d53d02478cc9360 -size 12316 +oid sha256:b4a91e81f7708c71c8d1f3c20b2718b9f036c088b90e3890e2078bddacdf4de3 +size 8643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_WebGPU_NativeSurface.png index f6feb7a58..19e0a05cd 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/CreateRegion_NestedRegionsAndStateIsolation_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3624cf051ee87ce47bd737c54bcd29ef6e1c909aa8fd751fa391abed05e26c70 -size 12257 +oid sha256:6ac1e5d9dee6fa9a98c4e41d36c9474df3eece3ccc52de6d2fdfc95613e6f2b8 +size 8512 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_Default.png index 1e58ef804..6fb9c7911 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8de3dfe58f5fedba85052153be27d985a319189b3b5d09691b6345ed23dbbddf -size 617 +oid sha256:9c01b5077acca5cb55e6eaf0e9c999699ba3d3f5ca106a4df324fb6dba8bd184 +size 581 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png index d6b153574..11fd80d4a 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e87dacd58a64a7082e1741b02e61342568f8bc3736ef580fcba12e935bdaa2f -size 771 +oid sha256:64d12dbda62fd48b1b60e2011d4e3adc53ef5a460eea42980a0f437d5f9f8cc0 +size 510 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_Default.png index aae566655..281cb1c6e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e8c48367ca984d33e8c9cbcb18857de3216ddb47cb5637d142bea599ae33c928 -size 152 +oid sha256:5bc01cd0976465f23819d3651afafb1240ea2aeeca4b7db07ccfa5ab4a5c7bcf +size 115 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png index 7b1779293..e1789317b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_PointStroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd168bf4a93f5d7650714aa989a2c3ac6ebc92e23a244035b25db444ed7c81e6 -size 185 +oid sha256:bac3b599fc1dcf1a94f7d26f1ac6f80659699ab706a739080f1e7d3547441ba8 +size 132 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_Default.png index a24c0faec..cca4f8ae1 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1001720816b7e9093737b4bfb96f8396876b2893c9299e685af2c19a6f97ed9 -size 1933 +oid sha256:d2a291e0e895d609a2d56ed301d8416f313119aa8e6ecccc88257b23e0107a57 +size 1790 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_WebGPU_NativeSurface.png index 2f4bc8a53..6f7c92be2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Butt_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a8aa27977e3028e144e5c1512afd48379f5c705ea097f9cce42e313b8fb8114 -size 1907 +oid sha256:7e1a210eadae5af13b8644919acf5068516c125c1eeda9a51c5bdf617f17ef7b +size 1848 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_Default.png index 0ac98b842..ced026b22 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a3e555fb00cfcd576ecf428bed184caa47f880600763198783b9e488519d05b -size 2188 +oid sha256:b94c66753bdcc56c6f845c7a85c79020d8232cbe06de3d94dc75d37ba284dc3d +size 2011 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png index 67df3ff2a..17d0d5747 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a034e0f12a80224fd70f0a4406cefa8de62885b33e2c525ce299b0c4c44db34 -size 2193 +oid sha256:616cfb2ed6114208f2cc72d8b6ccd41109da21dc1a6db0e365c758e1d9f11e44 +size 2183 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_Default.png index c9eee5c0f..e1916d1cc 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1847ea8c1075e6e08f6e286f2033a479df4773b3e040113ab52b44004b78ca6 -size 1964 +oid sha256:4f789cae2340ee66ad407b98b85ca654e9611dc19ec6b19fd69524b5395af019 +size 1863 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png index 3f9b73d13..a8431d950 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineCap_MatchesDefaultOutput_Square_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:638af32164dad3a9c46d90696745d9665297c8d9e5b3dcfea78b804390af56a2 -size 1944 +oid sha256:67b2bc7ffd475a721fcc2d8d4df83a3b79591e9d394dc73cb9e360d63a23b24e +size 1922 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_Default.png index 49b52728a..10b28ef46 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:18dda6a04c1073600f387a5b844e7ec7636412527ebe4ebbb61fa4260dbc53fe -size 7564 +oid sha256:144553e4b34c08b72cb6136e1c793179471ebe3a4bd310dd0aab79d27f7cff50 +size 7529 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_WebGPU_NativeSurface.png index 44a44add0..fb91b6359 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Bevel_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ced034c009bb67f7cd908988399f263ac6ef43e0f4ce6419b54521da41127bbd -size 6948 +oid sha256:39168b48f24c19427241c584fdfa86f283914e1532575dadc3db65248ec8a656 +size 6892 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_Default.png index 776b2a907..6efc5ddfb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8ccafb683bd723e5fc3ef132989f280cf5da6e0f90c642b332c2af08c4c5e34 -size 7956 +oid sha256:d46c19e3e59dc0df8c5dae7999e889fbca414cf1c71d80088f94eb8ccbd4cdb8 +size 7957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_WebGPU_NativeSurface.png index d6148f525..85039ae4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRevert_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa506a7d76a7b48cf4eaa362c1f20b6a2a6563728894fad6cdb2fb3a884d6812 -size 10810 +oid sha256:097da981e01ae4433b3665e437fbd00e992f3c9ac30c4f086db4ee6e9cfd943c +size 7267 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_Default.png index 776b2a907..6efc5ddfb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8ccafb683bd723e5fc3ef132989f280cf5da6e0f90c642b332c2af08c4c5e34 -size 7956 +oid sha256:d46c19e3e59dc0df8c5dae7999e889fbca414cf1c71d80088f94eb8ccbd4cdb8 +size 7957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_WebGPU_NativeSurface.png index d6148f525..85039ae4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_MiterRound_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa506a7d76a7b48cf4eaa362c1f20b6a2a6563728894fad6cdb2fb3a884d6812 -size 10810 +oid sha256:097da981e01ae4433b3665e437fbd00e992f3c9ac30c4f086db4ee6e9cfd943c +size 7267 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_Default.png index 776b2a907..6efc5ddfb 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8ccafb683bd723e5fc3ef132989f280cf5da6e0f90c642b332c2af08c4c5e34 -size 7956 +oid sha256:d46c19e3e59dc0df8c5dae7999e889fbca414cf1c71d80088f94eb8ccbd4cdb8 +size 7957 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_WebGPU_NativeSurface.png index d6148f525..85039ae4b 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Miter_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa506a7d76a7b48cf4eaa362c1f20b6a2a6563728894fad6cdb2fb3a884d6812 -size 10810 +oid sha256:097da981e01ae4433b3665e437fbd00e992f3c9ac30c4f086db4ee6e9cfd943c +size 7267 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_Default.png index 34ac2d2b7..7a67e330e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c013aa0e12f182330dd261f70b06e56f1c89c49cf66c6c73cb8cf274b94fbac -size 7819 +oid sha256:c4853dc191cbedcb3a243d3d5631eec8986dfe780e70c7b70bb25e6d7aa64a0a +size 7729 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png index 03462a6c5..ad257f2b4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_LineJoin_MatchesDefaultOutput_Round_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d70344ccca9b41549beef1f34f9525add05ed87994cb195333371702a47db184 -size 10557 +oid sha256:300d9ac7e51887d662e5e6d7e95c58b992af4f361837809d0e8385d500441998 +size 7059 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_Default.png index 469f7f57d..198949eb5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:71d65b2e9cde54299a0924486292388c7b19017b34bcf9da34267ce5de1a8434 -size 3138 +oid sha256:8f1f49fa671863807868b7a37b1274fa282222f9eb4af365a014396856e144a8 +size 3171 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_WebGPU_NativeSurface.png index 7cd83a297..75adc79e9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawPath_Stroke_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72b6664a1e2fc077f6a4ffa949f1e2226a11e79222fd6ce3cb607e70b1f2acdb -size 1958 +oid sha256:dc925539f04edb37fc366c5aaa7b611d01379a93796f28586607097df09bca41 +size 1944 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png new file mode 100644 index 000000000..07c675adc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a58cead7e52c6ea1829b82fa4f400fe32f91b8595202fd4477ea2c9078c19461 +size 66604 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b870c19fd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_acrylic_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ee23743376762e09ae283e99628dbce4b28a81c74cdc399b039ad6f9ae48cc5 +size 65249 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_Default.png new file mode 100644 index 000000000..bded2417d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ee918a85958ad8e1f126a5bd1f39884a467d5529b6cbb4c2b5a354eaf0afe70 +size 69679 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_WebGPU_NativeSurface.png new file mode 100644 index 000000000..4db4341b1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_blur_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1d1dc6a336308089ec99b3f2cd51778eb8c01ecacacd8a3a313ff71e7f0f8237 +size 69716 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png new file mode 100644 index 000000000..4f7743068 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64f3e5d3cf177a0df472940873ad98b468f64f827d3d6c9e61dcf2ff49a1f642 +size 77722 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ffb07805d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_brightness_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6532b2e82142d5303a95db86be0bd64ad25ea5fcfc6d9e01d1a005f082e11dbe +size 77829 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png new file mode 100644 index 000000000..e024ed48d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:17322cc550d237e05c534c91c641cd976f7cd836cde6229134a0ad0a71f27020 +size 72160 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d393c7da7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_contrast_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d38a0027007b860181c632bb9e1119df23de333f5810db7ed6535113eb8210c +size 72263 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png new file mode 100644 index 000000000..cc7588fe4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb34006faceb18b4fbd3961205973764b6036af538b3fee020cd8cff493153f0 +size 34267 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png new file mode 100644 index 000000000..510ef5d73 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_drop-shadow_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f495e08d10d7053b6cc5aa0f621796ccfcd510a8fb03f3b9c205087c151ddf6d +size 31625 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png new file mode 100644 index 000000000..d106f1f7a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:578b052de461c3a0084ea4b1e3b97801db01f4ec95e9f8adc89cfa74dc421b8b +size 82590 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3e411d0d9 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_grayscale_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f177e925938dce7bd2c7192419c20793abdb4a7bcb46525b4b74916761e3e922 +size 82875 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png new file mode 100644 index 000000000..316f373c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:701391f9c868691025c96857cfa9eb0a3df590a95169bafba0b19bc17ea4b47b +size 84664 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png new file mode 100644 index 000000000..64f17c702 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_hue-rotate_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:812085a35695ebece97fe85d3e18cfb8baded84f24534e6270ab9dd848d98ccd +size 84889 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png new file mode 100644 index 000000000..90fd5bb05 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2537b4d7c153b80f89a0538deb327aed7d12c82b1175f9f2bbfc33ab27a69ca +size 71742 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5979e0c0a --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_invert_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:319cf3dca8898dee0dd006894d50d69092a29fab7d50f6b67bd726c3754e7134 +size 71628 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png new file mode 100644 index 000000000..4f8fa50dd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b6058ac4dfece7e49bd39f185fe747d223413d0a406114d50f3c36e31e4b0ff +size 99356 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png new file mode 100644 index 000000000..27789bd26 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_opacity_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0f22a5329320dee31be8b6c95ac5d66483eee92d73d15c6f1291ef6ee293700 +size 99323 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png new file mode 100644 index 000000000..17b47fe7b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9da62ab6f283d632f4363eb46b3e1cd10826d7c516e9ad68fd1316ade708ddfa +size 82618 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png new file mode 100644 index 000000000..84000d3d7 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_saturate_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59fd15a088d928105d9345b153d6cf85c0e421b5fad474903e80a793e224b7b1 +size 82952 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png new file mode 100644 index 000000000..f60f6a9cd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ec9ba79f2d8c59040a7ecd4a9a0676094572b21932ef34b47311b65f45a5fad +size 80237 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png new file mode 100644 index 000000000..203c190a2 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBackdropFilterLayerEffects_MatchesDefaultOutput_sepia_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9382f596dc28c6d00b47f9c0caee5a2320c9b32cf0f989ecdfd8907b33957ab +size 79964 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..cf3561f86 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:600d8528434533716f60e0794ee7abd2b84d6c71f1e5a3079fa0083ec4273614 +size 5368 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..74348e7cc --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithBlurLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26dd0fb62b6e07c839dbf964afa152d26f4ed5e4faca43701bba8ab9cbb9684b +size 5402 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..7bc31727e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a19abb7f0e241ccbe0df04025c4b4ef3af66dc93979d5441b5d3e8bd3d753b9 +size 4199 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..a7c8108d8 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithColorMatrixLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:277eb95d415d1482fa9a565d1b7f8d201eb6b95d92631fe69649a989e1b192e2 +size 4274 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..34bfe2631 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a3b0508ede2f0c4aabbce9458921c68c13b4bab2e482b296cf8a55e016fffd4 +size 14597 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..3f0c84e20 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithDropShadowWriteBack_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9314e8e2dec92b5ec414fb00d4107b362e765c31047c5da100ac57eb90990fa1 +size 14576 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..efcedc97e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e41e7aebef1a3df5c4c0266ad31a58cc08f84740fbaaefe8dbf8762ae3b34647 +size 9946 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1b355fba5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithGlowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5344dbf11c23c15fd803f11aa71b1342341b2ebeb2000dee60939d3153fdbe18 +size 9833 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..b10afe350 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07d653b76945b0e3d568877896ab07e80ef588751486877acfe0076228370527 +size 7266 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..5bb89c1d6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithInnerShadowLayerEffect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d1fe6cd31f550af4b0dbdfa1ed8c7407ca55e4b9b9448f589b4fcb3d28be815 +size 7292 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..7e4576c9f --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d5c3f2278a3e6871320d503656aa034f2c68ba62f8e7a320590a987aedd8e1d +size 6147 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..da14f8469 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithPolygonLayerEffectRegion_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4cc46382ced75767332523251d2950f66f895262b57dc259f4002be7ca24ed6 +size 6199 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png index 0e2be9129..c4b6f0d95 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bea09c41618a5c396666825b57f2defc649e900d10bdbd2102a311f7afc1dba3 -size 10858 +oid sha256:0c61a178a6908cd03a7a436d1e9795449f12f69a2ef27fabea2f799c9e5921e6 +size 10534 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png index a289c9c99..2bc25c365 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_AfterClear_UsesBlendFastPath_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83ecec67faad7b98f0c68a5a86db414c98202f078bf8b9e72362c101a06b17dc -size 4785 +oid sha256:5c56ce62d0faf825ffeec92e622e5ba74225f009b35a5257eb079911220e9290 +size 10942 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png index 005e86998..f2e676e84 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13ab6cb9074db0fbc070bc97e631bbd38b031d4266029bf66846e180c747216b -size 6712 +oid sha256:a8e36e9f56a6cd19d7f55e42326533a94ec0cfdd577d4d58883bf4474dd6b56b +size 6681 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png index 9362116cd..2137643b2 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithRepeatedGlyphs_UsesCoverageCache_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74f4ab7ad7867b2f6c1f86cc44e94e86536094191291cde817d371ccc5c40477 -size 6695 +oid sha256:f85633558d6e7fd5be3a72ec98eb646e51a28f239fce087bb36330f197f68bdf +size 6713 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png index 2be1008bd..16b10d5c4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2051841f3e1182da9ff72bbbc27784e3848886b33e2f227eb01d2f1dd3dc99b5 -size 36533 +oid sha256:a6b4c172e4dc15d2fdc687c6a350e01750a065bf543a81ad4e09883c6f2b263e +size 32781 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png index d0794e7d0..e1bc818d5 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/DrawText_WithWebGPUCoverageBackend_RendersAndReleasesPreparedCoverage_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:165843cb3ef816e046075097da8f37ac8afee38f6174b50062c86594f5796b07 -size 37448 +oid sha256:d73f68ae8e559c5fa39f27b4a15466a7e01d802a3cc755b58f062fc8442fc4de +size 33235 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_UncontainedGeometry_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_UncontainedGeometry_MatchesDefaultOutput_Default.png index b96e4656b..dcab0aec9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_UncontainedGeometry_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_UncontainedGeometry_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5db9018a114a65e3e9db216807b92abd79b82584dba8faf9d22ffd5df1eca8a -size 1298 +oid sha256:1a203e363350fb2addabea3e81a238050385962c24750e54f61356ef9feb87e6 +size 1316 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_Default.png index 068a89979..3ee5fa5b4 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13405f754723b9cff04307e94079a2826a25c20e16957bb5abea556aceea4399 -size 38897 +oid sha256:f0302b87005c560b4c32b9705eac7865e08279d4f1586d6fcc208a986a9e02fc +size 39586 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_WebGPU_NativeSurface.png index c250aeded..17d40d97d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithEllipticGradientBrush_Reflect_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb4aa2e84a6133afff33ccbab87791eef154cef4ddae2ae10bd06e1d103df7b6 -size 39875 +oid sha256:38f3c4911b50fda06ca67633051bf7940a923f810b8f4bdfbaea55ca62cf75c3 +size 41712 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_Default.png index 1a905ff83..29b8b5c0f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 -size 566 +oid sha256:01714fd66f9ebe14953eef1a78c6fca871c2c8748a0eb3b1d2153640f5f9c8e0 +size 799 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png index 1a905ff83..29b8b5c0f 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13062ce218d198269d6b2f130182c0ea30bf12a3460e72d6dcb57a2975bdf719 -size 566 +oid sha256:01714fd66f9ebe14953eef1a78c6fca871c2c8748a0eb3b1d2153640f5f9c8e0 +size 799 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png index 1b1ed3e3b..24ef8c644 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c -size 826 +oid sha256:3442a977e8f47f788e5a118c207f9dac844c5592ba1afb46c2b7952ca4aaf37c +size 837 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png index 1b1ed3e3b..24ef8c644 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c77c3bad7249bdd0231f273e06c2ddfb46683aedc59644f1fd07baff3ecc9c -size 826 +oid sha256:3442a977e8f47f788e5a118c207f9dac844c5592ba1afb46c2b7952ca4aaf37c +size 837 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png index 00a793ec2..99e1f0a1e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 -size 798 +oid sha256:252c32d2064fbe1157a36988addb663a6a78a87f3939022577138043398cf610 +size 801 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png index 00a793ec2..99e1f0a1e 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e877874f1c5f36f423c177a9b891b52f748426fbd76c38744f28745ee8fb1cf9 -size 798 +oid sha256:252c32d2064fbe1157a36988addb663a6a78a87f3939022577138043398cf610 +size 801 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_Default.png index d835b86af..6d463fd95 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 +oid sha256:b3dc2518469e876466b5ba2cd4dd81e007968a73e7d0dd2f4cbedf65ce42bc73 +size 487 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png index d835b86af..b7aa6943d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 +oid sha256:a839010836a445dbccecbb3428fd403818adb6593101a8569a68af265d0cdd3b +size 496 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png index 71d06c28f..d01f6f601 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a -size 533 +oid sha256:b9a63a0fa9c1b0c3b687530c7a428f584a3a2fcc0f4e60d6b8675574284ec0f6 +size 821 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png index 71d06c28f..d01f6f601 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_ImageBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:211e9f0118bb44a4b539400d74aed635cf951a5834e330b2d74416d5e9b6dd0a -size 533 +oid sha256:b9a63a0fa9c1b0c3b687530c7a428f584a3a2fcc0f4e60d6b8675574284ec0f6 +size 821 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_Default.png index bcc59e5ae..80dbc62b9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 -size 472 +oid sha256:56952fe2202e1c86ee31ae0749aa8167efdade8864921ef5fae2c42b131c1b6e +size 487 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png index bcc59e5ae..80dbc62b9 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Add_Src_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4215d621ff15138795a72651e8aba14fca5aea4356b1d3a1687d78e2306e71f8 -size 472 +oid sha256:56952fe2202e1c86ee31ae0749aa8167efdade8864921ef5fae2c42b131c1b6e +size 487 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png index ff3590331..70ca754bc 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +oid sha256:49d1d5424cee2b9c57d3f7aa326d588ddcb756d67d287c2896af193ce19fe149 size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png index ff3590331..70ca754bc 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Darken_DestAtop_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b88bdda75c9f2addee9d898b9d9dcbfa45f247de2d9f4f771b3d31051fc8dd88 +oid sha256:49d1d5424cee2b9c57d3f7aa326d588ddcb756d67d287c2896af193ce19fe149 size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png index 43394d294..8de7f7e77 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +oid sha256:bd0a6f255abbe17a06f847e2443aaec6343a8af1b92eb41755106fdb997b2281 size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png index 43394d294..8de7f7e77 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Lighten_DestIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d274697d9d07a0f27e610e796452aa09db103a96473e7bac8decd0c656ee0d5 +oid sha256:bd0a6f255abbe17a06f847e2443aaec6343a8af1b92eb41755106fdb997b2281 size 471 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_Default.png index d835b86af..6d463fd95 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 +oid sha256:b3dc2518469e876466b5ba2cd4dd81e007968a73e7d0dd2f4cbedf65ce42bc73 +size 487 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png index d835b86af..b7aa6943d 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Normal_Clear_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38f8361e3cdac288d1276a33393c8dbbbb1bbe4e239dd99c36fba7b91ff0ff46 -size 446 +oid sha256:a839010836a445dbccecbb3428fd403818adb6593101a8569a68af265d0cdd3b +size 496 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png index 9f6074d7f..ee4a466a7 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b -size 474 +oid sha256:13851fbd2a795e73f30b79bb694ffbb23e999a9c0fc8c5616a3d0afba277c3d0 +size 493 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png index 9f6074d7f..ee4a466a7 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithGraphicsOptionsModes_SolidBrush_MatchesDefaultOutput_Overlay_SrcIn_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1433e8e3d4c0cf4f1a67080b5ceef482980177b4a6828048d05dea98e682697b -size 474 +oid sha256:13851fbd2a795e73f30b79bb694ffbb23e999a9c0fc8c5616a3d0afba277c3d0 +size 493 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_Default.png new file mode 100644 index 000000000..8bd5c0a57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6c76f85260f1d74474d759253eb94e881340309e2d08e66abbae3c8ddfdb980 +size 5124 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_WebGPU_NativeSurface.png new file mode 100644 index 000000000..8bd5c0a57 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Clamp_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6c76f85260f1d74474d759253eb94e881340309e2d08e66abbae3c8ddfdb980 +size 5124 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_Default.png new file mode 100644 index 000000000..11b21e367 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58f176b93a4df6be3d0b0cfd0af02cab6ba9ca7d24a01c773c9087ca55260cd2 +size 9322 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_WebGPU_NativeSurface.png new file mode 100644 index 000000000..11b21e367 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Clamp-Repeat_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:58f176b93a4df6be3d0b0cfd0af02cab6ba9ca7d24a01c773c9087ca55260cd2 +size 9322 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_Default.png new file mode 100644 index 000000000..beeea4a48 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1fcc224852063d95c33e3660aff1ccdba1f56f839cf2198ae525aecc65a60f2 +size 8450 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_WebGPU_NativeSurface.png new file mode 100644 index 000000000..beeea4a48 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Clamp_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1fcc224852063d95c33e3660aff1ccdba1f56f839cf2198ae525aecc65a60f2 +size 8450 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_Default.png new file mode 100644 index 000000000..ebbe013b6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a979b59b2565475ce0e54cf1416f45c2b55f30fe9cda46c006951c089b599f +size 11191 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_WebGPU_NativeSurface.png new file mode 100644 index 000000000..ebbe013b6 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Mirror_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:30a979b59b2565475ce0e54cf1416f45c2b55f30fe9cda46c006951c089b599f +size 11191 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_Default.png new file mode 100644 index 000000000..b34ce9bdd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e5c878849ca6e31ba823c603bcd02231a4fc2a31890171952304548ea36ffae +size 14246 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b34ce9bdd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Mirror-Repeat_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e5c878849ca6e31ba823c603bcd02231a4fc2a31890171952304548ea36ffae +size 14246 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_Default.png new file mode 100644 index 000000000..2e445ee23 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27d80e3abeb0a864e3cc711f6aa5eeef56b51642258d3ce1d0662f58f178453 +size 4361 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_WebGPU_NativeSurface.png new file mode 100644 index 000000000..2e445ee23 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_None-None_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27d80e3abeb0a864e3cc711f6aa5eeef56b51642258d3ce1d0662f58f178453 +size 4361 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_Default.png new file mode 100644 index 000000000..d1c2b5fcd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61c4a5cc1af26ac86d4b05478271f50ea1468795cf9ab5c211d72c6ec36d186 +size 10612 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_WebGPU_NativeSurface.png new file mode 100644 index 000000000..d1c2b5fcd --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Mirror_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c61c4a5cc1af26ac86d4b05478271f50ea1468795cf9ab5c211d72c6ec36d186 +size 10612 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_Default.png new file mode 100644 index 000000000..1181d9c00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba84c8624d96118dc4d6610385b8ad615eabbdbdebcdb86908a3f329bb38e56 +size 12643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_WebGPU_NativeSurface.png new file mode 100644 index 000000000..1181d9c00 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithImageBrushWrapModes_MatchesDefaultOutput_Repeat-Repeat_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba84c8624d96118dc4d6610385b8ad615eabbdbdebcdb86908a3f329bb38e56 +size 12643 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_WebGPU_NativeSurface.png index 5fe35068c..154575056 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithLinearGradientBrush_ThreePoint_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2cec37aa744945b7c075253238778b0092e6fa04e4a244a460a58b328f945dc +oid sha256:9b7f8eddcaff139bb9fe86c5bafc18ff3ff8493a70267b10286491fba2470f70 size 1367 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png index 3a8ff048a..2d7628f51 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_Diagonal_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:026e23fe75ae773b53c6063d3555acc50aba0f2c53beff2fd2c09467bd22331a -size 2052 +oid sha256:c5eaef4c95aa86e0655b87d82504b23aa99268501af4603f8ed46cda1b4e5c44 +size 5999 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_Horizontal_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_Horizontal_WebGPU_NativeSurface.png index c9e429e3f..6ab030eae 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_Horizontal_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithPatternBrush_MatchesDefaultOutput_Horizontal_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b10c9935727ea60fce6d8b2c554c19333978a999021510b1dc53e0db87c8d54c -size 145 +oid sha256:307414b963ea80f76cf65ea7b37098b37cfcf582020e71f526f7b30b98d1ddf7 +size 940 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png index 86da5bda0..8a18efb06 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_SingleCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63bd36716166044824a024b097a998333e20b7ab99e05516428fc5eaa392c1d1 -size 10370 +oid sha256:6c1574eeedd282bd335abdb2f66376fb1be37184ba6ec80b93ae4b0e9cb5e90b +size 10636 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png index e114907cc..006d21d92 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithRadialGradientBrush_TwoCircle_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9912f3fe725924a302683afd6d89178e518ba5b28540eecd923d17fc74424356 -size 8833 +oid sha256:32c8ef81ab1c429a96f3f676e1e984fd458925d82e73d17b339d04090fa60fe5 +size 9478 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png index 895678780..e31dc6594 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cb106015ce0b2d1a4171d9b31f9b88481b5764d48c130002f373fbec93252295 -size 15266 +oid sha256:d53d5e12d37b636245c4950bcd8f451ceda00942786c0d8523ced3d1d1998b66 +size 15267 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png index d9cc7c0e0..1332891e0 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithSweepGradientBrush_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:863826416e252b3d6ee0d3f737d20c8d0f25563fa228fea69c5efbb269df7fe9 -size 15816 +oid sha256:c593a8659089d650f3a2684b2cd8008eedee73d46ac4531647320cfeddc3d72c +size 15827 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..8d82b8ca3 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b8fdf795537a76e6c6d2cad30552507daff8576587305321f731ae74fd8fefd +size 30601 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..9c6075b2b --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/FillPath_WithTranslucentGradientBrushes_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9bfc6d87b1de3f96243937c021d524d6dc4cb2397b30875aba3de7be75582619 +size 17374 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png index c7a7ea6b8..7769792b3 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_Default.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82af0b7a5959132f9750349f330e11c2a8ed1c0a843a37386d838ac7a48c25d8 -size 12958 +oid sha256:cba5e51955bc1fcc91f7a4d5a5b2e094653f8706879cdf8bf34d05492e4a2c22 +size 13041 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png index d9230eb43..a83fe538a 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/Process_WithWebGPUBackend_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9fe3487d22c6358ab99ee78761b34be2636e54a5f9d7e161b39102f73f67cf8 -size 12933 +oid sha256:f0c56dd2fb41eeff013cc8b1ee953583916a0d2b7e2954fe709bc439d998c368 +size 13021 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..6b8503694 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:138f557a498e38acb370b5771ebff5e8bfff3d6c554ddf365247906cea65a484 +size 105 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..7fcbdcada --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_CompositesLayerOpacityAfterProcessing_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef30aa96c6c6077eb8e959ebbe1039a206cad020ecd7e52e6468a4e74f4be295 +size 105 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..991fbb6a4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc38fe1666b8e4d8dfa938b1be217d277b5b88c7175008408cc546517cc07bf9 +size 129 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..991fbb6a4 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesLayerTarget_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc38fe1666b8e4d8dfa938b1be217d277b5b88c7175008408cc546517cc07bf9 +size 129 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..825d14995 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be20d49f58bf7df231b0b6abd5782fc1ad6cb40b5ce3c6c431d66803d7cd1a36 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..825d14995 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_ProcessesNestedLayer_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:be20d49f58bf7df231b0b6abd5782fc1ad6cb40b5ce3c6c431d66803d7cd1a36 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..b6733e456 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47df18b4f78ae161fbda1832862f48b8ca943aaa18a5d7f4133e464e31162722 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..b6733e456 --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_Apply_RespectsLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47df18b4f78ae161fbda1832862f48b8ca943aaa18a5d7f4133e464e31162722 +size 104 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_Default.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_Default.png new file mode 100644 index 000000000..9a1233a5d --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_Default.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e611d1d098d8261280ab22abb395ea322333a8a432a5f9b81bc2de24482d0db +size 1179 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png new file mode 100644 index 000000000..6fa4dfb4e --- /dev/null +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_GaussianBlur_OffCanvasLayerBounds_MatchesDefaultOutput_WebGPU_NativeSurface.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adee30ae69498ad08919e1e3da8bd89ba688f2548b9b37621e0c734ee6ab3271 +size 1185 diff --git a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_ManyLayers_UsesClipReduce_AndMatchesDefaultOutput_WebGPU_NativeSurface.png b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_ManyLayers_UsesClipReduce_AndMatchesDefaultOutput_WebGPU_NativeSurface.png index 7b12248de..ead8a8233 100644 --- a/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_ManyLayers_UsesClipReduce_AndMatchesDefaultOutput_WebGPU_NativeSurface.png +++ b/tests/Images/ReferenceOutput/Drawing/WebGPUDrawingBackendTests/SaveLayer_ManyLayers_UsesClipReduce_AndMatchesDefaultOutput_WebGPU_NativeSurface.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:128e441de7d49e178b754b24ac8a96c22b07408b535d6bdc2975389bf4528fc1 +oid sha256:c6ae2a89c663c9309b3284c2ec44c580c0162792fd3c4f91a325511bf344f34e size 334 diff --git a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png index 88cbe43f5..2217a347a 100644 --- a/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png +++ b/tests/Images/ReferenceOutput/Issue_134/LowFontSizeRenderOK_Rgba32_True.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:273b71a15dff92405d71f6e48d4f8afc6ed3ef175c16f99343350c9f64ac470b -size 675 +oid sha256:b8e351dc56c0aad3f007c66e0a945934872bed3cbb88da5c60bc05d7e9a11c40 +size 669 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index a58f6b228..a66b212f4 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5149a0f36e750d7bfab1bd002def9151fe4ca0724c996ab06a340b088b39f951 -size 1486 +oid sha256:8a44c340b48e41dad04b551a496e92e3db146ad878b1d303504d957f09b1b05d +size 1538 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index 8137fee90..c43b8f69f 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9882717c6ee07369824e6f10fde375eb00ff78f2e60c95c1f7d98b9a8653c6a9 -size 1914 +oid sha256:fe380cf59d1766118e129dc04daf071858848d7fd4fbb70e3a631f89356aeec5 +size 1946 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index fb2284b4a..0b43703cf 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae96769ed5bec51c3301e16fff32179345ae070beb2023a5edba691609620185 -size 2000 +oid sha256:d7d79b308432130823e9a76a071d078d0bb1b16ac7b9337cb222c757e338a8cb +size 1855 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index f91cd6d63..3eae34e67 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Pattern_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29edced326ec76c891328b229b493eeee2648a1bdc82f5f83653730bef2dc254 -size 2104 +oid sha256:e89f3e12622d8e788152ff8a86342228d7531b1a01497c5d3ce888ccbe785774 +size 2061 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png index 7fc62cf10..8ab5dc903 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a532675b38d8c8c335069a05726987f36bdcf8b685434da52a7b2e0ce682688c -size 1417 +oid sha256:e2ab27967915e9f5425341ded19d7076305e88a8e6fb05c32ed8566894e8c1b8 +size 1447 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png index 7a4b43c2d..ed85147e0 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-0.7.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed3403f5dca596a1a8fa351b502a44a7d5c68043a41bf143065786dd4fb13d57 -size 1834 +oid sha256:758f6e30661bd8a0e37a42e5efb3631e5ae7f8a91cbf55e007f4c7b5104e9659 +size 1861 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png index 05236c6a4..1be30af79 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-1.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0b7e31aaa93aef0ebe1f6cd0bbd9cd4b09613b595b0790109e778fa77e0f6ee -size 2059 +oid sha256:a3df7d94657fb325110bbf5046dbd4a186c5629b52dcd7a520dd9f67fbc0bf49 +size 2040 diff --git a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png index 968c96d64..3c09cb148 100644 --- a/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png +++ b/tests/Images/ReferenceOutput/Issue_323/DrawPolygonMustDrawoutlineOnly_Rgba32_Solid300x300_(255,255,255,255)_scale-3.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2a500d1b093311a83f20fd60a0fa93a68234c7f932163b170ac0aaf5f57ffad -size 2350 +oid sha256:20161b9743eeb3235cf4bb2dcf458ab27a4f41640670d7f932e48fce11dea7c1 +size 2445 diff --git a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png index 086e32e4d..6c1e976db 100644 --- a/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png +++ b/tests/Images/ReferenceOutput/Issue_330/OffsetTextOutlines_Rgba32_Solid2084x2084_(138,43,226,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ae7557dfd6424b3ad16473a570856eaf5c56c2248ac43d9a69790697311d9f21 -size 148087 +oid sha256:dd99a270bd7c7a26cbcdee1484604c3e5684addcd299462f745a1f822ad07c1d +size 132521 diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png index 015bd5aa2..b409ec715 100644 --- a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_PathBuilder_Rgba32_Solid100x100_(0,0,0,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 -size 111 +oid sha256:2b3d5ded20deded3c20cb56395eec945690da92dddd9cdcea66d3dc73fe24cc8 +size 113 diff --git a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png index 015bd5aa2..b409ec715 100644 --- a/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png +++ b/tests/Images/ReferenceOutput/Issue_344/CanDrawWhereSegmentsOverlap_Rgba32_Solid100x100_(0,0,0,255).png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6628022994715b9a00cc6410236b8c2b26b2d1bbf50e1b9163a5b87a8a7d2bd0 -size 111 +oid sha256:2b3d5ded20deded3c20cb56395eec945690da92dddd9cdcea66d3dc73fe24cc8 +size 113 diff --git a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png index b79b6571d..e03ef7bfe 100644 --- a/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png +++ b/tests/Images/ReferenceOutput/Issue_367/BrushAndTextAlign_Rgba32.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39958ce05ba28b7cb423c273db83d082cf35df9cd5a0a82fe00117f20b68e10e -size 8883 +oid sha256:793b73e516798b16d39fd81b6b66ffc779b75bc813f3921a41ade715a21950bf +size 8171 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png index e7666e659..aaf5e25c3 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Difference_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43fb26b0590be0f4f41fa82445e9c8754bbaab493e1dbb8d93002ffdc5d34dab -size 4675 +oid sha256:9bf110aa83053a3849a78e8a633abcb0863572ce2fef3262a3611565c0ce8367 +size 4505 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png index 0127da8c9..51021d523 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Intersection_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e2a5d7e5d6b53950f7bd79a2bdbba48c2d373103ef9d3ffa7025388f34472df -size 4277 +oid sha256:4164bdfd56128859895310bf64638e2822e7f6a8f35ae644a1a2ea1041baafd1 +size 4243 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Union_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Union_IntersectingClip.png index 434278b6f..87d770b71 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Union_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Union_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff89de4bec0856bd6e05bba4c26bbf77de2d978eaeb0aa76772bf2d41863cf9d -size 6205 +oid sha256:1af63f60a83c57bd67e2da2efde4940ac04120be4d6cf34d12521b8de004cd95 +size 4601 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Xor_IntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Xor_IntersectingClip.png index 8affc7bea..43c0dd383 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Xor_IntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithIntersectingClip_Xor_IntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ea6f78b35838ac8e03f9ed74ae974349805a15f1f74f55a16fb2407ec20c911 -size 6205 +oid sha256:6643ac075d68275388d727b003f69da95f588361b1c27ba91c2eb7a883132e5c +size 4601 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png index 32398229c..7e7394fa3 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Difference_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3125d65a6a97d1b0e7752fe63cab529007e2acbd6658c06905ccef0de4a52bd -size 4820 +oid sha256:e4699a59315ecdbb0994e30876c87b154b046d3caaca92aec5955476b0717417 +size 4614 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png index b34ec1d73..69f6fe3ee 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Intersection_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:305803eab9be305f26cbed0b55dadbe022830d577c65d36624a32bc17395d009 -size 2823 +oid sha256:a1c12cf5ed192345e6f43b890c77f5d0c817b2376972874845c0ac1499ba4c82 +size 2713 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Union_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Union_NonIntersectingClip.png index 27a5357ba..ab79210b2 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Union_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Union_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d763346286309775c8c5d2a6353cf387ad6b44ac29900a036de43c49106a68ef -size 6922 +oid sha256:8937f283ccad2a003be5585df407bb62233a9966cb5ab4a3ee3d5d2fb9d77280 +size 5032 diff --git a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Xor_NonIntersectingClip.png b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Xor_NonIntersectingClip.png index 27a5357ba..ab79210b2 100644 --- a/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Xor_NonIntersectingClip.png +++ b/tests/Images/ReferenceOutput/Issue_397/DrawTextWithNonIntersectingClip_Xor_NonIntersectingClip.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d763346286309775c8c5d2a6353cf387ad6b44ac29900a036de43c49106a68ef -size 6922 +oid sha256:8937f283ccad2a003be5585df407bb62233a9966cb5ab4a3ee3d5d2fb9d77280 +size 5032 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme--5000000.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme--5000000.png new file mode 100644 index 000000000..8f833cef5 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme--5000000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b77288fa4279bfd5a251c3d2c76bf1cf960731b01e860e1f340d4438a866c5d2 +size 198 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png new file mode 100644 index 000000000..80bb1842c --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbd9c3534c8430c05c36a89e558d5fad0b0b214b1318cbc89c053472f813513 +size 91 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png new file mode 100644 index 000000000..8b1eb4724 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e1a5c2e2e9972db6259ddf496aa9a06870aac8b9c79a375bd44188679fe8458 +size 181 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-500000000.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-500000000.png new file mode 100644 index 000000000..a986fade1 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-500000000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73c712f1148f8b335238d1cd023e60d681395c552a7e39335dc4760b85dca2f3 +size 195 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-Infinity.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-Infinity.png new file mode 100644 index 000000000..80bb1842c --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-Infinity.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbd9c3534c8430c05c36a89e558d5fad0b0b214b1318cbc89c053472f813513 +size 91 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-NaN.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-NaN.png new file mode 100644 index 000000000..80bb1842c --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-NaN.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbd9c3534c8430c05c36a89e558d5fad0b0b214b1318cbc89c053472f813513 +size 91 diff --git a/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeYCoordinateDoesNotOverflow_Rgba32.png b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeYCoordinateDoesNotOverflow_Rgba32.png new file mode 100644 index 000000000..b57501ace --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/DrawLineWithHugeYCoordinateDoesNotOverflow_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aaff6a9347604506de5461fb81cf9b59af8db126961f559ff2e1fa3361261eb +size 195 diff --git a/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png b/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png new file mode 100644 index 000000000..80bb1842c --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-1E+20.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ffbd9c3534c8430c05c36a89e558d5fad0b0b214b1318cbc89c053472f813513 +size 91 diff --git a/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png b/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png new file mode 100644 index 000000000..622453e12 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_403/FillWithHugeCoordinateDoesNotOverflow_Rgba32_extreme-5000000.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc84314998e547b567a3697bcd5ee52c328be6974bc274af274e5f76f3149471 +size 134 diff --git a/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png b/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png new file mode 100644 index 000000000..3763af40b --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_405/UnderlineWithTracking_SpansLetterSpacing_Rgba32_Solid800x200_(255,255,255,255).png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a40d6dbf9e15baf6b89dd1ec149d0b19523f8c37bcf32acf8e018aaec90a460 +size 1833 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png index 3fbb58468..317ddfe8d 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:869cb2351bf41e448f7ef439ef2706968e82e3b5f90bb557be76aaea153e78aa -size 32037 +oid sha256:e26e47ca1ebf58e474b0d17af56730219e818127bb8b15a3ed9a4a2bb0330eea +size 33413 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png index e3d070f68..2a9bce567 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_ColrV1-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11d6ad961109384fbd085a5d05c97e5b71e35402ab6ff7ffe5bd8b9342643491 -size 10945 +oid sha256:8879be8652d558996081a543bba5e5f8a5d99ca078d8e5f55950309f36572631 +size 10624 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png index 72e16cd4f..36372a259 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-draw.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a5e93a97ed0961acc74965bdd79126828b4b3024f4841d4657f7b2cfb4afc29 -size 32042 +oid sha256:9ae0c505ede502283dee17e525a291d4ab555c0bc93263e6d883c39d6acf2aa1 +size 33476 diff --git a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png index e3d070f68..2a9bce567 100644 --- a/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png +++ b/tests/Images/ReferenceOutput/Issue_462/CanDrawEmojiFont_Rgba32_Solid492x360_(255,255,255,255)_Svg-fill.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11d6ad961109384fbd085a5d05c97e5b71e35402ab6ff7ffe5bd8b9342643491 -size 10945 +oid sha256:8879be8652d558996081a543bba5e5f8a5d99ca078d8e5f55950309f36572631 +size 10624 diff --git a/tests/coverlet.runsettings b/tests/coverlet.runsettings index 455b7fe84..d03da613e 100644 --- a/tests/coverlet.runsettings +++ b/tests/coverlet.runsettings @@ -6,11 +6,9 @@ lcov [SixLabors.*]* - - [SixLabors.ImageSharp.Drawing.WebGPU*]* + + **/Native/Bindings/Generated/**/*.cs,**/*.g.cs,**/shared-infrastructure/**/*.cs true