Skip to content

Commit 2f017d2

Browse files
committed
Host the Evergine sample in a Windows Forms window
The sample used to let FormsWindowsSystem create its own window. It now owns a MainForm with a toolbar and a status bar, and Evergine renders into an EvergineControl docked inside it, so the ray traced image sits in a normal WinForms layout. The HWND is read after the control is parented, because WinForms recreates a control's handle when it is added to a container. AutoRegisterWindow is turned off and the render loop is pointed at the form, so closing the window ends it. The status bar shows the live per-stage timings, the toolbar can freeze the camera and save a PNG on demand, and resizing only resizes the swapchain: the ray traced image keeps its own resolution and is stretched by the fullscreen triangle, so it costs nothing on the CPU.
1 parent 21c034a commit 2f017d2

5 files changed

Lines changed: 201 additions & 17 deletions

File tree

HelloEmbree.Evergine/MainForm.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using Evergine.Forms;
2+
using System;
3+
using System.Drawing;
4+
using System.Windows.Forms;
5+
6+
namespace HelloEmbree
7+
{
8+
/// <summary>
9+
/// Plain Windows Forms window hosting an <see cref="EvergineControl"/>. Evergine renders into
10+
/// that control's HWND, so the ray traced image lives inside a normal WinForms layout together
11+
/// with regular controls (a status bar and a couple of buttons here).
12+
/// </summary>
13+
internal sealed class MainForm : Form
14+
{
15+
private readonly ToolStripStatusLabel timingsLabel;
16+
private readonly ToolStripStatusLabel sceneLabel;
17+
18+
public MainForm(int renderWidth, int renderHeight)
19+
{
20+
this.Text = "HelloEmbree - Embree.NET on the Evergine low-level API";
21+
this.StartPosition = FormStartPosition.CenterScreen;
22+
this.ClientSize = new Size(renderWidth, renderHeight + 60);
23+
this.MinimumSize = new Size(400, 300);
24+
this.DoubleBuffered = false;
25+
26+
this.RenderControl = new EvergineControl
27+
{
28+
Dock = DockStyle.Fill,
29+
};
30+
31+
var toolStrip = new ToolStrip
32+
{
33+
GripStyle = ToolStripGripStyle.Hidden,
34+
RenderMode = ToolStripRenderMode.System,
35+
};
36+
37+
this.AnimateButton = new ToolStripButton("Pause camera")
38+
{
39+
CheckOnClick = true,
40+
DisplayStyle = ToolStripItemDisplayStyle.Text,
41+
};
42+
43+
this.ScreenshotButton = new ToolStripButton("Save screenshot")
44+
{
45+
DisplayStyle = ToolStripItemDisplayStyle.Text,
46+
};
47+
48+
toolStrip.Items.Add(this.AnimateButton);
49+
toolStrip.Items.Add(new ToolStripSeparator());
50+
toolStrip.Items.Add(this.ScreenshotButton);
51+
52+
this.timingsLabel = new ToolStripStatusLabel("Tracing...")
53+
{
54+
Spring = true,
55+
TextAlign = ContentAlignment.MiddleLeft,
56+
};
57+
58+
this.sceneLabel = new ToolStripStatusLabel(string.Empty);
59+
60+
var statusStrip = new StatusStrip();
61+
statusStrip.Items.Add(this.timingsLabel);
62+
statusStrip.Items.Add(this.sceneLabel);
63+
64+
// Fill last so the docked control gets the remaining area.
65+
this.Controls.Add(this.RenderControl);
66+
this.Controls.Add(toolStrip);
67+
this.Controls.Add(statusStrip);
68+
}
69+
70+
/// <summary>
71+
/// Gets the control Evergine renders into.
72+
/// </summary>
73+
public EvergineControl RenderControl { get; }
74+
75+
/// <summary>
76+
/// Gets the toggle that freezes the orbiting camera.
77+
/// </summary>
78+
public ToolStripButton AnimateButton { get; }
79+
80+
/// <summary>
81+
/// Gets the button that writes a PNG of the current frame.
82+
/// </summary>
83+
public ToolStripButton ScreenshotButton { get; }
84+
85+
public void SetSceneInfo(int triangleCount, int geometryCount, int renderWidth, int renderHeight)
86+
{
87+
this.sceneLabel.Text =
88+
$"{triangleCount:N0} triangles / {geometryCount} geometries | trace {renderWidth}x{renderHeight}";
89+
}
90+
91+
public void SetTimings(double traceMs, double uploadMs, double gpuMs)
92+
{
93+
double total = traceMs + uploadMs + gpuMs;
94+
this.timingsLabel.Text =
95+
$"trace {traceMs,6:F2} ms upload {uploadMs,5:F2} ms gpu {gpuMs,5:F2} ms | {total,6:F2} ms ({1000.0 / total,5:F1} fps)";
96+
}
97+
}
98+
}

