Skip to content

Commit

Permalink
Make render updates atomic
Browse files Browse the repository at this point in the history
  • Loading branch information
reflectronic committed Jun 19, 2024
1 parent a6fef47 commit 689684c
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 108 deletions.
198 changes: 93 additions & 105 deletions BnbnavNetClient/Controls/VirtualSurfaceControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,21 @@

namespace BnbnavNetClient.Controls;

internal abstract class VirtualSurfaceControl : Control, ICustomDrawOperation
internal abstract class VirtualSurfaceControl : Control
{
private struct Tile { public SKSurface Surface; public bool Dirty; }
private readonly record struct TileIndex(int X, int Y);

private const int TileSideExponent = 9;
private const int TileSide = 1 << TileSideExponent; // 256
private const int TileSide = 1 << TileSideExponent; // 512

private readonly Dictionary<TileIndex, Tile> _tileMap = [];

private uint _renderSequenceNumber;

private Rect _renderBounds;
private double _scale = 0.5;

public double Scale
{
get => _scale;
set
{
_scale = value;
InvalidateVisual();
}
}

private Point _pan = new(0, 0);
public Point Pan
{
get => _pan;
set
{
_pan = value;
InvalidateVisual();
}
}
public double Scale { get; set; } = 1;
public Point Pan { get; set; }

public void PanAndScale(Point pan, double scale)
{
_pan = pan;
_scale = scale;
InvalidateVisual();
}

public abstract void DrawTile(TileSurface surface, Rect worldCoordinates);

public void InvalidateTiles(Rect worldBounds)
Expand Down Expand Up @@ -84,44 +56,48 @@ public void InvalidateTiles()
{
Interlocked.Increment(ref _renderSequenceNumber);

foreach (var key in _tileMap.Keys)
foreach (var tileIndex in _tileMap.Keys)
{
ref var tile = ref CollectionsMarshal.GetValueRefOrNullRef(_tileMap, key);
ref var tile = ref CollectionsMarshal.GetValueRefOrNullRef(_tileMap, tileIndex);
tile.Dirty = true;
}

InvalidateVisual();
}

private static (TileIndex TopLeft, TileIndex BottomRight) GetExtents(PixelRect pixelBounds)
{
var topLeft = new TileIndex(pixelBounds.X >> TileSideExponent, pixelBounds.Y >> TileSideExponent);
var bottomRight = new TileIndex((pixelBounds.Right >> TileSideExponent) + 1, (pixelBounds.Bottom >> TileSideExponent) + 1);

return (topLeft, bottomRight);
}

void ICustomDrawOperation.Render(ImmediateDrawingContext context)
private static (TileIndex TopLeft, TileIndex BottomRight) GetWorldExtends(Rect worldBounds)
{
var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature is null)
{
return;
}

using var lease = leaseFeature.Lease();
var canvas = lease.SkCanvas;
var top = (int)double.Floor(worldBounds.Bottom / TileSide);
var left = (int)double.Floor(worldBounds.Left / TileSide);
var bottom = (int)double.Ceiling(worldBounds.Bottom / TileSide);
var right = (int)double.Ceiling(worldBounds.Right / TileSide);

return (new TileIndex(left, top), new TileIndex(right, bottom));
}

