Skip to content

Commit

Permalink
Poor attempt at bitmap interpolation of tiles
Browse files Browse the repository at this point in the history
  • Loading branch information
reflectronic committed Jun 25, 2024
1 parent a6ae1bc commit 1261d46
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 58 deletions.
167 changes: 111 additions & 56 deletions BnbnavNetClient/Controls/VirtualSurfaceControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

using SkiaSharp;

using System.Collections.Frozen;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
Expand All @@ -20,19 +19,27 @@ private struct Tile { public bool Dirty; public long Timestamp; }
private readonly record struct TileIndex(int X, int Y);
private readonly record struct TileRect(TileIndex TopLeft, TileIndex BottomRight)
{
public int Left => TopLeft.X;
public int Top => TopLeft.Y;
public int Bottom => BottomRight.Y;
public int Right => BottomRight.X;

public readonly bool Contains(TileIndex p)
{
return p.X >= TopLeft.X && p.X <= BottomRight.X
&& p.Y >= TopLeft.Y && p.Y <= BottomRight.Y;
}
}

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

private TileRect _previousVisibleTiles;
private readonly Dictionary<TileIndex, Tile> _tileMap = [];
private readonly Dictionary<TileIndex, SKSurface> _surfaceMap = [];

private long _lastRasterizationTimestamp;
private double _lastRasterizationScale = 1;

public double Scale { get; set; } = 1;
public Point Pan { get; set; }

Expand Down Expand Up @@ -98,69 +105,61 @@ private double DpiScale()
return 1;
}

private static TileRect GetTileExtents(Rect renderBounds, Point pan, double scale, double dpiScale)
private static TileRect GetTileExtents(Size renderBounds, Point pan, double scale, double dpiScale, double renderedTileSize)
{
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 topLeft = new TileIndex(pixelBounds.X >> TileSideExponent, pixelBounds.Y >> TileSideExponent);
var bottomRight = new TileIndex((pixelBounds.Right >> TileSideExponent) + 1, (pixelBounds.Bottom >> TileSideExponent) + 1);
var topLeft = new TileIndex((int)double.Floor(pixelBounds.X / renderedTileSize), (int)double.Floor(pixelBounds.Y / renderedTileSize));
var bottomRight = new TileIndex((int)double.Ceiling(pixelBounds.Right / renderedTileSize), (int)double.Ceiling(pixelBounds.Bottom / renderedTileSize));

return new(topLeft, bottomRight);
}

// This function is wrong
private static TileRect 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);
//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 TileRect(new(left, top), new(right, bottom));
//return new TileRect(new(left, top), new(right, bottom));
return default;
}

private const int TileStandbyListsCount = 16;
private readonly List<TileIndex>[] _tileStandbyLists = new List<TileIndex>[TileStandbyListsCount];

public override void Render(DrawingContext context)
{
var dirtyTiles = new Dictionary<TileIndex, SKPicture>();
Dictionary<TileIndex, SKPicture>? dirtyTiles = null;
var timestamp = Stopwatch.GetTimestamp();

var dpiScale = DpiScale();
var visibleTiles = GetTileExtents(Bounds, Pan, Scale, dpiScale);
var (topLeftTile, bottomRightTile) = visibleTiles;

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 var exists);

if (!exists || tile.Dirty)
{
var recorder = new SKPictureRecorder();
var surfaceCanvas = recorder.BeginRecording(new SKRect(0, 0, TileSide, TileSide));
var visibleTiles = GetTileExtents(Bounds.Size, Pan, Scale, dpiScale, TileTextureSide * (Scale / _lastRasterizationScale));

var worldCoordinates = new Rect(x * TileSide, y * TileSide, TileSide, TileSide) * (1 / (Scale * dpiScale));
bool newTilesVisible = visibleTiles.Left < _previousVisibleTiles.Left
|| visibleTiles.Top < _previousVisibleTiles.Top
|| visibleTiles.Right > _previousVisibleTiles.Right
|| visibleTiles.Bottom > _previousVisibleTiles.Bottom;

surfaceCanvas.Scale((float)(Scale * dpiScale));
surfaceCanvas.Translate((-worldCoordinates.TopLeft).ToSKPoint());
DrawTile(new TileSurface { Canvas = surfaceCanvas, CanvasSize = new(TileSide, TileSide) }, worldCoordinates);
bool rerasterize = newTilesVisible || Stopwatch.GetElapsedTime(_lastRasterizationTimestamp, timestamp).TotalMilliseconds > 200;

tile.Dirty = false;
tile.Timestamp = timestamp;
if (rerasterize)
{
_lastRasterizationScale = Scale;
_lastRasterizationTimestamp = timestamp;
visibleTiles = GetTileExtents(Bounds.Size, Pan, Scale, dpiScale, TileTextureSide);

dirtyTiles.Add(tileIndex, recorder.EndRecording());
}
}
dirtyTiles = [];
DrawDirtyTiles(dirtyTiles, timestamp, dpiScale, visibleTiles);
}