HelloEmbree.Evergine/Program.cs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ float4 PS(PS_IN input) : SV_Target
6767
private static double[] uploadMs;
6868
private static double[] gpuMs;
6969
private static double sceneBuildMs;
70+
private static MainForm form;
71+
private static bool resizePending;
72+
private static bool screenshotRequested;
73+
private static float cameraTime;
7074

7175
[STAThread]
7276
private static void Main(string[] args)
@@ -76,17 +80,26 @@ private static void Main(string[] args)
7680
screenshotPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "screenshot.png");
7781
screenshotPath = Path.GetFullPath(screenshotPath);
7882

79-
var windowSystem = new FormsWindowsSystem();
80-
var window = windowSystem.CreateWindow("Embree.NET + Evergine low-level", Width, Height);
83+
System.Windows.Forms.Application.EnableVisualStyles();
84+
System.Windows.Forms.Application.SetCompatibleTextRenderingDefault(false);
85+
86+
form = new MainForm((int)Width, (int)Height);
87+
form.ScreenshotButton.Click += (s, e) => screenshotRequested = true;
88+
89+
// The HWND must exist before the swapchain is built, and it must be read *after* the
90+
// control has been parented: WinForms recreates a control's handle when it is added to
91+
// a container, which would leave the swapchain bound to a dead window.
92+
form.CreateControl();
93+
IntPtr renderHandle = form.RenderControl.Handle;
8194

8295
graphicsContext = new DX11GraphicsContext();
8396
graphicsContext.CreateDevice();
8497

8598
var swapChainDescription = new SwapChainDescription()
8699
{
87-
Width = window.Width,
88-
Height = window.Height,
89-
SurfaceInfo = window.SurfaceInfo,
100+
Width = (uint)form.RenderControl.ClientSize.Width,
101+
Height = (uint)form.RenderControl.ClientSize.Height,
102+
SurfaceInfo = new SurfaceInfo(renderHandle, SurfaceInfo.SurfaceTypes.Forms),
90103
ColorTargetFormat = Evergine.Common.Graphics.PixelFormat.R8G8B8A8_UNorm,
91104
ColorTargetFlags = TextureFlags.RenderTarget | TextureFlags.ShaderResource,
92105
DepthStencilTargetFormat = Evergine.Common.Graphics.PixelFormat.D24_UNorm_S8_UInt,
@@ -101,9 +114,32 @@ private static void Main(string[] args)
101114
// VSync would clamp the measured frame time to the display refresh rate.
102115
swapChain.VerticalSync = !benchmark;
103116

117+
form.RenderControl.ClientSizeChanged += (s, e) => resizePending = true;
118+
119+
// Drive the render loop from the form itself, so it ends when the window is closed.
120+
var windowSystem = new FormsWindowsSystem { AutoRegisterWindow = false };
121+
windowSystem.RegisterLoopThreadControl(form);
104122
windowSystem.Run(Load, Draw);
105123
}
106124