public override void Render(DrawingContext context)
{
if (VisualRoot is not TopLevel { RenderScaling: var dpiScale })
{
return;
}

var originalSequenceNumber = _renderSequenceNumber;

var (panX, panY) = Pan * Scale * dpiScale;

var viewportRect = _renderBounds * dpiScale;
var viewportRect = Bounds * dpiScale;
var pixelBounds = new PixelRect((int)panX, (int)panY, (int)viewportRect.Width, (int)viewportRect.Height);

var (topLeftTile, bottomRightTile) = GetExtents(pixelBounds);

canvas.Save();

canvas.SetMatrix(canvas.TotalMatrix.PostConcat(SKMatrix.CreateScale((float)dpiScale, (float)dpiScale).Invert()));
var dirtyTiles = new Dictionary<TileIndex, SKPicture>();

for (var x = topLeftTile.X; x <= bottomRightTile.X; x++)
{
Expand All @@ -132,85 +108,97 @@ void ICustomDrawOperation.Render(ImmediateDrawingContext context)

if (!exists || tile.Dirty)
{
var surface = tile.Surface ??= SKSurface.Create(lease.GrContext, false, new SKImageInfo(TileSide, TileSide));
var surfaceCanvas = surface.Canvas;
var recorder = new SKPictureRecorder();
var surfaceCanvas = recorder.BeginRecording(new SKRect(0, 0, TileSide, TileSide));

var worldCoordinates = new Rect(x * TileSide, y * TileSide, TileSide, TileSide) * (1 / (Scale * dpiScale));

surfaceCanvas.Save();

surfaceCanvas.Scale((float)(Scale * dpiScale));
surfaceCanvas.Translate((-worldCoordinates.TopLeft).ToSKPoint());

DrawTile(new TileSurface
{
Surface = surface,
Canvas = surfaceCanvas,
CanvasSize = new SKSizeI(TileSide, TileSide)
}, worldCoordinates);
DrawTile(new TileSurface { Canvas = surfaceCanvas, CanvasSize = new(TileSide, TileSide) }, worldCoordinates);

surfaceCanvas.Restore();
tile.Dirty = false;

// If a region of the virtual surface was invalidated, the tile we're rendering may be outdated
// Another thread is racing to mark those tiles as dirty, so avoid overwriting that
// A redraw is queued, so any glitches will be fixed on the next redraw
if (originalSequenceNumber == _renderSequenceNumber)
{
tile.Dirty = false;
}
dirtyTiles.Add(tileIndex, recorder.EndRecording());
}

canvas.DrawSurface(tile.Surface, new SKPoint((tileIndex.X << TileSideExponent) - (float)panX, (tileIndex.Y << TileSideExponent) - (float)panY));
}
}

canvas.Restore();
}

private Point ToScreen(Point viewportPoint)
{
var mtx = Matrix.CreateScale(Scale, Scale) * Matrix.CreateTranslation(-Pan);

return mtx.Transform(viewportPoint);
context.Custom(new VirtualSurfaceRenderOperation(new Rect(0, 0, Bounds.Width, Bounds.Height), Pan, Scale, dpiScale, _tileMap, dirtyTiles));
}

private static (TileIndex TopLeft, TileIndex BottomRight) GetExtents(PixelRect pixelBounds)
private class VirtualSurfaceRenderOperation(

Check warning on line 131 in BnbnavNetClient/Controls/VirtualSurfaceControl.cs

View workflow job for this annotation

GitHub Actions / build (macos-13, x64)

Type 'VirtualSurfaceRenderOperation' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check warning on line 131 in BnbnavNetClient/Controls/VirtualSurfaceControl.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, x64)

Type 'VirtualSurfaceRenderOperation' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check warning on line 131 in BnbnavNetClient/Controls/VirtualSurfaceControl.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Type 'VirtualSurfaceRenderOperation' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check warning on line 131 in BnbnavNetClient/Controls/VirtualSurfaceControl.cs

View workflow job for this annotation

GitHub Actions / build (macos-13, arm64)

Type 'VirtualSurfaceRenderOperation' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check warning on line 131 in BnbnavNetClient/Controls/VirtualSurfaceControl.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, arm64)

Type 'VirtualSurfaceRenderOperation' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)
Rect renderBounds,
Point pan,
double scale,
double dpiScale,
Dictionary<TileIndex, Tile> tileMap,
Dictionary<TileIndex, SKPicture> dirtyTiles) : ICustomDrawOperation
{
var topLeft = new TileIndex(pixelBounds.X >> TileSideExponent, pixelBounds.Y >> TileSideExponent);
var bottomRight = new TileIndex((pixelBounds.Right >> TileSideExponent) + 1, (pixelBounds.Bottom >> TileSideExponent) + 1);
public Rect Bounds => renderBounds;

return (topLeft, bottomRight);
}
public bool HitTest(Point p) => true;

