Skip to content

Commit da11e33

Browse files
Refactor glyph drawing API and clipping flow
Reworked text rendering entry points to use positioned glyph spans (`glyphIds` + `points`) instead of `GlyphRun` in `DrawingCanvas` and `TextBuilder`, and updated call sites/tests accordingly. Image drawing was also tightened by moving clipping responsibility to callers so `DrawImageCore` now assumes pre-clipped input, reducing duplicate clipping paths and keeping rect mapping consistent. The changes also include small WebGPU/sample cleanup and analyzer-driven modernizations (`_ =` discard assignments, target-typed collection literals, simplified lambdas), plus package version bumps and refreshed image baselines impacted by text output changes.
1 parent 9c4d7fb commit da11e33

29 files changed

Lines changed: 215 additions & 232 deletions

samples/WebGPUExternalSurfaceDemo/Scenes/ApplyReadbackScene.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33

44
using SixLabors.ImageSharp.Drawing;
55
using SixLabors.ImageSharp.Drawing.Processing;
6-
using SixLabors.ImageSharp.PixelFormats;
76
using SixLabors.ImageSharp.Processing;
87
using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes;
98
using Color = SixLabors.ImageSharp.Color;

samples/WebGPUExternalSurfaceDemo/Scenes/ManualTextFlowScene.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,7 @@ private static void AddBandScanLines(
452452
/// <param name="scanYs">The scanline collection for the current band.</param>
453453
private static void AddScanY(float y, float bandTop, float bandBottom, List<float> scanYs)
454454
{
455-
const float DuplicateTolerance = 0.5F;
455+
const float duplicateTolerance = 0.5F;
456456

457457
// Scanlines exactly on the edge of the row do not tell us whether the
458458
// row itself is obstructed. The top/bottom samples are added with a small
@@ -467,7 +467,7 @@ private static void AddScanY(float y, float bandTop, float bandBottom, List<floa
467467
// doing far more work than polygons in this demo.
468468
for (int i = 0; i < scanYs.Count; i++)
469469
{
470-
if (MathF.Abs(scanYs[i] - y) < DuplicateTolerance)
470+
if (MathF.Abs(scanYs[i] - y) < duplicateTolerance)
471471
{
472472
return;
473473
}

samples/WebGPUExternalSurfaceDemo/Scenes/ShaderEffectsScene.cs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,11 @@ protected override Control CreateContent(WebGPURenderControl renderControl)
7878
}
7979

8080
/// <inheritdoc />
81-
public override void OnHostLoaded(WebGPUDeviceContext deviceContext)
82-
{
81+
public override void OnHostLoaded(WebGPUDeviceContext deviceContext) =>
82+
8383
// Compile the immutable WGSL program while the first tab is visible. Uniform values
8484
// change every frame, but every snapshot shares this cached program and pipeline.
8585
deviceContext.Precompile(this.shaderEffect);
86-
}
8786

8887
/// <inheritdoc />
8988
public override void Paint(DrawingCanvas canvas, TimeSpan deltaTime)
@@ -369,8 +368,8 @@ fn layer_effect(position: vec2<f32>) -> vec4<f32> {
369368
/// </summary>
370369
/// <param name="options">The values shared by the WGSL pass and CPU fallback.</param>
371370
public LivingNebulaShaderEffect(LivingNebulaShaderOptions options)
372-
: base(ShaderSource, UniformLayout, CreateFallback(options))
373-
{
371+
: base(ShaderSource, UniformLayout, CreateFallback(options)) =>
372+
374373
// The base type creates and freezes the pass. The effect supplies only values
375374
// matching its declared layout, which is the complete custom-shader workflow.
376375
this.AddShaderPass(uniforms =>
@@ -384,8 +383,6 @@ public LivingNebulaShaderEffect(LivingNebulaShaderOptions options)
384383
uniforms.SetFloat32("pulse_age", options.PulseAge);
385384
});
386385

387-
}
388-
389386
/// <summary>
390387
/// Creates the CPU image operation that reproduces the procedural WGSL pass.
391388
/// </summary>
@@ -450,7 +447,7 @@ private static Vector3 RenderPixel(Vector2 position, Vector2 resolution, LivingN
450447
density = MathF.Pow(density, 2.1F);
451448

452449
float wave = 0F;
453-
if (options.PulseAge >= 0F && options.PulseAge < 2F)
450+
if (options.PulseAge is >= 0F and < 2F)
454451
{
455452
// Evaluate the click in the same aspect-correct space as the clouds. Squaring
456453
// the signed distance to the expanding radius produces a narrow Gaussian ring.
@@ -608,7 +605,6 @@ private static Vector2 Rotate(Vector2 position, float angle)
608605
return new Vector2(
609606
(cosine * position.X) - (sine * position.Y),
610607
(sine * position.X) + (cosine * position.Y));
611-
612608
}
613609

614610
/// <summary>

samples/WebGPUExternalSurfaceDemo/Scenes/TigerViewerScene.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ protected override Control CreateContent(WebGPURenderControl renderControl)
128128
Size = new System.Drawing.Size(350, 100),
129129
BackColor = System.Drawing.Color.FromArgb(160, 0, 0, 0),
130130
ForeColor = System.Drawing.Color.White,
131-
Font = new System.Drawing.Font(System.Drawing.FontFamily.GenericMonospace, 9F),
131+
Font = new Font(FontFamily.GenericMonospace, 9F),
132132
Padding = new Padding(6),
133-
Location = new System.Drawing.Point(6, 6),
133+
Location = new Point(6, 6),
134134
};
135135

136136
renderControl.PaintFrame += (_, _) => statusLabel.Text = this.StatusText;

src/ImageSharp.Drawing.WebGPU/WebGPUShaderModuleSource.cs

Lines changed: 120 additions & 120 deletions
Large diffs are not rendered by default.

src/ImageSharp.Drawing.WebGPU/WebGPUShaderProgram.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ internal sealed class WebGPUShaderProgram
1919

2020
// Layouts are normally static declarations owned by an effect type. Weak keys allow dynamically
2121
// created layouts and their cached programs to be collected together.
22-
private static readonly ConditionalWeakTable<WebGPUShaderUniformLayout, ProgramCache> ProgramsByLayout = new();
22+
private static readonly ConditionalWeakTable<WebGPUShaderUniformLayout, ProgramCache> ProgramsByLayout = [];
2323

2424
private readonly object moduleSourceSync = new();
2525
private readonly Dictionary<

src/ImageSharp.Drawing.WebGPU/WebGPUSurfaceFactory.cs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ internal static unsafe class WebGPUSurfaceFactory
2020
/// <param name="instance">The instance that owns the surface.</param>
2121
/// <param name="source">The native window source.</param>
2222
/// <returns>The created surface, or <see langword="null"/> on failure.</returns>
23-
public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, INativeWindowSource source)
24-
{
23+
public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, INativeWindowSource source) =>
24+
2525
// Every backend entry point supplies an initialized window source. The Silk contract makes
2626
// Native nullable only for source implementations that have not created their window yet.
27-
return Create(api, instance, source.Native!, true);
28-
}
27+
Create(api, instance, source.Native!, true);
2928

3029
/// <summary>
3130
/// Creates a WebGPU surface for an externally-owned host.
@@ -34,14 +33,14 @@ internal static unsafe class WebGPUSurfaceFactory
3433
/// <param name="instance">The instance that owns the surface.</param>
3534
/// <param name="host">The external native host.</param>
3635
/// <returns>The created surface, or <see langword="null"/> on failure.</returns>
37-
public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, WebGPUSurfaceHost host)
38-
{
36+
public static WGPUSurfaceImpl* Create(WebGPU api, WGPUInstanceImpl* instance, WebGPUSurfaceHost host) =>
37+
3938
// External hosts model the descriptors accepted by wgpu-native directly. GLFW and SDL are
4039
// the only toolkit-level entries and are translated before reaching the native C API.
41-
return host.Kind switch
40+
host.Kind switch
4241
{
4342
WebGPUSurfaceHostKind.Glfw => Create(api, instance, new GlfwNativeWindow(GlfwProvider.GLFW.Value, (WindowHandle*)host.Handle0), false),
44-
WebGPUSurfaceHostKind.Sdl => Create(api, instance, new SdlNativeWindow(SdlProvider.SDL.Value, (Silk.NET.SDL.Window*)host.Handle0), false),
43+
WebGPUSurfaceHostKind.Sdl => Create(api, instance, new SdlNativeWindow(SdlProvider.SDL.Value, (Window*)host.Handle0), false),
4544
WebGPUSurfaceHostKind.Win32 => CreateWindowsSurface(api, instance, host.Handle0, host.Handle1),
4645
WebGPUSurfaceHostKind.X11 => CreateXlibSurface(api, instance, host.Handle0, host.Number0),
4746
WebGPUSurfaceHostKind.Cocoa => CreateCocoaSurface(api, instance, host.Handle0),
@@ -51,7 +50,6 @@ internal static unsafe class WebGPUSurfaceFactory
5150
WebGPUSurfaceHostKind.Android => CreateAndroidSurface(api, instance, host.Handle0),
5251
_ => null,
5352
};
54-
}
5553