_previousVisibleTiles = visibleTiles;

foreach (var list in _tileStandbyLists)
{
list.Clear();
Expand Down Expand Up @@ -206,22 +205,55 @@ public override void Render(DrawingContext context)
}
}

removedAll:
removedAll:
foreach (var tile in CollectionsMarshal.AsSpan(agedTiles))
{
_tileMap.Remove(tile);
}

context.Custom(new VirtualSurfaceRenderOperation(Bounds, Pan, Scale, dpiScale, _surfaceMap, dirtyTiles, agedTiles));
context.Custom(new VirtualSurfaceRenderOperation(Bounds, Pan, Scale, dpiScale, _lastRasterizationScale, dirtyTiles, _surfaceMap, agedTiles));
}

private void DrawDirtyTiles(Dictionary<TileIndex, SKPicture> dirtyTiles, long timestamp, double dpiScale, TileRect visibleTiles)
{
var (topLeftTile, bottomRightTile) = visibleTiles;

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 var exists);

if (!exists || tile.Dirty)
{
using var recorder = new SKPictureRecorder();
var surfaceCanvas = recorder.BeginRecording(new SKRect(0, 0, TileTextureSide, TileTextureSide));

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

surfaceCanvas.Scale((float)(Scale * dpiScale));
surfaceCanvas.Translate((-worldCoordinates.TopLeft).ToSKPoint());
DrawTile(new TileSurface { Canvas = surfaceCanvas, CanvasSize = new(TileTextureSide, TileTextureSide) }, worldCoordinates);

tile.Dirty = false;
tile.Timestamp = timestamp;

dirtyTiles.Add(tileIndex, recorder.EndRecording());
}
}
}
}

private class VirtualSurfaceRenderOperation(

Check warning on line 249 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 249 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 249 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 249 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)