private static (TileIndex TopLeft, TileIndex BottomRight) GetWorldExtends(Rect worldBounds)
{
var top = (int)double.Floor(worldBounds.Bottom / TileSide);
var left = (int)double.Floor(worldBounds.Left / TileSide);
var bottom = (int)double.Ceiling(worldBounds.Bottom / TileSide);
var right = (int)double.Ceiling(worldBounds.Right / TileSide);
public bool Equals(ICustomDrawOperation? other) => this == other;

return (new TileIndex(left, top), new TileIndex(right, bottom));
}

public override void Render(DrawingContext context)
{
_renderBounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.Custom(this);
}

Rect ICustomDrawOperation.Bounds => _renderBounds;
public void Dispose()
{
}

public void Render(ImmediateDrawingContext context)
{
var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
if (leaseFeature is null)
{
return;
}

bool ICustomDrawOperation.HitTest(Point p) => true;
bool IEquatable<ICustomDrawOperation>.Equals(ICustomDrawOperation? other) => this == other;
using var lease = leaseFeature.Lease();
var canvas = lease.SkCanvas;

public void Dispose()
{
var (panX, panY) = pan * scale * dpiScale;

var viewportRect = renderBounds * dpiScale;
var pixelBounds = new PixelRect((int)panX, (int)panY, (int)viewportRect.Width, (int)viewportRect.Height);

var (topLeftTile, bottomRightTile) = GetExtents(pixelBounds);

canvas.Save();

canvas.SetMatrix(canvas.TotalMatrix.PostConcat(SKMatrix.CreateScale((float)dpiScale, (float)dpiScale).Invert()));

for (var x = topLeftTile.X; x <= bottomRightTile.X; x++)
{
for (var y = topLeftTile.Y; y <= bottomRightTile.Y; y++)
{
var tileIndex = new TileIndex(x, y);
ref var tile = ref CollectionsMarshal.GetValueRefOrAddDefault(tileMap, tileIndex, out _);

if (dirtyTiles.TryGetValue(tileIndex, out var dirtyTilePicture))
{
var surface = tile.Surface ??= SKSurface.Create(lease.GrContext, false, new SKImageInfo(TileSide, TileSide));
var surfaceCanvas = surface.Canvas;

surfaceCanvas.Save();

surfaceCanvas.DrawPicture(dirtyTilePicture);

surfaceCanvas.Restore();
}

canvas.DrawSurface(tile.Surface, new SKPoint((tileIndex.X << TileSideExponent) - (float)panX, (tileIndex.Y << TileSideExponent) - (float)panY));
}
}

canvas.Restore();
}
}
}

internal struct TileSurface
internal readonly struct TileSurface
{
public SKSurface Surface { get; init; }
public SKCanvas Canvas { get; init; }

public SKSizeI CanvasSize { get; init; }
}
}
5 changes: 2 additions & 3 deletions BnbnavNetClient/Views/VirtualMapView.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ protected override void OnPointerMoved(PointerEventArgs e)
{
Pan += (previousPointerPosition - currentPosition.Position) / Scale;
MapViewModel.Pan = Pan;
InvalidateVisual();
}

previousPointerPosition = currentPosition.Position;
Expand All @@ -59,8 +60,6 @@ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);

var currentPosition = e.GetPosition(this);

var deltaScale = e.Delta.Y * Scale / 10.0;
Zoom(deltaScale, e.GetPosition(this));
}
Expand Down Expand Up @@ -124,7 +123,7 @@ public override void DrawTile(TileSurface surface, Rect worldCoordinates)

var canvas = surface.Canvas;

canvas.DrawColor(new SKColor((uint)Random.Shared.Next()));
canvas.Clear();

var noRender = new List<MapItem>();
noRender.AddRange(MapViewModel.MapEditorService.EditController.ItemsNotToRender);
Expand Down

0 comments on commit 689684c

Please sign in to comment.