BioCore  5.0.0
A Windows .NET microscopy library and application for viewing, editing, and annotating images.
Loading...
Searching...
No Matches
SlideGLArea.cs
Go to the documentation of this file.
1using System;
2using System.Collections.Generic;
3using System.Linq;
4using System.Runtime.InteropServices;
5using BruTile;
6using OpenTK.Graphics.OpenGL;
7using AForge;
8using PixelFormat = OpenTK.Graphics.OpenGL.PixelFormat;
9using OpenTK.GLControl;
10
11namespace BioCore
12{
13
14 public class SlideGLArea : GLControl
15 {
16 // ============================================================================
17 // GL Resources
18 // ============================================================================
19
20 private bool _glInitialized = false;
21 private int _vao;
22 private int _vbo;
23 private int _shaderProgram;
24
25 // Uniform locations (cached for performance)
26 private int _locPos;
27 private int _locSize;
28 private int _locViewportSize;
29 private int _locTex;
30
31 // ============================================================================
32 // Texture Cache - tiles uploaded to GPU
33 // ============================================================================
34
35 private Dictionary<TileIndex, int> _textureCache = new();
36 private const int MAX_CACHED_TEXTURES = 500;
37
38 // ============================================================================
39 // Rendering State
40 // ============================================================================
41
42 public List<TileRenderInfo> TilesToRender { get; } = new();
43 public bool NeedsRedraw { get; private set; }
44
45 // ============================================================================
46 // GL Ready Event — fires once after GL context is confirmed working
47 // ============================================================================
48
54 public event EventHandler GLReady;
55 private bool _glReadyFired = false;
56
61 public bool IsGLReady => _glInitialized;
62
63 // ============================================================================
64 // Shaders
65 // ============================================================================
66
67 private const string VertexShaderSource = @"
68#version 330 core
69
70layout(location=0) in vec2 aPos;
71layout(location=1) in vec2 aUV;
72
73uniform vec2 pos; // pixel-space top-left of tile
74uniform vec2 size; // pixel-space size of tile
75uniform vec2 viewportSize; // (width, height) in pixels
76
77out vec2 uv;
78
79void main()
80{
81 // aPos is in [0,1] x [0,1]
82 vec2 pixelPos = aPos * size + pos;
83
84 // Convert from pixel coords to NDC (-1..1), with Y flipped for GL
85 vec2 ndc;
86 ndc.x = (pixelPos.x / viewportSize.x) * 2.0 - 1.0;
87 ndc.y = 1.0 - (pixelPos.y / viewportSize.y) * 2.0;
88
89 gl_Position = vec4(ndc, 0.0, 1.0);
90 uv = aUV;
91}
92";
93
94 private const string FragmentShaderSource = @"
95#version 330 core
96
97in vec2 uv;
98out vec4 FragColor;
99uniform sampler2D tex;
100
101void main()
102{
103 // Image data is usually Top-Down. OpenGL Textures are Bottom-Up.
104 // This implicit flip means UV(0,0) (Top-Left Quad) maps to Texture Bottom (Image Top).
105 // This is the correct orientation for Top-Left origin rendering.
106 FragColor = texture(tex, vec2(uv.x, uv.y));
107}
108";
109
110 // ============================================================================
111 // Construction & Lifecycle
112 // ============================================================================
113
114 public SlideGLArea()
115 {
116 Paint += OnPaint;
117 Disposed += OnDisposed;
118 Resize += OnResized;
119 }
120
121 private void OnDisposed(object? sender, EventArgs e)
122 {
123 MakeCurrent();
124 CleanupGL();
125 }
126
130 private void OnPaint(object? sender, EventArgs e)
131 {
132 MakeCurrent();
133
134 if (!_glInitialized)
135 {
136 InitializeGL();
137
138 // Fire GLReady exactly once — this is the safe moment for the first tile load
139 if (_glInitialized && !_glReadyFired)
140 {
141 _glReadyFired = true;
142 Console.WriteLine("[SlideGLArea] GL initialized. Firing GLReady event.");
143 GLReady?.Invoke(this, EventArgs.Empty);
144 }
145 }
146
147 RenderFrame();
148 SwapBuffers();
149 NeedsRedraw = false;
150 }
151
152 private void OnResized(object? sender, EventArgs e)
153 {
154 if (!_glInitialized) return;
155 MakeCurrent();
156 Invalidate();
157 }
158
159 // ============================================================================
160 // GL Initialization (called once)
161 // ============================================================================
162
163 private void InitializeGL()
164 {
165 if (_glInitialized) return;
166
167 try
168 {
169 // Compile and link shader program
170 int vertexShader = CompileShader(ShaderType.VertexShader, VertexShaderSource);
171 int fragmentShader = CompileShader(ShaderType.FragmentShader, FragmentShaderSource);
172
173 _shaderProgram = GL.CreateProgram();
174 GL.AttachShader(_shaderProgram, vertexShader);
175 GL.AttachShader(_shaderProgram, fragmentShader);
176 GL.LinkProgram(_shaderProgram);
177
178 GL.GetProgram(_shaderProgram, GetProgramParameterName.LinkStatus, out int linkStatus);
179 if (linkStatus == 0)
180 {
181 string infoLog = GL.GetProgramInfoLog(_shaderProgram);
182 Console.WriteLine($"[SlideGLArea] Shader link FAILED: {infoLog}");
183 return;
184 }
185
186 GL.DeleteShader(vertexShader);
187 GL.DeleteShader(fragmentShader);
188
189 // Cache uniform locations
190 _locPos = GL.GetUniformLocation(_shaderProgram, "pos");
191 _locSize = GL.GetUniformLocation(_shaderProgram, "size");
192 _locViewportSize = GL.GetUniformLocation(_shaderProgram, "viewportSize");
193 _locTex = GL.GetUniformLocation(_shaderProgram, "tex");
194
195 Console.WriteLine($"[SlideGLArea] Uniform locations: pos={_locPos} size={_locSize} viewport={_locViewportSize} tex={_locTex}");
196
197 // Build the unit-quad VAO/VBO shared by all tile draws
198 float[] quadVertices =
199 {
200 // pos uv
201 0, 0, 0, 0,
202 1, 0, 1, 0,
203 1, 1, 1, 1,
204
205 0, 0, 0, 0,
206 1, 1, 1, 1,
207 0, 1, 0, 1
208 };
209
210 _vao = GL.GenVertexArray();
211 _vbo = GL.GenBuffer();
212
213 GL.BindVertexArray(_vao);
214 GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
215 GL.BufferData(BufferTarget.ArrayBuffer, quadVertices.Length * sizeof(float),
216 quadVertices, BufferUsageHint.StaticDraw);
217
218 // Position attribute (location = 0)
219 GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), 0);
220 GL.EnableVertexAttribArray(0);
221
222 // UV attribute (location = 1)
223 GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), 2 * sizeof(float));
224 GL.EnableVertexAttribArray(1);
225
226 GL.BindVertexArray(0);
227
228 // Verify no GL errors accumulated during setup
229 var err = GL.GetError();
230 if (err != ErrorCode.NoError)
231 Console.WriteLine($"[SlideGLArea] GL error after init: {err}");
232
233 _glInitialized = true;
234 Console.WriteLine($"[SlideGLArea] GL initialization complete. VAO={_vao} VBO={_vbo} Program={_shaderProgram}");
235 }
236 catch (Exception ex)
237 {
238 Console.WriteLine($"[SlideGLArea] GL initialization FAILED: {ex.Message}");
239 }
240 }
241
242 private int CompileShader(ShaderType type, string source)
243 {
244 int shader = GL.CreateShader(type);
245 GL.ShaderSource(shader, source);
246 GL.CompileShader(shader);
247
248 GL.GetShader(shader, ShaderParameter.CompileStatus, out int status);
249 if (status == 0)
250 {
251 string infoLog = GL.GetShaderInfoLog(shader);
252 Console.WriteLine($"[SlideGLArea] Shader compile FAILED ({type}): {infoLog}");
253 throw new Exception($"Shader compilation failed ({type}): {infoLog}");
254 }
255
256 Console.WriteLine($"[SlideGLArea] Shader compiled OK ({type})");
257 return shader;
258 }
259
260 private void CleanupGL()
261 {
262 if (_shaderProgram != 0)
263 {
264 GL.DeleteProgram(_shaderProgram);
265 _shaderProgram = 0;
266 }
267
268 if (_vbo != 0)
269 {
270 GL.DeleteBuffer(_vbo);
271 _vbo = 0;
272 }
273
274 if (_vao != 0)
275 {
276 GL.DeleteVertexArray(_vao);
277 _vao = 0;
278 }
279
281 _glInitialized = false;
282 }
283
284 // ============================================================================
285 // Rendering
286 // ============================================================================
287
288 private void RenderFrame()
289 {
290 int width = Width;
291 int height = Height;
292 if (width <= 0 || height <= 0) return;
293
294 // Handle High-DPI: viewport is in physical pixels, uniforms in logical pixels
295 double scale = DeviceDpi / 96.0;
296 GL.Viewport(0, 0, (int)(width * scale), (int)(height * scale));
297 GL.ClearColor(0f, 0f, 0f, 1f);
298 GL.Clear(ClearBufferMask.ColorBufferBit);
299
300 if (TilesToRender.Count == 0)
301 return;
302
303 GL.Enable(EnableCap.Blend);
304 GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
305
306 GL.UseProgram(_shaderProgram);
307 GL.BindVertexArray(_vao);
308
309 // Viewport size uniform is constant for all tiles this frame
310 GL.Uniform2(_locViewportSize, (float)width, (float)height);
311 GL.Uniform1(_locTex, 0);
312 GL.ActiveTexture(TextureUnit.Texture0);
313
314 int rendered = 0;
315 int skipped = 0;
316 foreach (var tile in TilesToRender)
317 {
318 if (!_textureCache.TryGetValue(tile.Index, out int texId))
319 {
320 skipped++;
321 continue;
322 }
323
324 GL.Uniform2(_locPos, tile.ScreenX, tile.ScreenY);
325 GL.Uniform2(_locSize, tile.ScreenWidth, tile.ScreenHeight);
326
327 GL.BindTexture(TextureTarget.Texture2D, texId);
328 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
329 rendered++;
330 }
331
332 // Diagnostic: log once when tiles first appear (or fail to appear)
333 if (rendered == 0 && skipped > 0)
334 Console.WriteLine($"[SlideGLArea] RenderFrame: {TilesToRender.Count} tiles to render, but ALL {skipped} skipped (no texture in cache).");
335 else if (rendered > 0)
336 Console.WriteLine($"[SlideGLArea] RenderFrame: rendered {rendered} tiles, skipped {skipped}.");
337
338 GL.BindVertexArray(0);
339 GL.UseProgram(0);
340 GL.Disable(EnableCap.Blend);
341 }
342
343 // ============================================================================
344 // Texture Management
345 // ============================================================================
346
351 public bool UploadTileTexture(TileIndex index, byte[] pixelData, int tileWidth, int tileHeight)
352 {
353 if (pixelData == null || pixelData.Length == 0)
354 return false;
355
356 // Guard: don't attempt GL operations if the context isn't ready
357 if (!IsHandleCreated)
358 {
359 Console.WriteLine($"[SlideGLArea] UploadTileTexture skipped — control handle not created yet.");
360 return false;
361 }
362
363 try
364 {
365 MakeCurrent();
366 }
367 catch (Exception ex)
368 {
369 Console.WriteLine($"[SlideGLArea] MakeCurrent failed in UploadTileTexture: {ex.Message}");
370 return false;
371 }
372
373 if (!_glInitialized)
374 InitializeGL();
375
376 if (!_glInitialized)
377 {
378 Console.WriteLine($"[SlideGLArea] UploadTileTexture skipped — GL init failed.");
379 return false;
380 }
381
382 // Already cached — nothing to do
383 if (_textureCache.ContainsKey(index))
384 return true;
385
386 // Evict old textures if cache is full
387 if (_textureCache.Count >= MAX_CACHED_TEXTURES)
388 EvictOldestTextures(MAX_CACHED_TEXTURES / 4);
389
390 // Validate pixel data size
391 int expectedBytes = tileWidth * tileHeight * 4;
392 if (pixelData.Length < expectedBytes)
393 {
394 Console.WriteLine($"[SlideGLArea] UploadTileTexture: pixel data too small. Expected {expectedBytes} bytes for {tileWidth}x{tileHeight}, got {pixelData.Length}.");
395 return false;
396 }
397
398 // Create and upload texture
399 int tex = GL.GenTexture();
400 GL.BindTexture(TextureTarget.Texture2D, tex);
401 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
402
403 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
404 tileWidth, tileHeight, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixelData);
405
406 var glErr = GL.GetError();
407 if (glErr != ErrorCode.NoError)
408 {
409 Console.WriteLine($"[SlideGLArea] GL error after TexImage2D ({tileWidth}x{tileHeight}): {glErr}");
410 GL.DeleteTexture(tex);
411 return false;
412 }
413
414 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
415 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
416 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
417 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
418
419 GL.BindTexture(TextureTarget.Texture2D, 0);
420 _textureCache[index] = tex;
421 return true;
422 }
423
427 public bool HasTileTexture(TileIndex index)
428 {
429 return _textureCache.ContainsKey(index);
430 }
431
435 public void ReleaseTileTexture(TileIndex index)
436 {
437 if (_textureCache.TryGetValue(index, out int tex))
438 {
439 MakeCurrent();
440 GL.DeleteTexture(tex);
441 _textureCache.Remove(index);
442 }
443 }
444
448 public void ReleaseLevelTextures(int level)
449 {
450 if (!_glInitialized) return;
451
452 MakeCurrent();
453
454 var toRemove = _textureCache.Where(kvp => kvp.Key.Level == level).ToList();
455 foreach (var kvp in toRemove)
456 {
457 GL.DeleteTexture(kvp.Value);
458 _textureCache.Remove(kvp.Key);
459 }
460 }
461
465 public void ClearTextureCache()
466 {
467 if (!_glInitialized) return;
468
469 MakeCurrent();
470
471 foreach (var tex in _textureCache.Values)
472 GL.DeleteTexture(tex);
473
474 _textureCache.Clear();
475 }
476
477 private void EvictOldestTextures(int count)
478 {
479 // Simple FIFO eviction — could be improved with LRU tracking
480 var toRemove = _textureCache.Take(count).ToList();
481 foreach (var kvp in toRemove)
482 {
483 GL.DeleteTexture(kvp.Value);
484 _textureCache.Remove(kvp.Key);
485 }
486 }
487
488 // ============================================================================
489 // Public API
490 // ============================================================================
491
495 public void RequestRedraw()
496 {
497 NeedsRedraw = true;
498 Invalidate();
499 }
500
504 public void SetTilesToRender(IEnumerable<TileRenderInfo> tiles)
505 {
506 TilesToRender.Clear();
507 TilesToRender.AddRange(tiles);
508 }
509
514 public byte[] ReadPixels()
515 {
516 if (!_glInitialized) return null;
517
518 MakeCurrent();
519
520 int width = Width;
521 int height = Height;
522
523 byte[] pixels = new byte[width * height * 4];
524 GL.ReadPixels(0, 0, width, height, PixelFormat.Rgba, PixelType.UnsignedByte, pixels);
525
526 return pixels;
527 }
528 }
529
530 // ============================================================================
531 // Supporting Types
532 // ============================================================================
533
537 public struct TileRenderInfo
538 {
539 public TileIndex Index;
540
541 // Screen-space position and size in pixels
542 public float ScreenX;
543 public float ScreenY;
544 public float ScreenWidth;
545 public float ScreenHeight;
546
547 public TileRenderInfo(TileIndex index, float x, float y, float w, float h)
548 {
549 Index = index;
550 ScreenX = x;
551 ScreenY = y;
552 ScreenWidth = w;
553 ScreenHeight = h;
554 }
555 }
556}
OpenTK.Graphics.OpenGL.PixelFormat PixelFormat
Definition SlideGLArea.cs:8
List< TileRenderInfo > TilesToRender
byte[] ReadPixels()
Read pixels from the current framebuffer (for export/save operations). This is slow — only use for ex...
void ReleaseTileTexture(TileIndex index)
Release a specific tile texture.
void RequestRedraw()
Request a redraw of the GLArea.
void ClearTextureCache()
Clear all cached textures.
EventHandler GLReady
Fires exactly once after the GL context has been successfully initialized. Subscribe to this from Ima...
bool HasTileTexture(TileIndex index)
Check if a tile texture is already in the GPU cache.
bool IsGLReady
True after the GL context has been fully initialized and is ready for texture uploads and rendering.
bool UploadTileTexture(TileIndex index, byte[] pixelData, int tileWidth, int tileHeight)
Upload a tile texture to the GPU. Call from the main thread. Returns true if upload succeeded.
void SetTilesToRender(IEnumerable< TileRenderInfo > tiles)
Prepare tiles for rendering. Call before RequestRedraw().
void ReleaseLevelTextures(int level)
Release all textures for a specific pyramid level.
Information needed to render a single tile.
TileRenderInfo(TileIndex index, float x, float y, float w, float h)