BioGTK  6.7.0
A .NET library & program for annotating, editing various microscopy imaging formats using Bioformats supported images. Including whole slide, pyramidal, and series.
Loading...
Searching...
No Matches
BioGTK.SlideRenderer Class Reference

Public Member Functions

 SlideRenderer (SlideGLArea glArea)
 
void SetSource (OpenSlideBase source)
 
void SetSource (SlideBase source)
 
async Task< bool > PopulateZarrStatisticsAsync (BioImage image, ZCT coordinate, CancellationToken ct=default)
 
async Task< double?> SampleZarrPixelAsync (BioImage image, ZCT coordinate, int pixelX, int pixelY, CancellationToken ct=default)
 
void ClearCache (bool resetLevel=true, bool cancelActiveFetches=true)
 
void InvalidateRenderState (bool cancelActiveFetches=true)
 Marks all in-flight tile work as stale. Call this before changing threshold/range state so old tiles cannot overwrite the current view.
 
void InvalidateViewport ()
 Marks screen-space layout work as stale without cancelling tile I/O. Tile pixels are independent of the current pan/zoom and remain useful cache entries for the next viewport render.
 
async Task UpdateViewAsync (PointD pyramidalOrigin, int viewportWidth, int viewportHeight, double resolution, ZCT coordinate)
 

Properties

bool LastRenderSkipped [get]
 
int CurrentLevel [get]
 
int CachedTextureCount [get]
 

Detailed Description

Definition at line 15 of file SlideRenderer.cs.

Constructor & Destructor Documentation

◆ SlideRenderer()

BioGTK.SlideRenderer.SlideRenderer ( SlideGLArea glArea)

Definition at line 61 of file SlideRenderer.cs.

62 {
63 _glArea = glArea;
64 }

Member Function Documentation

◆ ClearCache()

void BioGTK.SlideRenderer.ClearCache ( bool resetLevel = true,
bool cancelActiveFetches = true )

Definition at line 304 of file SlideRenderer.cs.

305 {
306 // Clearing the visible tile cache must also invalidate any in-flight
307 // fetches. Otherwise an old Z/C/T request can finish after the
308 // coordinate changes and repopulate the GPU cache with stale tiles.
309 Interlocked.Increment(ref _renderStateVersion);
310 if (cancelActiveFetches)
311 CancelActiveTileFetches();
312
313 _glArea.ClearTextureCache();
314 lock (_uploadedTileLock)
315 {
316 _uploadedTiles.Clear();
317 _uploadedTileInfos.Clear();
318 }
319 if (resetLevel)
320 _currentLevel = -1;
321 _tileCache?.Clear();
322 _openTileCache?.cache.cacheMap.Clear();
323
324 _slideBase?.cache?.Clear();
325 _openSlideBase?.cache?.cache.cacheMap.Clear();
326 _displayRangeSeedLevel = -1;
327
328 lock (_pendingLevelSwitchLock)
329 {
330 _pendingLevelRenderInfos = null;
331 _pendingLevel = -1;
332 _pendingPreviousLevel = -1;
333 }
334
335 lock (_pendingTileFetchLock)
336 {
337 _pendingTileFetches.Clear();
338 _tileRetryCounts.Clear();
339 }
340 }
void ClearTextureCache()
Clear all cached textures.

◆ InvalidateRenderState()

void BioGTK.SlideRenderer.InvalidateRenderState ( bool cancelActiveFetches = true)

Marks all in-flight tile work as stale. Call this before changing threshold/range state so old tiles cannot overwrite the current view.

Definition at line 346 of file SlideRenderer.cs.

347 {
348 Interlocked.Increment(ref _renderStateVersion);
349 if (cancelActiveFetches)
350 CancelActiveTileFetches();
351 lock (_pendingTileFetchLock)
352 {
353 _pendingTileFetches.Clear();
354 _tileRetryCounts.Clear();
355 }
356 }

◆ InvalidateViewport()

