BioCore  5.0.0
A Windows .NET microscopy library and application for viewing, editing, and annotating images.
Loading...
Searching...
No Matches
SlideRenderer.cs
Go to the documentation of this file.
1using AForge;
2using BioCore;
3using BioLib;
4using BruTile;
5using OpenSlideGTK;
6using System;
7using System.Collections.Generic;
8using System.Linq;
9using System.Threading.Tasks;
10
11namespace BioCore
12{
13 public class SlideRenderer
14 {
15 private readonly SlideGLArea _glArea;
16 private OpenSlideBase _openSlideBase;
17 private SlideBase _slideBase;
18 private bool _useOpenSlide;
19 private BioLib.TileCache _tileCache;
20 private OpenSlideGTK.TileCache _openTileCache;
21 private HashSet<TileIndex> _uploadedTiles = new();
22 private int _currentLevel = -1;
23 private int _renderVersion;
24 private bool _hasContentKey;
25 private int _contentZ;
26 private int _contentC;
27 private int _contentT;
28 private BioImage? _contentRgbZarr;
29 private int[]? _contentRgbSelection;
30
32 {
33 _glArea = glArea;
34 }
35
36 public void SetSource(OpenSlideBase source)
37 {
38 // Viewport updates reuse the source; only switching images should
39 // discard the textures currently being displayed.
40 if (_useOpenSlide && ReferenceEquals(_openSlideBase, source))
41 return;
42
43 _openSlideBase = source;
44 _slideBase = null;
45 _useOpenSlide = true;
46 _openTileCache = new OpenSlideGTK.TileCache(source, 200);
47 ClearCache();
48 }
49
50 public void SetSource(SlideBase source)
51 {
52 if (!_useOpenSlide && ReferenceEquals(_slideBase, source))
53 return;
54
55 _slideBase = source;
56 _openSlideBase = null;
57 _useOpenSlide = false;
58 _tileCache = new BioLib.TileCache(source, 200);
59 ClearCache();
60 }
61
62 public void ClearCache()
63 {
64 _renderVersion++;
65 _glArea.ClearTextureCache();
66 _uploadedTiles.Clear();
67 _currentLevel = -1;
68 _hasContentKey = false;
69 _contentRgbZarr = null;
70 _contentRgbSelection = null;
71 }
72
73 public async Task UpdateViewAsync(
74 PointD pyramidalOrigin,
75 int viewportWidth,
76 int viewportHeight,
77 double resolution,
78 ZCT coordinate,
79 BioImage? rgbZarr = null,
80 int[]? rgbSelection = null)
81 {
82 if (_openSlideBase == null && _slideBase == null)
83 return;
84 if (viewportWidth <= 1 && viewportHeight <= 1)
85 return;
86 var schema = _useOpenSlide ? _openSlideBase.Schema : _slideBase.Schema;
87
88 int level = TileUtil.GetLevel(schema.Resolutions, resolution);
89 var levelRes = schema.Resolutions[level];
90 double levelUnitsPerPixel = levelRes.UnitsPerPixel;
91
92 int[]? requestedRgbSelection = rgbSelection?.ToArray();
93 if (rgbZarr != null && requestedRgbSelection == null)
94 requestedRgbSelection = new[] { 0, 1, 2 };
95 bool contentChanged = !_hasContentKey
96 || coordinate.Z != _contentZ
97 || coordinate.C != _contentC
98 || coordinate.T != _contentT
99 || !ReferenceEquals(rgbZarr, _contentRgbZarr)
100 || !SelectionsEqual(requestedRgbSelection, _contentRgbSelection);
101
102 // Every invocation supersedes an older asynchronous viewport request.
103 // A content or pyramid-level change also invalidates GPU textures because
104 // their cache key is only TileIndex and does not include Z/C/T or RGB state.
105 int renderVersion = ++_renderVersion;
106 if (contentChanged || level != _currentLevel)
107 {
108 _glArea.ClearTextureCache();
109 _uploadedTiles.Clear();
110 _currentLevel = level;
111 }
112
113 _hasContentKey = true;
114 _contentZ = coordinate.Z;
115 _contentC = coordinate.C;
116 _contentT = coordinate.T;
117 _contentRgbZarr = rgbZarr;
118 _contentRgbSelection = requestedRgbSelection;
119
120 // Calculate world extent for the viewport
121 double minX = pyramidalOrigin.X * resolution;
122 double minY = -pyramidalOrigin.Y * resolution;
123 double width = viewportWidth * resolution;
124 double height = viewportHeight * resolution;
125 var worldExtent = new Extent(minX, minY - height, minX + width, minY);
126
127 // Get tiles that intersect the viewport
128 var tileInfos = schema.GetTileInfos(worldExtent, level).ToList();
129
130 if (tileInfos.Count == 0)
131 {
132 var pixelExtent = OpenSlideGTK.ExtentEx.WorldToPixelInvertedY(worldExtent, resolution);
133 tileInfos = schema.GetTileInfos(pixelExtent, level).ToList();
134 }
135
136 int[]? rgbChannels = requestedRgbSelection;
137 // OpenSlide tiles are fetched below through GetTileAsync. Its bulk
138 // prefetch uses separate stitch state that this renderer never reads,
139 // and fetching both ways can fail or duplicate the work.
140 if (rgbZarr == null && !_useOpenSlide)
141 await _slideBase.FetchTilesAsync(tileInfos.ToList(), level, coordinate, pyramidalOrigin, new AForge.Size(viewportWidth, viewportHeight));
142 if (renderVersion != _renderVersion)
143 return;
144
145 var renderInfos = new List<TileRenderInfo>();
146 foreach (var tileInfo in tileInfos)
147 {
148 if (!_glArea.HasTileTexture(tileInfo.Index))
149 {
150 byte[] tileData;
151 if (rgbZarr != null)
152 {
153 int tileWidth = schema.GetTileWidth(level);
154 int tileHeight = schema.GetTileHeight(level);
155 int tilePixelWidth = (int)Math.Round(tileInfo.Extent.Width / levelUnitsPerPixel);
156 int tilePixelHeight = (int)Math.Round(tileInfo.Extent.Height / levelUnitsPerPixel);
157 tileData = await ZarrChannelView.ReadRgbTileAsync(rgbZarr, level,
158 tileInfo.Index.Col * tileWidth, tileInfo.Index.Row * tileHeight,
159 tilePixelWidth, tilePixelHeight, coordinate, rgbChannels!);
160 }
161 else if (_useOpenSlide)
162 tileData = await _openSlideBase.GetTileAsync(tileInfo);
163 else
164 tileData = await _slideBase.GetTileAsync(tileInfo, coordinate);
165 if (renderVersion != _renderVersion)
166 return;
167 if (tileData != null && rgbZarr == null && rgbChannels != null)
168 {
169 tileData = (byte[])tileData.Clone();
170 for (int i = 0; i + 3 < tileData.Length; i += 4)
171 for (int color = 0; color < 3; color++)
172 if (rgbChannels[color] < 0) tileData[i + color] = 0;
173 }
174 if (tileData != null)
175 {
176 int tW = (int)Math.Round(tileInfo.Extent.Width / levelUnitsPerPixel);
177 int tH = (int)Math.Round(tileInfo.Extent.Height / levelUnitsPerPixel);
178
179 // BioLib/OpenSlide return schema-sized buffers with black
180 // padding at image edges, while TileInfo exposes the clipped
181 // edge extent. Repack a padded edge tile before uploading it;
182 // otherwise OpenGL treats the clipped width as the source row
183 // stride and the lower rows come from unrelated buffer offsets.
184 if (rgbZarr == null)
185 {
186 int sourceWidth = schema.GetTileWidth(level);
187 int sourceHeight = schema.GetTileHeight(level);
188 if ((tW != sourceWidth || tH != sourceHeight) &&
189 tileData.Length >= sourceWidth * sourceHeight * 4)
190 {
191 byte[] clipped = new byte[tW * tH * 4];
192 int copyBytes = tW * 4;
193 for (int row = 0; row < tH; row++)
194 Buffer.BlockCopy(tileData, row * sourceWidth * 4,
195 clipped, row * copyBytes, copyBytes);
196 tileData = clipped;
197 }
198 }
199
200 _glArea.UploadTileTexture(tileInfo.Index, tileData, tW, tH);
201 _uploadedTiles.Add(tileInfo.Index);
202 }
203 }
204
205 if (_glArea.HasTileTexture(tileInfo.Index))
206 {
207 var renderInfo = CalculateScreenPosition(
208 tileInfo,
209 pyramidalOrigin,
210 resolution,
211 level,
212 schema);
213 renderInfos.Add(renderInfo);
214 }
215 }
216
217 _glArea.SetTilesToRender(renderInfos);
218 _glArea.RequestRedraw();
219 }
220
221 private static bool SelectionsEqual(int[]? left, int[]? right)
222 {
223 if (ReferenceEquals(left, right))
224 return true;
225 if (left == null || right == null || left.Length != right.Length)
226 return false;
227
228 for (int i = 0; i < left.Length; i++)
229 if (left[i] != right[i])
230 return false;
231
232 return true;
233 }
234
235 private async Task<byte[]> FetchTileAsync(TileInfo tileInfo, int level, ZCT coordinate)
236 {
237 try
238 {
239 if (_useOpenSlide)
240 {
241 var bt = await _openTileCache.GetTile(new OpenSlideGTK.Info(coordinate, tileInfo.Index, tileInfo.Extent, level));
242 return bt;
243 }
244 else
245 {
246 return await _tileCache.GetTile(new BioLib.TileInformation(tileInfo.Index, tileInfo.Extent, coordinate));
247 }
248 }
249 catch (Exception ex)
250 {
251 Console.WriteLine($"Error fetching tile {tileInfo.Index}: {ex.Message}");
252 return null;
253 }
254 }
255
256 private TileRenderInfo CalculateScreenPosition(
257 TileInfo tile,
258 PointD pyramidalOrigin,
259 double viewResolution,
260 int level,
261 ITileSchema schema)
262 {
263 // Get the actual tile size from schema (typically 256x256)
264 int tileWidth = schema.GetTileWidth(level);
265 int tileHeight = schema.GetTileHeight(level);
266
267 // Convert tile extent to pixel coordinates
268 var pixelExtent = OpenSlideGTK.ExtentEx.WorldToPixelInvertedY(tile.Extent, viewResolution);
269
270 // Use approach 1: MinY with positive origin
271 double tileLeftPx = pixelExtent.MinX;
272 double tileTopPx = pixelExtent.MinY;
273 double viewXPx = pyramidalOrigin.X;
274 double viewYPx = pyramidalOrigin.Y;
275
276 float screenX = (float)(tileLeftPx - viewXPx);
277 float screenY = (float)(tileTopPx - viewYPx);
278
279 // KEY FIX: Use the extent dimensions directly, not fixed tile size
280 // The extent already accounts for any edge tiles that might be smaller
281 float screenWidth = (float)pixelExtent.Width;
282 float screenHeight = (float)pixelExtent.Height;
283
284 // Comprehensive debug logging
285 // Console.WriteLine(...) // Reduced noise for production
286
287 return new TileRenderInfo(tile.Index, screenX, screenY, screenWidth, screenHeight);
288 }
289
290 public int CurrentLevel => _currentLevel;
291 public int CachedTextureCount => _uploadedTiles.Count;
292 }
293}
void SetSource(OpenSlideBase source)
void SetSource(SlideBase source)
async Task UpdateViewAsync(PointD pyramidalOrigin, int viewportWidth, int viewportHeight, double resolution, ZCT coordinate, BioImage? rgbZarr=null, int[]? rgbSelection=null)
SlideRenderer(SlideGLArea glArea)
Information needed to render a single tile.