Skip to content

Commit 1defc56

Browse files
Use WebGPUHostedSurface and normalize API
1 parent d8706d4 commit 1defc56

38 files changed

Lines changed: 622 additions & 618 deletions

WinFormsSamples.sln

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 18
44
VisualStudioVersion = 18.4.11626.88
@@ -15,7 +15,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageSharp.Drawing.WebGPU.S
1515
EndProject
1616
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DrawingBackendBenchmark", "samples\DrawingBackendBenchmark\DrawingBackendBenchmark.csproj", "{0AF23A97-CD73-409A-AA29-D214AA400AB0}"
1717
EndProject
18-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUHostedWindowDemo", "samples\WebGPUHostedWindowDemo\WebGPUHostedWindowDemo.csproj", "{478855A1-ECEA-4DF6-9D7B-0342EA26726E}"
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebGPUHostedSurfaceDemo", "samples\WebGPUHostedSurfaceDemo\WebGPUHostedSurfaceDemo.csproj", "{478855A1-ECEA-4DF6-9D7B-0342EA26726E}"
1919
EndProject
2020
Global
2121
GlobalSection(SolutionConfigurationPlatforms) = preSolution

samples/DrawingBackendBenchmark/CpuBenchmarkBackend.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public BenchmarkRenderResult Render(ReadOnlySpan<VisualLine> lines, int width, i
3333
Buffer2DRegion<Bgra32> region = new(image.Frames.RootFrame.PixelBuffer, image.Bounds);
3434

3535
Stopwatch stopwatch = Stopwatch.StartNew();
36-
using (DrawingCanvas<Bgra32> canvas = new(this.configuration, region, new DrawingOptions()))
36+
using (DrawingCanvas<Bgra32> canvas = new(this.configuration, new DrawingOptions(), region))
3737
{
3838
VisualLine.RenderLinesToCanvas(canvas, lines);
3939
canvas.Flush();

samples/WebGPUHostedWindowDemo/Controls/WebGPURenderControl.cs renamed to samples/WebGPUHostedSurfaceDemo/Controls/WebGPURenderControl.cs

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66
using SixLabors.ImageSharp.Drawing.Processing;
77
using SixLabors.ImageSharp.Drawing.Processing.Backends;
88
using SixLabors.ImageSharp.PixelFormats;
9+
using ImageSharpSize = SixLabors.ImageSharp.Size;
910

10-
namespace WebGPUHostedWindowDemo.Controls;
11+
namespace WebGPUHostedSurfaceDemo.Controls;
1112

1213
/// <summary>
13-
/// A reusable WinForms control that embeds a <see cref="WebGPUHostedWindow{TPixel}"/> and drives a continuous
14+
/// A reusable WinForms control that embeds a <see cref="WebGPUHostedSurface{TPixel}"/> and drives a continuous
1415
/// render loop via <see cref="Application.Idle"/>. Callers hook <see cref="PaintFrame"/> with their scene logic;
1516
/// the control handles construction, resize, acquire/present, and teardown.
1617
/// </summary>
@@ -19,7 +20,8 @@ public sealed partial class WebGPURenderControl : Control
1920
private const int WM_MOVING = 0x0216;
2021
private const int WM_EXITSIZEMOVE = 0x0232;
2122

22-
private WebGPUHostedWindow<Bgra32>? window;
23+
private WebGPUHostedSurface<Bgra32>? surface;
24+
private Size framebufferSize;
2325
private bool idleHooked;
2426
private long lastTicks;
2527
private System.Windows.Forms.Timer? titleBarMoveTimer;
@@ -40,32 +42,40 @@ public WebGPURenderControl()
4042
}
4143

4244
/// <summary>
43-
/// Raised each frame once the hosted window has acquired a drawable frame.
45+
/// Raised each frame once the hosted surface has acquired a drawable frame. The canvas dimensions match
46+
/// <see cref="FramebufferSize"/>.
4447
/// </summary>
4548
public event Action<DrawingCanvas<Bgra32>, TimeSpan>? PaintFrame;
4649

50+
/// <summary>
51+
/// Gets the drawable framebuffer size in pixels.
52+
/// </summary>
53+
public Size FramebufferSize => this.framebufferSize;
54+
4755
/// <inheritdoc />
4856
protected override void OnHandleCreated(EventArgs e)
4957
{
5058
base.OnHandleCreated(e);
5159

52-
int width = Math.Max(this.ClientSize.Width, 1);
53-
int height = Math.Max(this.ClientSize.Height, 1);
60+
// WinForms ClientSize is the HWND client rectangle size; pass it through as the drawable framebuffer size.
61+
this.framebufferSize = this.ClientSize;
62+
ImageSharpSize initialFramebufferSize = new(
63+
Math.Max(this.framebufferSize.Width, 1),
64+
Math.Max(this.framebufferSize.Height, 1));
5465

55-
this.window = new WebGPUHostedWindow<Bgra32>(
56-
WebGPUWindowHost.Win32(
66+
this.surface = new WebGPUHostedSurface<Bgra32>(
67+
WebGPUSurfaceHost.Win32(
5768
this.Handle,
5869
Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)),
59-
width,
60-
height);
70+
initialFramebufferSize);
6171

6272
this.lastTicks = Stopwatch.GetTimestamp();
6373
Application.Idle += this.OnApplicationIdle;
6474
this.idleHooked = true;
6575

6676
// Application.Idle stops firing during a title-bar drag (modal move loop).
6777
// Attach a NativeWindow to the parent form's handle so we can see WM_MOVING
68-
// which fires only during title-bar drags, never during border resizes
78+
// which fires only during title-bar drags, never during border resizes,
6979
// and drive a short Timer during those. Border resize is left untouched.
7080
Form? parentForm = this.FindForm();
7181
if (parentForm is not null && parentForm.IsHandleCreated)
@@ -97,18 +107,21 @@ protected override void OnHandleDestroyed(EventArgs e)
97107
this.titleBarMoveTimer = null;
98108
}
99109

100-
this.window?.Dispose();
101-
this.window = null;
110+
this.surface?.Dispose();
111+
this.surface = null;
112+
this.framebufferSize = Size.Empty;
102113
base.OnHandleDestroyed(e);
103114
}
104115

