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];
26 private sealed class AtlasBatchBuffer(int size)
28 internal SKRect[]
Sprites {
get; } =
new SKRect[size];
29 internal SKRotationScaleMatrix[] Transforms {
get; } =
new SKRotationScaleMatrix[size];
32 private sealed class SnappedVertexBatchBuffer(int tileCount)
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);
39 private readonly record
struct FrameAtlasKey(
45 private readonly record
struct FrameAtlasInfo(
50 internal readonly record
struct FixedGridRenderPlan(
54 private readonly record
struct LayerScreenMap(
64 new RectangleF(0f, 0f, 1f, 1f));
66 return new(unit.Left, unit.Top, unit.Width, unit.Height);
69 internal RectangleF Map(Rectangle world) =>
71 OffsetX + world.Left * ScaleX,
72 OffsetY + world.Top * ScaleY,
74 world.Height * ScaleY);
76 internal RectangleF MapPixelSnapped(Rectangle world)
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);
83 return RectangleF.FromLTRB(left, top, right, bottom);
86 private static float Snap(
float value) =>
87 MathF.Round(value, MidpointRounding.AwayFromZero);
89 internal SKMatrix CreateWorldToScreenMatrix() =>
90 SKMatrix.CreateScaleTranslation(
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()
120 private readonly Dictionary<int, SnappedVertexBatchBuffer> _partialSnappedVertexBuffers =
121 PartialSnappedVertexBatchSizes.ToDictionary(
123 size =>
new SnappedVertexBatchBuffer(size));
124 private readonly SKPaint _snappedVertexPaint =
new()
127 BlendMode = SKBlendMode.SrcOver
130 private SKCanvas? _canvas;
131 private SKImage? _pendingAtlas;
132 private SKImage? _pendingSnappedVertexAtlas;
133 private int _pendingCount;
134 private int _pendingSnappedVertexCount;
136 internal int AtlasBatchCount {
get;
private set; }
137 internal int AtlasBatchedTileCount {
get;
private set; }
139 internal RecordingBackbuffer() : base(1, 1) { }
142 public override SKCanvas Canvas => _canvas ??
throw new InvalidOperationException(
"No recording is active.");
144 public override bool IsGlThreadRendered =>
true;
146 internal void Start(BackbufferBase source)
149 ResetSnappedVertexBatch();
150 DisposeAtlasShaders();
151 _frameAtlasCache.Clear();
153 AtlasBatchedTileCount = 0;
155 UpdateSize(source.Width, source.Height);
156 ClearColor = source.ClearColor;
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));
166 internal SKPicture Complete()
169 FlushSnappedVertexBatch();
170 var picture = _recorder.EndRecording();
173 ResetSnappedVertexBatch();
174 DisposeAtlasShaders();
181 ResetSnappedVertexBatch();
185 DisposeAtlasShaders();
189 var picture = _recorder.EndRecording();
192 DisposeAtlasShaders();
195 internal bool TryPrepareFixedGridLayer(
198 out FixedGridRenderPlan plan)
204 layer.TransformedTiles.Count != 0 ||
207 !layer.IsFixedGridSnapshotFastPathEligible ||
208 !RenderTileQuery.IsRenderOrdered(layer))
213 var queryRect = worldRect;
215 queryRect.Inflate(1, 1);
217 if (SpriteManager.Instance.HasVisibleSpriteInWorldRect(queryRect, layer) ||
218 DirectDrawingManager.Instance.HasVisibleDrawingForLayer(layer, worldRect))
223 plan =
new(layer, worldRect);
227 internal int DrawFixedGridLayer(
229 FixedGridRenderPlan plan,
232 var layer = plan.Layer;
233 LayerScreenMap screen = LayerScreenMap.Create(view, layer);
234 bool snapOrthogonalEdges = layer.CoordinateSystem is OrthogonalCoordinates;
238 Canvas.ClipRect(clipRect.ToSKRect());
240 if (!snapOrthogonalEdges)
242 var worldToScreen = screen.CreateWorldToScreenMatrix();
243 Canvas.Concat(ref worldToScreen);
248 RenderTileQuery.VisitCandidates(
249 layer.CoordinateSystem,
252 includeOverhang:
true,
255 if (!tile.Visible || tile.CurrentFrame.Tilesheet is null)
260 if (snapOrthogonalEdges)
261 DrawOrthogonalFixedGridTileSnapped(layer, screen, tile);
263 DrawFixedGridTileInWorldSpace(layer, tile);
268 if (snapOrthogonalEdges)
269 FlushSnappedVertexBatch();
281 internal bool TryDrawFixedGridDrawables(
283 IReadOnlyList<IDrawable> drawables,
286 if (drawables.Count == 0)
293 for (
int i = 0; i < drawables.Count; i++)
295 if (drawables[i].GetType() != typeof(SceneLayerTile) ||
296 drawables[i] is not SceneLayerTile tile ||
302 layer ??= tile.SceneLayer;
303 if (!ReferenceEquals(layer, tile.SceneLayer))
314 LayerScreenMap screen = LayerScreenMap.Create(view, layer);
317 Canvas.ClipRect(clipRect.ToSKRect());
319 if (layer.CoordinateSystem is OrthogonalCoordinates)
323 for (
int i = 0; i < drawables.Count; i++)
324 DrawOrthogonalFixedGridTileSnapped(
327 (SceneLayerTile)drawables[i]);
329 FlushSnappedVertexBatch();
338 var worldToScreen = screen.CreateWorldToScreenMatrix();
339 bool worldSpaceActive =
false;
343 for (
int i = 0; i < drawables.Count; i++)
345 var tile = (SceneLayerTile)drawables[i];
348 TryGetFrameAtlasInfo(tile.CurrentFrame, out _))
350 if (!worldSpaceActive)
353 Canvas.Concat(ref worldToScreen);
354 worldSpaceActive =
true;
357 DrawFixedGridTileInWorldSpace(layer, tile);
361 if (worldSpaceActive)
365 worldSpaceActive =
false;
368 DrawFixedGridTile(view, layer, screen, tile);
371 if (worldSpaceActive)
375 worldSpaceActive =
false;
382 if (worldSpaceActive)
392 private void DrawOrthogonalFixedGridTileSnapped(
394 LayerScreenMap screen,
400 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
401 if (!frame.Overhang.IsEmpty)
404 world.Left - frame.Overhang.Left,
405 world.Top - frame.Overhang.Top,
406 world.Right + frame.Overhang.Right,
407 world.Bottom + frame.Overhang.Bottom);
410 RectangleF destination = screen.MapPixelSnapped(world);
411 if (destination.Width <= 0f || destination.Height <= 0f)
414 QueueSnappedVertexAtlas(
421 FlushSnappedVertexBatch();
423 RectangleF transformedDestination =
425 if (transformedDestination.Width > 0f &&
426 transformedDestination.Height > 0f)
428 tile.
Draw(
this, transformedDestination);
432 private void DrawFixedGridTileInWorldSpace(
436 if (!TryGetFrameAtlasInfo(tile.
CurrentFrame, out var frame))
439 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
440 if (!frame.Overhang.IsEmpty)
443 world.Left - frame.Overhang.Left,
444 world.Top - frame.Overhang.Top,
445 world.Right + frame.Overhang.Right,
446 world.Bottom + frame.Overhang.Bottom);
453 if (TryQueueAtlas(frame.Atlas, frame.Source, world))
461 SKSamplingOptions.Default);
464 private void DrawFixedGridTile(
467 LayerScreenMap screen,
477 if (!TryGetFrameAtlasInfo(tile.
CurrentFrame, out var frame))
484 Rectangle world = RenderTileQuery.GetFixedTileCellBounds(layer, tile);
485 if (!frame.Overhang.IsEmpty)
488 world.Left - frame.Overhang.Left,
489 world.Top - frame.Overhang.Top,
490 world.Right + frame.Overhang.Right,
491 world.Bottom + frame.Overhang.Bottom);
494 RectangleF destination = screen.Map(world);
496 if (!TryQueueAtlas(frame.Atlas, frame.Source, destination))
499 tile.
Draw(
this, destination);
503 private void QueueSnappedVertexAtlas(
506 RectangleF destination)
508 if (_pendingSnappedVertexAtlas is not
null &&
509 !ReferenceEquals(_pendingSnappedVertexAtlas, atlas))
511 FlushSnappedVertexBatch();
514 _pendingSnappedVertexAtlas = atlas;
516 int tileIndex = _pendingSnappedVertexCount++;
517 int vertexIndex = tileIndex * 4;
519 float left = destination.Left;
520 float top = destination.Top;
521 float right = destination.Right;
522 float bottom = destination.Bottom;
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);
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);
538 if (_pendingSnappedVertexCount == MaxSnappedVertexBatchSize)
539 FlushFullSnappedVertexBatch();
542 private void FlushSnappedVertexBatch()
544 if (_pendingSnappedVertexCount == 0)
546 _pendingSnappedVertexAtlas =
null;
550 if (_pendingSnappedVertexAtlas is
null)
552 throw new InvalidOperationException(
553 "Snapped vertex batch has queued tiles without an atlas image.");
557 int remaining = _pendingSnappedVertexCount;
559 foreach (
int size
in PartialSnappedVertexBatchSizes)
561 while (remaining >= size)
563 var buffers = _partialSnappedVertexBuffers[size];
565 _pendingSnappedPositions,
571 _pendingSnappedTextureCoordinates,
573 buffers.TextureCoordinates,
577 DrawSnappedVertexBatch(
578 _pendingSnappedVertexAtlas,
580 buffers.TextureCoordinates,
589 ResetSnappedVertexBatch();
592 private void FlushFullSnappedVertexBatch()
594 if (_pendingSnappedVertexAtlas is
null ||
595 _pendingSnappedVertexCount != MaxSnappedVertexBatchSize)
600 DrawSnappedVertexBatch(
601 _pendingSnappedVertexAtlas,
602 _pendingSnappedPositions,
603 _pendingSnappedTextureCoordinates,
604 _fullSnappedVertexIndices,
605 MaxSnappedVertexBatchSize);
607 _pendingSnappedVertexCount = 0;
610 private void DrawSnappedVertexBatch(
613 SKPoint[] textureCoordinates,
617 using var vertices = SKVertices.CreateCopy(
618 SKVertexMode.Triangles,
624 if (!_atlasShaderCache.TryGetValue(atlas, out var shader))
626 shader = atlas.ToShader(
627 SKShaderTileMode.Clamp,
628 SKShaderTileMode.Clamp,
629 new SKSamplingOptions(
630 SKFilterMode.Nearest,
632 _atlasShaderCache.Add(atlas, shader);
635 _snappedVertexPaint.Shader = shader;
638 SKBlendMode.Modulate,
639 _snappedVertexPaint);
642 AtlasBatchedTileCount += tileCount;
645 private static ushort[] CreateQuadIndices(
int tileCount)
647 var indices =
new ushort[tileCount * 6];
649 for (
int tile = 0; tile < tileCount; tile++)
651 int vertex = tile * 4;
652 int index = tile * 6;
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);
665 private void ResetSnappedVertexBatch()
667 _pendingSnappedVertexAtlas =
null;
668 _pendingSnappedVertexCount = 0;
671 private void DisposeAtlasShaders()
673 _snappedVertexPaint.Shader =
null;
675 foreach (var shader
in _atlasShaderCache.Values)
678 _atlasShaderCache.Clear();
681 internal bool TryQueueTile(SceneLayerTile tile, RectangleF destination)
684 !TryGetFrameAtlasInfo(tile.
CurrentFrame, out var frame))
689 return TryQueueAtlas(frame.Atlas, frame.Source, destination);
692 private bool TryGetFrameAtlasInfo(Frame frame, out FrameAtlasInfo info)
700 var key =
new FrameAtlasKey(
706 if (_frameAtlasCache.TryGetValue(key, out info))
709 var atlas = frame.AtlasImage;
710 Rectangle source = frame.AtlasSourceBounds;
711 if (atlas is
null || source.IsEmpty)
721 _frameAtlasCache.Add(key, info);
725 private bool TryQueueAtlas(
728 RectangleF destination)
730 if (source.Width <= 0 ||
731 source.Height <= 0 ||
732 destination.Width <= 0 ||
733 destination.Height <= 0)
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);
742 if (!
float.IsFinite(scaleX) ||
743 !
float.IsFinite(scaleY) ||
744 MathF.Abs(scaleX - scaleY) > tolerance)
749 if (_pendingAtlas is not
null && !ReferenceEquals(_pendingAtlas, atlas))
752 _pendingAtlas = atlas;
753 int index = _pendingCount++;
754 _pendingSprites[index] = source;
755 _pendingTransforms[index] =
new SKRotationScaleMatrix(
761 if (_pendingCount == MaxAtlasBatchSize)
762 FlushFullTileBatch();
767 internal void FlushTileBatch()
769 if (_pendingCount == 0)
771 _pendingAtlas =
null;
775 if (_pendingAtlas is
null)
776 throw new InvalidOperationException(
"Atlas batch has queued tiles without an atlas image.");
779 int remaining = _pendingCount;
781 foreach (
int size
in PartialBatchSizes)
783 while (remaining >= size)
785 var buffers = _partialBatchBuffers[size];
786 Array.Copy(_pendingSprites, offset, buffers.Sprites, 0, size);
787 Array.Copy(_pendingTransforms, offset, buffers.Transforms, 0, size);
793 SKSamplingOptions.Default);
796 AtlasBatchedTileCount += size;
805 private void FlushFullTileBatch()
807 if (_pendingAtlas is
null || _pendingCount != MaxAtlasBatchSize)
814 SKSamplingOptions.Default);
817 AtlasBatchedTileCount += MaxAtlasBatchSize;
821 private void ResetPendingBatch()
823 _pendingAtlas =
null;
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.");
836 protected internal override void DrawTileFrame(Tile tile, RectangleF destRectScreen)
839 if (image is not
null) Canvas.DrawImage(image, destRectScreen.ToSKRect());
843 public override void Dispose()
846 _snappedVertexPaint.Dispose();
virtual Frame CurrentFrame
Gets or sets the current frame. The initial frame supplies the tile's default collision adjustment an...
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,...
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.
@ 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.
readonly Tilesheet Tilesheet
The tilesheet that contains the source bitmap for this frame.
readonly string RegionName
The tilesheet region that contains the source bitmap for this frame.
readonly int XTile
The horizontal tile coordinate (column index) within the tilesheet.
readonly? SKImage SkImage
Gets the SkiaSharp image for this frame.
readonly Spacing Overhang
Gets the overhang dimensions for this frame.