Skip to content

Commit b5c6458

Browse files
Add Apply readback scene and UI wiring
1 parent e79e82a commit b5c6458

8 files changed

Lines changed: 243 additions & 22 deletions

File tree

samples/WebGPUHostedSurfaceDemo/Controls/WebGPURenderControl.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public sealed partial class WebGPURenderControl : Control
3232
/// </summary>
3333
public WebGPURenderControl()
3434
{
35+
// WebGPU presents directly to the native surface. Normal WinForms buffering and background
36+
// painting would add flicker or unnecessary work, so the control opts into direct user painting.
3537
this.SetStyle(
3638
ControlStyles.AllPaintingInWmPaint |
3739
ControlStyles.Opaque |
@@ -57,12 +59,16 @@ protected override void OnHandleCreated(EventArgs e)
5759
{
5860
base.OnHandleCreated(e);
5961

62+
// A hosted surface can only be created once the native HWND exists. The surface borrows the HWND;
63+
// WinForms still owns the control, handle lifetime, and layout.
6064
// WinForms ClientSize is the HWND client rectangle size; pass it through as the drawable framebuffer size.
6165
this.framebufferSize = this.ClientSize;
6266
ImageSharpSize initialFramebufferSize = new(
6367
Math.Max(this.framebufferSize.Width, 1),
6468
Math.Max(this.framebufferSize.Height, 1));
6569

70+
// The module handle is required by the Win32 surface descriptor. It identifies the process module
71+
// that owns the window class backing this control.
6672
this.surface = new WebGPUHostedSurface<Bgra32>(
6773
WebGPUSurfaceHost.Win32(
6874
this.Handle,
@@ -177,6 +183,8 @@ private void RenderOnce()
177183
return;
178184
}
179185

186+
// Frame acquisition can fail transiently while the surface is unavailable, for example during resize
187+
// or device recovery. Skipping the frame keeps the UI message loop responsive.
180188
if (!this.surface.TryAcquireFrame(out WebGPUSurfaceFrame<Bgra32>? frame))
181189
{
182190
return;
@@ -188,6 +196,8 @@ private void RenderOnce()
188196

189197
using (frame)
190198
{
199+
// The canvas records drawing work against the acquired surface texture. Disposing the frame
200+
// flushes that work, presents the texture, and releases the per-frame native handles.
191201
this.PaintFrame?.Invoke(frame.Canvas, delta);
192202
}
193203
}

samples/WebGPUHostedSurfaceDemo/MainForm.cs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@
99
namespace WebGPUHostedSurfaceDemo;
1010

1111
/// <summary>
12-
/// Main window for the sample. A tab control switches between two demo scenes, each hosted by its own
12+
/// Main window for the sample. A tab control switches between demo scenes, each hosted by its own
1313
/// <see cref="WebGPURenderControl"/> instance. This demonstrates that multiple independent hosted surfaces
1414
/// can coexist in the same WinForms process without managing surfaces or swapchains in user code.
1515
/// </summary>
1616
internal sealed class MainForm : Form
1717
{
1818
private readonly WebGPURenderControl clockControl;
1919
private readonly WebGPURenderControl tigerControl;
20+
private readonly WebGPURenderControl applyControl;
2021
private readonly ClockScene clockScene;
2122
private readonly TigerViewerScene tigerScene;
23+
private readonly ApplyReadbackScene applyScene;
2224
private readonly Label tigerStatusLabel;
2325

2426
public MainForm()
@@ -30,12 +32,36 @@ public MainForm()
3032

3133
this.clockScene = new ClockScene();
3234
this.tigerScene = new TigerViewerScene();
35+
this.applyScene = new ApplyReadbackScene();
3336

37+
// Each tab gets its own render control and hosted surface. This mirrors real UI applications where
38+
// separate controls or panels own their own native drawable areas.
3439
this.clockControl = new WebGPURenderControl { Dock = DockStyle.Fill };
3540
this.clockControl.PaintFrame += this.OnPaintClock;
3641

3742
this.tigerControl = new WebGPURenderControl { Dock = DockStyle.Fill };
3843
this.tigerControl.PaintFrame += this.OnPaintTiger;
44+
45+
this.applyControl = new WebGPURenderControl { Dock = DockStyle.Fill };
46+
this.applyControl.PaintFrame += this.OnPaintApply;
47+
48+
// The Apply scene reacts to pointer movement so readback cost can be assessed interactively.
49+
this.applyControl.MouseDown += (_, e) =>
50+
{
51+
this.applyScene.OnMouseDown(e);
52+
this.applyControl.Invalidate();
53+
};
54+
this.applyControl.MouseMove += (_, e) =>
55+
{
56+
this.applyScene.OnMouseMove(e);
57+
this.applyControl.Invalidate();
58+
};
59+
this.applyControl.MouseWheel += (_, e) =>
60+
{
61+
this.applyScene.OnMouseWheel(e);
62+
this.applyControl.Invalidate();
63+
};
64+
3965
this.tigerStatusLabel = new Label
4066
{
4167
AutoSize = false,
@@ -48,6 +74,8 @@ public MainForm()
4874
Text = string.Empty,
4975
};
5076
this.tigerControl.Controls.Add(this.tigerStatusLabel);
77+
78+
// Mouse input stays in WinForms coordinates. The scene converts it into its own world transform.
5179
this.tigerControl.MouseDown += (_, e) =>
5280
{
5381
this.tigerScene.OnMouseDown(e);
@@ -81,19 +109,35 @@ public MainForm()
81109
tigerTab.Controls.Add(this.tigerControl);
82110
tabs.TabPages.Add(tigerTab);
83111

112+
TabPage applyTab = new(this.applyScene.DisplayName);
113+
applyTab.Controls.Add(this.applyControl);
114+
tabs.TabPages.Add(applyTab);
115+
84116
this.Controls.Add(tabs);
85117
}
86118

87119
private void OnPaintClock(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
88120
{
89121
Size s = this.clockControl.FramebufferSize;
122+
123+
// Scene coordinates match the drawable framebuffer, not the form's layout bounds.
90124
this.clockScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
91125
}
92126

93127
private void OnPaintTiger(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
94128
{
95129
Size s = this.tigerControl.FramebufferSize;
130+
131+
// Scene coordinates match the drawable framebuffer, not the form's layout bounds.
96132
this.tigerScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
97133
this.tigerStatusLabel.Text = this.tigerScene.StatusText;
98134
}
135+
136+
private void OnPaintApply(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
137+
{
138+
Size s = this.applyControl.FramebufferSize;
139+
140+
// Scene coordinates match the drawable framebuffer, not the form's layout bounds.
141+
this.applyScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
142+
}
99143
}

samples/WebGPUHostedSurfaceDemo/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@
33

44
namespace WebGPUHostedSurfaceDemo;
55

6+
/// <summary>
7+
/// Entry point for the hosted WebGPU surface sample.
8+
/// </summary>
69
internal static class Program
710
{
11+
/// <summary>
12+
/// Starts the WinForms message loop and shows the sample form.
13+
/// </summary>
814
[STAThread]
915
private static void Main()
1016
{

samples/WebGPUHostedSurfaceDemo/README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ Requirements:
2323
- a WebGPU-capable desktop backend such as D3D12 or Vulkan
2424
- adapter support for the storage-capable BGRA format required by `Bgra32`
2525

26-
When the sample starts you should see a WinForms window with two tabs:
26+
When the sample starts you should see a WinForms window with three tabs:
2727

2828
- `Clock`: a continuously-rendered animated clock scene
2929
- `Tiger`: an interactive SVG tiger viewer with pan and zoom
30+
- `Apply`: a reactive hosted-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
3031

3132
## Why This Sample Matters
3233

@@ -102,12 +103,13 @@ Frame pacing is delegated to the present mode. With the default `WebGPUPresentMo
102103

103104
## Scene Code
104105

105-
[MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/MainForm.cs) creates two independent `WebGPURenderControl` instances, one per tab. Each control owns its own hosted surface.
106+
[MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/MainForm.cs) creates independent `WebGPURenderControl` instances, one per tab. Each control owns its own hosted surface.
106107

107108
The scenes are deliberately ordinary canvas code:
108109

109110
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ClockScene.cs): animated vector clock
110111
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/TigerViewerScene.cs): pan and zoom SVG tiger viewer
112+
- [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ApplyReadbackScene.cs): `Apply(...)` scene that reads the hosted surface back into CPU processing
111113

112114
Each scene receives:
113115

@@ -121,4 +123,5 @@ Each scene receives:
121123
- [MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/MainForm.cs): tabs and scene wiring
122124
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ClockScene.cs): clock scene
123125
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/TigerViewerScene.cs): tiger viewer scene
126+
- [ApplyReadbackScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ApplyReadbackScene.cs): apply readback scene
124127
- [WebGPUHostedSurfaceDemo.csproj](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/WebGPUHostedSurfaceDemo.csproj): sample project file
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Drawing;
5+
using SixLabors.ImageSharp.Drawing.Processing;
6+
using SixLabors.ImageSharp.PixelFormats;
7+
using SixLabors.ImageSharp.Processing;
8+
using Brushes = SixLabors.ImageSharp.Drawing.Processing.Brushes;
9+
using Color = SixLabors.ImageSharp.Color;
10+
using Pen = SixLabors.ImageSharp.Drawing.Processing.Pen;
11+
using Pens = SixLabors.ImageSharp.Drawing.Processing.Pens;
12+
using PointF = SixLabors.ImageSharp.PointF;
13+
using Rectangle = SixLabors.ImageSharp.Rectangle;
14+
using Size = SixLabors.ImageSharp.Size;
15+
using SizeF = SixLabors.ImageSharp.SizeF;
16+
17+
namespace WebGPUHostedSurfaceDemo.Scenes;
18+
19+
/// <summary>
20+
/// Hosted-surface scene that exercises canvas readback by applying CPU image processors to regions of the current frame.
21+
/// Pointer movement changes the processed regions so readback cost can be assessed interactively.
22+
/// </summary>
23+
internal sealed class ApplyReadbackScene : RenderScene
24+
{
25+
private static readonly Color BackgroundColor = Color.MidnightBlue;
26+
private static readonly Color StripeA = Color.LimeGreen;
27+
private static readonly Color StripeB = Color.DodgerBlue;
28+
private static readonly Color StripeC = Color.Orange;
29+
private static readonly Color OutlineColor = Color.White;
30+
private static readonly Color GuideLineColor = Color.White.WithAlpha(.5F);
31+
32+
private PointF pointer;
33+
private bool hasPointer;
34+
private float regionScale = 1F;
35+
36+
public override string DisplayName => "Apply";
37+
38+
public override void Paint(DrawingCanvas<Bgra32> canvas, Size viewportSize, TimeSpan deltaTime)
39+
{
40+
if (viewportSize.Width <= 0 || viewportSize.Height <= 0)
41+
{
42+
return;
43+
}
44+
45+
canvas.Fill(Brushes.Solid(BackgroundColor), new RectangularPolygon(0, 0, viewportSize.Width, viewportSize.Height));
46+
DrawPattern(canvas, viewportSize);
47+
48+
// Keep both regions tied to the pointer so every movement exercises readback from a different part
49+
// of the surface texture instead of repeatedly processing a static rectangle.
50+
PointF focus = this.hasPointer ? this.pointer : new PointF(viewportSize.Width * .5F, viewportSize.Height * .5F);
51+
float effectSize = MathF.Max(48F, MathF.Min(viewportSize.Width, viewportSize.Height) * .26F * this.regionScale);
52+
PointF blurCenter = new(
53+
ClampCenter(viewportSize.Width - focus.X, effectSize * 1.25F, viewportSize.Width),
54+
ClampCenter(focus.Y, effectSize, viewportSize.Height));
55+
56+
Rectangle edgeRegion = CreateRegion(viewportSize, focus, effectSize);
57+
IPath blurRegion = new EllipsePolygon(
58+
blurCenter,
59+
new SizeF(effectSize * 1.25F, effectSize));
60+
61+
// Apply reads the affected pixels back from the current WebGPU frame, runs the CPU processor,
62+
// then writes the processed region back before the frame is presented.
63+
canvas.Apply(edgeRegion, ctx => ctx.DetectEdges());
64+
canvas.Apply(blurRegion, ctx => ctx.GaussianBlur(Math.Max(3F, Math.Min(viewportSize.Width, viewportSize.Height) / 120F)));
65+
66+
canvas.Draw(Pens.Solid(OutlineColor, 3), new RectangularPolygon(edgeRegion.X, edgeRegion.Y, edgeRegion.Width, edgeRegion.Height));
67+
canvas.Draw(Pens.Solid(OutlineColor, 3), blurRegion);
68+
}
69+
70+
public override void OnMouseDown(MouseEventArgs e) => this.SetPointer(e);
71+
72+
public override void OnMouseMove(MouseEventArgs e) => this.SetPointer(e);
73+
74+
public override void OnMouseWheel(MouseEventArgs e)
75+
{
76+
this.SetPointer(e);
77+
78+
// Wheel resizing changes the amount of data read back and written back each frame.
79+
float factor = e.Delta > 0 ? 1.12F : 1F / 1.12F;
80+
this.regionScale = Math.Clamp(this.regionScale * factor, .5F, 2.4F);
81+
}
82+
83+
private static void DrawPattern(DrawingCanvas<Bgra32> canvas, Size viewportSize)
84+
{
85+
float stripeWidth = Math.Max(18F, viewportSize.Width / 18F);
86+
float height = viewportSize.Height;
87+
88+
for (int i = -2; i < (viewportSize.Width / stripeWidth) + 3; i++)
89+
{
90+
Color color = (Math.Abs(i) % 3) switch
91+
{
92+
0 => StripeA,
93+
1 => StripeB,
94+
_ => StripeC,
95+
};
96+
97+
float x = i * stripeWidth;
98+
IPath stripe = new Polygon(
99+
new LinearLineSegment(
100+
new PointF(x, 0),
101+
new PointF(x + (stripeWidth * 1.6F), 0),
102+
new PointF(x + (stripeWidth * .6F), height),
103+
new PointF(x - stripeWidth, height)));
104+
105+
canvas.Fill(Brushes.Solid(color), stripe);
106+
}
107+
108+
Pen guideLinePen = Pens.Solid(GuideLineColor, 1.5F);
109+
for (int y = 0; y < viewportSize.Height; y += Math.Max(24, viewportSize.Height / 14))
110+
{
111+
canvas.DrawLine(
112+
guideLinePen,
113+
new PointF(0, y),
114+
new PointF(viewportSize.Width, y + (viewportSize.Width * .08F)));
115+
}
116+
}
117+
118+
private static Rectangle CreateRegion(Size viewportSize, PointF center, float size)
119+
{
120+
int width = Math.Max(1, Math.Min(viewportSize.Width, (int)size));
121+
int height = Math.Max(1, Math.Min(viewportSize.Height, (int)(size * .75F)));
122+
int x = Math.Clamp((int)(center.X - (width * .5F)), 0, Math.Max(0, viewportSize.Width - width));
123+
int y = Math.Clamp((int)(center.Y - (height * .5F)), 0, Math.Max(0, viewportSize.Height - height));
124+
125+
return new Rectangle(x, y, width, height);
126+
}
127+
128+
private static float ClampCenter(float value, float size, float limit)
129+
{
130+
float radius = MathF.Min(size * .5F, limit * .5F);
131+
132+
return Math.Clamp(value, radius, limit - radius);
133+
}
134+
135+
private void SetPointer(MouseEventArgs e)
136+
{
137+
this.pointer = new PointF(e.X, e.Y);
138+
this.hasPointer = true;
139+
}
140+
}

0 commit comments

Comments
 (0)