105116
/// <inheritdoc />
106117
protected override void OnResize(EventArgs e)
107118
{
108119
base.OnResize(e);
109-
if (this.window is not null && this.ClientSize.Width > 0 && this.ClientSize.Height > 0)
120+
if (this.surface is not null)
110121
{
111-
this.window.Resize(this.ClientSize.Width, this.ClientSize.Height);
122+
// WinForms ClientSize is the HWND client rectangle size; pass it through as the drawable framebuffer size.
123+
this.framebufferSize = this.ClientSize;
124+
this.surface.Resize(new ImageSharpSize(this.framebufferSize.Width, this.framebufferSize.Height));
112125
}
113126
}
114127

@@ -122,8 +135,8 @@ protected override void OnPaintBackground(PaintEventArgs pevent)
122135
protected override void OnPaint(PaintEventArgs e) => this.RenderOnce();
123136

124137
// Application.Idle fires once when the WinForms message queue drains. Rendering only once
125-
// would leave the scene frozen between user input events, so we loop here re-rendering as
126-
// long as the queue stays empty and exit the moment a message arrives so WinForms can
138+
// would leave the scene frozen between user input events, so we loop here, re-rendering as
139+
// long as the queue stays empty, and exit the moment a message arrives so WinForms can
127140
// process it. Frame pacing is delegated to the swap-chain present mode: with Fifo (v-sync)
128141
// TryAcquireFrame blocks until the display is ready, so this loop naturally runs at the
129142
// display's refresh rate without a software timer capping it.
@@ -159,12 +172,12 @@ protected override void WndProc(ref Message m)
159172

160173
private void RenderOnce()
161174
{
162-
if (this.window is null || this.ClientSize.Width <= 0 || this.ClientSize.Height <= 0)
175+
if (this.surface is null || this.framebufferSize.Width <= 0 || this.framebufferSize.Height <= 0)
163176
{
164177
return;
165178
}
166179

167-
if (!this.window.TryAcquireFrame(out WebGPUWindowFrame<Bgra32>? frame))
180+
if (!this.surface.TryAcquireFrame(out WebGPUSurfaceFrame<Bgra32>? frame))
168181
{
169182
return;
170183
}
@@ -180,7 +193,7 @@ private void RenderOnce()
180193
}
181194

182195
// PeekMessage with wRemoveMsg = 0 (PM_NOREMOVE) asks the OS "is there a message waiting?" without
183-
// dequeuing it. Returning 0 means the queue is empty the canonical "is the app idle right now"
196+
// dequeuing it. Returning 0 means the queue is empty, the canonical "is the app idle right now"
184197
// check used by the WinForms continuous-render idiom. Cheaper than any managed alternative and the
185198
// only way to tell whether WinForms is about to break out of Application.Idle on the next pump.
186199
[StructLayout(LayoutKind.Sequential)]

samples/WebGPUHostedWindowDemo/MainForm.cs renamed to samples/WebGPUHostedSurfaceDemo/MainForm.cs

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

44
using SixLabors.ImageSharp.Drawing.Processing;
55
using SixLabors.ImageSharp.PixelFormats;
6-
using WebGPUHostedWindowDemo.Controls;
7-
using WebGPUHostedWindowDemo.Scenes;
6+
using WebGPUHostedSurfaceDemo.Controls;
7+
using WebGPUHostedSurfaceDemo.Scenes;
88

9-
namespace WebGPUHostedWindowDemo;
9+
namespace WebGPUHostedSurfaceDemo;
1010

1111
/// <summary>
1212
/// Main window for the sample. A tab control switches between two demo scenes, each hosted by its own
13-
/// <see cref="WebGPURenderControl"/> instance. This demonstrates that multiple independent hosted windows
14-
/// can coexist in the same WinForms process without touching Silk, surfaces, or swap-chains in user code.
13+
/// <see cref="WebGPURenderControl"/> instance. This demonstrates that multiple independent hosted surfaces
14+
/// can coexist in the same WinForms process without managing surfaces or swapchains in user code.
1515
/// </summary>
1616
internal sealed class MainForm : Form
1717
{
@@ -23,7 +23,7 @@ internal sealed class MainForm : Form
2323

2424
public MainForm()
2525
{
26-
this.Text = "ImageSharp.Drawing WebGPU Hosted Window Demo";
26+
this.Text = "ImageSharp.Drawing WebGPU - Hosted Surface Demo";
2727
this.ClientSize = new Size(1280, 800);
2828
this.StartPosition = FormStartPosition.CenterScreen;
2929
this.BackColor = Color.FromArgb(11, 18, 32);
@@ -86,13 +86,13 @@ public MainForm()
8686

8787
private void OnPaintClock(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
8888
{
89-
Size s = this.clockControl.ClientSize;
89+
Size s = this.clockControl.FramebufferSize;
9090
this.clockScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
9191
}
9292

9393
private void OnPaintTiger(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
9494
{
95-
Size s = this.tigerControl.ClientSize;
95+
Size s = this.tigerControl.FramebufferSize;
9696
this.tigerScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
9797
this.tigerStatusLabel.Text = this.tigerScene.StatusText;
9898
}
File renamed without changes.

samples/WebGPUHostedWindowDemo/Program.cs renamed to samples/WebGPUHostedSurfaceDemo/Program.cs

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

4-
namespace WebGPUHostedWindowDemo;
4+
namespace WebGPUHostedSurfaceDemo;
55

66
internal static class Program
77
{

samples/WebGPUHostedWindowDemo/Properties/AssemblyInfo.cs renamed to samples/WebGPUHostedSurfaceDemo/Properties/AssemblyInfo.cs

File renamed without changes.
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# WebGPU Hosted Surface Demo
2+
3+
`WebGPUHostedSurfaceDemo` shows how to render ImageSharp.Drawing content into a UI object owned by an application framework. The sample uses a WinForms `Control`, but the API shape is intended for any externally-owned native drawable host.
4+
5+
It exists to demonstrate:
6+
7+
- creating a `WebGPUHostedSurface<Bgra32>` from a `WebGPUSurfaceHost`
8+
- keeping the hosted surface synchronized with the host control's drawable framebuffer size
9+
- acquiring `WebGPUSurfaceFrame<TPixel>` instances manually
10+
- drawing with the normal `DrawingCanvas<TPixel>` API
11+
- presenting by disposing the acquired frame
12+
13+
## Running
14+
15+
```bash
16+
dotnet run --project samples/WebGPUHostedSurfaceDemo -c Debug
17+
```
18+
19+
Requirements:
20+
21+
- .NET 8.0 SDK or later
22+
- Windows, because this sample is a WinForms app
23+
- a WebGPU-capable desktop backend such as D3D12 or Vulkan
24+
- adapter support for the storage-capable BGRA format required by `Bgra32`
25+
26+
When the sample starts you should see a WinForms window with two tabs:
27+
28+
- `Clock`: a continuously-rendered animated clock scene
29+
- `Tiger`: an interactive SVG tiger viewer with pan and zoom
30+
31+
## Why This Sample Matters
32+
33+
`WebGPUWindow<TPixel>` owns a top-level native window. `WebGPUHostedSurface<TPixel>` is different: it attaches WebGPU rendering to something the application already owns, such as a control, view, widget, or native surface.
34+
35+
That makes it the integration path for UI frameworks. The host owns:
36+
37+
- the UI object and its lifecycle
38+
- the platform handle
39+
- layout and resize events
40+
- input events
41+
42+
The hosted surface owns:
43+
44+
- the WebGPU surface created for that host
45+
- swapchain configuration
46+
- frame acquisition
47+
- presentation
48+
- per-frame texture and texture-view handles
49+
50+
## Code Tour
51+
52+
The reusable integration point is [WebGPURenderControl.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Controls/WebGPURenderControl.cs).
53+
54+
### Surface Creation
55+
56+
`WebGPURenderControl.OnHandleCreated(...)` creates the hosted surface from the WinForms control handle:
57+
58+
```csharp
59+
this.surface = new WebGPUHostedSurface<Bgra32>(
60+
WebGPUSurfaceHost.Win32(
61+
this.Handle,
62+
Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)),
63+
initialFramebufferSize);
64+
```
65+
66+
`WebGPUSurfaceHost` is a small public descriptor for externally-owned native handles. The host keeps ownership of those handles; `WebGPUHostedSurface<TPixel>` only uses them to create and manage the WebGPU rendering surface.
67+
68+
### Resize
69+
70+
`WebGPUHostedSurface<TPixel>.Resize(...)` expects the drawable framebuffer size in pixels:
71+
72+
```csharp
73+
this.framebufferSize = this.ClientSize;
74+
this.surface.Resize(new ImageSharpSize(this.framebufferSize.Width, this.framebufferSize.Height));
75+
```
76+
77+
The sample stores that size as `FramebufferSize` so scene code can draw in the same pixel coordinate space as the acquired frame.
78+
79+
### Frame Acquisition
80+
81+
`RenderOnce()` acquires a frame, invokes user drawing code, and disposes the frame:
82+
83+
```csharp
84+
if (!this.surface.TryAcquireFrame(out WebGPUSurfaceFrame<Bgra32>? frame))
85+
{
86+
return;
87+
}
88+
89+
using (frame)
90+
{
91+
this.PaintFrame?.Invoke(frame.Canvas, delta);
92+
}
93+
```
94+
95+
Disposing the frame flushes pending canvas work, presents the surface texture, and releases the per-frame WebGPU handles.
96+
97+
### Rendering Loop
98+
99+
The sample uses `Application.Idle` for continuous rendering. While the WinForms message queue is empty, the control renders frames. When input, resize, or other window messages arrive, the loop exits so WinForms can process them.
100+
101+
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.
102+
103+
## Scene Code
104+
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+
107+
The scenes are deliberately ordinary canvas code:
108+
109+
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ClockScene.cs): animated vector clock
110+
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/TigerViewerScene.cs): pan and zoom SVG tiger viewer
111+
112+
Each scene receives:
113+
114+
- `DrawingCanvas<Bgra32>` for the acquired frame
115+
- the current framebuffer size
116+
- elapsed time since the previous frame
117+
118+
## Files
119+
120+
- [WebGPURenderControl.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Controls/WebGPURenderControl.cs): reusable WinForms hosted-surface control
121+
- [MainForm.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/MainForm.cs): tabs and scene wiring
122+
- [ClockScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/ClockScene.cs): clock scene
123+
- [TigerViewerScene.cs](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/Scenes/TigerViewerScene.cs): tiger viewer scene
124+
- [WebGPUHostedSurfaceDemo.csproj](d:/GitHub/SixLabors/ImageSharp.Drawing/samples/WebGPUHostedSurfaceDemo/WebGPUHostedSurfaceDemo.csproj): sample project file

samples/WebGPUHostedWindowDemo/Scenes/ClockScene.cs renamed to samples/WebGPUHostedSurfaceDemo/Scenes/ClockScene.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
using SystemFonts = SixLabors.Fonts.SystemFonts;
2323
using VerticalAlignment = SixLabors.Fonts.VerticalAlignment;
2424

25-
namespace WebGPUHostedWindowDemo.Scenes;
25+
namespace WebGPUHostedSurfaceDemo.Scenes;
2626

2727
/// <summary>
2828
/// Animated analog clock. Validates continuous render, curves, thin-stroke antialiasing, and text rendering.

samples/WebGPUHostedWindowDemo/Scenes/RenderScene.cs renamed to samples/WebGPUHostedSurfaceDemo/Scenes/RenderScene.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
using SixLabors.ImageSharp.PixelFormats;
66
using Size = SixLabors.ImageSharp.Size;
77

8-
namespace WebGPUHostedWindowDemo.Scenes;
8+
namespace WebGPUHostedSurfaceDemo.Scenes;
99

1010
/// <summary>
1111
/// Base class for a demo scene rendered into a shared <see cref="WebGPURenderControl"/>.
@@ -21,7 +21,7 @@ internal abstract class RenderScene
2121
/// <summary>
2222
/// Draws the scene into <paramref name="canvas"/> for the current frame.
2323
/// </summary>
24-
/// <param name="canvas">The per-frame drawing canvas bound to the hosted window's swap-chain texture.</param>
24+
/// <param name="canvas">The per-frame drawing canvas bound to the hosted surface's swap-chain texture.</param>
2525
/// <param name="viewportSize">The framebuffer size in pixels.</param>
2626
/// <param name="deltaTime">Elapsed time since the previous frame.</param>
2727
public abstract void Paint(DrawingCanvas<Bgra32> canvas, Size viewportSize, TimeSpan deltaTime);

0 commit comments

Comments
 (0)