Skip to content

Commit d8c0e28

Browse files
Merge pull request #410 from SixLabors/js/skia-parity
Skia parity: premultiplied rendering, layer effects, and WebGPU interop rework
2 parents 01607ee + 959d2df commit d8c0e28

858 files changed

Lines changed: 63119 additions & 9162 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,15 @@
8484
# treat as binary
8585
###############################################################################
8686
*.basis binary
87+
*.a binary
8788
*.dll binary
89+
*.dylib binary
8890
*.exe binary
8991
*.pdf binary
9092
*.ppt binary
9193
*.pptx binary
9294
*.pvr binary
95+
*.so binary
9396
*.snk binary
9497
*.xls binary
9598
*.xlsx binary

.github/copilot-instructions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# GitHub Copilot Instructions
2+
3+
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.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,3 +231,6 @@ SixLabors.Shapes.Coverage.xml
231231
.dotnet
232232
.codex-*
233233
.claude
234+
/plans/
235+
236+
samples/Avalonia**

AGENTS.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Six Labors AI Coding Guidelines
2+
3+
These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree.
4+
5+
## Working Practices
6+
7+
- 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.
8+
- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn.
9+
- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative.
10+
- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details.
11+
- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested.
12+
- Build .NET projects in Release configuration unless explicitly instructed otherwise.
13+
14+
## Performance
15+
16+
- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths.
17+
- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data.
18+
- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits.
19+
- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness.
20+
- 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.
21+
- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes.
22+
23+
## C# Conventions
24+
25+
- Follow the existing code around the change; local patterns take precedence over generic preferences.
26+
- Do not use `record` or `record struct` types.
27+
- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states.
28+
- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction.
29+
- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace.
30+
- 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.
31+
- 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.
32+
- 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.
33+
- 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.
34+
35+
## Verification
36+
37+
- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project.
38+
- 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.
39+
- 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.
40+
- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change.
41+
- Report what changed, the verification performed, and any remaining risks or unverified assumptions.

CLAUDE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Claude Code Instructions
2+
3+
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.

GEMINI.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Gemini CLI Instructions
2+
3+
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.

samples/DrawShapesWithImageSharp/Program.cs

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,24 +157,15 @@ private static void DrawPosterComposition()
157157

158158
canvas.Fill(Brushes.Solid(Color.SeaGreen), shorelineShape);
159159

160-
// Clipping demo: canvas.Save accepts a clip path plus DrawingOptions whose
161-
// BooleanOperation controls how the new clip combines with the existing one. Using
162-
// Intersection means subsequent draws are masked to the oval highlight shape, so a
163-
// rectangular gradient fill can be reused without re-shaping it as an ellipse.
164160
EllipsePolygon lakeHighlight = new(725, 512, 285, 86);
165-
DrawingOptions lakeHighlightClipOptions = new()
166-
{
167-
ShapeOptions = new ShapeOptions
168-
{
169-
BooleanOperation = BooleanOperation.Intersection
170-
}
171-
};
172161

173162
RectangleF lakeHighlightBounds = lakeHighlight.Bounds;
174163

175-
// Save pushes a clipping state; Restore pops it. Anything drawn between the two is
164+
// Save pushes a state; Clip narrows it. Anything drawn between the two is
176165
// confined to lakeHighlight even though the brush spans its full bounding rectangle.
177-
canvas.Save(lakeHighlightClipOptions, lakeHighlight);
166+
canvas.Save();
167+
canvas.Clip(lakeHighlight);
168+
178169
canvas.Fill(Brushes.ForwardDiagonal(Color.White.WithAlpha(.36F), Color.Transparent), new RectanglePolygon(lakeHighlightBounds));
179170
canvas.Restore();
180171

@@ -910,7 +901,7 @@ private static void DrawImageProcessingMask()
910901

911902
canvas.DrawText(
912903
new RichTextOptions(subtitleFont) { Origin = new PointF(40, 66) },
913-
"Apply() runs a processor inside an IPath, ImageBrush fills one with a photo, and Save() clips drawing to one.",
904+
"Apply() runs a processor inside an IPath, ImageBrush fills one with a photo, and Clip() limits drawing to one.",
914905
Brushes.Solid(secondaryColor),
915906
pen: null);
916907

@@ -1068,20 +1059,15 @@ static void DrawPhotoInTextPanel(
10681059
imageArea.Y + ((imageArea.Height - textBounds.Height) / 2F) - textBounds.Y),
10691060
};
10701061

1071-
// GeneratePaths returns one IPath per glyph. canvas.Save accepts params IPath[] so
1072-
// the whole collection becomes a compound clip, but ShapeOptions.BooleanOperation
1073-
// must be set to Intersection: the default Difference would cut the glyph shapes
1074-
// OUT of the photograph, the opposite of "image inside text".
1062+
// GeneratePaths returns one IPath per glyph. Clip accepts params IPath[] so
1063+
// the whole collection becomes one compound clip for the photograph.
10751064
IPathCollection letters = TextBuilder.GeneratePaths("MASK", glyphOptions);
10761065
IPath[] glyphClips = [.. letters];
10771066

