Skip to content

Commit d3c3979

Browse files
Add hosted window API and samples + fix bugs/perf
1 parent ecc069f commit d3c3979

67 files changed

Lines changed: 3793 additions & 2523 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.
Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio Version 18
4-
VisualStudioVersion = 18.4.11626.88 stable
4+
VisualStudioVersion = 18.4.11626.88
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}"
77
EndProject
@@ -15,6 +15,8 @@ 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}"
19+
EndProject
1820
Global
1921
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2022
Debug|Any CPU = Debug|Any CPU
@@ -73,6 +75,18 @@ Global
7375
{0AF23A97-CD73-409A-AA29-D214AA400AB0}.Release|x64.Build.0 = Release|Any CPU
7476
{0AF23A97-CD73-409A-AA29-D214AA400AB0}.Release|x86.ActiveCfg = Release|Any CPU
7577
{0AF23A97-CD73-409A-AA29-D214AA400AB0}.Release|x86.Build.0 = Release|Any CPU
78+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
79+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|Any CPU.Build.0 = Debug|Any CPU
80+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|x64.ActiveCfg = Debug|Any CPU
81+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|x64.Build.0 = Debug|Any CPU
82+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|x86.ActiveCfg = Debug|Any CPU
83+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Debug|x86.Build.0 = Debug|Any CPU
84+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|Any CPU.ActiveCfg = Release|Any CPU
85+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|Any CPU.Build.0 = Release|Any CPU
86+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|x64.ActiveCfg = Release|Any CPU
87+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|x64.Build.0 = Release|Any CPU
88+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|x86.ActiveCfg = Release|Any CPU
89+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E}.Release|x86.Build.0 = Release|Any CPU
7690
EndGlobalSection
7791
GlobalSection(SolutionProperties) = preSolution
7892
HideSolutionNode = FALSE
@@ -82,6 +96,7 @@ Global
8296
{439C7CCB-78F0-4F24-9D7F-6D6659495DEF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
8397
{AEFE7C59-5D1C-4F14-A83F-0FD665130FA3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
8498
{0AF23A97-CD73-409A-AA29-D214AA400AB0} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
99+
{478855A1-ECEA-4DF6-9D7B-0342EA26726E} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA}
85100
EndGlobalSection
86101
GlobalSection(SharedMSBuildProjectFiles) = preSolution
87102
shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.projitems*{042b144e-5444-4b88-b4f2-038e54fc25d0}*SharedItemsImports = 5
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Diagnostics;
5+
using System.Runtime.InteropServices;
6+
using SixLabors.ImageSharp.Drawing.Processing;
7+
using SixLabors.ImageSharp.Drawing.Processing.Backends;
8+
using SixLabors.ImageSharp.PixelFormats;
9+
10+
namespace WebGPUHostedWindowDemo.Controls;
11+
12+
/// <summary>
13+
/// A reusable WinForms control that embeds a <see cref="WebGPUHostedWindow{TPixel}"/> and drives a continuous
14+
/// render loop via <see cref="Application.Idle"/>. Callers hook <see cref="PaintFrame"/> with their scene logic;
15+
/// the control handles construction, resize, acquire/present, and teardown.
16+
/// </summary>
17+
public sealed partial class WebGPURenderControl : Control
18+
{
19+
private const int WM_MOVING = 0x0216;
20+
private const int WM_EXITSIZEMOVE = 0x0232;
21+
22+
private WebGPUHostedWindow<Bgra32>? window;
23+
private bool idleHooked;
24+
private long lastTicks;
25+
private System.Windows.Forms.Timer? titleBarMoveTimer;
26+
private TitleBarListener? titleBarListener;
27+
28+
/// <summary>
29+
/// Initializes a new instance of the <see cref="WebGPURenderControl"/> class.
30+
/// </summary>
31+
public WebGPURenderControl()
32+
{
33+
this.SetStyle(
34+
ControlStyles.AllPaintingInWmPaint |
35+
ControlStyles.Opaque |
36+
ControlStyles.UserPaint,
37+
true);
38+
39+
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
40+
}
41+
42+
/// <summary>
43+
/// Raised each frame once the hosted window has acquired a drawable frame.
44+
/// </summary>
45+
public event Action<DrawingCanvas<Bgra32>, TimeSpan>? PaintFrame;
46+
47+
/// <inheritdoc />
48+
protected override void OnHandleCreated(EventArgs e)
49+
{
50+
base.OnHandleCreated(e);
51+
52+
int width = Math.Max(this.ClientSize.Width, 1);
53+
int height = Math.Max(this.ClientSize.Height, 1);
54+
55+
this.window = new WebGPUHostedWindow<Bgra32>(
56+
WebGPUWindowHost.Win32(
57+
this.Handle,
58+
Marshal.GetHINSTANCE(typeof(WebGPURenderControl).Module)),
59+
width,
60+
height);
61+
62+
this.lastTicks = Stopwatch.GetTimestamp();
63+
Application.Idle += this.OnApplicationIdle;
64+
this.idleHooked = true;
65+
66+
// Application.Idle stops firing during a title-bar drag (modal move loop).
67+
// 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 —
69+
// and drive a short Timer during those. Border resize is left untouched.
70+
Form? parentForm = this.FindForm();
71+
if (parentForm is not null && parentForm.IsHandleCreated)
72+
{
73+
this.titleBarMoveTimer = new System.Windows.Forms.Timer { Interval = 16 };
74+
this.titleBarMoveTimer.Tick += this.OnTitleBarMoveTick;
75+
this.titleBarListener = new TitleBarListener(this);
76+
this.titleBarListener.AssignHandle(parentForm.Handle);
77+
}
78+
}
79+
80+
/// <inheritdoc />
81+
protected override void OnHandleDestroyed(EventArgs e)
82+
{
83+
if (this.idleHooked)
84+
{
85+
Application.Idle -= this.OnApplicationIdle;
86+
this.idleHooked = false;
87+
}
88+
89+
this.titleBarListener?.ReleaseHandle();
90+
this.titleBarListener = null;
91+
92+
if (this.titleBarMoveTimer is not null)
93+
{
94+
this.titleBarMoveTimer.Stop();
95+
this.titleBarMoveTimer.Tick -= this.OnTitleBarMoveTick;
96+
this.titleBarMoveTimer.Dispose();
97+
this.titleBarMoveTimer = null;
98+
}
99+
100+
this.window?.Dispose();
101+
this.window = null;
102+
base.OnHandleDestroyed(e);
103+
}
104+
105+
/// <inheritdoc />
106+
protected override void OnResize(EventArgs e)
107+
{
108+
base.OnResize(e);
109+
if (this.window is not null && this.ClientSize.Width > 0 && this.ClientSize.Height > 0)
110+
{
111+
this.window.Resize(this.ClientSize.Width, this.ClientSize.Height);
112+
}
113+
}
114+
115+
/// <inheritdoc />
116+
protected override void OnPaintBackground(PaintEventArgs pevent)
117+
{
118+
// WebGPU owns the surface; suppress the WinForms background paint.
119+
}
120+
121+
/// <inheritdoc />
122+
protected override void OnPaint(PaintEventArgs e) => this.RenderOnce();
123+
124+
// 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
127+
// process it. Frame pacing is delegated to the swap-chain present mode: with Fifo (v-sync)
128+
// TryAcquireFrame blocks until the display is ready, so this loop naturally runs at the
129+
// display's refresh rate without a software timer capping it.
130+
private void OnApplicationIdle(object? sender, EventArgs e)
131+
{
132+
while (IsApplicationIdle())
133+
{
134+
this.RenderOnce();
135+
}
136+
}
137+
138+
private void OnTitleBarMoveTick(object? sender, EventArgs e) => this.RenderOnce();
139+
140+
// Hooks the parent form's handle so the control can observe WM_MOVING / WM_EXITSIZEMOVE
141+
// without requiring the form to know this control exists.
142+
private sealed class TitleBarListener(WebGPURenderControl owner) : NativeWindow
143+
{
144+
protected override void WndProc(ref Message m)
145+
{
146+
switch (m.Msg)
147+
{
148+
case WM_MOVING:
149+
owner.titleBarMoveTimer?.Start();
150+
break;
151+
case WM_EXITSIZEMOVE:
152+
owner.titleBarMoveTimer?.Stop();
153+
break;
154+
}
155+
156+
base.WndProc(ref m);
157+
}
158+
}
159+
160+
private void RenderOnce()
161+
{
162+
if (this.window is null || this.ClientSize.Width <= 0 || this.ClientSize.Height <= 0)
163+
{
164+
return;
165+
}
166+
167+
if (!this.window.TryAcquireFrame(out WebGPUWindowFrame<Bgra32>? frame))
168+
{
169+
return;
170+
}
171+
172+
long now = Stopwatch.GetTimestamp();
173+
TimeSpan delta = TimeSpan.FromSeconds((now - this.lastTicks) / (double)Stopwatch.Frequency);
174+
this.lastTicks = now;
175+
176+
using (frame)
177+
{
178+
this.PaintFrame?.Invoke(frame.Canvas, delta);
179+
}
180+
}
181+
182+
// 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"
184+
// check used by the WinForms continuous-render idiom. Cheaper than any managed alternative and the
185+
// only way to tell whether WinForms is about to break out of Application.Idle on the next pump.
186+
[StructLayout(LayoutKind.Sequential)]
187+
private struct NativeMessage
188+
{
189+
public nint Handle;
190+
public uint Message;
191+
public nint WParam;
192+
public nint LParam;
193+
public uint Time;
194+
public Point Location;
195+
}
196+
197+
[LibraryImport("user32.dll", EntryPoint = "PeekMessageW")]
198+
private static partial int PeekMessage(out NativeMessage msg, nint hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);
199+
200+
private static bool IsApplicationIdle() => PeekMessage(out _, 0, 0, 0, 0) == 0;
201+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using SixLabors.ImageSharp.Drawing.Processing;
5+
using SixLabors.ImageSharp.PixelFormats;
6+
using WebGPUHostedWindowDemo.Controls;
7+
using WebGPUHostedWindowDemo.Scenes;
8+
9+
namespace WebGPUHostedWindowDemo;
10+
11+
/// <summary>
12+
/// 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.
15+
/// </summary>
16+
internal sealed class MainForm : Form
17+
{
18+
private readonly WebGPURenderControl clockControl;
19+
private readonly WebGPURenderControl tigerControl;
20+
private readonly ClockScene clockScene;
21+
private readonly TigerViewerScene tigerScene;
22+
private readonly Label tigerStatusLabel;
23+
24+
public MainForm()
25+
{
26+
this.Text = "ImageSharp.Drawing WebGPU — Hosted Window Demo";
27+
this.ClientSize = new Size(1280, 800);
28+
this.StartPosition = FormStartPosition.CenterScreen;
29+
this.BackColor = Color.FromArgb(11, 18, 32);
30+
31+
this.clockScene = new ClockScene();
32+
this.tigerScene = new TigerViewerScene();
33+
34+
this.clockControl = new WebGPURenderControl { Dock = DockStyle.Fill };
35+
this.clockControl.PaintFrame += this.OnPaintClock;
36+
37+
this.tigerControl = new WebGPURenderControl { Dock = DockStyle.Fill };
38+
this.tigerControl.PaintFrame += this.OnPaintTiger;
39+
this.tigerStatusLabel = new Label
40+
{
41+
AutoSize = false,
42+
Size = new Size(350, 100),
43+
BackColor = Color.FromArgb(160, 0, 0, 0),
44+
ForeColor = Color.White,
45+
Font = new Font(FontFamily.GenericMonospace, 9f),
46+
Padding = new Padding(6),
47+
Location = new Point(6, 6),
48+
Text = string.Empty,
49+
};
50+
this.tigerControl.Controls.Add(this.tigerStatusLabel);
51+
this.tigerControl.MouseDown += (_, e) =>
52+
{
53+
this.tigerScene.OnMouseDown(e);
54+
this.tigerControl.Invalidate();
55+
};
56+
this.tigerControl.MouseMove += (_, e) =>
57+
{
58+
this.tigerScene.OnMouseMove(e);
59+
this.tigerStatusLabel.Text = this.tigerScene.StatusText;
60+
this.tigerControl.Invalidate();
61+
};
62+
this.tigerControl.MouseUp += (_, e) =>
63+
{
64+
this.tigerScene.OnMouseUp(e);
65+
this.tigerControl.Invalidate();
66+
};
67+
this.tigerControl.MouseWheel += (_, e) =>
68+
{
69+
this.tigerScene.OnMouseWheel(e);
70+
this.tigerStatusLabel.Text = this.tigerScene.StatusText;
71+
this.tigerControl.Invalidate();
72+
};
73+
74+
TabControl tabs = new() { Dock = DockStyle.Fill };
75+
76+
TabPage clockTab = new(this.clockScene.DisplayName);
77+
clockTab.Controls.Add(this.clockControl);
78+
tabs.TabPages.Add(clockTab);
79+
80+
TabPage tigerTab = new(this.tigerScene.DisplayName);
81+
tigerTab.Controls.Add(this.tigerControl);
82+
tabs.TabPages.Add(tigerTab);
83+
84+
this.Controls.Add(tabs);
85+
}
86+
87+
private void OnPaintClock(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
88+
{
89+
Size s = this.clockControl.ClientSize;
90+
this.clockScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
91+
}
92+
93+
private void OnPaintTiger(DrawingCanvas<Bgra32> canvas, TimeSpan delta)
94+
{
95+
Size s = this.tigerControl.ClientSize;
96+
this.tigerScene.Paint(canvas, new SixLabors.ImageSharp.Size(s.Width, s.Height), delta);
97+
this.tigerStatusLabel.Text = this.tigerScene.StatusText;
98+
}
99+
}

0 commit comments

Comments
 (0)