17 private OpenSlideBase _openSlideBase;
18 private SlideBase _slideBase;
19 private bool _useOpenSlide;
20 private HashSet<TileIndex> _uploadedTiles =
new();
21 private int _currentLevel = -1;
22 private bool _hasPendingView;
23 private PointD _pendingPyramidalOrigin;
24 private int _pendingViewportWidth;
25 private int _pendingViewportHeight;
26 private double _pendingResolution;
27 private ZCT _pendingCoordinate;
28 private readonly SemaphoreSlim _renderSemaphore =
new(1, 1);
29 private readonly SemaphoreSlim _fetchSemaphore =
new(6, 6);
30 private static readonly TimeSpan RemoteTileFetchTimeout = TimeSpan.FromSeconds(60);
31 private readonly HashSet<(TileIndex Index,
int Version)> _pendingTileFetches =
new();
32 private readonly
object _pendingTileFetchLock =
new();
33 private int _redrawQueued = 0;
34 private int _contentVersion;
35 private (
int Z,
int C,
int T)? _contentCoordinate;
37 private int[]? _rgbSelection;
39 private int[]? _pendingRgbSelection;
44 _glArea.GLReady += OnGlReady;
50 _openSlideBase = source;
60 _openSlideBase =
null;
61 _useOpenSlide =
false;
67 return _useOpenSlide && ReferenceEquals(_openSlideBase, source);
72 return !_useOpenSlide && ReferenceEquals(_slideBase, source);
78 _contentCoordinate =
null;
79 _glArea.ClearTextureCache();
80 _uploadedTiles.Clear();
82 lock (_pendingTileFetchLock)
84 _pendingTileFetches.Clear();
95 int[]? rgbSelection =
null)
97 _pendingPyramidalOrigin = pyramidalOrigin;
98 _pendingViewportWidth = viewportWidth;
99 _pendingViewportHeight = viewportHeight;
100 _pendingResolution = resolution;
101 _pendingCoordinate = coordinate;
102 _pendingRgbZarr = rgbZarr;
103 _pendingRgbSelection = rgbSelection?.ToArray();
104 _hasPendingView =
true;
106 if (_openSlideBase ==
null && _slideBase ==
null)
108 if (viewportWidth <= 1 || viewportHeight <= 1 || resolution <= 0 || !
double.IsFinite(resolution))
110 if (!_glArea.IsGLReady)
112 if (!_renderSemaphore.Wait(0))
119 var schema = _useOpenSlide ? _openSlideBase.Schema : _slideBase.Schema;
120 int level = TileUtil.GetLevel(schema.Resolutions, resolution);
121 var levelRes = schema.Resolutions[level];
122 double levelUnitsPerPixel = levelRes.UnitsPerPixel;
124 var contentCoordinate = (coordinate.Z, coordinate.C, coordinate.T);
125 if (_contentCoordinate != contentCoordinate || !ReferenceEquals(_rgbZarr, rgbZarr) ||
126 !SelectionsEqual(_rgbSelection, rgbSelection) || level != _currentLevel)
130 _contentCoordinate = contentCoordinate;
132 _rgbSelection = rgbSelection?.ToArray();
133 _currentLevel = level;
135 int contentVersion = _contentVersion;
137 double minX = pyramidalOrigin.X;
138 double width = viewportWidth * resolution;
139 double height = viewportHeight * resolution;
140 var viewportExtent =
new Extent(minX, -pyramidalOrigin.Y - height, minX + width, -pyramidalOrigin.Y);
142 double tileMarginX = levelRes.TileWidth * levelUnitsPerPixel;
143 double tileMarginY = levelRes.TileHeight * levelUnitsPerPixel;
144 var fetchExtent =
new Extent(
145 viewportExtent.MinX - tileMarginX,
146 viewportExtent.MinY - tileMarginY,
147 viewportExtent.MaxX + tileMarginX,
148 viewportExtent.MaxY + tileMarginY);
150 var tileInfos = schema.GetTileInfos(fetchExtent, level).ToList();
151 if (tileInfos.Count == 0)
153 var lookupExtent = BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
154 tileInfos = schema.GetTileInfos(lookupExtent, level).ToList();
156 var renderInfos =
new List<TileRenderInfo>();
157 var pendingFetches =
new List<(TileInfo Tile, Task<byte[]> FetchTask)>();
158 foreach (var tileInfo
in tileInfos)
160 if (_glArea.HasTileTexture(tileInfo.Index))
162 renderInfos.Add(CalculateScreenPosition(
172 lock (_pendingTileFetchLock)
174 alreadyPending = !_pendingTileFetches.Add((tileInfo.Index, contentVersion));
179 pendingFetches.Add((tileInfo, FetchTileAsync(tileInfo, level, coordinate, schema, rgbZarr, rgbSelection?.ToArray(), RemoteTileFetchTimeout)));
181 renderInfos.Add(CalculateScreenPosition(
182 tileInfo, pyramidalOrigin, resolution, level, schema));
185 foreach (var pending
in pendingFetches)
187 _ = ProcessPendingFetchAsync(
198 _glArea.SetTilesToRender(renderInfos);
199 _glArea.RequestRedraw();
203 _renderSemaphore.Release();
207 private void OnGlReady(
object sender, EventArgs e)
209 if (!_hasPendingView)
212 _ = _glArea.BeginInvoke(
new System.Action(async () =>
217 _pendingPyramidalOrigin,
218 _pendingViewportWidth,
219 _pendingViewportHeight,
223 _pendingRgbSelection);
229 private async Task<byte[]> FetchTileAsync(TileInfo tileInfo,
int level,
ZCT coordinate,
230 ITileSchema schema,
BioImage? rgbZarr,
int[]? rgbSelection, TimeSpan timeout)
233 var openSource = _openSlideBase;
234 var slideSource = _slideBase;
237 using var cts =
new CancellationTokenSource(timeout);
238 await _fetchSemaphore.WaitAsync(cts.Token);
244 double units = schema.Resolutions[level].UnitsPerPixel;
245 int width = (int)Math.Round(tileInfo.Extent.Width / units);
246 int height = (int)Math.Round(tileInfo.Extent.Height / units);
247 data = await ZarrChannelView.ReadRgbTileAsync(rgbZarr, level,
248 tileInfo.Index.Col * schema.GetTileWidth(level),
249 tileInfo.Index.Row * schema.GetTileHeight(level),
250 width, height, coordinate, rgbSelection!).WaitAsync(cts.Token);
252 for (
int i = 0; i + 3 < data.Length; i += 4)
253 (data[i], data[i + 2]) = (data[i + 2], data[i]);
257 data = openSource !=
null
258 ? await openSource.GetTileAsync(tileInfo).WaitAsync(cts.Token)
259 : await slideSource.GetTileAsync(tileInfo, coordinate).WaitAsync(cts.Token);
260 if (data !=
null && rgbSelection !=
null &&
261 data.Length == schema.GetTileWidth(level) * schema.GetTileHeight(level) * 4)
263 data = (
byte[])data.Clone();
264 for (
int i = 0; i + 3 < data.Length; i += 4)
265 for (
int color = 0; color < 3; color++)
266 if (rgbSelection[color] < 0) data[i + 2 - color] = 0;
271 finally { _fetchSemaphore.Release(); }
275 System.Diagnostics.Trace.WriteLine($
"Could not fetch tile {tileInfo.Index}: {ex.Message}");
280 private async Task ProcessPendingFetchAsync(
282 Task<
byte[]> fetchTask,
292 byte[] tileData = await fetchTask.ConfigureAwait(
false);
293 if (tileData ==
null || contentVersion != _contentVersion)
296 double units = schema.Resolutions[level].UnitsPerPixel;
297 int tW = (int)Math.Round(tile.Extent.Width / units);
298 int tH = (int)Math.Round(tile.Extent.Height / units);
299 if (tW <= 0 || tH <= 0)
return;
302 int sourceWidth = schema.GetTileWidth(level);
303 int sourceHeight = schema.GetTileHeight(level);
305 int pixels = sourceWidth * sourceHeight;
306 int bytesPerPixel = pixels > 0 && tileData.Length % pixels == 0 ? tileData.Length / pixels : 0;
307 if ((tW != sourceWidth || tH != sourceHeight) && bytesPerPixel is >= 1 and <= 4)
309 byte[] clipped =
new byte[checked(tW * tH * bytesPerPixel)];
310 int rowBytes = tW * bytesPerPixel;
311 for (
int row = 0; row < tH; row++)
312 Buffer.BlockCopy(tileData, row * sourceWidth * bytesPerPixel, clipped, row * rowBytes, rowBytes);
317 await RunOnUiThreadAsync(() =>
319 if (contentVersion != _contentVersion)
return;
320 if (_glArea.UploadTileTexture(tile.Index, tileData, tW, tH))
322 _uploadedTiles.Add(tile.Index);
323 _glArea.RequestRedraw();
332 lock (_pendingTileFetchLock)
334 _pendingTileFetches.Remove((tile.Index, contentVersion));
337 if (Interlocked.Exchange(ref _redrawQueued, 1) == 0)
341 await RunOnUiThreadAsync(() => _glArea.RequestRedraw());
345 Interlocked.Exchange(ref _redrawQueued, 0);
351 private Task RunOnUiThreadAsync(System.Action action)
353 if (_glArea.IsDisposed)
354 return Task.CompletedTask;
356 if (_glArea.InvokeRequired)
358 var tcs =
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
361 _glArea.BeginInvoke(
new System.Action(() =>
366 tcs.TrySetResult(true);
370 tcs.TrySetException(ex);
376 tcs.TrySetException(ex);
383 return Task.CompletedTask;
389 double viewResolution,
393 var tileExtent = tile.Extent;
394 var clippedExtent =
new Extent(
395 Math.Max(tileExtent.MinX, schema.Extent.MinX),
396 Math.Max(tileExtent.MinY, schema.Extent.MinY),
397 Math.Min(tileExtent.MaxX, schema.Extent.MaxX),
398 Math.Min(tileExtent.MaxY, schema.Extent.MaxY));
400 if (clippedExtent.MaxX <= clippedExtent.MinX || clippedExtent.MaxY <= clippedExtent.MinY)
403 double originScreenX = pyramidalOrigin.X / viewResolution;
404 double originScreenY = pyramidalOrigin.Y / viewResolution;
406 float screenX = (float)(clippedExtent.MinX / viewResolution - originScreenX);
407 float screenY = (float)(-clippedExtent.MaxY / viewResolution - originScreenY);
408 float screenWidth = (float)((clippedExtent.MaxX - clippedExtent.MinX) / viewResolution);
409 float screenHeight = (float)((clippedExtent.MaxY - clippedExtent.MinY) / viewResolution);
411 return new TileRenderInfo(tile.Index, screenX, screenY, screenWidth, screenHeight);
414 private static bool SelectionsEqual(
int[]? left,
int[]? right) =>
415 ReferenceEquals(left, right) || (left !=
null && right !=
null && left.SequenceEqual(right));
416 public int CurrentLevel => _currentLevel;
417 public int CachedTextureCount => _uploadedTiles.Count;