void BioGTK.SlideRenderer.InvalidateViewport ( )

Marks screen-space layout work as stale without cancelling tile I/O. Tile pixels are independent of the current pan/zoom and remain useful cache entries for the next viewport render.

Definition at line 363 of file SlideRenderer.cs.

364 {
365 Interlocked.Increment(ref _viewportVersion);
366 lock (_pendingLevelSwitchLock)
367 {
368 _pendingLevelRenderInfos = null;
369 _pendingLevel = -1;
370 _pendingPreviousLevel = -1;
371 }
372 }

◆ PopulateZarrStatisticsAsync()

async Task< bool > BioGTK.SlideRenderer.PopulateZarrStatisticsAsync ( BioImage image,
ZCT coordinate,
CancellationToken ct = default )

Definition at line 173 of file SlideRenderer.cs.

177 {
178 if (image?.IsZarrSource != true || image.Channels == null || image.Channels.Count == 0)
179 return false;
180
181 List<ResolutionLevelNode> levels = await GetDirectZarrLevelsAsync(image, ct).ConfigureAwait(false);
182 if (levels == null || levels.Count == 0 || image.Resolutions == null || image.Resolutions.Count == 0)
183 return false;
184
185 // Use a compact but representative pyramid level. This avoids reading
186 // the full-resolution plane merely to draw a small histogram.
187 int usableLevels = Math.Min(levels.Count, image.Resolutions.Count);
188 int sampleLevel = 0;
189 long bestDistance = long.MaxValue;
190 const int targetDimension = 1024;
191 for (int level = 0; level < usableLevels; level++)
192 {
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)
196 {
197 bestDistance = distance;
198 sampleLevel = level;
199 }
200 }
201
202 int width = image.Resolutions[sampleLevel].SizeX;
203 int height = image.Resolutions[sampleLevel].SizeY;
204 if (width <= 0 || height <= 0)
205 return false;
206
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;
215
216 for (int channelIndex = 0; channelIndex < image.Channels.Count; channelIndex++)
217 {
218 int samplesForChannel = packedPlanarRgb ? sourceChannelCount : 1;
219 var channelStatistics = new Statistics[samplesForChannel];
220 for (int sample = 0; sample < samplesForChannel; sample++)
221 {
222 ct.ThrowIfCancellationRequested();
223 int sourceChannel = packedPlanarRgb ? sample : channelIndex;
224 var plane = await levelNode.ReadTileAsync(
225 0, 0, width, height,
226 z: coordinate.Z,
227 c: sourceChannel,
228 t: coordinate.T,
229 ct: ct).ConfigureAwait(false);
230 byte[] data = plane?.Data;
231 if (data == null || data.Length == 0)
232 continue;
233
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)
243 continue;
244
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;
251 }
252
253 if (channelStatistics.All(statistics => statistics != null))
254 image.Channels[channelIndex].stats = channelStatistics;
255 }
256
257 if (!gotAnyStatistics)
258 return false;
259
260 imageStatistics.MeanHistogram();
261 image.Statistics = imageStatistics;
262 return true;
263 }

◆ SampleZarrPixelAsync()

async Task< double?> BioGTK.SlideRenderer.SampleZarrPixelAsync ( BioImage image,
ZCT coordinate,
int pixelX,
int pixelY,
CancellationToken ct = default )

Definition at line 265 of file SlideRenderer.cs.

271 {
272 if (image?.IsZarrSource != true || pixelX < 0 || pixelY < 0 ||
273 pixelX >= image.SizeX || pixelY >= image.SizeY)
274 return null;
275
276 List<ResolutionLevelNode> levels = await GetDirectZarrLevelsAsync(image, ct).ConfigureAwait(false);
277 if (levels == null || levels.Count == 0)
278 return null;
279
280 var plane = await levels[0].ReadTileAsync(
281 pixelX, pixelY, 1, 1,
282 z: coordinate.Z,
283 c: coordinate.C,
284 t: coordinate.T,
285 ct: ct).ConfigureAwait(false);
286 byte[] data = plane?.Data;
287 if (data == null || data.Length == 0)
288 return null;
289
290 PixelFormat format = image.Resolutions.Count > 0
291 ? image.Resolutions[0].PixelFormat
292 : PixelFormat.Format8bppIndexed;
293 return format switch
294 {
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),
300 _ => data[0]
301 };
302 }

