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
GpuBackbuffer.cs
1using System.Drawing;
4using SkiaSharp;
5
7
39public class GpuBackbuffer : BackbufferBase
40{
41 // Surface state. All access MUST occur on the GL thread after Initialize() is called.
42 private SKBitmap? _cpuBitmap; // temporary CPU surface used before GRContext is ready
43 private SKSurface? _surface;
44 private bool _disposed;
45 private GRContext? _context;
46 private sealed record Resolution(int Width, int Height);
47 private Resolution? _requestedResolution;
48
49 private int _targetFps = 60;
50 private readonly object _msaaSyncRoot = new();
51 private int _msaaSampleCount = 1;
52 private long _msaaConfigurationRevision;
53 private long _appliedMsaaConfigurationRevision = -1;
54 private int _actualMsaaSampleCount = 1;
55 private int _maxSupportedMsaaSampleCount = 1;
56
57 // Frame counter used to compute the actual rendered FPS.
58 // Incremented on the GL thread by RecordFrame(); consumed atomically by the engine's CPS sampler.
59 private long _frameCount;
60
73 public int TargetFps
74 {
75 get => _targetFps;
76 set => _targetFps = value < 0 ? 0 : value;
77 }
78
99 public bool VSync { get; set; } = true;
100
130 {
131 get
132 {
133 lock (_msaaSyncRoot)
134 return _msaaSampleCount;
135 }
136 set
137 {
138 int normalized = value < 1 ? 1 : value;
139
140 lock (_msaaSyncRoot)
141 {
142 if (_msaaSampleCount == normalized)
143 return;
144
145 _msaaSampleCount = normalized;
146 _msaaConfigurationRevision++;
147 }
148 }
149 }
150
160 public int ActualMsaaSampleCount => Volatile.Read(ref _actualMsaaSampleCount);
161
170 public int MaxSupportedMsaaSampleCount => Volatile.Read(ref _maxSupportedMsaaSampleCount);
171
175 internal bool IsMsaaSurfaceRecreationPending =>
176 Volatile.Read(ref _appliedMsaaConfigurationRevision) !=
177 Volatile.Read(ref _msaaConfigurationRevision);
178
184 public GpuBackbuffer(int width, int height)
185 : base(width, height)
186 {
187 // Apply the current engine configuration defaults for the settings this backbuffer
188 // explicitly copies at construction time: TargetFps, VSync, and MsaaSampleCount.
189 var config = Engine.Instance.Configuration;
190 TargetFps = config.TargetFPS;
191 VSync = config.VSync;
192 MsaaSampleCount = config.MsaaSampleCount;
193
194 CreateCpuSurface(width, height);
195 }
196
202 public override bool IsGlThreadRendered => true;
203
215 public void Initialize(GRContext grContext, int width, int height)
216 {
217 if (width <= 0 || height <= 0)
218 return;
219 if (_disposed) return;
220
221 (int requestedSampleCount, long configurationRevision) = GetMsaaConfigurationSnapshot();
222 int maxSupportedSampleCount = grContext.GetMaxSurfaceSampleCount(SKColorType.Rgba8888);
223 Volatile.Write(ref _maxSupportedMsaaSampleCount, maxSupportedSampleCount);
224
225 DisposeSurface();
226 int actualSampleCount = CreateGpuSurface(grContext, width, height, requestedSampleCount);
227 _context = grContext;
228 // SizeChanged updates live viewports. Only this handoff needs the simulation
229 // gate; GPU creation and ordinary command replay never hold it.
230 lock (RenderStateSynchronization.SyncRoot)
231 UpdateSize(width, height);
232
233 MarkMsaaConfigurationApplied(configurationRevision, actualSampleCount);
234
235 // Set canvas into a known state for the first frame on the new surface.
236 BeginFrame();
237 Canvas.Clear(ClearColor);
238 }
239
245 protected internal override void RequestResize(int width, int height)
246 => Interlocked.Exchange(ref _requestedResolution, new(width, height));
247
255 public bool EnsureInitialized(GRContext context)
256 {
257 ArgumentNullException.ThrowIfNull(context);
258 if (_disposed) return false;
259 var request = Interlocked.Exchange(ref _requestedResolution, null);
260 int width = request?.Width ?? Width;
261 int height = request?.Height ?? Height;
262 if (ReferenceEquals(_context, context) &&
263 width == Width &&
264 height == Height &&
265 !IsMsaaSurfaceRecreationPending)
266 {
267 return false;
268 }
269
270 Initialize(context, width, height);
271 return true;
272 }
273
277 public override SKCanvas Canvas => _surface?.Canvas
278 ?? throw new InvalidOperationException($"{nameof(GpuBackbuffer)} surface is not initialized.");
279
283 protected internal override void BeginFrame()
284 {
285 if (_disposed || _surface is null) return;
286 var c = _surface.Canvas;
287 c.RestoreToCount(1);
288 c.Save();
289 c.ResetMatrix();
290 c.ClipRect(new SKRect(0, 0, Width, Height));
291 }
292
296 protected internal override void EndFrame()
297 {
298 if (_disposed || _surface is null) return;
299 _surface.Flush();
300 }
301
307 protected internal override void DrawTileFrame(Tile tile, RectangleF destRectScreen)
308 {
309 var image = tile.CurrentFrame.SkImage;
310 if (image is null || _surface is null) return;
311
312 var canvas = _surface.Canvas;
313
314 canvas.DrawImage(image, destRectScreen.ToSKRect());
315 }
316
327 protected internal override SKImage Snapshot()
328 {
329 if (_disposed || _surface is null)
330 throw new ObjectDisposedException(nameof(GpuBackbuffer));
331
332 return _surface.Snapshot();
333 }
334
335 // ── Actual FPS tracking ──────────────────────────────────────────────────
336
341 public void RecordFrame() => Interlocked.Increment(ref _frameCount);
342
348 internal long ConsumeFrameCount() => Interlocked.Exchange(ref _frameCount, 0);
349
350 // ── Surface creation helpers ─────────────────────────────────────────────
351
352 internal (int SampleCount, long Revision) GetMsaaConfigurationSnapshot()
353 {
354 lock (_msaaSyncRoot)
355 return (_msaaSampleCount, _msaaConfigurationRevision);
356 }
357
358 internal void MarkMsaaConfigurationApplied(long revision, int actualSampleCount)
359 {
360 Volatile.Write(ref _actualMsaaSampleCount, actualSampleCount);
361 Volatile.Write(ref _appliedMsaaConfigurationRevision, revision);
362 }
363
364 private int CreateGpuSurface(
365 GRContext grContext,
366 int width,
367 int height,
368 int requestedSampleCount)
369 {
370 // Rgba8888 / Premul is the natural format for an OpenGL render target.
371 var info = new SKImageInfo(width, height, SKColorType.Rgba8888, SKAlphaType.Premul);
372 _surface = SKSurface.Create(grContext, budgeted: true, info, requestedSampleCount);
373
374 if (_surface is not null)
375 return requestedSampleCount;
376
377 // SKSurface.Create returns null when the requested MSAA sample count is not supported
378 // by the hardware or driver. Fall back to no MSAA (sample count 1) so the surface
379 // is always valid after Initialize() completes.
380 if (requestedSampleCount > 1)
381 _surface = SKSurface.Create(grContext, budgeted: true, info, sampleCount: 1);
382
383 if (_surface is null)
384 throw new InvalidOperationException(
385 $"Failed to create a {nameof(GpuBackbuffer)} GPU surface ({width}x{height}). " +
386 "The GRContext may be invalid or the pixel format is not renderable.");
387
388 return 1;
389 }
390
391 private void CreateCpuSurface(int width, int height)
392 {
393 // Bgra8888 matches the native Windows GDI pixel format; used only before
394 // GRContext becomes available so rendering is always valid from frame one.
395 var info = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul);
396 _cpuBitmap = new SKBitmap(info);
397 _surface = SKSurface.Create(info, _cpuBitmap.GetPixels(), _cpuBitmap.Info.RowBytes);
398 }
399
400 private void DisposeSurface()
401 {
402 _surface?.Dispose();
403 _surface = null;
404 _cpuBitmap?.Dispose();
405 _cpuBitmap = null;
406 }
407
408 // Called by a platform's GL deinitialization callback, while its old context
409 // is still current. Logical state and Engine-owned recordings survive a
410 // temporary context loss; EnsureInitialized will attach the next context.
411 internal void ReleaseContext()
412 {
413 DisposeSurface();
414 _context = null;
415 }
416
420 public override void Dispose()
421 {
422 if (_disposed) return;
423 _disposed = true;
424 base.Dispose();
425 DisposeSurface();
426 GC.SuppressFinalize(this);
427 }
428}
Represents an abstract base class for drawable tiles in the Gondwana engine. Provides core functional...
Definition Tile.cs:19
virtual Frame CurrentFrame
Gets or sets the current frame. The initial frame supplies the tile's default collision adjustment an...
Definition Tile.cs:176
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
EngineConfiguration Configuration
Gets the current engine configuration settings.
Definition Engine.cs:928
int Width
Gets the current Backbuffer width in a thread-safe manner.
void UpdateSize(int width, int height)
Updates the stored backbuffer dimensions and raises the SizeChanged event.
int Height
Gets the current Backbuffer height in a thread-safe manner.
SKColor ClearColor
Gets or sets the color used to clear the drawing surface.
bool VSync
Gets or sets a value indicating whether vertical synchronisation (vsync) is enabled.
int ActualMsaaSampleCount
Gets the sample count used by the currently active GPU render-target surface.
int MsaaSampleCount
Gets or sets the number of MSAA (multisample anti-aliasing) samples used when creating the GPU render...
GpuBackbuffer(int width, int height)
Initializes a new instance of the GpuBackbuffer class with the specified dimensions.
int MaxSupportedMsaaSampleCount
Gets the maximum MSAA sample count reported by the active GPU context for SKColorType....
void Initialize(GRContext grContext, int width, int height)
Creates (or recreates) the GPU render-target surface for this backbuffer.
int TargetFps
Gets or sets the target frame rate associated with this GPU backbuffer.
bool EnsureInitialized(GRContext context)
Initializes on the owning GL thread, or applies an explicit logical resolution request....
void RecordFrame()
Records that one frame has been rendered. Called by the platform adapter on every PaintSurface callba...
override void Dispose()
Releases all resources used by this GpuBackbuffer.
override SKCanvas Canvas
Gets the SkiaSharp canvas for drawing operations.
readonly? SKImage SkImage
Gets the SkiaSharp image for this frame.
Definition Frame.cs:70