Check warning on line 249 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)
Rect renderBounds,
Point pan,
double scale,
double dpiScale,
double lastRasterizationScale,
Dictionary<TileIndex, SKPicture>? dirtyTiles,
Dictionary<TileIndex, SKSurface> surfaceMap,
Dictionary<TileIndex, SKPicture> dirtyTiles,
List<TileIndex> agedTiles) : ICustomDrawOperation
{
public Rect Bounds => renderBounds;
Expand All @@ -234,6 +266,11 @@ public void Dispose()
{
}

private static readonly SKPaint HighQualityFilter = new()
{
FilterQuality = SKFilterQuality.High
};

public void Render(ImmediateDrawingContext context)
{
var leaseFeature = context.TryGetFeature<ISkiaSharpApiLeaseFeature>();
Expand All @@ -250,40 +287,58 @@ public void Render(ImmediateDrawingContext context)
var undoDpiScale = canvas.TotalMatrix.PostConcat(SKMatrix.CreateScale((float)dpiScale, (float)dpiScale).Invert());
canvas.SetMatrix(undoDpiScale);

bool rerasterize = dirtyTiles != null;

float comp = rerasterize ? 1f : (float)(scale / lastRasterizationScale);

var (panX, panY) = pan * scale * dpiScale;
var visibleTiles = GetTileExtents(renderBounds, pan, scale, dpiScale);
var visibleTiles = GetTileExtents(renderBounds.Size, pan, scale, dpiScale, TileTextureSide * comp);
var (topLeftTile, bottomRightTile) = visibleTiles;

var timestamp = Stopwatch.GetTimestamp();

if (!rerasterize)
{
var scaleInterpolate = canvas.TotalMatrix.PreConcat(SKMatrix.CreateScale(comp, comp, (float)-panX, (float)-panY));
canvas.SetMatrix(scaleInterpolate);
}

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(surfaceMap, tileIndex, out bool exists);

if (dirtyTiles.Remove(tileIndex, out var dirtyTilePicture))
{
var surface = tile ??= SKSurface.Create(lease.GrContext, false, new SKImageInfo(TileSide, TileSide));
var surfaceCanvas = surface.Canvas;
ref var tile = ref CollectionsMarshal.GetValueRefOrAddDefault(surfaceMap, tileIndex, out bool existed);

surfaceCanvas.Save();
surfaceCanvas.DrawPicture(dirtyTilePicture);
surfaceCanvas.Restore();

dirtyTilePicture.Dispose();
}
else
if (rerasterize)
{
Debug.Assert(exists, "If a tile surface was just created, it must be dirty");
if (dirtyTiles!.Remove(tileIndex, out var dirtyTilePicture))
{
var surface = tile ??= SKSurface.Create(lease.GrContext, false, new SKImageInfo(TileTextureSide, TileTextureSide));
var surfaceCanvas = surface.Canvas;

surfaceCanvas.Save();
surfaceCanvas.DrawPicture(dirtyTilePicture);
surfaceCanvas.Restore();

dirtyTilePicture.Dispose();
}
else
{
Debug.Assert(existed, "If a tile surface was just created, it must be dirty");
}
}

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

canvas.Restore();

Debug.Assert(dirtyTiles.Count == 0, "All dirty tiles should have been consumed");
if (rerasterize)
{
Debug.Assert(dirtyTiles!.Count == 0, "All dirty tiles should have been consumed");
}

foreach (var tile in CollectionsMarshal.AsSpan(agedTiles))
{
Expand Down
17 changes: 15 additions & 2 deletions BnbnavNetClient/Views/VirtualMapView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.Skia;

using BnbnavNetClient.Controls;
Expand All @@ -10,6 +10,8 @@

using SkiaSharp;

using System.Reflection;

namespace BnbnavNetClient.Views;

internal partial class VirtualMapView : VirtualSurfaceControl

Check warning on line 17 in BnbnavNetClient/Views/VirtualMapView.axaml.cs

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

Type 'VirtualMapView' 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 17 in BnbnavNetClient/Views/VirtualMapView.axaml.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, x64)

Type 'VirtualMapView' 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 17 in BnbnavNetClient/Views/VirtualMapView.axaml.cs

View workflow job for this annotation

GitHub Actions / build (macos-13, x64)

Type 'VirtualMapView' 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 17 in BnbnavNetClient/Views/VirtualMapView.axaml.cs

View workflow job for this annotation

GitHub Actions / build (windows-latest, arm64)

Type 'VirtualMapView' 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 17 in BnbnavNetClient/Views/VirtualMapView.axaml.cs

View workflow job for this annotation

GitHub Actions / build (macos-13, arm64)

Type 'VirtualMapView' 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)
Expand All @@ -25,6 +27,11 @@ protected override void OnInitialized()
{
base.OnInitialized();

if (VisualRoot is TopLevel tl)
{
tl.RendererDiagnostics.DebugOverlays |= RendererDebugOverlays.Fps;
}

//<LinearGradientBrush StartPoint="-100%,0%" EndPoint="100%, 0%">
// <GradientStop Color="#640000" Offset="0"/>
// <GradientStop Color="#640000" Offset="0.3"/>
Expand Down Expand Up @@ -97,12 +104,18 @@ private Point ToWorld(Point viewportPoint)

public MapViewModel MapViewModel { get; set; }

private static SKColor[] colors = typeof(SKColors)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(p => p.FieldType == typeof(SKColor))
.Select(p => (SKColor)p.GetValue(null)!)
.ToArray();

public override void DrawTile(TileSurface surface, Rect worldCoordinates)
{
ThemeResources = (IResourceDictionary)this.FindResource(ActualThemeVariant.ToString())!;

var canvas = surface.Canvas;
canvas.Clear();
canvas.Clear(/*colors[int.Abs(worldCoordinates.GetHashCode()) % colors.Length]*/);

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

0 comments on commit 1261d46

Please sign in to comment.