1078-
DrawingOptions clipToGlyphs = new()
1079-
{
1080-
ShapeOptions = new ShapeOptions { BooleanOperation = BooleanOperation.Intersection },
1081-
};
1082-
10831067
canvas.Fill(Brushes.Solid(Color.ParseHex("#E2DCC2")), new RectanglePolygon(imageArea));
1084-
canvas.Save(clipToGlyphs, glyphClips);
1068+
canvas.Save();
1069+
canvas.Clip(glyphClips);
1070+
10851071
canvas.DrawImage(source, source.Bounds, imageArea, null);
10861072
canvas.Restore();
10871073

samples/DrawShapesWithImageSharp/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Poster-style landscape that exercises the building blocks:
99

1010
- `LinearGradientBrush` and `RadialGradientBrush` for the sky, lake, and sun.
1111
- `PathBuilder` + `CloseFigure` for the mountain ridges, lake, and shoreline.
12-
- `canvas.Save(options, IPath)` with `BooleanOperation.Intersection` for the lake highlight clip.
12+
- `canvas.Save()` plus `canvas.Clip(IPath)` for the lake highlight clip.
1313
- `canvas.Save` + `canvas.SaveLayer` to apply a Z rotation and then composite the title panel and text together with `GraphicsOptions.BlendPercentage`.
1414
- `TextMeasurer.MeasureRenderableBounds` to size the title panel to the laid-out text.
1515

@@ -38,7 +38,7 @@ Image-compositing scene demonstrating four ways a photograph (`tests/Images/Inpu
3838
- **Before / after wipe**`canvas.Apply(rightHalfRect, ctx => ctx.OilPaint(15, 5))` scopes an `OilPaint` processor to the right half of the photograph.
3939
- **Privacy redaction**`canvas.Apply(ellipse, ctx => ctx.Pixelate(10))` pixelates an elliptical face-shaped region and leaves the rest untouched.
4040
- **Image as a brush**`new ImageBrush<Rgba32>(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.
41-
- **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.
41+
- **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.
4242

4343
## Running
4444

samples/WebGPUExternalSurfaceDemo/Controls/WebGPURenderControl.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public sealed partial class WebGPURenderControl : Control
1717
{
1818
private const int WM_MOVING = 0x0216;
1919
private const int WM_EXITSIZEMOVE = 0x0232;
20+
private static readonly nint ProcessModuleHandle = GetModuleHandle(null);
2021

22+
private readonly WebGPUSurfaceSession surfaceSession;
2123
private WebGPUExternalSurface? surface;
2224
private Size framebufferSize;
2325
private bool idleHooked;
@@ -29,8 +31,11 @@ public sealed partial class WebGPURenderControl : Control
2931
/// <summary>
3032
/// Initializes a new instance of the <see cref="WebGPURenderControl"/> class.
3133
/// </summary>
32-
public WebGPURenderControl()
34+
/// <param name="surfaceSession">The session shared by the application's related presentation surfaces.</param>
35+
public WebGPURenderControl(WebGPUSurfaceSession surfaceSession)
3336
{
37+
this.surfaceSession = surfaceSession;
38+
3439
// WebGPU presents directly to the native surface. Normal WinForms buffering and background
3540
// painting would add flicker or unnecessary work, so the control opts into direct user painting.
3641
this.SetStyle(
@@ -81,12 +86,12 @@ protected override void OnHandleCreated(EventArgs e)
8186
Math.Max(this.framebufferSize.Width, 1),
8287
Math.Max(this.framebufferSize.Height, 1));
8388

84-
// The module handle is required by the Win32 surface descriptor. It identifies the process module
85-
// that owns the window class backing this control.
86-
this.surface = new WebGPUExternalSurface(
89+
// WinForms registers its window classes against the process executable module. Use that same
90+
// HINSTANCE in the WebGPU descriptor instead of the managed assembly's module handle.
91+
this.surface = this.surfaceSession.CreateSurface(
8792
WebGPUSurfaceHost.Win32(
8893
this.Handle,
89-
Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)),
94+
ProcessModuleHandle),
9095
initialFramebufferSize,
9196
new WebGPUExternalSurfaceOptions
9297
{
@@ -322,5 +327,8 @@ private struct NativeMessage
322327
[LibraryImport("user32.dll", EntryPoint = "PeekMessageW")]
323328
private static partial int PeekMessage(out NativeMessage msg, nint hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
324329

330+
[LibraryImport("kernel32.dll", EntryPoint = "GetModuleHandleW", StringMarshalling = StringMarshalling.Utf16)]
331+
private static partial nint GetModuleHandle(string? moduleName);
332+
325333
private static bool IsApplicationIdle() => PeekMessage(out _, 0, 0, 0, 0) == 0;
326334
}

0 commit comments

Comments
 (0)