5654
/// <summary>
5755
/// Resolves a Silk native window to one of the platform handles represented by the WebGPU C surface descriptors.

src/ImageSharp.Drawing/ImageSharp.Drawing.csproj

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,9 @@
4040
<None Include="..\..\shared-infrastructure\branding\icons\imagesharp.drawing\sixlabors.imagesharp.drawing.128.png" Pack="true" PackagePath="" />
4141
</ItemGroup>
4242
<ItemGroup>
43-
<PackageReference Include="SixLabors.Fonts" Version="3.0.1-alpha.0.8" />
44-
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.1-alpha.0.11" />
45-
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.1-alpha.0.5" />
46-
</ItemGroup>
47-
<ItemGroup>
48-
<!--<ProjectReference Include="..\..\..\Fonts\src\SixLabors.Fonts\SixLabors.Fonts.csproj" />
49-
<ProjectReference Include="..\..\..\ImageSharp\src\ImageSharp\ImageSharp.csproj" />
50-
<ProjectReference Include="..\..\..\PolygonClipper\src\PolygonClipper\PolygonClipper.csproj" />-->
43+
<PackageReference Include="SixLabors.Fonts" Version="3.0.1-alpha.0.15" />
44+
<PackageReference Include="SixLabors.ImageSharp" Version="4.0.1-alpha.0.20" />
45+
<PackageReference Include="SixLabors.PolygonClipper" Version="1.0.1-alpha.0.6" />
5146
</ItemGroup>
5247