125+
/// <summary>
126+
/// Matches the swapchain to the current size of the hosting control. The ray traced image
127+
/// keeps its own fixed resolution and is stretched by the fullscreen triangle, so resizing
128+
/// costs nothing on the CPU side.
129+
/// </summary>
130+
private static void ApplyPendingResize()
131+
{
132+
resizePending = false;
133+
134+
uint width = (uint)Math.Max(form.RenderControl.ClientSize.Width, 1);
135+
uint height = (uint)Math.Max(form.RenderControl.ClientSize.Height, 1);
136+
137+
swapChain.ResizeSwapChain(width, height);
138+
139+
viewports[0] = new Viewport(0, 0, width, height);
140+
scissors[0] = new Evergine.Mathematics.Rectangle(0, 0, (int)width, (int)height);
141+
}
142+
107143
private static void Load()
108144
{
109145
long buildStart = Stopwatch.GetTimestamp();
@@ -134,7 +170,8 @@ private static void Load()
134170
};
135171
rayTexture = graphicsContext.Factory.CreateTexture(ref textureDescription);
136172

137-
var samplerDescription = SamplerStates.PointClamp;
173+
// Linear so the fixed-resolution ray traced image scales cleanly when the window is resized.
174+
var samplerDescription = SamplerStates.LinearClamp;
138175
var sampler = graphicsContext.Factory.CreateSamplerState(ref samplerDescription);
139176

140177
var vertexShaderDescription = new ShaderDescription(
@@ -176,15 +213,29 @@ private static void Load()
176213

177214
viewports = new[] { new Viewport(0, 0, Width, Height) };
178215
scissors = new[] { new Evergine.Mathematics.Rectangle(0, 0, (int)Width, (int)Height) };
216+
217+
form.SetSceneInfo(raytracer.TriangleCount, raytracer.GeometryCount, (int)Width, (int)Height);
218+
ApplyPendingResize();
179219
}
180220

181221
private static void Draw()
182222
{
223+
if (resizePending)
224+
{
225+
ApplyPendingResize();
226+
}
227+
183228
swapChain.InitFrame();
184229

230+
// The camera is frozen while benchmarking so every frame traces the same image.
231+
if (!benchmark && !form.AnimateButton.Checked)
232+
{
233+
cameraTime = (float)clock.Elapsed.TotalSeconds;
234+
}
235+
185236
// 1. CPU ray tracing with Embree.
186237
long t0 = Stopwatch.GetTimestamp();
187-
raytracer.Render(pixels, (int)Width, (int)Height, benchmark ? 0.0f : (float)clock.Elapsed.TotalSeconds);
238+
raytracer.Render(pixels, (int)Width, (int)Height, cameraTime);
188239
long t1 = Stopwatch.GetTimestamp();
189240

190241
// 2. Upload to the GPU texture.
@@ -234,8 +285,11 @@ private static void Draw()
234285
return;
235286
}
236287

