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
RenderSurfaceHostBase.cs
6using SkiaSharp;
7
8namespace Gondwana.Rendering;
9
14public abstract class RenderSurfaceHostBase : IDisposable
15{
17 public Diagnostics.TelemetrySource? Telemetry { get; internal set; }
18 internal RenderFrameMailbox FrameMailbox { get; } = new();
19 internal bool UsesRenderFrameSnapshots => !OperatingSystem.IsBrowser() && Backbuffer is GpuBackbuffer;
20 internal virtual void ProduceRenderFrameSnapshot(long tick) { }
21
31 {
32 Effects = new EffectsManager(this);
33 RenderSurfaceHostRegistry.Register(this);
34 }
35
41
45 public abstract BackbufferBase Backbuffer { get; }
46
48 public float PresentationScale => RenderSurfaceAdapter?.PresentationScale ?? 0f;
49
50 internal virtual void RequestRenderScale(float scale) { }
51 internal virtual void InvalidatePresentation() { }
52
56 public abstract Scene Scene { get; }
57
63
75 public abstract ViewManager ViewManager { get; }
76
80 public EffectsManager Effects { get; }
81
86 public event Action<GpuRenderSynchronizationDiagnostics>? GpuRenderSynchronizationDiagnosticsCalculated;
87
114 public SKImage? GlRenderAndSnapshot()
115 {
116 if (!Backbuffer.IsGlThreadRendered)
117 return null;
118
119 if (UsesRenderFrameSnapshots)
120 return ReplayRenderFrameSnapshot();
121
122 bool collectSynchronizationDiagnostics =
124 long waitStarted = collectSynchronizationDiagnostics
125 ? HighResTimer.GetCurrentTick()
126 : 0;
127 long lockAcquired = 0;
128 long lockReleased = 0;
129 SKImage? image;
130
131 RenderStateSynchronization.EnterGpuRender();
132 try
133 {
134 if (collectSynchronizationDiagnostics)
135 lockAcquired = HighResTimer.GetCurrentTick();
136
137 var tick = HighResTimer.GetCurrentTick();
138
139 RenderToBackbuffer(tick);
140 Backbuffer.EndFrame();
141
142 image = Backbuffer.Snapshot();
143
144 Backbuffer.BeginFrame();
145
146 if (collectSynchronizationDiagnostics)
147 lockReleased = HighResTimer.GetCurrentTick();
148 }
149 finally
150 {
151 RenderStateSynchronization.ExitGpuRender();
152 }
153
154 if (collectSynchronizationDiagnostics)
155 {
158 HighResTimer.GetDuration(waitStarted, lockAcquired) * 1000d,
159 HighResTimer.GetDuration(lockAcquired, lockReleased) * 1000d));
160 }
161
162 return image;
163 }
164
165 private SKImage? ReplayRenderFrameSnapshot()
166 {
168 long telemetryGeneration = Telemetry?.BeginSample() ?? 0;
169 bool collectDiagnostics = diagnostics is not null || telemetryGeneration != 0;
170 var slot = FrameMailbox.TryAcquire();
171 if (slot is null) return Backbuffer.Snapshot();
172
173 long acquired = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
174 var counters = !collectDiagnostics ? default : FrameMailbox.Counters;
175 long replayEnd = acquired;
176 bool replayCompleted = false;
177 long pictureReplayTicks = 0;
178 long backbufferFlushTicks = 0;
179 long snapshotTicks = 0;
180 int commands = !collectDiagnostics ? 0 : slot.Frame!.CommandCount;
181 double age = !collectDiagnostics ? 0 :
182 HighResTimer.GetDuration(slot.Frame!.ProducedTick, acquired) * 1000d;
183
184 try
185 {
186 var frame = slot.Frame!;
187
188 // A logical resize invalidates geometry, but context/MSAA recreation does
189 // not: recordings contain CPU resources and are uploaded by the current GL context.
190 if (frame.Width != Backbuffer.Width || frame.Height != Backbuffer.Height)
191 {
192 long snapshotStart = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
193 var resizedSnapshot = Backbuffer.Snapshot();
194 if (collectDiagnostics)
195 snapshotTicks = HighResTimer.GetCurrentTick() - snapshotStart;
196 return resizedSnapshot;
197 }
198
199 Backbuffer.BeginFrame();
200
201 long pictureStart = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
202 frame.Replay(Backbuffer.Canvas);
203 long pictureEnd = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
204
205 Backbuffer.EndFrame();
206 long flushEnd = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
207 replayCompleted = true;
208
209 if (collectDiagnostics)
210 {
211 pictureReplayTicks = pictureEnd - pictureStart;
212 backbufferFlushTicks = flushEnd - pictureEnd;
213 replayEnd = flushEnd;
214 }
215
216 long snapshotStartTick = !collectDiagnostics ? 0 : HighResTimer.GetCurrentTick();
217 var snapshot = Backbuffer.Snapshot();
218 if (collectDiagnostics)
219 snapshotTicks = HighResTimer.GetCurrentTick() - snapshotStartTick;
220
221 return snapshot;
222 }
223 finally
224 {
225 try { Backbuffer.BeginFrame(); }
226 finally { FrameMailbox.Release(slot); }
227
228 if (collectDiagnostics)
229 {
230 double replayMs = HighResTimer.GetDuration(acquired, replayEnd) * 1000d;
231 double pictureMs = HighResTimer.GetDuration(0, pictureReplayTicks) * 1000d;
232 double flushMs = HighResTimer.GetDuration(0, backbufferFlushTicks) * 1000d;
233 double snapshotMs = HighResTimer.GetDuration(0, snapshotTicks) * 1000d;
234 if (telemetryGeneration != 0)
235 {
236 if (replayCompleted)
237 {
238 Telemetry!.Record(telemetryGeneration, "replay.cpu.ms", replayMs);
239 Telemetry.Record(telemetryGeneration, "picture.cpu.ms", pictureMs);
240 Telemetry.Record(telemetryGeneration, "backbuffer.flush.cpu.ms", flushMs);
241 }
242
243 Telemetry!.Record(telemetryGeneration, "snapshot.cpu.ms", snapshotMs);
244 Telemetry.Record(telemetryGeneration, "snapshot.age.ms", age);
245 Telemetry.Record(telemetryGeneration, "mailbox.published.lifetime", counters.Published);
246 Telemetry.Record(telemetryGeneration, "mailbox.dropped.lifetime", counters.Dropped);
247 Telemetry.Record(telemetryGeneration, "mailbox.slots", counters.InUse);
248 Telemetry.Record(telemetryGeneration, "snapshot.commands.approximate", commands);
249 }
250 diagnostics?.Invoke(new(0, 0)
251 {
252 ReplayMilliseconds = replayMs,
253 PictureReplayMilliseconds = pictureMs,
254 BackbufferFlushMilliseconds = flushMs,
255 SnapshotMilliseconds = snapshotMs,
256 SnapshotAgeMilliseconds = age,
257 PublishedSnapshots = counters.Published,
258 DroppedSnapshots = counters.Dropped,
259 SnapshotSlotsInUse = counters.InUse,
260 SnapshotCommandCount = commands
261 });
262 }
263 }
264 }
265
278 public bool GlRenderToCanvas(SKCanvas destinationCanvas)
279 {
280 ArgumentNullException.ThrowIfNull(destinationCanvas);
281
282 if (!Backbuffer.IsGlThreadRendered)
283 return false;
284
285 if (UsesRenderFrameSnapshots)
286 {
287 using var image = ReplayRenderFrameSnapshot();
288 DrawCurrentSurface(destinationCanvas);
289 return true;
290 }
291
292 RenderStateSynchronization.EnterGpuRender();
293 try
294 {
295 var tick = HighResTimer.GetCurrentTick();
296
297 RenderToBackbuffer(tick);
298 Backbuffer.EndFrame();
299
300 try
301 {
302 DrawCurrentSurface(destinationCanvas);
303 return true;
304 }
305 finally
306 {
307 Backbuffer.BeginFrame();
308 }
309 }
310 finally
311 {
312 RenderStateSynchronization.ExitGpuRender();
313 }
314 }
315
327 public bool GlDrawCurrentFrameToCanvas(SKCanvas destinationCanvas)
328 {
329 ArgumentNullException.ThrowIfNull(destinationCanvas);
330
331 if (!Backbuffer.IsGlThreadRendered)
332 return false;
333
334 DrawCurrentSurface(destinationCanvas);
335 return true;
336 }
337
338 private void DrawCurrentSurface(SKCanvas canvas)
339 {
340 var presentation = RenderSurfaceAdapter?.Presentation ?? default;
341 canvas.Clear(Backbuffer.ClearColor);
342 if (presentation.Scale <= 0) return;
343 canvas.Save();
344 try
345 {
346 canvas.Translate(presentation.DestinationRect.Left, presentation.DestinationRect.Top);
347 canvas.Scale(presentation.Scale);
348 bool needsLinearSampling = Engine.Instance.Configuration.RenderScalingFilter == RenderScalingFilter.Linear &&
349 (presentation.Scale != 1f ||
350 presentation.DestinationRect.Left != MathF.Floor(presentation.DestinationRect.Left) ||
351 presentation.DestinationRect.Top != MathF.Floor(presentation.DestinationRect.Top));
352 if (needsLinearSampling)
353 {
354 // Skia's DrawSurface does not expose sampling and ignores SKPaint.FilterQuality.
355 // A scoped snapshot is a GPU texture view, not a readback or an intermediate copy.
356 using var image = Backbuffer.Snapshot();
357 canvas.DrawImage(image, 0, 0, RenderSurfaceAdapterBase.PresentationSampling);
358 }
359 else
360 {
361 canvas.DrawSurface(Backbuffer.Canvas.Surface, 0, 0);
362 }
363 }
364 finally { canvas.Restore(); }
365 }
366
385 public SKImage? GlSnapshotCurrentFrame()
386 {
387 if (!Backbuffer.IsGlThreadRendered)
388 return null;
389
390 return Backbuffer.Snapshot();
391 }
392
398 internal abstract void RenderToBackbuffer(long tick);
399
404 internal abstract void PresentBackbufferToAdapter();
405
420 public void Dispose()
421 {
422 lock (RenderStateSynchronization.SyncRoot)
423 Dispose(true);
424 GC.SuppressFinalize(this);
425 }
426
440 protected virtual void Dispose(bool disposing)
441 {
442 Telemetry?.Dispose();
443 if (disposing)
444 {
445 FrameMailbox.Dispose();
446 Effects.Dispose();
447 }
448
449 RenderSurfaceHostRegistry.Unregister(this);
450 }
451}
Bounded extension source. Define measurements once, capture a generation before work,...
Owns and advances display effects for one RenderSurfaceHost.
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
int Width
Gets the current Backbuffer width in a thread-safe manner.
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.
SKCanvas Canvas
Gets the SkiaSharp canvas used for drawing operations on this backbuffer.
Provides a fully GPU-rendered backbuffer implementation using SkiaSharp and OpenGL.
Represents the abstract base class for render surface adapters that present backbuffer output to a de...
PresentationTransform Presentation
Current immutable aspect-preserving mapping of Backbuffer ScreenPx to adapter pixels.
Represents a base class for hosting a render surface, providing functionality for managing rendering ...
RenderSurfaceHostBase()
Initializes a new instance of the RenderSurfaceHostBase class and registers it with the RenderSurface...
EffectsManager Effects
Gets the manager that owns presentation effects for this render surface.
Action< GpuRenderSynchronizationDiagnostics >? GpuRenderSynchronizationDiagnosticsCalculated
Occurs after a GPU render/snapshot call when a subscriber requests timing for the shared render-state...
virtual void Dispose(bool disposing)
Releases resources used by this RenderSurfaceHostBase instance and unregisters it from the RenderSurf...
Diagnostics.? TelemetrySource Telemetry
Neutral session-local measurements for this host; null when source capacity is exhausted or the share...
Scene Scene
Gets the source Scenes.Scene used for rendering operations.
RenderSurfaceAdapterBase? RenderSurfaceAdapter
Gets the platform-specific RenderSurfaceAdapterBase responsible for rendering the image from the Back...
float PresentationScale
Actual scale used to fit this Backbuffer into its current adapter.
ViewManager ViewManager
Gets the view manager that controls camera positions, viewports, and multi-view rendering for this re...
bool GlDrawCurrentFrameToCanvas(SKCanvas destinationCanvas)
Draws the existing GPU backbuffer surface directly to another GPU canvas without rendering a new scen...
void Dispose()
Releases all resources used by this RenderSurfaceHostBase instance.
SKImage? GlRenderAndSnapshot()
Renders the current scene frame on the active GL/WebGL thread and returns a snapshot of the GPU backb...
BackbufferBase Backbuffer
Gets the in-memory BackbufferBase associated with the current rendering context.
bool GlRenderToCanvas(SKCanvas destinationCanvas)
Renders the current scene frame and draws the GPU backbuffer surface directly to another GPU canvas....
SKImage? GlSnapshotCurrentFrame()
Returns a snapshot of the current GPU backbuffer without rendering a new scene frame.
record GpuRenderSynchronizationDiagnostics(double LockWaitMilliseconds, double LockHeldMilliseconds)
Captures time spent waiting for and holding the shared live render-state synchronization gate.
@ Linear
Bilinear presentation filtering.