Gondwana Game Engine Development (master) - 2.6.1
Gondwana is a cross-platform 2.5D game and rendering engine written in C#/.NET 8. It provides fine-grained control over rendering, timing, and scene composition, with built-in support for parallax, z-ordering, pixel overhang, collision detection, and particle effects. Gondwana targets desktop and web platforms using SkiaSharp for graphics and pluggable audio backends for desktop and browser targets.
Loading...
Searching...
No Matches
WinFormGpuRenderSurfaceAdapter.cs
4using SkiaSharp;
5using SkiaSharp.Views.Desktop;
6
8
19{
20 private readonly SKGLControl _glControl;
21 private readonly Control _presentationControl;
22 private readonly EventHandler _resizeHandler;
23
24 // ── GL-thread path ───────────────────────────────────────────────────────
25 private RenderSurfaceHostBase? _host;
26 private GpuBackbuffer? _gpuBackbuffer;
27 private Action? _afterFrameRenderHandler;
28
29 // Tracks the last VSync value applied to the GL control so we can detect changes.
30 // Null means "not yet applied" and forces an apply on the first PaintSurface call.
31 private bool? _appliedVSync;
32
33 // ── Shared state ─────────────────────────────────────────────────────────
34 // Tracks whether GrContext has been captured and the first-available event fired.
35 private bool _grContextReady;
36
37 // Set to 1 when the presentation area is resized so OnPaintSurface can fire ResizeRequested.
38 private int _pendingResize;
39
40 // Set to 1 while an invalidate callback is queued on the UI dispatcher. The callback clears
41 // the flag whether or not WinForms ultimately produces a paint, so a zero-sized presentation
42 // cannot permanently latch rendering off.
43 private int _pendingInvalidate;
44
48 public SKColor ClearColor { get; set; } = SKColors.Black;
49
53 public GRContext? GrContext { get; private set; }
54
60 public event Action<GRContext>? GrContextFirstAvailable;
61
68 public event Action<GRContext, int, int>? ResizeRequested;
69
73 public event Action<WinFormGpuFrameDiagnostics>? FrameDiagnosticsCalculated;
74
80 {
81 if (_presentationControl.IsDisposed || !_presentationControl.IsHandleCreated) return;
82
83 var sz = _presentationControl.ClientSize;
84 SetDestinationSize(sz.Width, sz.Height);
85
86 if (sz.Width > 0 && sz.Height > 0)
87 Interlocked.Exchange(ref _pendingResize, 1);
88 else
89 Interlocked.Exchange(ref _pendingResize, 0);
90 }
91
96 public WinFormGpuRenderSurfaceAdapter(SKGLControl gl)
97 : this(gl, gl)
98 {
99 }
100
107 internal WinFormGpuRenderSurfaceAdapter(SKGLControl gl, Control presentationControl)
108 : base(
109 Math.Max(1, presentationControl.ClientSize.Width),
110 Math.Max(1, presentationControl.ClientSize.Height),
111 presentationControl.ClientSize.Width > 0 && presentationControl.ClientSize.Height > 0)
112 {
113 _glControl = gl ?? throw new ArgumentNullException(nameof(gl));
114 _presentationControl = presentationControl ?? throw new ArgumentNullException(nameof(presentationControl));
115
116 // Track the actual presentation control rather than the GL drawable. The wrapper may keep
117 // the drawable at 1×1 while its own client area is 0×0 so OpenTK/GLFW never receives a
118 // zero-sized native surface.
119 _resizeHandler = (_, _) => RefreshDestinationSize();
120
121 // Wire events
122 _glControl.PaintSurface += OnPaintSurface;
123 _presentationControl.Resize += _resizeHandler;
124
125 // On modern SkiaSharp, SKGLControl exposes GRContext; otherwise capture it in the first paint.
126 GrContext = _glControl.GRContext; // may be null until first paint; we also set in OnPaintSurface
127 }
128
142 {
143 _host = host ?? throw new ArgumentNullException(nameof(host));
144 _host.Telemetry?.Define("presentation.cpu.ms");
145 _host.Telemetry?.Define("render.snapshot.cpu.ms");
146 _host.Telemetry?.Define("blit.cpu.ms");
147 _host.Telemetry?.Define("flush.cpu.ms");
148
149 // Cache the GpuBackbuffer so OnPaintSurface can read its settings.
150 _gpuBackbuffer = _host.Backbuffer as GpuBackbuffer;
151
152 // Invalidate the GL control on the UI thread after every engine foreground cycle.
153 // _pendingInvalidate tracks the queued dispatcher callback, not PaintSurface itself.
154 // Invalidate() is only a paint request and can be dropped while the presentation has no
155 // area, so tying this latch to PaintSurface can permanently starve rendering after restore.
156 _afterFrameRenderHandler = QueueInvalidate;
157 Engine.Instance.AfterFrameRender += _afterFrameRenderHandler;
158 }
159
167 public override void Present(SKImage bufferImage, SKRectI bufferRect, SKRect destRect)
168 {
169 bufferImage.Dispose();
170 }
171
172 private void QueueInvalidate()
173 {
174 if (_glControl.IsDisposed || _presentationControl.IsDisposed) return;
175 if (Interlocked.CompareExchange(ref _pendingInvalidate, 1, 0) != 0) return;
176
178 {
179 try
180 {
181 if (!_glControl.IsDisposed
182 && !_presentationControl.IsDisposed
183 && _presentationControl.ClientSize.Width > 0
184 && _presentationControl.ClientSize.Height > 0)
185 {
186 _glControl.Invalidate();
187 }
188 }
189 finally
190 {
191 // The dispatcher callback completed. Do not wait for PaintSurface: WinForms is
192 // allowed to coalesce or discard an invalidation, especially at zero size.
193 Interlocked.Exchange(ref _pendingInvalidate, 0);
194 }
195 });
196 }
197
198 private void OnPaintSurface(object? sender, SKPaintGLSurfaceEventArgs e)
199 {
200 long telemetryGeneration = _host?.Telemetry?.BeginSample() ?? 0;
201 bool collectDiagnostics = FrameDiagnosticsCalculated is not null || telemetryGeneration != 0;
202 long callbackStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
203 long renderAndSnapshotTicks = 0;
204 long blitTicks = 0;
205 long flushTicks = 0;
206
207 // Lazily sync VSync → GLControl.VSync whenever the backbuffer value changes.
208 if (_gpuBackbuffer != null)
209 {
210 bool desiredVSync = _gpuBackbuffer.VSync;
211 if (_appliedVSync != desiredVSync)
212 {
213 _glControl.VSync = desiredVSync;
214 _appliedVSync = desiredVSync;
215 }
216 }
217
218 // Capture/refresh the GRContext so callers can wire the backbuffer to the same one.
219 GrContext = _glControl.GRContext;
220
221 if (GrContext != null)
222 {
223 if (!_grContextReady)
224 {
225 // First time the context is available: clear any resize that happened before the
226 // first paint (the Initialize call will use the current adapter dimensions).
227 _grContextReady = true;
228 Interlocked.Exchange(ref _pendingResize, 0);
230 }
231 else if (Interlocked.Exchange(ref _pendingResize, 0) == 1)
232 {
233 // Notify consumers about presentation resource resize on the GL thread.
234 // WinForms controls can report zero size while minimized or before layout completes;
235 // suppress invalid resize requests so downstream GPU initialization is not attempted
236 // with non-positive dimensions.
237 var width = Width;
238 var height = Height;
239 if (width > 0 && height > 0)
240 ResizeRequested?.Invoke(GrContext, width, height);
241 }
242 }
243
244 if (GrContext != null)
245 _gpuBackbuffer?.EnsureInitialized(GrContext);
246
247 var canvas = e.Surface.Canvas;
248
249 // The physical GL drawable is intentionally kept at least 1×1, even when the wrapper has
250 // no presentation area. Avoid doing scene work for that clipped placeholder surface.
251 if (Width <= 0 || Height <= 0)
252 {
253 canvas.Clear(ClearColor);
254 return;
255 }
256
257 // Render + blit entirely on the GL thread.
258 // GlRenderAndSnapshot replays immutable Engine commands on the GPU surface and returns a
259 // lightweight GPU-backed snapshot. Both the snapshot texture and e.Surface share the
260 // same GRContext, so DrawImage is a zero-copy GPU blit.
261 if (_host != null)
262 {
263 long renderStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
264 using var img = _host.GlRenderAndSnapshot();
265 long renderEndTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
266
267 if (collectDiagnostics)
268 renderAndSnapshotTicks = renderEndTick - renderStartTick;
269
270 if (img != null)
271 {
272 long blitStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
273 DrawImage(canvas, img, ClearColor);
274 long blitEndTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
275
276 if (collectDiagnostics)
277 blitTicks = blitEndTick - blitStartTick;
278 }
279 else
280 {
281 canvas.Clear(ClearColor);
282 }
283 }
284 else
285 {
286 canvas.Clear(ClearColor);
287 }
288
289 // Optional: flush to ensure work is queued to GPU before we hand new images next frame.
290 long flushStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
291 _glControl.GRContext?.Flush();
292 long flushEndTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
293
294 if (collectDiagnostics)
295 flushTicks = flushEndTick - flushStartTick;
296
297 // Record the completed frame so the engine's CPS sampler can compute actual rendered FPS.
298 _gpuBackbuffer?.RecordFrame();
299
300 if (collectDiagnostics)
301 {
302 long callbackEndTick = HighResTimer.GetCurrentTick();
303 if (telemetryGeneration != 0)
304 {
305 var source = _host!.Telemetry!;
306 source.Record(telemetryGeneration, "presentation.count", 1);
307 source.Record(telemetryGeneration, "presentation.cpu.ms", HighResTimer.GetDuration(callbackStartTick, callbackEndTick) * 1000d);
308 source.Record(telemetryGeneration, "render.snapshot.cpu.ms", HighResTimer.GetDuration(0, renderAndSnapshotTicks) * 1000d);
309 source.Record(telemetryGeneration, "blit.cpu.ms", HighResTimer.GetDuration(0, blitTicks) * 1000d);
310 source.Record(telemetryGeneration, "flush.cpu.ms", HighResTimer.GetDuration(0, flushTicks) * 1000d);
311 }
313 HighResTimer.GetDuration(callbackStartTick, callbackEndTick) * 1000d,
314 HighResTimer.GetDuration(0, renderAndSnapshotTicks) * 1000d,
315 HighResTimer.GetDuration(0, blitTicks) * 1000d,
316 HighResTimer.GetDuration(0, flushTicks) * 1000d));
317 }
318 }
319
323 public void Dispose()
324 {
325 if (_afterFrameRenderHandler != null)
326 {
327 Engine.Instance.AfterFrameRender -= _afterFrameRenderHandler;
328 _afterFrameRenderHandler = null;
329 }
330
331 if (!_glControl.IsDisposed)
332 _glControl.PaintSurface -= OnPaintSurface;
333
334 if (!_presentationControl.IsDisposed)
335 _presentationControl.Resize -= _resizeHandler;
336 }
337}
Represents the core game engine singleton responsible for managing the main update loop,...
Definition Engine.cs:36
static Engine Instance
Gets the singleton instance of the Engine.
Definition Engine.cs:49
IUiDispatcher? UiDispatcher
Gets the UI dispatcher used for marshalling operations to the UI thread.
Definition Engine.cs:791
Provides a fully GPU-rendered backbuffer implementation using SkiaSharp and OpenGL.
int Width
Gets the current width of the render surface in pixels.
void SetDestinationSize(int destWidth, int destHeight)
Sets the destination size of the render surface and raises the Resized event if the dimensions have c...
RenderSurfaceAdapterBase(int destWidth, int destHeight)
Initializes a new instance of the RenderSurfaceAdapterBase class with the specified dimensions.
int Height
Gets the current height of the render surface in pixels.
void DrawImage(SKCanvas canvas, SKImage image, SKColor clearColor)
Presents a complete image using the same transform as pointer normalization.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
Action< WinFormGpuFrameDiagnostics >? FrameDiagnosticsCalculated
Occurs after a completed GPU paint callback when a subscriber requests timing diagnostics.
void SetHost(RenderSurfaceHostBase host)
Registers the RenderSurfaceHostBase whose scene this adapter should render and wires the GL paint loo...
override void Present(SKImage bufferImage, SKRectI bufferRect, SKRect destRect)
Not used in the GL-thread path. Rendering is driven by SetHost and SKGLControl.PaintSurface....
Action< GRContext >? GrContextFirstAvailable
Raised once, the first time the GRContext becomes available in OnPaintSurface. Subscribe to this even...
Action< GRContext, int, int >? ResizeRequested
Raised on the GL thread when the control has been resized and a valid GRContext is available....
void Dispose()
Releases all resources used by the WinFormGpuRenderSurfaceAdapter.
GRContext? GrContext
Gets the GL/Skia GPU context that should be shared with the GpuBackbuffer.
void RefreshDestinationSize()
Refreshes the destination size based on the current client size of the presentation control....
SKColor ClearColor
Gets or sets the color used to clear the surface before rendering.
WinFormGpuRenderSurfaceAdapter(SKGLControl gl)
Initializes a new instance of the WinFormGpuRenderSurfaceAdapter class.
void Post(Action action)
Asynchronously posts an action to be executed on the UI thread. This method queues the action and ret...
record WinFormGpuFrameDiagnostics(double TotalCallbackMilliseconds, double RenderAndSnapshotMilliseconds, double BlitMilliseconds, double FlushMilliseconds)
Captures measured work performed by one WinForms GPU paint callback.