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
RecordingBackbuffer.cs
1using System.Drawing;
10using SkiaSharp;
11
13
18internal sealed class RecordingBackbuffer : BackbufferBase
19{
20 private const int MaxAtlasBatchSize = 256;
21 private const int MaxSnappedVertexBatchSize = 4096;
22 private static readonly int[] PartialBatchSizes = [128, 64, 32, 16, 8, 4, 2, 1];
23 private static readonly int[] PartialSnappedVertexBatchSizes =
24 [2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1];
25
26 private sealed class AtlasBatchBuffer(int size)
27 {
28 internal SKRect[] Sprites { get; } = new SKRect[size];
29 internal SKRotationScaleMatrix[] Transforms { get; } = new SKRotationScaleMatrix[size];
30 }
31
32 private sealed class SnappedVertexBatchBuffer(int tileCount)
33 {
34 internal SKPoint[] Positions { get; } = new SKPoint[tileCount * 4];
35 internal SKPoint[] TextureCoordinates { get; } = new SKPoint[tileCount * 4];
36 internal ushort[] Indices { get; } = CreateQuadIndices(tileCount);
37 }
38
39 private readonly record struct FrameAtlasKey(
40 Tilesheet Tilesheet,
41 string RegionName,
42 int XTile,
43 int YTile);
44
45 private readonly record struct FrameAtlasInfo(
46 SKImage Atlas,
47 SKRect Source,
48 Spacing Overhang);
49
50 internal readonly record struct FixedGridRenderPlan(
51 SceneLayer Layer,
52 Rectangle WorldRect);
53
54 private readonly record struct LayerScreenMap(
55 float OffsetX,
56 float OffsetY,
57 float ScaleX,
58 float ScaleY)
59 {
60 internal static LayerScreenMap Create(View view, SceneLayer layer)
61 {
62 RectangleF unit = view.WorldRectToScreenRect(
63 layer,
64 new RectangleF(0f, 0f, 1f, 1f));
65
66 return new(unit.Left, unit.Top, unit.Width, unit.Height);
67 }
68
69 internal RectangleF Map(Rectangle world) =>
70 new(
71 OffsetX + world.Left * ScaleX,
72 OffsetY + world.Top * ScaleY,
73 world.Width * ScaleX,
74 world.Height * ScaleY);
75
76 internal RectangleF MapPixelSnapped(Rectangle world)
77 {
78 float left = Snap(OffsetX + world.Left * ScaleX);
79 float top = Snap(OffsetY + world.Top * ScaleY);
80 float right = Snap(OffsetX + world.Right * ScaleX);
81 float bottom = Snap(OffsetY + world.Bottom * ScaleY);
82
83 return RectangleF.FromLTRB(left, top, right, bottom);
84 }
85
86 private static float Snap(float value) =>
87 MathF.Round(value, MidpointRounding.AwayFromZero);
88
89 internal SKMatrix CreateWorldToScreenMatrix() =>
90 SKMatrix.CreateScaleTranslation(
91 ScaleX,
92 ScaleY,
93 OffsetX,
94 OffsetY);
95 }
96
97 private readonly SKPictureRecorder _recorder = new();
98 private readonly SKRect[] _pendingSprites = new SKRect[MaxAtlasBatchSize];
99 private readonly SKRotationScaleMatrix[] _pendingTransforms =
100 new SKRotationScaleMatrix[MaxAtlasBatchSize];
101 private readonly SKPoint[] _pendingSnappedPositions =
102 new SKPoint[MaxSnappedVertexBatchSize * 4];
103 private readonly SKPoint[] _pendingSnappedTextureCoordinates =
104 new SKPoint[MaxSnappedVertexBatchSize * 4];
105 private readonly ushort[] _fullSnappedVertexIndices =
106 CreateQuadIndices(MaxSnappedVertexBatchSize);
107 private readonly Dictionary<FrameAtlasKey, FrameAtlasInfo> _frameAtlasCache = [];
108 private readonly Dictionary<SKImage, SKShader> _atlasShaderCache = [];
109 private readonly Dictionary<int, AtlasBatchBuffer> _partialBatchBuffers = new()
110 {
111 [1] = new(1),
112 [2] = new(2),
113 [4] = new(4),
114 [8] = new(8),
115 [16] = new(16),
116 [32] = new(32),
117 [64] = new(64),
118 [128] = new(128)
119 };
120 private readonly Dictionary<int, SnappedVertexBatchBuffer> _partialSnappedVertexBuffers =
121 PartialSnappedVertexBatchSizes.ToDictionary(
122 size => size,
123 size => new SnappedVertexBatchBuffer(size));
124 private readonly SKPaint _snappedVertexPaint = new()
125 {
126 IsAntialias = false,
127 BlendMode = SKBlendMode.SrcOver
128 };
129
130 private SKCanvas? _canvas;
131 private SKImage? _pendingAtlas;
132 private SKImage? _pendingSnappedVertexAtlas;
133 private int _pendingCount;
134 private int _pendingSnappedVertexCount;
135
136 internal int AtlasBatchCount { get; private set; }
137 internal int AtlasBatchedTileCount { get; private set; }
138
139 internal RecordingBackbuffer() : base(1, 1) { }
140
142 public override SKCanvas Canvas => _canvas ?? throw new InvalidOperationException("No recording is active.");
144 public override bool IsGlThreadRendered => true;
145
146 internal void Start(BackbufferBase source)
147 {
148 ResetPendingBatch();
149 ResetSnappedVertexBatch();
150 DisposeAtlasShaders();
151 _frameAtlasCache.Clear();
152 AtlasBatchCount = 0;
153 AtlasBatchedTileCount = 0;
154
155 UpdateSize(source.Width, source.Height);
156 ClearColor = source.ClearColor;
157 FogPaint.Dispose();
158 GridLinePaint.Dispose();
159 CollisionBoxPaint.Dispose();
160 FogPaint = source.FogPaint.Clone();
161 GridLinePaint = source.GridLinePaint.Clone();
162 CollisionBoxPaint = source.CollisionBoxPaint.Clone();
163 _canvas = _recorder.BeginRecording(new SKRect(0, 0, Width, Height));
164 }
165
166 internal SKPicture Complete()
167 {
168 FlushTileBatch();
169 FlushSnappedVertexBatch();
170 var picture = _recorder.EndRecording();
171 _canvas = null;
172 ResetPendingBatch();
173 ResetSnappedVertexBatch();
174 DisposeAtlasShaders();
175 return picture;
176 }
177
178 internal void Cancel()
179 {
180 ResetPendingBatch();
181 ResetSnappedVertexBatch();
182
183 if (_canvas is null)
184 {
185 DisposeAtlasShaders();
186 return;
187 }
188
189 var picture = _recorder.EndRecording();
190 _canvas = null;
191 picture.Dispose();
192 DisposeAtlasShaders();
193 }
194
195 internal bool TryPrepareFixedGridLayer(
196 SceneLayer layer,
197 Rectangle worldRect,
198 out FixedGridRenderPlan plan)
199 {
200 plan = default;
201
202 if (layer.WrapHorizontally ||
203 layer.WrapVertically ||
204 layer.TransformedTiles.Count != 0 ||
205 layer.ShowGridLines ||
206 layer.ShowCollisionBoxes ||
207 !layer.IsFixedGridSnapshotFastPathEligible ||
208 !RenderTileQuery.IsRenderOrdered(layer))
209 {
210 return false;
211 }
212
213 var queryRect = worldRect;
214 queryRect.Inflate(layer.TileWidth, layer.TileHeight);
215 queryRect.Inflate(1, 1);
216
217 if (SpriteManager.Instance.HasVisibleSpriteInWorldRect(queryRect, layer) ||
218 DirectDrawingManager.Instance.HasVisibleDrawingForLayer(layer, worldRect))
219 {
220 return false;
221 }
222
223 plan = new(layer, worldRect);
224 return true;
225 }
226
227 internal int DrawFixedGridLayer(
228 View view,
229 FixedGridRenderPlan plan,
230 Rectangle clipRect)
231 {
232 var layer = plan.Layer;
233 LayerScreenMap screen = LayerScreenMap.Create(view, layer);
234 bool snapOrthogonalEdges = layer.CoordinateSystem is OrthogonalCoordinates;
235 int tileCount = 0;
236
237 Canvas.Save();
238 Canvas.ClipRect(clipRect.ToSKRect());
239
240 if (!snapOrthogonalEdges)
241 {
242 var worldToScreen = screen.CreateWorldToScreenMatrix();
243 Canvas.Concat(ref worldToScreen);
244 }
245
246 try
247 {
248 RenderTileQuery.VisitCandidates(
249 layer.CoordinateSystem,
250 layer,
251 plan.WorldRect,
252 includeOverhang: true,
253 tile =>
254 {
255 if (!tile.Visible || tile.CurrentFrame.Tilesheet is null)
256 return true;
257
258 tileCount++;
259
260 if (snapOrthogonalEdges)
261 DrawOrthogonalFixedGridTileSnapped(layer, screen, tile);
262 else
263 DrawFixedGridTileInWorldSpace(layer, tile);
264
265 return true;
266 });
267
268 if (snapOrthogonalEdges)
269 FlushSnappedVertexBatch();
270 else
271 FlushTileBatch();
272
273 return tileCount;
274 }
275 finally
276 {
277 Canvas.Restore();
278 }
279 }
280
281 internal bool TryDrawFixedGridDrawables(
282 View view,
283 IReadOnlyList<IDrawable> drawables,
284 Rectangle clipRect)
285 {
286 if (drawables.Count == 0)
287 return true;
288
289 SceneLayer? layer = null;
290
291 // This path deliberately handles only the engine's fixed tile type and no
292 // post-tile debug/fog overlays. Anything richer retains the generic renderer.
293 for (int i = 0; i < drawables.Count; i++)
294 {
295 if (drawables[i].GetType() != typeof(SceneLayerTile) ||
296 drawables[i] is not SceneLayerTile tile ||
297 tile.EnableFog)
298 {
299 return false;
300 }
301
302 layer ??= tile.SceneLayer;
303 if (!ReferenceEquals(layer, tile.SceneLayer))
304 return false;
305 }
306
307 if (layer is null ||
308 layer.ShowGridLines ||
309 layer.ShowCollisionBoxes)
310 {
311 return false;
312 }
313
314 LayerScreenMap screen = LayerScreenMap.Create(view, layer);
315
316 Canvas.Save();
317 Canvas.ClipRect(clipRect.ToSKRect());
318
319 if (layer.CoordinateSystem is OrthogonalCoordinates)
320 {
321 try
322 {
323 for (int i = 0; i < drawables.Count; i++)
324 DrawOrthogonalFixedGridTileSnapped(
325 layer,
326 screen,
327 (SceneLayerTile)drawables[i]);
328
329 FlushSnappedVertexBatch();
330 return true;
331 }
332 finally
333 {
334 Canvas.Restore();
335 }
336 }
337
338 var worldToScreen = screen.CreateWorldToScreenMatrix();
339 bool worldSpaceActive = false;
340
341 try
342 {
343 for (int i = 0; i < drawables.Count; i++)
344 {
345 var tile = (SceneLayerTile)drawables[i];
346
347 if (tile.Transform == TileTransform.Identity &&
348 TryGetFrameAtlasInfo(tile.CurrentFrame, out _))
349 {
350 if (!worldSpaceActive)
351 {
352 Canvas.Save();
353 Canvas.Concat(ref worldToScreen);
354 worldSpaceActive = true;
355 }
356
357 DrawFixedGridTileInWorldSpace(layer, tile);
358 continue;
359 }
360
361 if (worldSpaceActive)
362 {
363 FlushTileBatch();
364 Canvas.Restore();
365 worldSpaceActive = false;
366 }
367
368 DrawFixedGridTile(view, layer, screen, tile);
369 }
370
371 if (worldSpaceActive)
372 {
373 FlushTileBatch();
374 Canvas.Restore();
375 worldSpaceActive = false;
376 }
377
378 return true;
379 }
380 finally
381 {
382 if (worldSpaceActive)
383 {
384 FlushTileBatch();
385 Canvas.Restore();
386 }
387
388 Canvas.Restore();
389 }
390 }
391
392 private void DrawOrthogonalFixedGridTileSnapped(
393 SceneLayer layer,
394 LayerScreenMap screen,
395 SceneLayerTile tile)
396 {
397 if (tile.Transform == TileTransform.Identity &&
398 TryGetFrameAtlasInfo(tile.CurrentFrame, out var frame))
399 {
400 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
401 if (!frame.Overhang.IsEmpty)
402 {
403 world = Rectangle.FromLTRB(
404 world.Left - frame.Overhang.Left,
405 world.Top - frame.Overhang.Top,
406 world.Right + frame.Overhang.Right,
407 world.Bottom + frame.Overhang.Bottom);
408 }
409
410 RectangleF destination = screen.MapPixelSnapped(world);
411 if (destination.Width <= 0f || destination.Height <= 0f)
412 return;
413
414 QueueSnappedVertexAtlas(
415 frame.Atlas,
416 frame.Source,
417 destination);
418 return;
419 }
420
421 FlushSnappedVertexBatch();
422
423 RectangleF transformedDestination =
424 screen.MapPixelSnapped(tile.DrawLocationWorld);
425 if (transformedDestination.Width > 0f &&
426 transformedDestination.Height > 0f)
427 {
428 tile.Draw(this, transformedDestination);
429 }
430 }
431
432 private void DrawFixedGridTileInWorldSpace(
433 SceneLayer layer,
434 SceneLayerTile tile)
435 {
436 if (!TryGetFrameAtlasInfo(tile.CurrentFrame, out var frame))
437 return;
438
439 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
440 if (!frame.Overhang.IsEmpty)
441 {
442 world = Rectangle.FromLTRB(
443 world.Left - frame.Overhang.Left,
444 world.Top - frame.Overhang.Top,
445 world.Right + frame.Overhang.Right,
446 world.Bottom + frame.Overhang.Bottom);
447 }
448
449 // Record atlas entries in integer world coordinates. A single canvas transform
450 // scales/translates the complete fixed-grid layer to screen space, so adjacent
451 // tile edges are transformed from the same world boundary instead of from
452 // independently rounded fractional destination rectangles.
453 if (TryQueueAtlas(frame.Atlas, frame.Source, world))
454 return;
455
456 FlushTileBatch();
457 Canvas.DrawImage(
458 frame.Atlas,
459 frame.Source,
460 world.ToSKRect(),
461 SKSamplingOptions.Default);
462 }
463
464 private void DrawFixedGridTile(
465 View view,
466 SceneLayer layer,
467 LayerScreenMap screen,
468 SceneLayerTile tile)
469 {
470 if (tile.Transform != TileTransform.Identity)
471 {
472 FlushTileBatch();
473 tile.Draw(this, tile.GetDrawLocationScreen(view));
474 return;
475 }
476
477 if (!TryGetFrameAtlasInfo(tile.CurrentFrame, out var frame))
478 {
479 FlushTileBatch();
480 tile.Draw(this, tile.GetDrawLocationScreen(view));
481 return;
482 }
483
484 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
485 if (!frame.Overhang.IsEmpty)
486 {
487 world = Rectangle.FromLTRB(
488 world.Left - frame.Overhang.Left,
489 world.Top - frame.Overhang.Top,
490 world.Right + frame.Overhang.Right,
491 world.Bottom + frame.Overhang.Bottom);
492 }
493
494 RectangleF destination = screen.Map(world);
495
496 if (!TryQueueAtlas(frame.Atlas, frame.Source, destination))
497 {
498 FlushTileBatch();
499 tile.Draw(this, destination);
500 }
501 }
502
503 private void QueueSnappedVertexAtlas(
504 SKImage atlas,
505 SKRect source,
506 RectangleF destination)
507 {
508 if (_pendingSnappedVertexAtlas is not null &&
509 !ReferenceEquals(_pendingSnappedVertexAtlas, atlas))
510 {
511 FlushSnappedVertexBatch();
512 }
513
514 _pendingSnappedVertexAtlas = atlas;
515
516 int tileIndex = _pendingSnappedVertexCount++;
517 int vertexIndex = tileIndex * 4;
518
519 float left = destination.Left;
520 float top = destination.Top;
521 float right = destination.Right;
522 float bottom = destination.Bottom;
523
524 _pendingSnappedPositions[vertexIndex] = new(left, top);
525 _pendingSnappedPositions[vertexIndex + 1] = new(right, top);
526 _pendingSnappedPositions[vertexIndex + 2] = new(right, bottom);
527 _pendingSnappedPositions[vertexIndex + 3] = new(left, bottom);
528
529 _pendingSnappedTextureCoordinates[vertexIndex] =
530 new(source.Left, source.Top);
531 _pendingSnappedTextureCoordinates[vertexIndex + 1] =
532 new(source.Right, source.Top);
533 _pendingSnappedTextureCoordinates[vertexIndex + 2] =
534 new(source.Right, source.Bottom);
535 _pendingSnappedTextureCoordinates[vertexIndex + 3] =
536 new(source.Left, source.Bottom);
537
538 if (_pendingSnappedVertexCount == MaxSnappedVertexBatchSize)
539 FlushFullSnappedVertexBatch();
540 }
541
542 private void FlushSnappedVertexBatch()
543 {
544 if (_pendingSnappedVertexCount == 0)
545 {
546 _pendingSnappedVertexAtlas = null;
547 return;
548 }
549
550 if (_pendingSnappedVertexAtlas is null)
551 {
552 throw new InvalidOperationException(
553 "Snapped vertex batch has queued tiles without an atlas image.");
554 }
555
556 int offset = 0;
557 int remaining = _pendingSnappedVertexCount;
558
559 foreach (int size in PartialSnappedVertexBatchSizes)
560 {
561 while (remaining >= size)
562 {
563 var buffers = _partialSnappedVertexBuffers[size];
564 Array.Copy(
565 _pendingSnappedPositions,
566 offset * 4,
567 buffers.Positions,
568 0,
569 size * 4);
570 Array.Copy(
571 _pendingSnappedTextureCoordinates,
572 offset * 4,
573 buffers.TextureCoordinates,
574 0,
575 size * 4);
576
577 DrawSnappedVertexBatch(
578 _pendingSnappedVertexAtlas,
579 buffers.Positions,
580 buffers.TextureCoordinates,
581 buffers.Indices,
582 size);
583
584 offset += size;
585 remaining -= size;
586 }
587 }
588
589 ResetSnappedVertexBatch();
590 }
591
592 private void FlushFullSnappedVertexBatch()
593 {
594 if (_pendingSnappedVertexAtlas is null ||
595 _pendingSnappedVertexCount != MaxSnappedVertexBatchSize)
596 {
597 return;
598 }
599
600 DrawSnappedVertexBatch(
601 _pendingSnappedVertexAtlas,
602 _pendingSnappedPositions,
603 _pendingSnappedTextureCoordinates,
604 _fullSnappedVertexIndices,
605 MaxSnappedVertexBatchSize);
606
607 _pendingSnappedVertexCount = 0;
608 }
609
610 private void DrawSnappedVertexBatch(
611 SKImage atlas,
612 SKPoint[] positions,
613 SKPoint[] textureCoordinates,
614 ushort[] indices,
615 int tileCount)
616 {
617 using var vertices = SKVertices.CreateCopy(
618 SKVertexMode.Triangles,
619 positions,
620 textureCoordinates,
621 null,
622 indices);
623
624 if (!_atlasShaderCache.TryGetValue(atlas, out var shader))
625 {
626 shader = atlas.ToShader(
627 SKShaderTileMode.Clamp,
628 SKShaderTileMode.Clamp,
629 new SKSamplingOptions(
630 SKFilterMode.Nearest,
631 SKMipmapMode.None));
632 _atlasShaderCache.Add(atlas, shader);
633 }
634
635 _snappedVertexPaint.Shader = shader;
636 Canvas.DrawVertices(
637 vertices,
638 SKBlendMode.Modulate,
639 _snappedVertexPaint);
640
641 AtlasBatchCount++;
642 AtlasBatchedTileCount += tileCount;
643 }
644
645 private static ushort[] CreateQuadIndices(int tileCount)
646 {
647 var indices = new ushort[tileCount * 6];
648
649 for (int tile = 0; tile < tileCount; tile++)
650 {
651 int vertex = tile * 4;
652 int index = tile * 6;
653
654 indices[index] = (ushort)vertex;
655 indices[index + 1] = (ushort)(vertex + 1);
656 indices[index + 2] = (ushort)(vertex + 2);
657 indices[index + 3] = (ushort)vertex;
658 indices[index + 4] = (ushort)(vertex + 2);
659 indices[index + 5] = (ushort)(vertex + 3);
660 }
661
662 return indices;
663 }
664
665 private void ResetSnappedVertexBatch()
666 {
667 _pendingSnappedVertexAtlas = null;
668 _pendingSnappedVertexCount = 0;
669 }
670
671 private void DisposeAtlasShaders()
672 {
673 _snappedVertexPaint.Shader = null;
674
675 foreach (var shader in _atlasShaderCache.Values)
676 shader.Dispose();
677
678 _atlasShaderCache.Clear();
679 }
680
681 internal bool TryQueueTile(SceneLayerTile tile, RectangleF destination)
682 {
683 if (tile.Transform != TileTransform.Identity ||
684 !TryGetFrameAtlasInfo(tile.CurrentFrame, out var frame))
685 {
686 return false;
687 }
688
689 return TryQueueAtlas(frame.Atlas, frame.Source, destination);
690 }
691
692 private bool TryGetFrameAtlasInfo(Frame frame, out FrameAtlasInfo info)
693 {
694 if (frame.Tilesheet is null)
695 {
696 info = default;
697 return false;
698 }
699
700 var key = new FrameAtlasKey(
701 frame.Tilesheet,
702 frame.RegionName ?? string.Empty,
703 frame.XTile,
704 frame.YTile);
705
706 if (_frameAtlasCache.TryGetValue(key, out info))
707 return true;
708
709 var atlas = frame.AtlasImage;
710 Rectangle source = frame.AtlasSourceBounds;
711 if (atlas is null || source.IsEmpty)
712 {
713 info = default;
714 return false;
715 }
716
717 info = new(
718 atlas,
719 source.ToSKRect(),
720 frame.Overhang);
721 _frameAtlasCache.Add(key, info);
722 return true;
723 }
724
725 private bool TryQueueAtlas(
726 SKImage atlas,
727 SKRect source,
728 RectangleF destination)
729 {
730 if (source.Width <= 0 ||
731 source.Height <= 0 ||
732 destination.Width <= 0 ||
733 destination.Height <= 0)
734 {
735 return false;
736 }
737
738 float scaleX = destination.Width / source.Width;
739 float scaleY = destination.Height / source.Height;
740 float tolerance = MathF.Max(0.0001f, MathF.Abs(scaleX) * 0.0001f);
741
742 if (!float.IsFinite(scaleX) ||
743 !float.IsFinite(scaleY) ||
744 MathF.Abs(scaleX - scaleY) > tolerance)
745 {
746 return false;
747 }
748
749 if (_pendingAtlas is not null && !ReferenceEquals(_pendingAtlas, atlas))
750 FlushTileBatch();
751
752 _pendingAtlas = atlas;
753 int index = _pendingCount++;
754 _pendingSprites[index] = source;
755 _pendingTransforms[index] = new SKRotationScaleMatrix(
756 scaleX,
757 0f,
758 destination.Left,
759 destination.Top);
760
761 if (_pendingCount == MaxAtlasBatchSize)
762 FlushFullTileBatch();
763
764 return true;
765 }
766
767 internal void FlushTileBatch()
768 {
769 if (_pendingCount == 0)
770 {
771 _pendingAtlas = null;
772 return;
773 }
774
775 if (_pendingAtlas is null)
776 throw new InvalidOperationException("Atlas batch has queued tiles without an atlas image.");
777
778 int offset = 0;
779 int remaining = _pendingCount;
780
781 foreach (int size in PartialBatchSizes)
782 {
783 while (remaining >= size)
784 {
785 var buffers = _partialBatchBuffers[size];
786 Array.Copy(_pendingSprites, offset, buffers.Sprites, 0, size);
787 Array.Copy(_pendingTransforms, offset, buffers.Transforms, 0, size);
788
789 Canvas.DrawAtlas(
790 _pendingAtlas,
791 buffers.Sprites,
792 buffers.Transforms,
793 SKSamplingOptions.Default);
794
795 AtlasBatchCount++;
796 AtlasBatchedTileCount += size;
797 offset += size;
798 remaining -= size;
799 }
800 }
801
802 ResetPendingBatch();
803 }
804
805 private void FlushFullTileBatch()
806 {
807 if (_pendingAtlas is null || _pendingCount != MaxAtlasBatchSize)
808 return;
809
810 Canvas.DrawAtlas(
811 _pendingAtlas,
812 _pendingSprites,
813 _pendingTransforms,
814 SKSamplingOptions.Default);
815
816 AtlasBatchCount++;
817 AtlasBatchedTileCount += MaxAtlasBatchSize;
818 _pendingCount = 0;
819 }
820
821 private void ResetPendingBatch()
822 {
823 _pendingAtlas = null;
824 _pendingCount = 0;
825 }
826
828 protected internal override void BeginFrame() { }
830 protected internal override void EndFrame() { }
832 protected internal override SKImage Snapshot() =>
833 throw new NotSupportedException("A recording has commands, not pixels. Read pixels on the GL thread after replay.");
834
836 protected internal override void DrawTileFrame(Tile tile, RectangleF destRectScreen)
837 {
838 var image = tile.CurrentFrame.SkImage;
839 if (image is not null) Canvas.DrawImage(image, destRectScreen.ToSKRect());
840 }
841
843 public override void Dispose()
844 {
845 Cancel();
846 _snappedVertexPaint.Dispose();
847 _recorder.Dispose();
848 base.Dispose();
849 }
850}
virtual Frame CurrentFrame
Gets or sets the current frame. The initial frame supplies the tile's default collision adjustment an...
Definition Tile.cs:176
virtual RectangleF GetDrawLocationScreen(View view)
Transforms the drawable bounds into screen pixels for the specified view.
Represents a base class for managing a graphical backbuffer, and is the in-memory surface where rende...
RectangleF WorldRectToScreenRect(SceneLayer layer, RectangleF worldRect)
Converts a world-space pixel rectangle into a screen-space rectangle for this View,...
Definition View.cs:324
override void Draw(Gondwana.Rendering.Backbuffers.BackbufferBase backbuffer, RectangleF destRectScreen)
Draws the current content into the backbuffer at the supplied screen-pixel bounds.
TileTransform Transform
Gets or sets this placement's orthogonal orientation without changing its source frame.
override Rectangle DrawLocationWorld
Gets the world-space pixel rectangle where this tile should be drawn.
bool WrapVertically
Gets or sets a value indicating whether this layer repeats along its row axis at the grid boundaries.
bool WrapHorizontally
Gets or sets a value indicating whether this layer repeats along its column axis at the grid boundari...
bool ShowGridLines
Gets or sets a value indicating whether grid lines should be rendered over this layer for debugging.
int TileHeight
Gets or sets the height of each tile in this layer, measured in pixels.
int TileWidth
Gets or sets the width of each tile in this layer, measured in pixels.
bool ShowCollisionBoxes
Gets or sets a value indicating whether collision boxes should be rendered over this layer for debugg...
@ 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.
TileTransform
The eight orthogonal orientations of artwork placed in a fixed scene cell.
record struct Spacing([property:JsonProperty("left")] int Left, [property:JsonProperty("top")] int Top, [property:JsonProperty("right")] int Right, [property:JsonProperty("bottom")] int Bottom)
Represents dimensions in pixels when spacing Tilesheets.
Definition Spacing.cs:12
@ Cancel
The dialog was cancelled.
@ Sprites
Represents the sprites state, including sprite instances, positions, properties, and sprite-related g...
readonly int YTile
The vertical tile coordinate (row index) within the tilesheet.
Definition Frame.cs:31
readonly Tilesheet Tilesheet
The tilesheet that contains the source bitmap for this frame.
Definition Frame.cs:16
readonly string RegionName
The tilesheet region that contains the source bitmap for this frame.
Definition Frame.cs:21
readonly int XTile
The horizontal tile coordinate (column index) within the tilesheet.
Definition Frame.cs:26
readonly? SKImage SkImage
Gets the SkiaSharp image for this frame.
Definition Frame.cs:70
readonly Spacing Overhang
Gets the overhang dimensions for this frame.
Definition Frame.cs:86