18 private OpenSlideBase _openSlideBase;
19 private SlideBase _slideBase;
20 private bool _useOpenSlide;
21 private BioLib.TileCache _tileCache;
22 private OpenSlideGTK.TileCache _openTileCache;
23 private HashSet<TileIndex> _uploadedTiles =
new();
26 private Dictionary<TileIndex, TileInfo> _uploadedTileInfos =
new();
27 private readonly
object _uploadedTileLock =
new object();
28 private int _currentLevel = -1;
29 private readonly
object _pendingLevelSwitchLock =
new object();
30 private List<TileRenderInfo> _pendingLevelRenderInfos;
31 private int _pendingLevel = -1;
32 private int _pendingPreviousLevel = -1;
33 private readonly SemaphoreSlim _renderSemaphore =
new SemaphoreSlim(1, 1);
34 private readonly SemaphoreSlim _fetchSemaphore =
new SemaphoreSlim(6, 6);
35 private static readonly TimeSpan RemoteTileFetchTimeout = TimeSpan.FromSeconds(120);
36 private const int MaxMissingTileRetries = 8;
39 private volatile int _renderStateVersion = 0;
42 private volatile int _viewportVersion = 0;
43 public bool LastRenderSkipped {
get;
private set; }
45 private volatile bool _pendingRequeue =
false;
48 private readonly HashSet<TileIndex> _pendingTileFetches =
new();
49 private readonly Dictionary<TileIndex, int> _tileRetryCounts =
new();
50 private readonly
object _pendingTileFetchLock =
new object();
51 private readonly
object _activeTileFetchLock =
new object();
52 private CancellationTokenSource _activeTileFetchCts;
53 private int _redrawQueued = 0;
54 private int _displayRangeProbeStarted = 0;
55 private int _displayRangeSeedLevel = -1;
56 private readonly SemaphoreSlim _directZarrInitSemaphore =
new SemaphoreSlim(1, 1);
57 private OmeZarrReader _directZarrReader;
58 private List<ResolutionLevelNode> _directZarrLevels;
59 private string _directZarrSource;
66 private static Task RunOnGtkThreadAsync(System.Action action)
68 var tcs =
new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
69 Gtk.Application.Invoke((s, a) =>
74 tcs.TrySetResult(
true);
78 tcs.TrySetException(ex);
84 public void SetSource(OpenSlideBase source)
86 ResetDirectZarrReader();
87 _glArea.DiscardCompleteFrame();
88 _openSlideBase = source;
91 _openTileCache =
new OpenSlideGTK.TileCache(source, 200);
95 public void SetSource(SlideBase source)
97 ResetDirectZarrReader();
98 _glArea.DiscardCompleteFrame();
100 _openSlideBase =
null;
101 _useOpenSlide =
false;
102 _tileCache =
new BioLib.TileCache(source, 200);
106 private void ResetDirectZarrReader()
108 _directZarrReader =
null;
109 _directZarrLevels =
null;
110 _directZarrSource =
null;
113 private void CancelActiveTileFetches()
115 CancellationTokenSource active;
116 lock (_activeTileFetchLock)
118 active = _activeTileFetchCts;
119 _activeTileFetchCts =
null;
129 catch (ObjectDisposedException)
135 private async Task<List<ResolutionLevelNode>> GetDirectZarrLevelsAsync(BioImage image, CancellationToken ct)
137 string source = image?.SourceFile;
138 if (
string.IsNullOrWhiteSpace(source))
141 if (source.StartsWith(
"s3://", StringComparison.OrdinalIgnoreCase))
143 source =
"https://uk1s3.embassy.ebi.ac.uk/" +
144 source.Substring(
"s3://".Length).TrimStart(
'/');
147 if (_directZarrLevels !=
null &&
148 string.Equals(_directZarrSource, source, StringComparison.Ordinal))
150 return _directZarrLevels;
153 await _directZarrInitSemaphore.WaitAsync(ct).ConfigureAwait(
false);
156 if (_directZarrLevels ==
null ||
157 !
string.Equals(_directZarrSource, source, StringComparison.Ordinal))
159 _directZarrReader = await OmeZarrReader.OpenAsync(source, ct).ConfigureAwait(
false);
160 var multiscale = await _directZarrReader.AsMultiscaleImageAsync(ct).ConfigureAwait(
false);
161 _directZarrLevels = (await multiscale.OpenAllResolutionLevelsAsync().ConfigureAwait(
false)).ToList();
162 _directZarrSource = source;
165 return _directZarrLevels;
169 _directZarrInitSemaphore.Release();
173 public async Task<bool> PopulateZarrStatisticsAsync(
176 CancellationToken ct =
default)
178 if (image?.IsZarrSource !=
true || image.Channels ==
null || image.Channels.Count == 0)
181 List<ResolutionLevelNode> levels = await GetDirectZarrLevelsAsync(image, ct).ConfigureAwait(
false);
182 if (levels ==
null || levels.Count == 0 || image.Resolutions ==
null || image.Resolutions.Count == 0)
187 int usableLevels = Math.Min(levels.Count, image.Resolutions.Count);
189 long bestDistance =
long.MaxValue;
190 const int targetDimension = 1024;
191 for (
int level = 0; level < usableLevels; level++)
193 int maxDimension = Math.Max(image.Resolutions[level].SizeX, image.Resolutions[level].SizeY);
194 long distance = Math.Abs((
long)maxDimension - targetDimension);
195 if (maxDimension > 0 && distance < bestDistance)
197 bestDistance = distance;
202 int width = image.Resolutions[sampleLevel].SizeX;
203 int height = image.Resolutions[sampleLevel].SizeY;
204 if (width <= 0 || height <= 0)
207 ResolutionLevelNode levelNode = levels[sampleLevel];
208 bool packedPlanarRgb = image.Resolutions[0].PixelFormat == PixelFormat.Format48bppRgb &&
209 image.Channels.Count == 1;
210 int sourceChannelCount = packedPlanarRgb
211 ? Math.Max(3, image.RGBChannelCount)
212 : image.Channels.Count;
213 var imageStatistics =
new Statistics(image.bitsPerPixel > 8);
214 bool gotAnyStatistics =
false;
216 for (
int channelIndex = 0; channelIndex < image.Channels.Count; channelIndex++)
218 int samplesForChannel = packedPlanarRgb ? sourceChannelCount : 1;
219 var channelStatistics =
new Statistics[samplesForChannel];
220 for (
int sample = 0; sample < samplesForChannel; sample++)
222 ct.ThrowIfCancellationRequested();
223 int sourceChannel = packedPlanarRgb ? sample : channelIndex;
224 var plane = await levelNode.ReadTileAsync(
229 ct: ct).ConfigureAwait(
false);
230 byte[] data = plane?.Data;
231 if (data ==
null || data.Length == 0)
234 int bytesPerSample = Math.Max(1, data.Length / Math.Max(1, width * height));
235 int bitsPerSample = bytesPerSample >= 2 ? 16 : 8;
236 PixelFormat statisticsFormat = bitsPerSample > 8
237 ? PixelFormat.Format16bppGrayScale
238 : PixelFormat.Format8bppIndexed;
239 int stride = width * (bitsPerSample / 8);
240 Statistics[] rawStatistics = Statistics.FromBytes(
241 data, width, height, 1, bitsPerSample, stride, statisticsFormat);
242 if (rawStatistics ==
null || rawStatistics.Length == 0 || rawStatistics[0] ==
null)
245 var aggregate =
new Statistics(bitsPerSample > 8);
246 aggregate.AddStatistics(rawStatistics[0]);
247 aggregate.MeanHistogram();
248 channelStatistics[sample] = aggregate;
249 imageStatistics.AddStatistics(rawStatistics[0]);
250 gotAnyStatistics =
true;
253 if (channelStatistics.All(statistics => statistics !=
null))
254 image.Channels[channelIndex].stats = channelStatistics;
257 if (!gotAnyStatistics)
260 imageStatistics.MeanHistogram();
261 image.Statistics = imageStatistics;
265 public async Task<double?> SampleZarrPixelAsync(
270 CancellationToken ct =
default)
272 if (image?.IsZarrSource !=
true || pixelX < 0 || pixelY < 0 ||
273 pixelX >= image.SizeX || pixelY >= image.SizeY)
276 List<ResolutionLevelNode> levels = await GetDirectZarrLevelsAsync(image, ct).ConfigureAwait(
false);
277 if (levels ==
null || levels.Count == 0)
280 var plane = await levels[0].ReadTileAsync(
281 pixelX, pixelY, 1, 1,
285 ct: ct).ConfigureAwait(
false);
286 byte[] data = plane?.Data;
287 if (data ==
null || data.Length == 0)
290 PixelFormat format = image.Resolutions.Count > 0
291 ? image.Resolutions[0].PixelFormat
292 : PixelFormat.Format8bppIndexed;
295 PixelFormat.Format16bppGrayScale or PixelFormat.Format48bppRgb
296 when data.Length >= 2 => BitConverter.ToUInt16(data, 0),
297 PixelFormat.Short when data.Length >= 2 => BitConverter.ToInt16(data, 0),
298 PixelFormat.Float when data.Length >= 4 => BitConverter.ToSingle(data, 0),
299 PixelFormat.UInt when data.Length >= 4 => BitConverter.ToUInt32(data, 0),
304 public void ClearCache(
bool resetLevel =
true,
bool cancelActiveFetches =
true)
309 Interlocked.Increment(ref _renderStateVersion);
310 if (cancelActiveFetches)
311 CancelActiveTileFetches();
313 _glArea.ClearTextureCache();
314 lock (_uploadedTileLock)
316 _uploadedTiles.Clear();
317 _uploadedTileInfos.Clear();
322 _openTileCache?.cache.cacheMap.Clear();
324 _slideBase?.cache?.Clear();
325 _openSlideBase?.cache?.cache.cacheMap.Clear();
326 _displayRangeSeedLevel = -1;
328 lock (_pendingLevelSwitchLock)
330 _pendingLevelRenderInfos =
null;
332 _pendingPreviousLevel = -1;
335 lock (_pendingTileFetchLock)
337 _pendingTileFetches.Clear();
338 _tileRetryCounts.Clear();
348 Interlocked.Increment(ref _renderStateVersion);
349 if (cancelActiveFetches)
350 CancelActiveTileFetches();
351 lock (_pendingTileFetchLock)
353 _pendingTileFetches.Clear();
354 _tileRetryCounts.Clear();
365 Interlocked.Increment(ref _viewportVersion);
366 lock (_pendingLevelSwitchLock)
368 _pendingLevelRenderInfos =
null;
370 _pendingPreviousLevel = -1;
374 public async Task UpdateViewAsync(
375 PointD pyramidalOrigin,
381 if (_openSlideBase ==
null && _slideBase ==
null)
383 if (viewportWidth <= 1 && viewportHeight <= 1)
389 if (!_renderSemaphore.Wait(0))
391 LastRenderSkipped =
true;
395 LastRenderSkipped =
false;
399 int renderVersion = _renderStateVersion;
400 int viewportVersion = _viewportVersion;
401 CancellationTokenSource tileFetchCts;
402 lock (_activeTileFetchLock)
408 if (_activeTileFetchCts ==
null || _activeTileFetchCts.IsCancellationRequested)
409 _activeTileFetchCts =
new CancellationTokenSource();
410 tileFetchCts = _activeTileFetchCts;
413 var schema = _useOpenSlide ? _openSlideBase.Schema : _slideBase.Schema;
415 int level = TileUtil.GetLevel(schema.Resolutions, resolution);
416 var levelRes = schema.Resolutions[level];
417 double levelUnitsPerPixel = levelRes.UnitsPerPixel;
419 await EnsureGlobalDisplayRangeAsync(coordinate, renderVersion, level).ConfigureAwait(
false);
421 bool levelChanged = level != _currentLevel;
428 double originX = pyramidalOrigin.X;
429 double originY = pyramidalOrigin.Y;
430 double minX = originX;
431 double width = viewportWidth * resolution;
432 double height = viewportHeight * resolution;
436 var viewportExtent =
new Extent(minX, -originY - height, minX + width, -originY);
440 double imageMinX = schema.Extent.MinX;
441 double imageMaxX = schema.Extent.MaxX;
442 double imageMinY = schema.Extent.MinY;
443 double imageMaxY = schema.Extent.MaxY;
444 _glArea.ImageScreenX = (float)((imageMinX - pyramidalOrigin.X) / resolution);
445 _glArea.ImageScreenY = (float)((-imageMaxY - pyramidalOrigin.Y) / resolution);
446 _glArea.ImageScreenW = (float)((imageMaxX - imageMinX) / resolution);
447 _glArea.ImageScreenH = (float)((imageMaxY - imageMinY) / resolution);
451 double tileMarginX = levelRes.TileWidth * levelUnitsPerPixel;
452 double tileMarginY = levelRes.TileHeight * levelUnitsPerPixel;
453 var fetchExtent =
new Extent(
454 viewportExtent.MinX - tileMarginX,
455 viewportExtent.MinY - tileMarginY,
456 viewportExtent.MaxX + tileMarginX,
457 viewportExtent.MaxY + tileMarginY);
459 List<TileInfo> tileInfos;
460 List<TileInfo> fallbackInfos;
464 var lookupExtent =
BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
465 tileInfos = schema.GetTileInfos(lookupExtent, level).ToList();
466 fallbackInfos = schema.GetTileInfos(fetchExtent, level).ToList();
468 if (fallbackInfos.Count > 0)
470 var seen =
new HashSet<TileIndex>(tileInfos.Select(t => t.Index));
471 foreach (var info
in fallbackInfos)
473 if (seen.Add(info.Index))
483 tileInfos = GetTileInfosIncludingBoundaries(schema, fetchExtent, level);
484 fallbackInfos =
new List<TileInfo>();
486 if (tileInfos.Count == 0)
488 var pixelToWorldExtent =
BioLib.ExtentEx.PixelToWorldInvertedY(fetchExtent, levelUnitsPerPixel);
489 fallbackInfos = schema.GetTileInfos(pixelToWorldExtent, level).ToList();
491 if (fallbackInfos.Count == 0)
493 var worldToPixelExtent =
BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
494 fallbackInfos = schema.GetTileInfos(worldToPixelExtent, level).ToList();
497 if (fallbackInfos.Count > 0)
498 tileInfos = fallbackInfos;
502 var renderInfos =
new List<TileRenderInfo>();
503 var pendingFetches =
new List<(TileInfo Tile, Task<byte[]> FetchTask)>();
504 foreach (var tileInfo
in tileInfos)
506 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
509 var fetchTileInfo = tileInfo;
510 var renderTileInfo = tileInfo;
512 var renderInfo = CalculateScreenPosition(
518 renderInfos.Add(renderInfo);
520 if (!_glArea.HasTileTexture(fetchTileInfo.Index))
523 lock (_pendingTileFetchLock)
525 alreadyPending = !_pendingTileFetches.Add(fetchTileInfo.Index);
529 pendingFetches.Add((fetchTileInfo, FetchTileAsync(fetchTileInfo, level, coordinate, tileFetchCts.Token)));
536 await RunOnGtkThreadAsync(() =>
537 _glArea.SetProtectedTileIndices(renderInfos.Select(info => info.Index)))
538 .ConfigureAwait(
false);
540 if (pendingFetches.Count > 0)
542 foreach (var pending
in pendingFetches)
543 _ = ProcessPendingFetchAsync(pending.Tile, pending.FetchTask, tileFetchCts.Token, renderVersion, pyramidalOrigin, resolution, level, schema);
546 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
549 bool hasRenderableTexture = renderInfos.Any(tile => _glArea.HasTileTexture(tile.Index));
552 var fallbackRenderInfos = GetCachedLevelRenderInfos(
560 if (hasRenderableTexture)
562 bool canSwapNow = renderInfos.All(tile => _glArea.HasTileTexture(tile.Index));
565 _glArea.PreservePreviousFrameWhileIncomplete =
false;
566 _glArea.ReleaseLevelTextures(_currentLevel);
567 _currentLevel = level;
568 lock (_pendingLevelSwitchLock)
570 _pendingLevelRenderInfos =
null;
572 _pendingPreviousLevel = -1;
577 _glArea.PreservePreviousFrameWhileIncomplete =
true;
578 lock (_pendingLevelSwitchLock)
580 _pendingLevelRenderInfos =
new List<TileRenderInfo>(renderInfos);
581 _pendingLevel = level;
582 _pendingPreviousLevel = _currentLevel;
584 await ShowImmediateZoomFallbackAsync(fallbackRenderInfos).ConfigureAwait(
false);
590 _glArea.PreservePreviousFrameWhileIncomplete =
true;
591 lock (_pendingLevelSwitchLock)
593 _pendingLevelRenderInfos =
new List<TileRenderInfo>(renderInfos);
594 _pendingLevel = level;
595 _pendingPreviousLevel = _currentLevel;
597 await ShowImmediateZoomFallbackAsync(fallbackRenderInfos).ConfigureAwait(
false);
602 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
605 await RunOnGtkThreadAsync(() =>
607 _glArea.SetTilesToRender(renderInfos);
608 _glArea.RequestRedraw();
611 catch (OperationCanceledException)
618 _renderSemaphore.Release();
620 if (LastRenderSkipped)
622 LastRenderSkipped =
false;
623 Gtk.Application.Invoke((s, a) => App.viewer?.RequestDeferredRender());
628 private static List<TileInfo> GetTileInfosIncludingBoundaries(
630 Extent requestedExtent,
633 var tiles = schema.GetTileInfos(requestedExtent, level).ToList();
634 if (!schema.Resolutions.TryGetValue(level, out var resolution))
637 double tileWorldWidth = resolution.TileWidth * resolution.UnitsPerPixel;
638 double tileWorldHeight = resolution.TileHeight * resolution.UnitsPerPixel;
639 if (tileWorldWidth <= 0 || tileWorldHeight <= 0)
642 double minX = Math.Max(requestedExtent.MinX, schema.Extent.MinX);
643 double maxX = Math.Min(requestedExtent.MaxX, schema.Extent.MaxX);
644 double minY = Math.Max(requestedExtent.MinY, schema.Extent.MinY);
645 double maxY = Math.Min(requestedExtent.MaxY, schema.Extent.MaxY);
646 if (maxX <= minX || maxY <= minY)
649 int maxColumn = Math.Max(0, (
int)Math.Ceiling(schema.Extent.Width / tileWorldWidth) - 1);
650 int maxRow = Math.Max(0, (
int)Math.Ceiling(schema.Extent.Height / tileWorldHeight) - 1);
651 double originX = schema.Extent.MinX;
652 double originY = schema.Extent.MaxY;
653 int firstColumn = Math.Clamp(
654 (
int)Math.Floor((minX - originX) / tileWorldWidth),
657 int lastColumn = Math.Clamp(
658 (
int)Math.Floor((Math.BitDecrement(maxX) - originX) / tileWorldWidth),
661 int firstRow = Math.Clamp(
662 (
int)Math.Floor((originY - maxY) / tileWorldHeight),
665 int lastRow = Math.Clamp(
666 (
int)Math.Floor((originY - Math.BitIncrement(minY)) / tileWorldHeight),
670 var existing =
new HashSet<TileIndex>(tiles.Select(tile => tile.Index));
671 for (
int row = firstRow; row <= lastRow; row++)
673 for (
int column = firstColumn; column <= lastColumn; column++)
675 var index =
new TileIndex(column, row, level);
676 if (!existing.Add(index))
679 double tileMinX = originX + column * tileWorldWidth;
680 double tileMaxY = originY - row * tileWorldHeight;
681 tiles.Add(
new TileInfo
686 tileMaxY - tileWorldHeight,
687 tileMinX + tileWorldWidth,
696 private List<TileRenderInfo> GetCachedLevelRenderInfos(
698 PointD pyramidalOrigin,
705 return new List<TileRenderInfo>();
707 List<KeyValuePair<TileIndex, TileInfo>> uploadedTileSnapshot;
708 lock (_uploadedTileLock)
709 uploadedTileSnapshot = _uploadedTileInfos.ToList();
711 return uploadedTileSnapshot
712 .Where(pair => pair.Key.Level == level && _glArea.HasTileTexture(pair.Key))
713 .Select(pair => CalculateScreenPosition(
720 tile.ScreenWidth > 0 && tile.ScreenHeight > 0 &&
721 tile.ScreenX < viewportWidth && tile.ScreenY < viewportHeight &&
722 tile.ScreenX + tile.ScreenWidth > 0 &&
723 tile.ScreenY + tile.ScreenHeight > 0)
727 private Task ShowImmediateZoomFallbackAsync(List<TileRenderInfo> fallbackRenderInfos)
729 if (fallbackRenderInfos ==
null || fallbackRenderInfos.Count == 0)
730 return Task.CompletedTask;
732 return RunOnGtkThreadAsync(() =>
734 _glArea.PreservePreviousFrameWhileIncomplete =
true;
735 _glArea.SetTilesToRender(fallbackRenderInfos);
736 _glArea.RequestRedraw();
740 private async Task ProcessPendingFetchAsync(
742 Task<
byte[]> fetchTask,
743 CancellationToken ct,
745 PointD pyramidalOrigin,
750 bool tileUploaded =
false;
751 bool retryMissingTile =
false;
754 byte[] tileData = await fetchTask.ConfigureAwait(
false);
755 if (ct.IsCancellationRequested || renderVersion != _renderStateVersion)
758 if (tileData ==
null)
760 retryMissingTile =
true;
764 int tW = schema.Resolutions[level].TileWidth;
765 int tH = schema.Resolutions[level].TileHeight;
769 if (tileData.Length < (
long)tW * tH * 4)
771 retryMissingTile =
true;
775 await RunOnGtkThreadAsync(() =>
776 _glArea.UploadTileTexture(tile.Index, tileData, tW, tH));
777 if (!_glArea.HasTileTexture(tile.Index))
779 retryMissingTile =
true;
782 lock (_uploadedTileLock)
784 _uploadedTiles.Add(tile.Index);
785 _uploadedTileInfos[tile.Index] = tile;
789 List<TileRenderInfo> pendingLevelRenderInfos =
null;
790 int pendingPreviousLevel = -1;
791 bool applyPendingLevel =
false;
792 lock (_pendingLevelSwitchLock)
794 bool completeLevelCanRender = _pendingLevelRenderInfos !=
null &&
795 _pendingLevelRenderInfos.All(info => _glArea.HasTileTexture(info.Index));
796 if (_pendingLevelRenderInfos !=
null &&
797 level == _pendingLevel &&
798 completeLevelCanRender)
800 applyPendingLevel =
true;
801 pendingLevelRenderInfos = _pendingLevelRenderInfos;
802 pendingPreviousLevel = _pendingPreviousLevel;
803 _pendingLevelRenderInfos =
null;
805 _pendingPreviousLevel = -1;
806 _currentLevel = level;
810 if (applyPendingLevel)
812 await RunOnGtkThreadAsync(() =>
814 if (pendingPreviousLevel >= 0 && pendingPreviousLevel != level)
815 _glArea.ReleaseLevelTextures(pendingPreviousLevel);
816 _glArea.PreservePreviousFrameWhileIncomplete =
false;
817 _glArea.SetTilesToRender(pendingLevelRenderInfos);
818 _glArea.RequestRedraw();
822 if (renderVersion == _renderStateVersion &&
823 Interlocked.Exchange(ref _redrawQueued, 1) == 0)
825 await RunOnGtkThreadAsync(() =>
829 _glArea.RequestRedraw();
833 Interlocked.Exchange(ref _redrawQueued, 0);
838 catch (OperationCanceledException)
843 Console.WriteLine($
"Error processing tile {tile.Index}: {ex.Message}");
844 retryMissingTile = !ct.IsCancellationRequested &&
845 renderVersion == _renderStateVersion;
849 int retryAttempt = 0;
850 lock (_pendingTileFetchLock)
852 _pendingTileFetches.Remove(tile.Index);
855 _tileRetryCounts.Remove(tile.Index);
857 else if (retryMissingTile && !ct.IsCancellationRequested &&
858 renderVersion == _renderStateVersion)
860 _tileRetryCounts.TryGetValue(tile.Index, out
int previousAttempts);
861 retryAttempt = previousAttempts + 1;
862 _tileRetryCounts[tile.Index] = retryAttempt;
866 if (retryAttempt > 0 && retryAttempt <= MaxMissingTileRetries)
867 _ = RequestMissingTileRetryAsync(renderVersion, retryAttempt);
871 private async Task RequestMissingTileRetryAsync(
int renderVersion,
int retryAttempt)
875 int exponentialStep = Math.Min(retryAttempt - 1, 4);
876 int delayMilliseconds = Math.Min(4000, 250 * (1 << exponentialStep));
877 await Task.Delay(delayMilliseconds).ConfigureAwait(
false);
878 if (renderVersion != _renderStateVersion)
881 await RunOnGtkThreadAsync(() => App.viewer?.RequestDeferredRender())
882 .ConfigureAwait(
false);
890 private static byte[] CropBgraTile(
byte[] src,
int srcW,
int srcH,
int srcX,
int srcY,
int dstW,
int dstH)
892 if (src ==
null || src.Length == 0 || srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0)
893 return Array.Empty<
byte>();
894 if (srcX < 0 || srcY < 0 || srcX >= srcW || srcY >= srcH)
895 return Array.Empty<
byte>();
896 if (srcX == 0 && srcY == 0 && srcW == dstW && srcH == dstH)
899 int srcRowBytes = srcW * 4;
900 int dstRowBytes = dstW * 4;
901 byte[] cropped =
new byte[dstRowBytes * dstH];
902 int copyRows = Math.Min(dstH, srcH);
903 for (
int y = 0; y < copyRows; y++)
905 int srcRow = (srcY + y) * srcRowBytes + srcX * 4;
906 int dstRow = y * dstRowBytes;
907 if (srcRow < 0 || srcRow >= src.Length)
909 int bytesToCopy = Math.Min(dstRowBytes, Math.Max(0, src.Length - srcRow));
910 if (bytesToCopy <= 0)
912 Buffer.BlockCopy(src, srcRow, cropped, dstRow, bytesToCopy);
917 private async Task EnsureGlobalDisplayRangeAsync(ZCT coordinate,
int renderVersion,
int renderLevel)
919 if (_useOpenSlide || _slideBase?.Image?.BioImage ==
null)
922 var bioImage = _slideBase.Image.BioImage;
925 if (bioImage.ZarrDisplayMax > bioImage.ZarrDisplayMin &&
926 _displayRangeSeedLevel == seedLevel)
928 if (Interlocked.Exchange(ref _displayRangeProbeStarted, 1) != 0)
930 if (bioImage.Resolutions ==
null || bioImage.Resolutions.Count == 0)
932 Interlocked.Exchange(ref _displayRangeProbeStarted, 0);
936 var pixelFormat = bioImage.Resolutions[0].PixelFormat;
941 if (pixelFormat != PixelFormat.Format16bppGrayScale)
943 Interlocked.Exchange(ref _displayRangeProbeStarted, 0);
947 int width = bioImage.Resolutions[seedLevel].SizeX;
948 int height = bioImage.Resolutions[seedLevel].SizeY;
949 if (width <= 0 || height <= 0)
951 Interlocked.Exchange(ref _displayRangeProbeStarted, 0);
955 int frameIndex = bioImage.GetFrameIndex(coordinate.Z, coordinate.C, coordinate.T);
959 int sampleW = Math.Min(width, 128);
960 int sampleH = Math.Min(height, 128);
964 Math.Max(0, (width - sampleW) / 2),
965 Math.Max(0, width - sampleW)
970 Math.Max(0, (height - sampleH) / 2),
971 Math.Max(0, height - sampleH)
975 bool gotRange =
false;
976 foreach (
int sampleY
in ys)
978 foreach (
int sampleX
in xs)
980 var seedBitmap = await bioImage.GetTile(frameIndex, seedLevel, sampleX, sampleY, sampleW, sampleH,
null,
true);
981 if (TryComputeDisplayRangeFromUsedBits(seedBitmap, out ushort displayMin, out ushort displayMax))
983 if (displayMax > bestMax)
984 bestMax = displayMax;
987 seedBitmap?.Dispose();
991 if (gotRange && bestMax > 0)
993 if (renderVersion != _renderStateVersion)
995 Interlocked.Exchange(ref _displayRangeProbeStarted, 0);
999 bool rangeChanged = bioImage.ZarrDisplayMin != 0 ||
1000 bioImage.ZarrDisplayMax != bestMax;
1001 bioImage.ZarrDisplayMin = 0;
1002 bioImage.ZarrDisplayMax = bestMax;
1003 _displayRangeSeedLevel = seedLevel;
1007 await RunOnGtkThreadAsync(() =>
1012 ClearCache(resetLevel:
false, cancelActiveFetches:
false);
1013 _displayRangeSeedLevel = seedLevel;
1014 _glArea.RequestRedraw();
1015 App.viewer?.RequestDeferredRender();
1018 else if (renderVersion == _renderStateVersion)
1020 await RunOnGtkThreadAsync(() => _glArea.RequestRedraw());
1023 Interlocked.Exchange(ref _displayRangeProbeStarted, 0);
1026 private static ushort SnapUsedBitCeiling(ushort value)
1032 while (ceiling - 1 < value && ceiling < (1u << 16))
1035 return (ushort)Math.Min(ushort.MaxValue, ceiling - 1);
1038 private static bool TryComputeDisplayRangeFromUsedBits(Bitmap bitmap, out ushort displayMin, out ushort displayMax)
1043 if (bitmap ==
null || bitmap.Bytes ==
null || bitmap.Bytes.Length < 2)
1045 if (bitmap.PixelFormat != PixelFormat.Format16bppGrayScale &&
1046 bitmap.PixelFormat != PixelFormat.Format48bppRgb)
1049 byte[] bytes = bitmap.Bytes;
1050 bool littleEndian = bitmap.LittleEndian;
1051 int valueCount = bytes.Length / 2;
1052 if (valueCount <= 0)
1061 for (
int valueIndex = 0; valueIndex < valueCount; valueIndex++)
1063 int offset = valueIndex * 2;
1064 ushort value = littleEndian
1065 ? (ushort)(bytes[offset] | (bytes[offset + 1] << 8))
1066 : (ushort)((bytes[offset] << 8) | bytes[offset + 1]);
1067 if (value > rawMax) rawMax = value;
1074 displayMax = SnapUsedBitCeiling(rawMax);
1078 private async Task<byte[]> FetchTileAsync(TileInfo tileInfo,
int level, ZCT coordinate, CancellationToken ct)
1080 bool semaphoreEntered =
false;
1083 await _fetchSemaphore.WaitAsync(ct).ConfigureAwait(
false);
1084 semaphoreEntered =
true;
1089 if (ShouldComposeRgbTile())
1090 return await FetchRgbCompositeTileAsync(tileInfo, level, coordinate, ct).ConfigureAwait(
false);
1092 return await FetchSingleTileAsync(tileInfo, level, coordinate, ct).ConfigureAwait(
false);
1094 catch (OperationCanceledException) when (ct.IsCancellationRequested)
1100 catch (Exception ex)
1102 Console.WriteLine($
"Error fetching tile {tileInfo.Index}: {ex.Message}");
1107 if (semaphoreEntered)
1108 _fetchSemaphore.Release();
1112 private bool ShouldComposeRgbTile()
1114 if (_useOpenSlide || App.viewer?.Mode != ImageView.ViewMode.RGBImage)
1117 var bioImage = _slideBase?.Image?.BioImage;
1118 if (bioImage ==
null || bioImage.Channels ==
null)
1120 if (bioImage.Resolutions ==
null || bioImage.Resolutions.Count == 0)
1123 var pixelFormat = bioImage.Resolutions[0].PixelFormat;
1128 if (bioImage.IsZarrSource && pixelFormat == PixelFormat.Format48bppRgb)
1131 if (bioImage.Channels.Count <= 1)
1134 return pixelFormat != PixelFormat.Format24bppRgb &&
1135 pixelFormat != PixelFormat.Format48bppRgb;
1138 private async Task<byte[]> FetchRgbCompositeTileAsync(TileInfo tileInfo,
int level, ZCT coordinate, CancellationToken ct)
1140 var bioImage = _slideBase?.Image?.BioImage;
1141 if (bioImage ==
null)
1142 return await FetchSingleTileAsync(tileInfo, level, coordinate, ct).ConfigureAwait(
false);
1144 bool planarRgb16 = bioImage.IsZarrSource &&
1145 bioImage.Resolutions.Count > 0 &&
1146 bioImage.Resolutions[0].PixelFormat == PixelFormat.Format48bppRgb;
1147 int channelCount = planarRgb16 ? 3 : Math.Max(1, bioImage.Channels.Count);
1148 int redIndex = Math.Clamp(bioImage.rgbChannels[0], 0, channelCount - 1);
1149 int greenIndex = Math.Clamp(bioImage.rgbChannels[1], 0, channelCount - 1);
1150 int blueIndex = Math.Clamp(bioImage.rgbChannels[2], 0, channelCount - 1);
1152 Task<byte[]> redTask = FetchSlideTileDirectAsync(tileInfo, level,
new ZCT(coordinate.Z, redIndex, coordinate.T), ct);
1153 Task<byte[]> greenTask = FetchSlideTileDirectAsync(tileInfo, level,
new ZCT(coordinate.Z, greenIndex, coordinate.T), ct);
1154 Task<byte[]> blueTask = FetchSlideTileDirectAsync(tileInfo, level,
new ZCT(coordinate.Z, blueIndex, coordinate.T), ct);
1156 await Task.WhenAll(redTask, greenTask, blueTask).ConfigureAwait(
false);
1157 return ComposeBgraTiles(redTask.Result, greenTask.Result, blueTask.Result);
1160 private async Task<byte[]> FetchSlideTileDirectAsync(TileInfo tileInfo,
int level, ZCT coordinate, CancellationToken ct)
1162 if (IsPlanar16BitZarrLevel(level))
1163 return await FetchGray16ZarrTileAsync(tileInfo, level, coordinate, ct).ConfigureAwait(
false);
1167 return await _slideBase.GetTileAsync(
1168 new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent, coordinate),
1170 ct).ConfigureAwait(
false);
1173 private async Task<byte[]> FetchSingleTileAsync(TileInfo tileInfo,
int level, ZCT coordinate, CancellationToken ct)
1175 if (IsPlanar16BitZarrLevel(level))
1176 return await FetchGray16ZarrTileAsync(tileInfo, level, coordinate, ct).ConfigureAwait(
false);
1178 var fetchTask = _useOpenSlide
1179 ? Task.Run(() => _openTileCache.GetTile(
new OpenSlideGTK.Info(coordinate, tileInfo.Index, tileInfo.Extent, level)))
1180 : _tileCache.GetTile(
new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent, coordinate), ct);
1184 var bt = await fetchTask.ConfigureAwait(
false);
1188 return await fetchTask.ConfigureAwait(
false);
1191 private bool IsPlanar16BitZarrLevel(
int level)
1193 var image = _slideBase?.Image?.BioImage;
1194 return !_useOpenSlide && image !=
null && image.IsZarrSource &&
1195 level >= 0 && level < image.Resolutions.Count &&
1196 (image.Resolutions[level].PixelFormat == PixelFormat.Format16bppGrayScale ||
1197 image.Resolutions[level].PixelFormat == PixelFormat.Format48bppRgb);
1200 private async Task<byte[]> FetchGray16ZarrTileAsync(TileInfo tileInfo,
int level, ZCT coordinate, CancellationToken ct)
1202 var image = _slideBase.Image.BioImage;
1203 var tileResolution = _slideBase.Schema.Resolutions[level];
1204 int tileWidth = tileResolution.TileWidth;
1205 int tileHeight = tileResolution.TileHeight;
1206 int x = checked(tileInfo.Index.Col * tileWidth);
1207 int y = checked(tileInfo.Index.Row * tileHeight);
1208 var dimensions = _slideBase.Image.GetLevelDimension(level);
1209 int width = (int)Math.Max(0, Math.Min(tileWidth, dimensions.Width - x));
1210 int height = (int)Math.Max(0, Math.Min(tileHeight, dimensions.Height - y));
1211 if (width == 0 || height == 0 || x < 0 || y < 0)
1212 return new byte[tileWidth * tileHeight * 4];
1218 bool planarRgb16 = image.Resolutions[level].PixelFormat == PixelFormat.Format48bppRgb;
1224 List<ResolutionLevelNode> levels = await GetDirectZarrLevelsAsync(image, ct).ConfigureAwait(
false);
1225 if (levels ==
null || levels.Count == 0)
1228 ResolutionLevelNode levelNode = levels[Math.Clamp(level, 0, levels.Count - 1)];
1229 var plane = await levelNode.ReadTileAsync(
1230 x, y, width, height,
1234 ct: ct).ConfigureAwait(
false);
1239 raw = await image.GetTileBytesRaw(
1240 image.GetFrameIndex(coordinate.Z, coordinate.C, coordinate.T),
1241 level, x, y, width, height, coordinate)
1242 .WaitAsync(RemoteTileFetchTimeout, ct).ConfigureAwait(
false);
1244 if (raw ==
null || raw.Length == 0)
1247 int min = image.ZarrDisplayMin;
1248 int max = image.ZarrDisplayMax;
1252 max = ushort.MaxValue;
1254 return Gray16TileConverter.ToBgra(raw, width, height, tileWidth, tileHeight, min, max);
1257 private static byte[] ComposeBgraTiles(
byte[] redTile,
byte[] greenTile,
byte[] blueTile)
1259 byte[] fallback = redTile ?? greenTile ?? blueTile;
1260 if (fallback ==
null)
1263 redTile ??= fallback;
1264 greenTile ??= fallback;
1265 blueTile ??= fallback;
1267 int byteCount = Math.Min(redTile.Length, Math.Min(greenTile.Length, blueTile.Length));
1268 byteCount -= byteCount % 4;
1272 byte[] composed =
new byte[byteCount];
1273 for (
int i = 0; i < byteCount; i += 4)
1275 byte r = ExtractIntensity(redTile, i);
1276 byte g = ExtractIntensity(greenTile, i);
1277 byte b = ExtractIntensity(blueTile, i);
1278 byte a = (byte)Math.Max(redTile[i + 3], Math.Max(greenTile[i + 3], blueTile[i + 3]));
1279 if (a == 0 && (r != 0 || g != 0 || b != 0))
1283 composed[i + 1] = g;
1284 composed[i + 2] = r;
1285 composed[i + 3] = a;
1291 private static byte ExtractIntensity(
byte[] tile,
int offset)
1293 return (
byte)Math.Max(tile[offset], Math.Max(tile[offset + 1], tile[offset + 2]));
1296 private TileRenderInfo CalculateScreenPosition(
1298 PointD pyramidalOrigin,
1299 double viewResolution,
1310 var tileExtent = tile.Extent;
1311 if (tileExtent.MaxX <= tileExtent.MinX || tileExtent.MaxY <= tileExtent.MinY)
1312 return new TileRenderInfo(tile.Index, -9999, -9999, 0, 0);
1320 double originScreenX = pyramidalOrigin.X;
1321 double originScreenY = pyramidalOrigin.Y;
1323 float screenX = (float)((tileExtent.MinX - originScreenX) / viewResolution);
1324 float screenY = (float)((-tileExtent.MaxY - originScreenY) / viewResolution);
1325 float screenWidth = (float)((tileExtent.MaxX - tileExtent.MinX) / viewResolution);
1326 float screenHeight = (float)((tileExtent.MaxY - tileExtent.MinY) / viewResolution);
1328 return new TileRenderInfo(tile.Index, screenX, screenY, screenWidth, screenHeight, u0, v0, u1, v1);
1331 public int CurrentLevel => _currentLevel;
1332 public int CachedTextureCount
1336 lock (_uploadedTileLock)
1337 return _uploadedTiles.Count;