5348
<!--

src/ImageSharp.Drawing/Processing/DrawingCanvas.cs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Numerics;
45
using SixLabors.Fonts;
56
using SixLabors.ImageSharp.Drawing.Processing.Backends;
67
using SixLabors.ImageSharp.Drawing.Text;
@@ -379,15 +380,12 @@ public abstract void DrawText(
379380
/// <summary>
380381
/// Draws positioned glyphs onto this canvas.
381382
/// </summary>
382-
/// <param name="glyphRun">The positioned glyphs.</param>
383+
/// <param name="glyphIds">The glyph identifiers.</param>
384+
/// <param name="points">The absolute glyph origins in pixel units.</param>
383385
/// <param name="options">The glyph rendering options, including the font and optional glyph paint.</param>
384386
/// <param name="brush">Default brush used to fill glyphs when <see cref="RichGlyphOptions.Brush"/> is not set.</param>
385387
/// <param name="pen">Default pen used to outline glyphs when <see cref="RichGlyphOptions.Pen"/> is not set.</param>
386-
public abstract void DrawText(
387-
GlyphRun glyphRun,
388-
RichGlyphOptions options,
389-
Brush? brush,
390-
Pen? pen);
388+
public abstract void DrawText(ReadOnlySpan<ushort> glyphIds, ReadOnlySpan<Vector2> points, RichGlyphOptions options, Brush? brush, Pen? pen);
391389

392390
/// <summary>
393391
/// Draws layered glyph geometry.

src/ImageSharp.Drawing/Processing/DrawingCanvas{TPixel}.cs

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ private int SaveEffectLayerCore(GraphicsOptions layerOptions, IPath region, Laye
460460
bool pushed = options is not null;
461461
if (pushed)
462462
{
463-
this.Save(options!);
463+
_ = this.Save(options!);
464464
}
465465

466466
this.ApplyCore(region, effect.CreateOperation(), effect, effect.WriteBackOptions, effect.WriteBackOffset);
@@ -994,17 +994,13 @@ public override void DrawText(
994994
}
995995

996996
/// <inheritdoc />
997-
public override void DrawText(
998-
GlyphRun glyphRun,
999-
RichGlyphOptions options,
1000-
Brush? brush,
1001-
Pen? pen)
997+
public override void DrawText(ReadOnlySpan<ushort> glyphIds, ReadOnlySpan<Vector2> points, RichGlyphOptions options, Brush? brush, Pen? pen)
1002998
{
1003999
this.EnsureNotDisposed();
1004-
Guard.NotNull(glyphRun, nameof(glyphRun));
10051000
Guard.NotNull(options, nameof(options));
1001+
Guard.IsTrue(glyphIds.Length == points.Length, nameof(points), "Glyph id and point counts must match.");
10061002

1007-
if (glyphRun.Count == 0)
1003+
if (glyphIds.IsEmpty)
10081004
{
10091005
return;
10101006
}
@@ -1016,7 +1012,7 @@ public override void DrawText(
10161012

10171013
using RichTextGlyphRenderer glyphRenderer = new(effectiveOptions, path: null, pen, brush, this.textCache, this.textOperations);
10181014
TextRenderer renderer = new(glyphRenderer);
1019-
renderer.Render(glyphRun, options);
1015+
renderer.Render(glyphIds, points, options);
10201016

10211017
this.DrawTextOperations(this.BatchGlyphRunOperations(glyphRenderer.DrawingOperations), effectiveOptions);
10221018
}
@@ -1137,7 +1133,7 @@ public override void DrawImage(
11371133

11381134
if (image is Image<TPixel> specificImage)
11391135
{
1140-
this.DrawImageCore(specificImage, sourceRect, destinationRect, sampler, wrapX, wrapY, ownsSourceImage: false);
1136+
this.DrawImage(specificImage, sourceRect, destinationRect, wrapX, wrapY, sampler);
11411137
return;
11421138
}
11431139

@@ -1152,13 +1148,13 @@ public override void DrawImage(
11521148
if (clippedSourceRect == image.Bounds)
11531149
{
11541150
Image<TPixel> convertedImage = image.CloneAs<TPixel>();
1155-
this.DrawImageCore(convertedImage, sourceRect, destinationRect, sampler, wrapX, wrapY, ownsSourceImage: true);
1151+
this.DrawImageCore(convertedImage, clippedSourceRect, clippedDestinationRect, sampler, wrapX, wrapY, true);
11561152
return;
11571153
}
11581154

11591155
using Image croppedSource = image.Clone(ctx => ctx.Crop(clippedSourceRect));
11601156
Image<TPixel> convertedRegion = croppedSource.CloneAs<TPixel>();
1161-
this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, wrapX, wrapY, ownsSourceImage: true);
1157+
this.DrawImageCore(convertedRegion, convertedRegion.Bounds, clippedDestinationRect, sampler, wrapX, wrapY, true);
11621158
}
11631159

11641160
/// <inheritdoc cref="DrawingCanvas.DrawImage(Image, Rectangle, RectangleF, IResampler?)" />
@@ -1180,7 +1176,13 @@ public void DrawImage(
11801176
{
11811177
this.EnsureNotDisposed();
11821178
Guard.NotNull(image, nameof(image));
1183-
this.DrawImageCore(image, sourceRect, destinationRect, sampler, wrapX, wrapY, ownsSourceImage: false);
1179+
1180+
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
1181+
{
1182+
return;
1183+
}
1184+
1185+
this.DrawImageCore(image, clippedSourceRect, clippedDestinationRect, sampler, wrapX, wrapY, false);
11841186
}
11851187

11861188
/// <inheritdoc />
@@ -1260,12 +1262,12 @@ public override void CopyPixelsFrom(DrawingCanvas source, Rectangle sourceRectan
12601262
}
12611263

12621264
/// <summary>
1263-
/// Normalizes an image draw into an image-brush fill: source pixels are cropped/scaled,
1265+
/// Normalizes a pre-clipped image draw into an image-brush fill: source pixels are cropped/scaled,
12641266
/// then baked through the canvas transform, and the result is queued as a rectangle fill.
12651267
/// </summary>
12661268
/// <param name="image">The source image in the target pixel format.</param>
1267-
/// <param name="sourceRect">The source rectangle within <paramref name="image"/>.</param>
1268-
/// <param name="destinationRect">The destination rectangle in local canvas coordinates.</param>
1269+
/// <param name="clippedSourceRect">The source rectangle, already clipped to the bounds of <paramref name="image"/>.</param>
1270+
/// <param name="clippedDestinationRect">The destination rectangle matching <paramref name="clippedSourceRect"/>, in local canvas coordinates.</param>
12691271
/// <param name="sampler">Optional resampler used when scaling or transforming the image.</param>
12701272
/// <param name="wrapX">The horizontal wrap mode applied when sampling beyond the destination rectangle.</param>
12711273
/// <param name="wrapY">The vertical wrap mode applied when sampling beyond the destination rectangle.</param>
@@ -1275,8 +1277,8 @@ public override void CopyPixelsFrom(DrawingCanvas source, Rectangle sourceRectan
12751277
/// </param>
12761278
private void DrawImageCore(
12771279
Image<TPixel> image,
1278-
Rectangle sourceRect,
1279-
RectangleF destinationRect,
1280+
Rectangle clippedSourceRect,
1281+
RectangleF clippedDestinationRect,
12801282
IResampler? sampler,
12811283
WrapMode wrapX,
12821284
WrapMode wrapY,
@@ -1291,11 +1293,6 @@ private void DrawImageCore(
12911293
Image<TPixel>? ownedImage = null;
12921294
try
12931295
{
1294-
if (!TryGetDrawImageClip(sourceRect, destinationRect, image.Bounds, out Rectangle clippedSourceRect, out RectangleF clippedDestinationRect))
1295-
{
1296-
return;
1297-
}
1298-
12991296
Size scaledSize = new(
13001297
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Width)),
13011298
Math.Max(1, (int)MathF.Ceiling(clippedDestinationRect.Height)));

0 commit comments

Comments
 (0)