◆ SetSource() [1/2]

void BioGTK.SlideRenderer.SetSource ( OpenSlideBase source)

Definition at line 84 of file SlideRenderer.cs.

85 {
86 ResetDirectZarrReader();
87 _glArea.DiscardCompleteFrame();
88 _openSlideBase = source;
89 _slideBase = null;
90 _useOpenSlide = true;
91 _openTileCache = new OpenSlideGTK.TileCache(source, 200);
92 ClearCache();
93 }

◆ SetSource() [2/2]

void BioGTK.SlideRenderer.SetSource ( SlideBase source)

Definition at line 95 of file SlideRenderer.cs.

96 {
97 ResetDirectZarrReader();
98 _glArea.DiscardCompleteFrame();
99 _slideBase = source;
100 _openSlideBase = null;
101 _useOpenSlide = false;
102 _tileCache = new BioLib.TileCache(source, 200);
103 ClearCache();
104 }
Definition Zarr.cs:20

◆ UpdateViewAsync()

async Task BioGTK.SlideRenderer.UpdateViewAsync ( PointD pyramidalOrigin,
int viewportWidth,
int viewportHeight,
double resolution,
ZCT coordinate )

Definition at line 374 of file SlideRenderer.cs.

380 {
381 if (_openSlideBase == null && _slideBase == null)
382 return;
383 if (viewportWidth <= 1 && viewportHeight <= 1)
384 return;
385
386 // Only allow one render pass at a time. Without this gate, rapid pan
387 // events can pile up overlapping async tile fetches and keep large
388 // byte[] buffers alive until every stale render finishes.
389 if (!_renderSemaphore.Wait(0))
390 {
391 LastRenderSkipped = true;
392 return;
393 }
394
395 LastRenderSkipped = false;
396
397 try
398 {
399 int renderVersion = _renderStateVersion;
400 int viewportVersion = _viewportVersion;
401 CancellationTokenSource tileFetchCts;
402 lock (_activeTileFetchLock)
403 {
404 // This token represents render invalidation only. A timeout on a
405 // shared token would cancel every request in the batch after 60
406 // seconds, including tiles that were started much later. Each
407 // individual tile gets its own timeout in FetchTileAsync.
408 if (_activeTileFetchCts == null || _activeTileFetchCts.IsCancellationRequested)
409 _activeTileFetchCts = new CancellationTokenSource();
410 tileFetchCts = _activeTileFetchCts;
411 }
412
413 var schema = _useOpenSlide ? _openSlideBase.Schema : _slideBase.Schema;
414
415 int level = TileUtil.GetLevel(schema.Resolutions, resolution);
416 var levelRes = schema.Resolutions[level];
417 double levelUnitsPerPixel = levelRes.UnitsPerPixel;
418
419 await EnsureGlobalDisplayRangeAsync(coordinate, renderVersion, level).ConfigureAwait(false);
420
421 bool levelChanged = level != _currentLevel;
422
423 // Keep tile geometry for uploaded textures across viewport changes.
424 // It lets pan/zoom immediately reproject the current pyramid level
425 // while sharper tiles for a new level are still downloading. Entries
426 // whose GPU textures were evicted are filtered by HasTileTexture.
427
428 double originX = pyramidalOrigin.X;
429 double originY = pyramidalOrigin.Y;
430 double minX = originX;
431 double width = viewportWidth * resolution;
432 double height = viewportHeight * resolution;
433 // BioLib pyramids use OSM-style inverted Y coordinates. Keep the
434 // renderer in the same convention rather than trying to infer a
435 // positive-down variant from the schema bounds.
436 var viewportExtent = new Extent(minX, -originY - height, minX + width, -originY);
437
438 // Keep the GL scissor aligned with the actual image boundary so
439 // edge tiles don't leak padded black pixels outside the image.
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);
448
449 // Prefetch one tile beyond each viewport edge so partially visible
450 // edge tiles are available without overfetching an arbitrary 2x area.
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);
458
459 List<TileInfo> tileInfos;
460 List<TileInfo> fallbackInfos;
461 if (_useOpenSlide)
462 {
463 // OpenSlide schemas use their historical pixel-space lookup path.
464 var lookupExtent = BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
465 tileInfos = schema.GetTileInfos(lookupExtent, level).ToList();
466 fallbackInfos = schema.GetTileInfos(fetchExtent, level).ToList();
467
468 if (fallbackInfos.Count > 0)
469 {
470 var seen = new HashSet<TileIndex>(tileInfos.Select(t => t.Index));
471 foreach (var info in fallbackInfos)
472 {
473 if (seen.Add(info.Index))
474 tileInfos.Add(info);
475 }
476 }
477 }
478 else
479 {
480 // BioLib/Zarr SlideBase schemas already use the same inverted-Y
481 // world convention as BioLib's slice path. Keep lookups in that
482 // space so translated datasets stay aligned with the tile schema.
483 tileInfos = GetTileInfosIncludingBoundaries(schema, fetchExtent, level);
484 fallbackInfos = new List<TileInfo>();
485
486 if (tileInfos.Count == 0)
487 {
488 var pixelToWorldExtent = BioLib.ExtentEx.PixelToWorldInvertedY(fetchExtent, levelUnitsPerPixel);
489 fallbackInfos = schema.GetTileInfos(pixelToWorldExtent, level).ToList();
490
491 if (fallbackInfos.Count == 0)
492 {
493 var worldToPixelExtent = BioLib.ExtentEx.WorldToPixelInvertedY(fetchExtent, levelUnitsPerPixel);
494 fallbackInfos = schema.GetTileInfos(worldToPixelExtent, level).ToList();
495 }
496
497 if (fallbackInfos.Count > 0)
498 tileInfos = fallbackInfos;
499 }
500 }
501
502 var renderInfos = new List<TileRenderInfo>();
503 var pendingFetches = new List<(TileInfo Tile, Task<byte[]> FetchTask)>();
504 foreach (var tileInfo in tileInfos)
505 {
506 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
507 return;
508
509 var fetchTileInfo = tileInfo;
510 var renderTileInfo = tileInfo;
511
512 var renderInfo = CalculateScreenPosition(
513 renderTileInfo,
514 pyramidalOrigin,
515 resolution,
516 level,
517 schema);
518 renderInfos.Add(renderInfo);
519
520 if (!_glArea.HasTileTexture(fetchTileInfo.Index))
521 {
522 bool alreadyPending;
523 lock (_pendingTileFetchLock)
524 {
525 alreadyPending = !_pendingTileFetches.Add(fetchTileInfo.Index);
526 }
527
528 if (!alreadyPending)
529 pendingFetches.Add((fetchTileInfo, FetchTileAsync(fetchTileInfo, level, coordinate, tileFetchCts.Token)));
530 }
531 }
532
533 // Keep the visible target set resident even before it becomes the
534 // displayed set. Otherwise FIFO eviction can continuously discard
535 // newly uploaded target tiles and the level switch never completes.
536 await RunOnGtkThreadAsync(() =>
537 _glArea.SetProtectedTileIndices(renderInfos.Select(info => info.Index)))
538 .ConfigureAwait(false);
539
540 if (pendingFetches.Count > 0)
541 {
542 foreach (var pending in pendingFetches)
543 _ = ProcessPendingFetchAsync(pending.Tile, pending.FetchTask, tileFetchCts.Token, renderVersion, pyramidalOrigin, resolution, level, schema);
544 }
545
546 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
547 return;
548
549 bool hasRenderableTexture = renderInfos.Any(tile => _glArea.HasTileTexture(tile.Index));
550 if (levelChanged)
551 {
552 var fallbackRenderInfos = GetCachedLevelRenderInfos(
553 _currentLevel,
554 pyramidalOrigin,
555 resolution,
556 viewportWidth,
557 viewportHeight,
558 schema);
559
560 if (hasRenderableTexture)
561 {
562 bool canSwapNow = renderInfos.All(tile => _glArea.HasTileTexture(tile.Index));
563 if (canSwapNow)
564 {
565 _glArea.PreservePreviousFrameWhileIncomplete = false;
566 _glArea.ReleaseLevelTextures(_currentLevel);
567 _currentLevel = level;
568 lock (_pendingLevelSwitchLock)
569 {
570 _pendingLevelRenderInfos = null;
571 _pendingLevel = -1;
572 _pendingPreviousLevel = -1;
573 }
574 }
575 else
576 {
577 _glArea.PreservePreviousFrameWhileIncomplete = true;
578 lock (_pendingLevelSwitchLock)
579 {
580 _pendingLevelRenderInfos = new List<TileRenderInfo>(renderInfos);
581 _pendingLevel = level;
582 _pendingPreviousLevel = _currentLevel;
583 }
584 await ShowImmediateZoomFallbackAsync(fallbackRenderInfos).ConfigureAwait(false);
585 return;
586 }
587 }
588 else
589 {
590 _glArea.PreservePreviousFrameWhileIncomplete = true;
591 lock (_pendingLevelSwitchLock)
592 {
593 _pendingLevelRenderInfos = new List<TileRenderInfo>(renderInfos);
594 _pendingLevel = level;
595 _pendingPreviousLevel = _currentLevel;
596 }
597 await ShowImmediateZoomFallbackAsync(fallbackRenderInfos).ConfigureAwait(false);
598 return;
599 }
600 }
601
602 if (renderVersion != _renderStateVersion || viewportVersion != _viewportVersion)
603 return;
604
605 await RunOnGtkThreadAsync(() =>
606 {
607 _glArea.SetTilesToRender(renderInfos);
608 _glArea.RequestRedraw();
609 });
610 }
611 catch (OperationCanceledException)
612 {
613 // Cancellation is normal when a source closes or display state is
614 // replaced. Never let it fault the fire-and-forget UI render task.
615 }
616 finally
617 {
618 _renderSemaphore.Release();
619
620 if (LastRenderSkipped)
621 {
622 LastRenderSkipped = false;
623 Gtk.Application.Invoke((s, a) => App.viewer?.RequestDeferredRender());
624 }
625 }
626 }
void RequestRedraw()
Request a redraw of the GLArea.
bool HasTileTexture(TileIndex index)
Check if a tile texture is already in the GPU cache.
void SetProtectedTileIndices(IEnumerable< TileIndex > indices)
Protects incoming visible tiles from FIFO eviction. The currently displayed set is included automatic...
void ReleaseLevelTextures(int level)
Release all textures for a specific pyramid level.
void SetTilesToRender(IEnumerable< TileRenderInfo > tiles)
Prepare tiles for rendering. Call before RequestRedraw().

Property Documentation

◆ CachedTextureCount

int BioGTK.SlideRenderer.CachedTextureCount
get

Definition at line 1332 of file SlideRenderer.cs.

1333 {
1334 get
1335 {
1336 lock (_uploadedTileLock)
1337 return _uploadedTiles.Count;
1338 }
1339 }

◆ CurrentLevel

int BioGTK.SlideRenderer.CurrentLevel
get

Definition at line 1331 of file SlideRenderer.cs.

◆ LastRenderSkipped

bool BioGTK.SlideRenderer.LastRenderSkipped
get

Definition at line 43 of file SlideRenderer.cs.

43{ get; private set; }

The documentation for this class was generated from the following file: