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
RenderSurfaceHost.cs
1using System.Drawing;
10using Gondwana.Timers;
11using Microsoft.Extensions.Logging;
12using SkiaSharp;
13
14namespace Gondwana.Rendering;
15
30public sealed class RenderSurfaceHost<TBackbuffer> : RenderSurfaceHostBase
31 where TBackbuffer : BackbufferBase
32{
33 private TBackbuffer _backbuffer;
34 private bool _resolutionEstablished;
35 private int _logicalWidth, _logicalHeight;
36 private int _presentationInvalidated = 1;
37 private float _deferredRenderScale = float.NaN;
38 private Scene _scene = Scene.Empty;
39 private RecordingBackbuffer? _recordingBackbuffer;
40 private long _snapshotSequence;
41 private GpuRenderFrameDiagnostics? _recordingDiagnostics;
42
43 private readonly RenderSurfaceAdapterBase _renderSurfaceAdapter;
44 private readonly ViewManager _viewManager;
45
54 public event EventHandler<RenderSurfaceHostBindEventArgs>? BindToScene;
55
59 public event Action? RenderBackbufferBegin;
60
64 public event Action? RenderBackbufferEnd;
65
69 public event Action? RenderBackbufferNoOp;
70
74 public event Action<GpuRenderFrameDiagnostics>? GpuRenderFrameDiagnosticsCalculated;
75
112 public event Action<SKCanvas>? RenderBackbufferPostScene;
113
114 private RenderSurfaceHost() : base() => _viewManager = new ViewManager(this);
115
122 public RenderSurfaceHost(RenderSurfaceAdapterBase renderSurfaceAdapter) : this()
123 {
124 _renderSurfaceAdapter = renderSurfaceAdapter ?? throw new ArgumentNullException(nameof(renderSurfaceAdapter));
125
126 // Host layout changes presentation; only the initial valid layout establishes resolution.
127 RenderSurfaceAdapter.Resized += OnRenderSurfaceAdapterResized;
128
129 var w = RenderSurfaceAdapter.Width;
130 var h = RenderSurfaceAdapter.Height;
131
132 if (w <= 0 || h <= 0)
133 throw new InvalidOperationException("RenderSurfaceAdapter has non-positive dimensions.");
134
135 _resolutionEstablished = RenderSurfaceAdapter.InitialSizeAvailable;
136 var scale = Engine.Instance.Configuration.RenderScale;
137 _logicalWidth = _resolutionEstablished ? PresentationTransform.ScaleDimension(w, scale) : 1;
138 _logicalHeight = _resolutionEstablished ? PresentationTransform.ScaleDimension(h, scale) : 1;
139 _backbuffer = CreateBackbuffer(_logicalWidth, _logicalHeight);
140 Telemetry = Diagnostics.RenderTelemetry.Register(_backbuffer is GpuBackbuffer);
141 RenderSurfaceAdapter.SetBackbufferSize(_logicalWidth, _logicalHeight);
142 Backbuffer.BeginFrame();
143
144 Backbuffer.SizeChanged += OnBackbufferSizeChanged;
145 }
146
147 private static TBackbuffer CreateBackbuffer(int width, int height)
148 {
149 var typeName = typeof(TBackbuffer).FullName;
150 Console.WriteLine($"CreateBackbuffer called for type: {typeName}, dimensions: {width}x{height}");
151
152 // Use explicit type checking instead of reflection to avoid trimming issues in browser/WASM
153 if (typeof(TBackbuffer) == typeof(Backbuffers.BitmapBackbuffer))
154 {
155 Engine.Logger.LogInformation("Using explicit BitmapBackbuffer constructor for type {TypeName}", typeName);
156 return (TBackbuffer)(object)new Backbuffers.BitmapBackbuffer(width, height);
157 }
158
159 if (typeof(TBackbuffer) == typeof(Backbuffers.GpuBackbuffer))
160 {
161 Engine.Logger.LogInformation("Using explicit GpuBackbuffer constructor for type {TypeName}", typeName);
162 return (TBackbuffer)(object)new Backbuffers.GpuBackbuffer(width, height);
163 }
164
165 // Fallback to Activator for any other backbuffer types
166 Engine.Logger.LogInformation("Falling back to Activator.CreateInstance for type {TypeName}", typeName);
167 return (TBackbuffer)Activator.CreateInstance(typeof(TBackbuffer), width, height)!;
168 }
169
181 public override BackbufferBase Backbuffer => _backbuffer;
182
193 public override Scene Scene => _scene;
194
206 public override RenderSurfaceAdapterBase RenderSurfaceAdapter => _renderSurfaceAdapter;
207
219 public override ViewManager ViewManager => _viewManager;
220
234 public bool RedrawDirtyRectangleOnly { get; set; } = true;
235
259 public void Bind(Scene newScene, bool limitCameraToWorldBoundPx = true)
260 {
261 ArgumentNullException.ThrowIfNull(newScene, "Cannot bind to null Scene.");
262
263 if (ReferenceEquals(newScene, _scene))
264 return;
265
266 // Claim the new scene before releasing the old one. If another host owns it,
267 // Bind fails without disturbing this host's current scene.
268 newScene.BindRenderSurfaceHost(this);
269
270 var oldScene = _scene;
271
272 try
273 {
274 // Unregister from and release the old scene.
275 if (!ReferenceEquals(oldScene, Scene.Empty))
276 {
277 oldScene.SceneDisposing -= OnSourceDisposing;
278 oldScene.UnbindRenderSurfaceHost(this);
279 }
280
281 _scene = newScene;
282 ViewManager.BindToScene(_scene, limitCameraToWorldBoundPx);
283
284 _scene.SceneDisposing += OnSourceDisposing;
285 _scene.FullRefreshNeeded = true;
286 }
287 catch
288 {
289 // Release the failed new binding and restore the previous scene.
290 newScene.UnbindRenderSurfaceHost(this);
291 _scene = oldScene;
292
293 if (!ReferenceEquals(oldScene, Scene.Empty))
294 {
295 oldScene.BindRenderSurfaceHost(this);
296 oldScene.SceneDisposing += OnSourceDisposing;
297 }
298
299 throw;
300 }
301
302 BindToScene?.Invoke(this, new RenderSurfaceHostBindEventArgs(oldScene, _scene));
303 }
304
310 internal override void RenderToBackbuffer(long tick)
311 {
312 Backbuffer.BeginFrame();
313 RenderBackbufferBegin?.Invoke();
314
315 // For GL-thread-rendered backbuffers (GpuBackbuffer), the RefreshQueue mechanism is
316 // unreliable: AddWorldRect and ClearRefreshQueue both post to the engine thread, so
317 // enqueued rects are not present when CollectDirtyScreenArea runs, and posted clears
318 // can silently discard dirty rects that were added after the last GL frame.
319 // Instead, always re-render the entire surface each GL paint callback.
320 if (Backbuffer.IsGlThreadRendered)
321 RenderToBackbufferGpuFull(tick);
322 else
323 RenderToBackbufferBitmap(tick);
324
325 RenderBackbufferEnd?.Invoke();
326 }
327
330 internal override void ProduceRenderFrameSnapshot(long tick)
331 {
332 if (!UsesRenderFrameSnapshots || _disposed) return;
333 var slot = FrameMailbox.TryBeginBuild();
334 if (slot is null) return;
335 var recorder = _recordingBackbuffer ??= new RecordingBackbuffer();
336 bool transferred = false;
337 long telemetryGeneration = Telemetry?.BeginSample() ?? 0;
338 long started = GpuRenderFrameDiagnosticsCalculated is null && telemetryGeneration == 0 ? 0 : HighResTimer.GetCurrentTick();
339 _recordingDiagnostics = null;
340 try
341 {
342 recorder.Start(Backbuffer);
343 RenderBackbufferBegin?.Invoke();
344 RenderToBackbufferGpuFull(tick, recorder);
345 RenderBackbufferEnd?.Invoke();
346 var frame = new RenderFrameSnapshot(recorder.Complete(), ++_snapshotSequence,
347 HighResTimer.GetCurrentTick(), recorder.Width, recorder.Height);
348 transferred = true;
349 FrameMailbox.Publish(slot, frame);
350 if (telemetryGeneration != 0)
351 {
352 Telemetry!.Record(telemetryGeneration, "build.cpu.ms", HighResTimer.GetDuration(started, HighResTimer.GetCurrentTick()) * 1000d);
353 Telemetry.Record(telemetryGeneration, "atlas.batches", recorder.AtlasBatchCount);
354 Telemetry.Record(telemetryGeneration, "atlas.tiles", recorder.AtlasBatchedTileCount);
355 }
356 if (_recordingDiagnostics is { } diagnostics)
357 GpuRenderFrameDiagnosticsCalculated?.Invoke(diagnostics with
358 {
359 TotalRenderMilliseconds =
360 HighResTimer.GetDuration(started, HighResTimer.GetCurrentTick()) * 1000d,
361 AtlasBatchCount = recorder.AtlasBatchCount,
362 AtlasBatchedTileCount = recorder.AtlasBatchedTileCount
363 });
364 }
365 catch
366 {
367 recorder.Cancel();
368 if (!transferred) FrameMailbox.AbortBuild(slot);
369 throw;
370 }
371 }
372
405 private void RenderToBackbufferGpuFull(long tick, BackbufferBase? destination = null)
406 {
407 var target = destination ?? Backbuffer;
408 long telemetryGeneration = Telemetry?.BeginSample() ?? 0;
409 bool legacyDiagnostics = GpuRenderFrameDiagnosticsCalculated is not null;
410 bool collectDiagnostics = legacyDiagnostics || telemetryGeneration != 0;
411 long diagnosticsStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
412 long queryAndSortTicks = 0;
413 long queryTicks = 0;
414 long sortTicks = 0;
415 long drawTicks = 0;
416 long overlayTicks = 0;
417 int drawableCount = 0;
418 int tileCount = 0;
419 List<GpuLayerRenderDiagnostics>? layerDiagnostics = legacyDiagnostics
420 ? new List<GpuLayerRenderDiagnostics>(Scene.VisibleSceneLayers.Count)
421 : null;
422
423 // When there are no views at all, clear the whole surface and bail.
424 // If there ARE views but no scene layers, we fall through so view-mode
425 // DirectDrawings (e.g. a splash screen overlay) are still rendered.
426 if (ViewManager.Views.Count == 0)
427 {
428 target.ClearRect(new Rectangle(0, 0, target.Width, target.Height));
429 Scene.FullRefreshNeeded = false;
430
431 if (telemetryGeneration != 0 && destination is not RecordingBackbuffer)
432 {
433 long diagnosticsEndTick = HighResTimer.GetCurrentTick();
434 Telemetry!.Record(
435 telemetryGeneration,
436 "build.cpu.ms",
437 HighResTimer.GetDuration(diagnosticsStartTick, diagnosticsEndTick) * 1000d);
438 }
439
440 return;
441 }
442
443 // A single clear lets translucent, wiped, or translated views reveal the
444 // views beneath them. Each view paints its own background as part of its
445 // presentation group below.
446 target.ClearRect(new Rectangle(0, 0, target.Width, target.Height));
447
448 foreach (var view in ViewManager.Views)
449 {
450 RenderContext.Push(view, tick);
451
452 try
453 {
454 // 1) View overlays are drawn unconditionally by this full-frame path.
455 // They must not enqueue dirty regions that GL rendering never consumes.
456 var overlays = DirectDrawingManager.Instance.GetDrawingsForView(view);
457
458 var vp = view.GetRenderViewportTargetRectPx();
459
460 // 2) Clip to this view's viewport, excluding areas covered by higher Z-order views.
461 target.Canvas.Save();
462 target.Canvas.ResetMatrix();
463 target.Canvas.ClipRect(vp.ToSKRect(), SKClipOperation.Intersect, antialias: false);
464
465 foreach (var blocker in ViewManager.GetViewsAbove(view))
466 {
467 if (!blocker.BlocksViewsBelow)
468 continue;
469
470 var overlap = Rectangle.Intersect(vp, blocker.GetRenderViewportTargetRectPx());
471 if (!overlap.IsEmpty)
472 target.Canvas.ClipRect(overlap.ToSKRect(), SKClipOperation.Difference, antialias: false);
473 }
474
475 int viewPresentation = BeginPresentation(
476 view.GetPresentationBoundsPx(),
477 view.EffectOpacity,
478 view.EffectReveal,
479 view.EffectRevealDirection, target);
480
481 if (viewPresentation > 0)
482 {
483 // The view background belongs inside the group so it fades,
484 // wipes, and slides with the rest of the view.
485 target.ClearRect(view.GetPresentationBoundsPx().ToPixelAlignedRect());
486
487 // 4) Render every visible layer for the full viewport extent (layers are drawn
488 // back-to-front by ascending Z-order, which VisibleSceneLayers already provides).
489 var sceneLayers = Scene.VisibleSceneLayers;
490
491 for (int i = 0; i < sceneLayers.Count; i++)
492 {
493 var layer = sceneLayers[i];
494
495 int layerPresentation = BeginPresentation(
496 GetLayerPresentationBounds(view, layer),
497 layer.EffectOpacity,
498 layer.EffectReveal,
499 layer.EffectRevealDirection, target);
500
501 if (layerPresentation == 0)
502 continue;
503
504 // Compute the world-space rect visible through this viewport for this layer,
505 // expanded by one tile in each direction to cover boundary rounding.
506 var layerWorldRectF = view.ScreenRectToWorldRect(layer, vp);
507 layerWorldRectF.Inflate(layer.TileWidth, layer.TileHeight);
508 var layerWorldRect = layerWorldRectF.ToPixelAlignedRect();
509
510 long queryStartTick =
511 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
512 long layerSortTicks = 0;
513 int layerDrawableCount;
514 int layerTileCount;
515
516 var recording = target as RecordingBackbuffer;
517 RecordingBackbuffer.FixedGridRenderPlan fixedGridPlan = default;
518 bool streamed = recording is not null &&
519 recording.TryPrepareFixedGridLayer(
520 layer,
521 layerWorldRect,
522 out fixedGridPlan);
523
524 long queryEndTick;
525 long drawStartTick;
526 long drawEndTick;
527
528 if (streamed)
529 {
530 queryEndTick =
531 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
532
533 drawStartTick =
534 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
535 layerTileCount = recording!.DrawFixedGridLayer(
536 view,
537 fixedGridPlan,
538 vp);
539 layerDrawableCount = layerTileCount;
540 drawEndTick =
541 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
542 }
543 else
544 {
545 List<IDrawable> drawables;
546 if (collectDiagnostics)
547 {
548 drawables = layer.GetDrawablesInWorldRectWithDiagnostics(
549 layerWorldRect,
550 out layerSortTicks);
551 }
552 else
553 {
554 drawables = layer.GetDrawablesInWorldRect(layerWorldRect);
555 }
556
557 queryEndTick =
558 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
559
560 layerDrawableCount = drawables.Count;
561 layerTileCount = 0;
562 if (collectDiagnostics)
563 {
564 for (int d = 0; d < drawables.Count; d++)
565 {
566 IDrawable drawable = drawables[d] is WrappedDrawable wrapped
567 ? wrapped.Owner
568 : drawables[d];
569
570 if (drawable is Tile)
571 layerTileCount++;
572 }
573 }
574
575 drawStartTick =
576 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
577 target.DrawDrawables(view, drawables, vp);
578 drawEndTick =
579 collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
580 }
581
582 if (collectDiagnostics)
583 {
584 long layerQueryAndSortTicks = queryEndTick - queryStartTick;
585 long layerQueryTicks = Math.Max(
586 0L,
587 layerQueryAndSortTicks - layerSortTicks);
588 long layerDrawTicks = drawEndTick - drawStartTick;
589 queryAndSortTicks += layerQueryAndSortTicks;
590 queryTicks += layerQueryTicks;
591 sortTicks += layerSortTicks;
592 drawTicks += layerDrawTicks;
593 drawableCount += layerDrawableCount;
594 tileCount += layerTileCount;
595
596 if (telemetryGeneration != 0 && i < Diagnostics.RenderTelemetry.Layers.Length)
597 {
598 var keys = Diagnostics.RenderTelemetry.Layers[i];
599 Telemetry!.Record(telemetryGeneration, keys[0], HighResTimer.GetDuration(0, layerQueryTicks) * 1000d);
600 Telemetry.Record(telemetryGeneration, keys[1], HighResTimer.GetDuration(0, layerDrawTicks) * 1000d);
601 Telemetry.Record(telemetryGeneration, keys[2], layerDrawableCount);
602 Telemetry.Record(telemetryGeneration, keys[3], layerTileCount);
603 Telemetry.Record(telemetryGeneration, keys[4], layer.TransformedTiles.Count);
604 Telemetry.Record(telemetryGeneration, keys[5], layer.TileWidth);
605 Telemetry.Record(telemetryGeneration, keys[6], layer.TileHeight);
606 Telemetry.Record(telemetryGeneration, keys[7], layer.ZOrder);
607 }
608
609 layerDiagnostics?.Add(new GpuLayerRenderDiagnostics(
610 i,
611 layer.ID,
612 layer.ZOrder,
613 layerDrawableCount,
614 layerTileCount,
615 layer.TransformedTiles.Count,
616 layer.TileWidth,
617 layer.TileHeight,
618 HighResTimer.GetDuration(0, layerQueryAndSortTicks) * 1000d,
619 HighResTimer.GetDuration(0, layerDrawTicks) * 1000d));
620 }
621
622 EndPresentation(layerPresentation, target);
623 }
624
625 // 5) Render view-based DirectDrawings on top.
626 long overlayStartTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
627 for (int i = 0; i < overlays.Count; i++)
628 overlays[i].Draw(target, overlays[i].GetDrawLocationScreen(view));
629 long overlayEndTick = collectDiagnostics ? HighResTimer.GetCurrentTick() : 0;
630
631 if (collectDiagnostics)
632 overlayTicks += overlayEndTick - overlayStartTick;
633
634 EndPresentation(viewPresentation, target);
635 }
636
637 target.Canvas.Restore();
638 }
639 finally
640 {
641 RenderContext.Pop();
642 }
643 }
644
645 Scene.FullRefreshNeeded = false;
646
647 // Desktop snapshots record these hooks on the Engine thread; WebGL invokes
648 // them synchronously against its GPU canvas.
649 InvokePostSceneCanvasHooks(target);
650
651 if (collectDiagnostics)
652 {
653 long diagnosticsEndTick = HighResTimer.GetCurrentTick();
654 if (telemetryGeneration != 0)
655 {
656 if (destination is not RecordingBackbuffer)
657 Telemetry!.Record(telemetryGeneration, "build.cpu.ms", HighResTimer.GetDuration(diagnosticsStartTick, diagnosticsEndTick) * 1000d);
658 Telemetry!.Record(telemetryGeneration, "query.cpu.ms", HighResTimer.GetDuration(0, queryTicks) * 1000d);
659 Telemetry.Record(telemetryGeneration, "sort.cpu.ms", HighResTimer.GetDuration(0, sortTicks) * 1000d);
660 Telemetry.Record(telemetryGeneration, "record.cpu.ms", HighResTimer.GetDuration(0, drawTicks) * 1000d);
661 Telemetry.Record(telemetryGeneration, "overlay.cpu.ms", HighResTimer.GetDuration(0, overlayTicks) * 1000d);
662 Telemetry.Record(telemetryGeneration, "visible.drawables", drawableCount);
663 Telemetry.Record(telemetryGeneration, "visible.tiles", tileCount);
664 Telemetry.Record(telemetryGeneration, "layers.omitted", Math.Max(0, Scene.VisibleSceneLayers.Count - Diagnostics.RenderTelemetry.Layers.Length));
665 if (Scene.VisibleSceneLayers.Count > Diagnostics.RenderTelemetry.Layers.Length)
666 Telemetry.MarkTruncated(telemetryGeneration);
667 }
668 if (!legacyDiagnostics) return;
669 var diagnostics = new GpuRenderFrameDiagnostics(
670 HighResTimer.GetDuration(diagnosticsStartTick, diagnosticsEndTick) * 1000d,
671 HighResTimer.GetDuration(0, queryAndSortTicks) * 1000d,
672 HighResTimer.GetDuration(0, drawTicks) * 1000d,
673 HighResTimer.GetDuration(0, overlayTicks) * 1000d,
674 drawableCount,
675 tileCount,
676 layerDiagnostics!)
677 {
678 QueryMilliseconds = HighResTimer.GetDuration(0, queryTicks) * 1000d,
679 SortMilliseconds = HighResTimer.GetDuration(0, sortTicks) * 1000d
680 };
681 if (destination is RecordingBackbuffer) _recordingDiagnostics = diagnostics;
682 else GpuRenderFrameDiagnosticsCalculated?.Invoke(diagnostics);
683 }
684 }
685
721 private void RenderToBackbufferBitmap(long tick)
722 {
723 if (ViewManager.Views.Count == 0)
724 {
725 Backbuffer.ClearRect(new Rectangle(0, 0, Backbuffer.Width, Backbuffer.Height));
726 Scene.FullRefreshNeeded = false;
727 return;
728 }
729
730 bool fullViewComposition = ViewManager.Views.Any(view => view.HasPresentationEffect)
731 || Scene.VisibleSceneLayers.Any(layer => layer.WrapHorizontally || layer.WrapVertically);
732
733 // 0) If there are no visible SceneLayers, just clear and publish the full frame.
734 if (Scene.CountOfVisibleLayers == 0)
735 {
736 Backbuffer.ClearRect(new Rectangle(0, 0, Backbuffer.Width, Backbuffer.Height));
737 Scene.FullRefreshNeeded = false;
738 }
739 else
740 {
741 // 1) Handle full scene refresh once (camera moved, zoom changed, etc.): clear and mark all layers as dirty.
742 // This already clears the whole backbuffer and enqueues a full rect per layer.
743 if (Scene.FullRefreshNeeded || (fullViewComposition && Scene.IsDirty))
744 {
745 // this will mark the Scene.IsDirty flag as true
746 EnqueueFullSceneRefresh();
747 }
748 else
749 {
750 // scene is not dirty, this frame is done...
751 if (!Scene.IsDirty)
752 {
753 RenderBackbufferNoOp?.Invoke();
754 return;
755 }
756 }
757 }
758
759 if (fullViewComposition)
760 Backbuffer.ClearRect(new Rectangle(0, 0, Backbuffer.Width, Backbuffer.Height));
761
762 // 2) Render all views to Backbuffer. Draw layers back -> front (ascending Z).
763 foreach (var view in ViewManager.Views)
764 {
765 RenderContext.Push(view, tick);
766
767 try
768 {
769 // 2.1) Force-refresh all DirectDrawings that overlay this view
770 var overlays = DirectDrawingManager.Instance.GetDrawingsForView(view);
771 foreach (var overlay in overlays)
772 {
773 overlay.ForceRefresh();
774 }
775
776 // 2.2) identify dirty SCREEN areas across all layers for this view
777 var dirtyScreenRects = CollectDirtyScreenArea(view);
778
779 // 2.3) Clip to this view's viewport
780 var vp = view.GetRenderViewportTargetRectPx();
781
782 // clip inclusive to current viewport
783 Backbuffer.Canvas.Save();
784 Backbuffer.Canvas.ResetMatrix();
785 Backbuffer.Canvas.ClipRect(vp.ToSKRect(), SKClipOperation.Intersect, antialias: false);
786
787 // clip exclusive to higher Z-order views
788 foreach (var blocker in ViewManager.GetViewsAbove(view))
789 {
790 if (!blocker.BlocksViewsBelow)
791 continue;
792
793 var overlap = Rectangle.Intersect(vp, blocker.GetRenderViewportTargetRectPx());
794 if (!overlap.IsEmpty)
795 Backbuffer.Canvas.ClipRect(overlap.ToSKRect(), SKClipOperation.Difference, antialias: false);
796 }
797
798 int viewPresentation = BeginPresentation(
799 view.GetPresentationBoundsPx(),
800 view.EffectOpacity,
801 view.EffectReveal,
802 view.EffectRevealDirection);
803
804 if (viewPresentation > 0)
805 {
806 if (fullViewComposition)
807 {
808 Backbuffer.ClearRect(view.GetPresentationBoundsPx().ToPixelAlignedRect());
809 }
810 else
811 {
812 // 2.4) Pre-clear dirty areas on backbuffer to Backbuffer.ClearColor
813 PreclearScreenAreas(view, dirtyScreenRects);
814 }
815
816 // 2.5) Render each visible layer's dirty regions for this view
817 // this will draw SceneLayerTiles, Sprites, and SceneLayer-based DirectDrawings
818 var sceneLayers = Scene.VisibleSceneLayers;
819
820 for (int i = 0; i < sceneLayers.Count; i++)
821 {
822 var layer = sceneLayers[i];
823 int layerPresentation = BeginPresentation(
824 GetLayerPresentationBounds(view, layer),
825 layer.EffectOpacity,
826 layer.EffectReveal,
827 layer.EffectRevealDirection);
828
829 if (layerPresentation == 0)
830 continue;
831
832 RenderLayerDirtyRegions(view, layer);
833 EndPresentation(layerPresentation);
834 }
835
836 // 2.6) draw all View-based DirectDrawings for this view
837 for (int i = 0; i < overlays.Count; i++)
838 overlays[i].Draw(Backbuffer, overlays[i].GetDrawLocationScreen(view));
839
840 EndPresentation(viewPresentation);
841 }
842
843 // 2.7) Restore from viewport clip
844 Backbuffer.Canvas.Restore();
845 }
846 finally
847 {
848 RenderContext.Pop();
849 }
850 }
851
852 // 3) Clear layer queues now that we've consumed them (avoids re-drawing same tiles next frame)
853 for (int i = 0; i < Scene.CountOfVisibleLayers; i++)
854 Scene.VisibleSceneLayers[i].RefreshQueue.ClearRefreshQueue();
855
856 Scene.FullRefreshNeeded = false;
857
858 // Notify subscribers that all scene content has been drawn and the canvas is ready for
859 // post-scene effects. Fires before RenderBackbufferEnd so subscribers still have a
860 // chance to draw into the canvas before it is presented to the adapter.
861 InvokePostSceneCanvasHooks();
862 }
863
864 #region DrawRefreshQueueToBackbuffer helpers
865
866 private void EnqueueFullSceneRefresh()
867 {
868 foreach (var view in ViewManager.Views)
869 {
870 var viewport = view.Viewport;
871 var screenRect = viewport.TargetRectPx;
872
873 for (int i = 0; i < Scene.CountOfVisibleLayers; i++)
874 {
875 var layer = Scene.VisibleSceneLayers[i];
876
877 // 1) Compute layer-specific visible world rect
878 var layerWorldRect = view.ScreenRectToWorldRect(layer, screenRect);
879
880 // 2) Expand world rect by **one full tile** in all directions.
881 // This compensates for fractional-layer motion (parallax),
882 // camera motion, and tile boundaries.
883 int expandX = layer.TileWidth;
884 int expandY = layer.TileHeight;
885
886 layerWorldRect.Inflate(expandX, expandY);
887
888 // 3) Round to ints and enqueue
889 var worldRectInt = layerWorldRect.ToPixelAlignedRect();
890
891 layer.RefreshQueue.AddWorldRect(worldRectInt);
892 }
893 }
894 }
895
896 private List<Rectangle> CollectDirtyScreenArea(View view)
897 {
898 var dirty = new List<Rectangle>(64);
899 var viewportRect = view.GetRenderViewportTargetRectPx();
900
901 foreach (var sceneLayer in Scene.VisibleSceneLayers)
902 {
903 var refreshQueue = sceneLayer.RefreshQueue;
904 if (!refreshQueue.IsDirty)
905 continue;
906
907 foreach (var worldRect in refreshQueue.SnapshotWorldRects())
908 {
909 var screenRectF = view.WorldRectToScreenRect(sceneLayer, worldRect);
910 var rect = Rectangle.Intersect(
911 screenRectF.ToPixelAlignedRect(),
912 viewportRect);
913
914 if (rect.IsEmpty)
915 continue;
916
917 dirty.AddDeduped(rect);
918 }
919 }
920
921 return dirty;
922 }
923
924 private void PreclearScreenAreas(View view, List<Rectangle> screenRects)
925 {
926 if (Backbuffer is null || screenRects is null || screenRects.Count == 0)
927 return;
928
929 foreach (var screenRect in screenRects)
930 {
931 var screenRectViewport = Rectangle.Intersect(screenRect, view.GetRenderViewportTargetRectPx());
932
933 if (screenRectViewport.IsEmpty || screenRectViewport.Width <= 0 || screenRectViewport.Height <= 0)
934 continue;
935
936 // Clear just this patch (overwrite with Backbuffer.ClearColor)
937 Backbuffer.ClearRect(screenRectViewport);
938
939 EnqueueForOverlappingSceneLayers(view, screenRectViewport);
940 }
941 }
942
943 private void EnqueueForOverlappingSceneLayers(View sourceView, Rectangle clearedScreenRect)
944 {
945 var overlap = Rectangle.Intersect(clearedScreenRect, sourceView.GetRenderViewportTargetRectPx());
946
947 if (overlap.IsEmpty)
948 return;
949
950 foreach (var sceneLayer in Scene.VisibleSceneLayers)
951 {
952 var worldRectF = sourceView.ScreenRectToWorldRect(sceneLayer, overlap);
953 var worldRect = worldRectF.ToPixelAlignedRect();
954
955 if (!worldRect.IsEmpty)
956 sceneLayer.RefreshQueue.AddWorldRect(worldRect);
957 }
958 }
959
960 private void RenderLayerDirtyRegions(View view, SceneLayer layer)
961 {
962 var refreshQueue = layer.RefreshQueue;
963
964 // if this layer has no dirty regions and we are not forcing a full redraw, skip it.
965 if (!refreshQueue.IsDirty)
966 return;
967
968 foreach (var worldRect in refreshQueue.SnapshotWorldRects())
969 {
970 // draw tiles/sprites/direct drawings in this world rect
971 var drawables = layer.GetDrawablesInWorldRect(worldRect);
972
973 // project world → screen for adapter dirty
974 var screenRect = view.WorldRectToScreenRect(layer, worldRect).ToPixelAlignedRect();
975
976 Backbuffer.DrawDrawables(view, drawables, screenRect);
977 }
978 }
979
980 private int BeginPresentation(
981 RectangleF bounds,
982 float opacity,
983 float reveal,
984 EffectDirection direction,
985 BackbufferBase? destination = null)
986 {
987 var target = destination ?? Backbuffer;
988 opacity = Math.Clamp(opacity, 0f, 1f);
989 reveal = Math.Clamp(reveal, 0f, 1f);
990
991 if (opacity <= 0.0001f || reveal <= 0.0001f || bounds.IsEmpty)
992 return 0;
993
994 int saveCount = 1;
995 target.Canvas.Save();
996
997 if (reveal < 0.9999f)
998 {
999 RectangleF revealRect = EffectGeometry.GetRevealRect(bounds, direction, reveal);
1000 target.Canvas.ClipRect(
1001 revealRect.ToSKRect(),
1002 SKClipOperation.Intersect,
1003 antialias: false);
1004 }
1005
1006 if (opacity < 0.9999f)
1007 {
1008 using var paint = new SKPaint
1009 {
1010 Color = new SKColor(255, 255, 255, (byte)Math.Round(opacity * 255f))
1011 };
1012
1013 target.Canvas.SaveLayer(bounds.ToSKRect(), paint);
1014 saveCount++;
1015 }
1016
1017 return saveCount;
1018 }
1019
1020 private void EndPresentation(int saveCount, BackbufferBase? destination = null)
1021 {
1022 var target = destination ?? Backbuffer;
1023 while (saveCount-- > 0)
1024 target.Canvas.Restore();
1025 }
1026
1027 private static RectangleF GetLayerPresentationBounds(View view, SceneLayer layer)
1028 {
1029 RectangleF viewport = view.GetRenderViewportTargetRectPx();
1030 PointF offset = view.GetEffectOffsetPx(layer);
1031
1032 return new RectangleF(
1033 viewport.Left + offset.X,
1034 viewport.Top + offset.Y,
1035 viewport.Width,
1036 viewport.Height);
1037 }
1038
1039 #endregion DrawRefreshQueueToBackbuffer helpers
1040
1049 internal override void PresentBackbufferToAdapter()
1050 {
1051 if (RenderSurfaceAdapter is null)
1052 return;
1053
1054 Backbuffer.EndFrame();
1055
1056 if (RedrawDirtyRectangleOnly && Interlocked.Exchange(ref _presentationInvalidated, 0) == 0)
1057 PresentBackbufferRect();
1058 else
1059 PresentBackbufferAll();
1060
1061 Backbuffer.ClearDirtyRectangle();
1062 Backbuffer.BeginFrame();
1063 }
1064
1065 #region IDisposable
1066
1067 private bool _disposed;
1068
1070 protected override void Dispose(bool disposing)
1071 {
1072 if (_disposed)
1073 return;
1074
1075 if (disposing)
1076 {
1077 if (_scene != null)
1078 {
1079 _scene.SceneDisposing -= OnSourceDisposing;
1080 _scene.UnbindRenderSurfaceHost(this);
1081 _scene = Scene.Empty;
1082 }
1083
1084 RenderSurfaceAdapter.Resized -= OnRenderSurfaceAdapterResized;
1085 Backbuffer.SizeChanged -= OnBackbufferSizeChanged;
1086 _recordingBackbuffer?.Dispose();
1087 // The adapter owns GPU teardown on its context thread; keep the reference
1088 // valid for an already acquired replay until that callback returns.
1089 }
1090
1091 _disposed = true;
1092
1093 base.Dispose(disposing);
1094 }
1095
1096 #endregion IDisposable
1097
1098 #region private methods
1099
1100 private void OnSourceDisposing(Scene scene)
1101 {
1102 scene.SceneDisposing -= OnSourceDisposing;
1103 scene.UnbindRenderSurfaceHost(this);
1104 _scene = Scene.Empty;
1105 }
1106
1107 private void OnRenderSurfaceAdapterResized(RenderSurfaceAdapterResizedEventArgs args)
1108 {
1109 Interlocked.Exchange(ref _presentationInvalidated, 1);
1110 if (float.IsFinite(_deferredRenderScale) && args.NewWidth > 0 && args.NewHeight > 0)
1111 RequestRenderScale(_deferredRenderScale);
1112 if (!_resolutionEstablished && args.NewWidth > 0 && args.NewHeight > 0)
1113 {
1114 _resolutionEstablished = true;
1115 RequestRenderScale(Engine.Instance.Configuration.RenderScale);
1116 }
1117 }
1118
1119 internal override void InvalidatePresentation() => Interlocked.Exchange(ref _presentationInvalidated, 1);
1120
1121 internal override void RequestRenderScale(float scale)
1122 {
1123 if (!_resolutionEstablished || _disposed) return;
1124 var adapter = RenderSurfaceAdapter;
1125 // Zero-size requests are deferred until a valid presentation size exists.
1126 if (adapter.Width <= 0 || adapter.Height <= 0)
1127 {
1128 _deferredRenderScale = scale;
1129 return;
1130 }
1131 _deferredRenderScale = float.NaN;
1132 Backbuffer.RequestResize(PresentationTransform.ScaleDimension(adapter.Width, scale),
1133 PresentationTransform.ScaleDimension(adapter.Height, scale));
1134 }
1135
1136 private void OnBackbufferSizeChanged(int width, int height)
1137 {
1138 float scaleX = (float)width / _logicalWidth;
1139 float scaleY = (float)height / _logicalHeight;
1140 _logicalWidth = width;
1141 _logicalHeight = height;
1142 RenderSurfaceAdapter.SetBackbufferSize(width, height);
1143 foreach (var view in ViewManager.Views)
1144 {
1145 var old = view.Viewport.TargetRectPx;
1146 view.Viewport.TargetRectPx = Rectangle.FromLTRB(
1147 (int)Math.Round(old.Left * scaleX), (int)Math.Round(old.Top * scaleY),
1148 (int)Math.Round(old.Right * scaleX), (int)Math.Round(old.Bottom * scaleY));
1149 }
1150 Scene.FullRefreshNeeded = true;
1151 Interlocked.Exchange(ref _presentationInvalidated, 1);
1152 }
1153
1154 private void PresentBackbufferAll()
1155 {
1156 if (Backbuffer == null)
1157 return;
1158
1159 var img = Backbuffer.Snapshot();
1160 var src = new SKRectI(0, 0, img.Width, img.Height);
1161 var dst = RenderSurfaceAdapter.Presentation.DestinationRect;
1162
1163 // Post to UI thread
1164 Engine.Instance.UiDispatcher!.Post(() => RenderSurfaceAdapter.Present(img, src, dst));
1165 }
1166
1167 private void PresentBackbufferRect()
1168 {
1169 if (Backbuffer == null)
1170 return;
1171
1172 var dirty = Backbuffer.DirtyRectangle;
1173 if (dirty.IsEmpty)
1174 return;
1175
1176 var bounds = new Rectangle(0, 0, Backbuffer.Width, Backbuffer.Height);
1177 var clamped = Rectangle.Intersect(dirty, bounds);
1178 if (clamped.IsEmpty)
1179 return;
1180
1181 var img = Backbuffer.Snapshot();
1182
1183 // Post to UI thread
1184 Engine.Instance.UiDispatcher!.Post(() =>
1185 RenderSurfaceAdapter!.Present(img, clamped.ToSKRectI(),
1186 PresentationTransform.Fit(img.Width, img.Height, RenderSurfaceAdapter.Width, RenderSurfaceAdapter.Height)
1187 .ScreenRectToAdapterRect(clamped).ToSKRect()));
1188 }
1189
1190 private void InvokePostSceneCanvasHooks(BackbufferBase? destination = null)
1191 {
1192 var target = destination ?? Backbuffer;
1193 var hasSurfaceHandlers = RenderBackbufferPostScene is not null;
1194 var hasPlugins = EnginePluginRegistry.All.Count > 0;
1195
1196 if (!hasSurfaceHandlers && !hasPlugins)
1197 return;
1198
1199 if (!target.IsGlThreadRendered)
1200 target.AddToBackbufferDirtyRectangle(new Rectangle(0, 0, target.Width, target.Height));
1201
1202 if (hasSurfaceHandlers)
1203 RenderBackbufferPostScene?.Invoke(target.Canvas);
1204
1205 if (hasPlugins)
1206 EnginePluginRegistry.InvokePostRenderCanvas(Engine.Instance, this, target.Canvas);
1207 }
1208
1209 #endregion private methods
1210}
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
static ILogger< Engine > Logger
Gets the logger instance used by the engine for diagnostic and informational messages.
Definition Engine.cs:59
EngineConfiguration Configuration
Gets the current engine configuration settings.
Definition Engine.cs:928
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
Provides a bitmap-based backbuffer implementation for rendering operations using SkiaSharp....
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...
int Width
Gets the current width of the render surface in pixels.
void Present(SKImage bufferImage, SKRectI bufferRect, SKRect destRect)
Presents the specified portion of the Backbuffer image to the destination rectangle on the RenderSurf...
PresentationTransform Presentation
Current immutable aspect-preserving mapping of Backbuffer ScreenPx to adapter pixels.
int Height
Gets the current height of the render surface in pixels.
Provides data for the RenderSurfaceAdapterBase.Resized event.
int NewWidth
Gets the new width of the render surface adapter after the resize.
int NewHeight
Gets the new height of the render surface adapter after the resize.
Action< SKCanvas >? RenderBackbufferPostScene
Occurs after all scene content (layers, sprites, and direct drawings) has been drawn to the backbuffe...
override ViewManager ViewManager
Gets the view manager that controls camera positions, viewports, and multi-view rendering.
override Scene Scene
Gets the scene currently bound to this render surface host.
Action? RenderBackbufferEnd
Occurs at the end of the backbuffer rendering process.
RenderSurfaceHost() RenderSurfaceHost(RenderSurfaceAdapterBase renderSurfaceAdapter)
Initializes a new instance of the RenderSurfaceHost<TBackbuffer> class with the specified render surf...
override void Dispose(bool disposing)
Releases resources used by this RenderSurfaceHostBase instance and unregisters it from the RenderSurf...
override RenderSurfaceAdapterBase RenderSurfaceAdapter
Gets the platform-specific adapter that provides the underlying rendering surface.
Action< GpuRenderFrameDiagnostics >? GpuRenderFrameDiagnosticsCalculated
Occurs after a full-frame GPU render when at least one subscriber has requested diagnostics.
Action? RenderBackbufferNoOp
Occurs when a backbuffer render operation is skipped because the scene is not dirty.
void Bind(Scene newScene, bool limitCameraToWorldBoundPx=true)
Binds a scene to this render surface host, replacing any previously bound scene.
Action? RenderBackbufferBegin
Occurs at the beginning of the backbuffer rendering process.
override BackbufferBase Backbuffer
Gets the backbuffer used for off-screen rendering before presentation to the adapter.
bool RedrawDirtyRectangleOnly
Gets or sets a value indicating whether only the dirty (changed) regions of the backbuffer are presen...
EventHandler< RenderSurfaceHostBindEventArgs >? BindToScene
Occurs when a scene is bound to or unbound from this render surface host.
RenderSurfaceHostBase()
Initializes a new instance of the RenderSurfaceHostBase class and registers it with the RenderSurface...
Diagnostics.? TelemetrySource Telemetry
Neutral session-local measurements for this host; null when source capacity is exhausted or the share...
Provides data for the RenderSurfaceHost<TBackbuffer>.BindToScene event.
IReadOnlyList< View > GetViewsAbove(View view)
Returns all views that are rendered in front of (above) the specified view, based on Z-order....
ReadOnlyCollection< View > Views
Gets a read-only collection of all views currently managed by this view manager. Views are sorted by ...
RectangleF WorldRectToScreenRect(SceneLayer layer, RectangleF worldRect)
Converts a world-space pixel rectangle into a screen-space rectangle for this View,...
Definition View.cs:324
RectangleF ScreenRectToWorldRect(SceneLayer layer, RectangleF screenRect)
Converts a screen-space rectangle (on the logical Backbuffer) into a world-space rectangle for the gi...
Definition View.cs:371
Represents a game scene containing multiple layers of tiles, sprites, and game objects....
Definition Scene.cs:34
static Scene Empty
Gets the singleton empty scene instance.
Definition Scene.cs:948
@ Rectangle
Particles are spawned uniformly within a rectangular area. This is the default distribution.
@ View
Performs direct drawing at the view level using screen-space coordinates.
@ SceneLayer
Performs direct drawing within a scene layer using world-space coordinates.
EffectDirection
Specifies the direction in which a directional display effect travels.
record GpuLayerRenderDiagnostics(int LayerIndex, string LayerId, int ZOrder, int DrawableCount, int TileCount, int TransformedTileCount, int TileWidth, int TileHeight, double QueryAndSortMilliseconds, double DrawMilliseconds)
Captures the measured work performed for one scene layer during a full-frame GPU render.
readonly record struct PresentationTransform(float Scale, SKRect DestinationRect)
Aspect-preserving mapping between logical Backbuffer ScreenPx and adapter pixels.
record GpuRenderFrameDiagnostics(double TotalRenderMilliseconds, double QueryAndSortMilliseconds, double DrawMilliseconds, double OverlayMilliseconds, int DrawableCount, int TileCount, IReadOnlyList< GpuLayerRenderDiagnostics > Layers)
Captures measured scene-render work for one full-frame GPU render.
@ Scene
Displays scene layer, grid-cell, and active tile-animation counts.
@ Backbuffer
Displays logical backbuffer dimensions.