237-
if (frameIndex == ScreenshotFrame)
288+
form.SetTimings(ToMilliseconds(t1 - t0), ToMilliseconds(t2 - t1), ToMilliseconds(t3 - t2));
289+
290+
if (frameIndex == ScreenshotFrame || screenshotRequested)
238291
{
292+
screenshotRequested = false;
239293
SaveScreenshot();
240294

241295
if (exitAfterScreenshot)
@@ -305,6 +359,10 @@ private static unsafe void SaveScreenshot()
305359
{
306360
Texture source = swapChain.FrameBuffer.ColorTargets[0].Texture;
307361

362+
// The swapchain follows the control size, which is not the ray tracing resolution.
363+
int width = (int)source.Description.Width;
364+
int height = (int)source.Description.Height;
365+
308366
var stagingDescription = source.Description;
309367
stagingDescription.Flags = TextureFlags.None;
310368
stagingDescription.CpuAccess = ResourceCpuAccess.Read;
@@ -323,20 +381,20 @@ private static unsafe void SaveScreenshot()
323381

324382
try
325383
{
326-
using var bitmap = new System.Drawing.Bitmap((int)Width, (int)Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
384+
using var bitmap = new System.Drawing.Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
327385
var bitmapData = bitmap.LockBits(
328-
new System.Drawing.Rectangle(0, 0, (int)Width, (int)Height),
386+
new System.Drawing.Rectangle(0, 0, width, height),
329387
System.Drawing.Imaging.ImageLockMode.WriteOnly,
330388
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
331389

332390
try
333391
{
334-
for (int y = 0; y < Height; y++)
392+
for (int y = 0; y < height; y++)
335393
{
336394
byte* sourceRow = (byte*)mapped.Data + (y * mapped.RowPitch);
337395
byte* destinationRow = (byte*)bitmapData.Scan0 + (y * bitmapData.Stride);
338396

339-
for (int x = 0; x < Width; x++)
397+
for (int x = 0; x < width; x++)
340398
{
341399
// Swapchain is RGBA, GDI+ expects BGRA.
342400
destinationRow[(x * 4) + 0] = sourceRow[(x * 4) + 2];

HelloEmbree.Evergine/README.md

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,36 @@
22

33
CPU ray tracing with `Evergine.Bindings.Embree`, presented through the **Evergine low-level
44
graphics API** — no Evergine Framework, no scene graph, just `GraphicsContext`, `SwapChain`,
5-
`CommandQueue` and a fullscreen triangle.
5+
`CommandQueue` and a fullscreen triangle — inside a plain **Windows Forms** window.
66

7-
![Ray traced scene](docs/screenshot.png)
7+
![The sample running](docs/window.png)
8+
9+
## Hosting inside a WinForms window
10+
11+
`MainForm` is an ordinary `Form` with a toolbar and a status bar. Evergine renders into an
12+
[`EvergineControl`](https://github.com/EvergineTeam/Evergine.Public) docked in the middle of it,
13+
so the ray traced image participates in a normal WinForms layout:
14+
15+
```csharp
16+
form.CreateControl(); // realize the HWND first
17+
var surfaceInfo = new SurfaceInfo(form.RenderControl.Handle, SurfaceInfo.SurfaceTypes.Forms);
18+
// ... swapChainDescription.SurfaceInfo = surfaceInfo ...
19+
20+
var windowSystem = new FormsWindowsSystem { AutoRegisterWindow = false };
21+
windowSystem.RegisterLoopThreadControl(form); // loop ends when the form closes
22+
windowSystem.Run(Load, Draw);
23+
```
24+
25+
Two details matter here:
26+
27+
- The HWND has to be read **after** the control is parented. WinForms recreates a control's
28+
handle when it is added to a container, which would leave the swapchain bound to a dead window.
29+
- `AutoRegisterWindow = false` stops `FormsWindowsSystem` from creating its own window;
30+
`RegisterLoopThreadControl(form)` points the render loop at our form instead.
31+
32+
The status bar shows the live per-stage cost, the toolbar can freeze the camera and write a PNG.
33+
Resizing the window only resizes the swapchain — the ray traced image keeps its own fixed
34+
resolution and is stretched by the fullscreen triangle, so resizing costs nothing on the CPU.
835

936
## What it does
1037

@@ -44,7 +71,7 @@ Options:
4471

4572
| Flag | Effect |
4673
|---|---|
47-
| *(none)* | Opens the window, camera orbits the scene, writes `screenshot.png` at frame 10 |
74+
| *(none)* | Opens the window, camera orbits the scene, writes `screenshot.png` at frame 10 and on demand from the toolbar |
4875
| `--exit` | Same, but closes right after the screenshot |
4976
| `--bench` | Disables VSync, discards 20 warm-up frames, times 100 frames and prints a breakdown |
5077

72.9 KB
Loading

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,10 @@ HelloEmbree.Evergine/ CPU ray tracer drawn with the Evergine low-level
7474

7575
[HelloEmbree.Evergine](HelloEmbree.Evergine/README.md) traces a small scene on the CPU with
7676
`rtcIntersect1`/`rtcOccluded1`, uploads the result to a texture every frame and blits it to a
77-
DX11 swapchain. It also has a `--bench` mode that reports the cost of each stage.
77+
DX11 swapchain hosted in a Windows Forms window. It also has a `--bench` mode that reports the
78+
cost of each stage.
7879

79-
![Ray traced scene](HelloEmbree.Evergine/docs/screenshot.png)
80+
![The Evergine sample running](HelloEmbree.Evergine/docs/window.png)
8081

8182
Regenerate the bindings after changing a header with:
8283

0 commit comments

Comments
 (0)