BioImager  5.0.0
A .NET microscopy imaging application based on Bio library. Supports various microscopes by using imported libraries & GUI automation. Supports XInput game controllers to move stage, take images, run ImageJ macros on images or Bio C# scripts.
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 BioImager
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 private int _overlayTexture;
39 private int _overlayWidth;
40 private int _overlayHeight;
41 private byte[] _overlayPixels;
42 private bool _overlayDirty;
43
44 // ============================================================================
45 // Rendering State
46 // ============================================================================
47
48 public List<TileRenderInfo> TilesToRender { get; } = new();
49 public bool NeedsRedraw { get; private set; }
50
51 // ============================================================================
52 // GL Ready Event — fires once after GL context is confirmed working
53 // ============================================================================
54
60 public event EventHandler GLReady;
61 private bool _glReadyFired = false;
62
67 public bool IsGLReady => _glInitialized;
68
69 // ============================================================================
70 // Shaders
71 // ============================================================================
72
73 private const string VertexShaderSource = @"
74#version 330 core
75
76layout(location=0) in vec2 aPos;
77layout(location=1) in vec2 aUV;
78
79uniform vec2 pos; // pixel-space top-left of tile
80uniform vec2 size; // pixel-space size of tile
81uniform vec2 viewportSize; // (width, height) in pixels
82
83out vec2 uv;
84
85void main()
86{
87 // aPos is in [0,1] x [0,1]
88 vec2 pixelPos = aPos * size + pos;
89
90 // Convert from pixel coords to NDC (-1..1), with Y flipped for GL
91 vec2 ndc;
92 ndc.x = (pixelPos.x / viewportSize.x) * 2.0 - 1.0;
93 ndc.y = 1.0 - (pixelPos.y / viewportSize.y) * 2.0;
94
95 gl_Position = vec4(ndc, 0.0, 1.0);
96 uv = aUV;
97}
98";
99
100 private const string FragmentShaderSource = @"
101#version 330 core
102
103in vec2 uv;
104out vec4 FragColor;
105uniform sampler2D tex;
106
107void main()
108{
109 // Image data is usually Top-Down. OpenGL Textures are Bottom-Up.
110 // This implicit flip means UV(0,0) (Top-Left Quad) maps to Texture Bottom (Image Top).
111 // This is the correct orientation for Top-Left origin rendering.
112 FragColor = texture(tex, vec2(uv.x, uv.y));
113}
114";
115
116 // ============================================================================
117 // Construction & Lifecycle
118 // ============================================================================
119
120 public SlideGLArea()
121 {
122 Paint += OnPaint;
123 Disposed += OnDisposed;
124 Resize += OnResized;
125 }
126
127 private void OnDisposed(object? sender, EventArgs e)
128 {
129 MakeCurrent();
130 CleanupGL();
131 }
132
136 private void OnPaint(object? sender, EventArgs e)
137 {
138 MakeCurrent();
139
140 if (!_glInitialized)
141 {
142 InitializeGL();
143
144 // Fire GLReady exactly once — this is the safe moment for the first tile load
145 if (_glInitialized && !_glReadyFired)
146 {
147 _glReadyFired = true;
148 Console.WriteLine("[SlideGLArea] GL initialized. Firing GLReady event.");
149 GLReady?.Invoke(this, EventArgs.Empty);
150 }
151 }
152
153 RenderFrame();
154 SwapBuffers();
155 NeedsRedraw = false;
156 }
157
158 private void OnResized(object? sender, EventArgs e)
159 {
160 if (!_glInitialized) return;
161 MakeCurrent();
162 Invalidate();
163 }
164
165 // ============================================================================
166 // GL Initialization (called once)
167 // ============================================================================
168
169 private void InitializeGL()
170 {
171 if (_glInitialized) return;
172
173 try
174 {
175 // Compile and link shader program
176 int vertexShader = CompileShader(ShaderType.VertexShader, VertexShaderSource);
177 int fragmentShader = CompileShader(ShaderType.FragmentShader, FragmentShaderSource);
178
179 _shaderProgram = GL.CreateProgram();
180 GL.AttachShader(_shaderProgram, vertexShader);
181 GL.AttachShader(_shaderProgram, fragmentShader);
182 GL.LinkProgram(_shaderProgram);
183
184 GL.GetProgram(_shaderProgram, GetProgramParameterName.LinkStatus, out int linkStatus);
185 if (linkStatus == 0)
186 {
187 string infoLog = GL.GetProgramInfoLog(_shaderProgram);
188 Console.WriteLine($"[SlideGLArea] Shader link FAILED: {infoLog}");
189 return;
190 }
191
192 GL.DeleteShader(vertexShader);
193 GL.DeleteShader(fragmentShader);
194
195 // Cache uniform locations
196 _locPos = GL.GetUniformLocation(_shaderProgram, "pos");
197 _locSize = GL.GetUniformLocation(_shaderProgram, "size");
198 _locViewportSize = GL.GetUniformLocation(_shaderProgram, "viewportSize");
199 _locTex = GL.GetUniformLocation(_shaderProgram, "tex");
200
201 Console.WriteLine($"[SlideGLArea] Uniform locations: pos={_locPos} size={_locSize} viewport={_locViewportSize} tex={_locTex}");
202
203 // Build the unit-quad VAO/VBO shared by all tile draws
204 float[] quadVertices =
205 {
206 // pos uv
207 0, 0, 0, 0,
208 1, 0, 1, 0,
209 1, 1, 1, 1,
210
211 0, 0, 0, 0,
212 1, 1, 1, 1,
213 0, 1, 0, 1
214 };
215
216 _vao = GL.GenVertexArray();
217 _vbo = GL.GenBuffer();
218
219 GL.BindVertexArray(_vao);
220 GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
221 GL.BufferData(BufferTarget.ArrayBuffer, quadVertices.Length * sizeof(float),
222 quadVertices, BufferUsageHint.StaticDraw);
223
224 // Position attribute (location = 0)
225 GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), 0);
226 GL.EnableVertexAttribArray(0);
227
228 // UV attribute (location = 1)
229 GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), 2 * sizeof(float));
230 GL.EnableVertexAttribArray(1);
231
232 GL.BindVertexArray(0);
233
234 // Verify no GL errors accumulated during setup
235 var err = GL.GetError();
236 if (err != ErrorCode.NoError)
237 Console.WriteLine($"[SlideGLArea] GL error after init: {err}");
238
239 _glInitialized = true;
240 Console.WriteLine($"[SlideGLArea] GL initialization complete. VAO={_vao} VBO={_vbo} Program={_shaderProgram}");
241 }
242 catch (Exception ex)
243 {
244 Console.WriteLine($"[SlideGLArea] GL initialization FAILED: {ex.Message}");
245 }
246 }
247
248 private int CompileShader(ShaderType type, string source)
249 {
250 int shader = GL.CreateShader(type);
251 GL.ShaderSource(shader, source);
252 GL.CompileShader(shader);
253
254 GL.GetShader(shader, ShaderParameter.CompileStatus, out int status);
255 if (status == 0)
256 {
257 string infoLog = GL.GetShaderInfoLog(shader);
258 Console.WriteLine($"[SlideGLArea] Shader compile FAILED ({type}): {infoLog}");
259 throw new Exception($"Shader compilation failed ({type}): {infoLog}");
260 }
261
262 Console.WriteLine($"[SlideGLArea] Shader compiled OK ({type})");
263 return shader;
264 }
265
266 private void CleanupGL()
267 {
268 if (_shaderProgram != 0)
269 {
270 GL.DeleteProgram(_shaderProgram);
271 _shaderProgram = 0;
272 }
273
274 if (_vbo != 0)
275 {
276 GL.DeleteBuffer(_vbo);
277 _vbo = 0;
278 }
279
280 if (_vao != 0)
281 {
282 GL.DeleteVertexArray(_vao);
283 _vao = 0;
284 }
285
286 if (_overlayTexture != 0)
287 {
288 GL.DeleteTexture(_overlayTexture);
289 _overlayTexture = 0;
290 }
291
293 _glInitialized = false;
294 }
295
296 // ============================================================================
297 // Rendering
298 // ============================================================================
299
300 private void RenderFrame()
301 {
302 int width = Width;
303 int height = Height;
304 if (width <= 0 || height <= 0) return;
305
306 // Handle High-DPI: viewport is in physical pixels, uniforms in logical pixels
307 double scale = DeviceDpi / 96.0;
308 GL.Viewport(0, 0, (int)(width * scale), (int)(height * scale));
309 GL.ClearColor(0.2f, 0.2f, 0.2f, 1f);
310 GL.Clear(ClearBufferMask.ColorBufferBit);
311
312 GL.Enable(EnableCap.Blend);
313 GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
314
315 GL.UseProgram(_shaderProgram);
316 GL.BindVertexArray(_vao);
317
318 // Viewport size uniform is constant for all tiles this frame
319 GL.Uniform2(_locViewportSize, (float)width, (float)height);
320 GL.Uniform1(_locTex, 0);
321 GL.ActiveTexture(TextureUnit.Texture0);
322
323 if (TilesToRender.Count > 0)
324 {
325 int rendered = 0;
326 int skipped = 0;
327 foreach (var tile in TilesToRender)
328 {
329 if (!_textureCache.TryGetValue(tile.Index, out int texId))
330 {
331 skipped++;
332 continue;
333 }
334
335 GL.Uniform2(_locPos, tile.ScreenX, tile.ScreenY);
336 GL.Uniform2(_locSize, tile.ScreenWidth, tile.ScreenHeight);
337
338 GL.BindTexture(TextureTarget.Texture2D, texId);
339 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
340 rendered++;
341 }
342 }
343
344 UploadOverlayTextureIfNeeded();
345 if (_overlayTexture != 0)
346 {
347 GL.Uniform2(_locPos, 0f, 0f);
348 GL.Uniform2(_locSize, (float)width, (float)height);
349 GL.BindTexture(TextureTarget.Texture2D, _overlayTexture);
350 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
351 }
352
353 GL.BindVertexArray(0);
354 GL.UseProgram(0);
355 GL.Disable(EnableCap.Blend);
356 }
357
358 private void UploadOverlayTextureIfNeeded()
359 {
360 if (!_overlayDirty)
361 return;
362
363 _overlayDirty = false;
364
365 try
366 {
367 MakeCurrent();
368 }
369 catch
370 {
371 return;
372 }
373
374 if (!_glInitialized)
375 InitializeGL();
376
377 if (!_glInitialized)
378 return;
379
380 if (_overlayTexture != 0)
381 {
382 GL.DeleteTexture(_overlayTexture);
383 _overlayTexture = 0;
384 }
385
386 if (_overlayPixels == null || _overlayPixels.Length == 0 || _overlayWidth <= 0 || _overlayHeight <= 0)
387 return;
388
389 _overlayTexture = GL.GenTexture();
390 GL.BindTexture(TextureTarget.Texture2D, _overlayTexture);
391 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
392 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
393 _overlayWidth, _overlayHeight, 0, PixelFormat.Bgra, PixelType.UnsignedByte, _overlayPixels);
394
395 var glErr = GL.GetError();
396 if (glErr != ErrorCode.NoError)
397 {
398 GL.DeleteTexture(_overlayTexture);
399 _overlayTexture = 0;
400 return;
401 }
402
403 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
404 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
405 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
406 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
407 GL.BindTexture(TextureTarget.Texture2D, 0);
408 }
409
410 // ============================================================================
411 // Texture Management
412 // ============================================================================
413
418 public bool UploadTileTexture(TileIndex index, byte[] pixelData, int tileWidth, int tileHeight)
419 {
420 if (pixelData == null || pixelData.Length == 0)
421 return false;
422
423 // Guard: don't attempt GL operations if the context isn't ready
424 if (!IsHandleCreated)
425 {
426 return false;
427 }
428
429 try
430 {
431 MakeCurrent();
432 }
433 catch
434 {
435 return false;
436 }
437
438 if (!_glInitialized)
439 InitializeGL();
440
441 if (!_glInitialized)
442 {
443 return false;
444 }
445
446 // Already cached — nothing to do
447 if (_textureCache.ContainsKey(index))
448 return true;
449
450 // Evict old textures if cache is full
451 if (_textureCache.Count >= MAX_CACHED_TEXTURES)
452 EvictOldestTextures(MAX_CACHED_TEXTURES / 4);
453
454 int expectedPixels = tileWidth * tileHeight;
455 if (expectedPixels <= 0)
456 return false;
457
458 byte[] uploadData = pixelData;
459 PixelFormat uploadFormat = PixelFormat.Bgra;
460 int expectedBytes = expectedPixels * 4;
461
462 if (pixelData.Length == expectedBytes)
463 {
464 // Already BGRA.
465 }
466 else if (pixelData.Length == expectedPixels * 3)
467 {
468 uploadData = Convert24BitRgbToBgra(pixelData, expectedPixels);
469 }
470 else if (pixelData.Length >= expectedPixels * 2)
471 {
472 uploadData = Convert16BitGrayToBgra(pixelData, expectedPixels);
473 }
474 else if (pixelData.Length >= expectedPixels)
475 {
476 uploadData = Convert8BitGrayToBgra(pixelData, expectedPixels);
477 }
478 else
479 {
480 return false;
481 }
482
483 // Create and upload texture
484 int tex = GL.GenTexture();
485 GL.BindTexture(TextureTarget.Texture2D, tex);
486 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
487
488 // OpenSlide-style tile buffers arrive as BGRA on little-endian systems.
489 // Using the matching OpenGL pixel format avoids channel/alpha mismatches.
490 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
491 tileWidth, tileHeight, 0, uploadFormat, PixelType.UnsignedByte, uploadData);
492
493 var glErr = GL.GetError();
494 if (glErr != ErrorCode.NoError)
495 {
496 GL.DeleteTexture(tex);
497 return false;
498 }
499
500 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
501 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
502 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
503 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
504
505 GL.BindTexture(TextureTarget.Texture2D, 0);
506 _textureCache[index] = tex;
507 return true;
508 }
509
510 private static byte[] Convert8BitGrayToBgra(byte[] grayBytes, int pixelCount)
511 {
512 byte[] rgba = new byte[pixelCount * 4];
513 int availablePixels = Math.Min(pixelCount, grayBytes.Length);
514 for (int i = 0, src = 0, dst = 0; i < availablePixels; i++, src++, dst += 4)
515 {
516 byte value = grayBytes[src];
517 rgba[dst] = value;
518 rgba[dst + 1] = value;
519 rgba[dst + 2] = value;
520 rgba[dst + 3] = 255;
521 }
522 return rgba;
523 }
524
525 private static byte[] Convert16BitGrayToBgra(byte[] grayBytes, int pixelCount)
526 {
527 byte[] rgba = new byte[pixelCount * 4];
528 int availablePixels = Math.Min(pixelCount, grayBytes.Length / 2);
529
530 if (availablePixels <= 0)
531 return rgba;
532
533 ushort min = ushort.MaxValue;
534 ushort max = ushort.MinValue;
535 for (int i = 0, src = 0; i < availablePixels; i++, src += 2)
536 {
537 ushort value = (ushort)(grayBytes[src] | (grayBytes[src + 1] << 8));
538 if (value < min)
539 min = value;
540 if (value > max)
541 max = value;
542 }
543
544 int range = max - min;
545 if (range <= 0)
546 range = 1;
547
548 for (int i = 0, src = 0, dst = 0; i < availablePixels; i++, src += 2, dst += 4)
549 {
550 ushort value = (ushort)(grayBytes[src] | (grayBytes[src + 1] << 8));
551 byte scaled = (byte)(((value - min) * 255) / range);
552 rgba[dst] = scaled;
553 rgba[dst + 1] = scaled;
554 rgba[dst + 2] = scaled;
555 rgba[dst + 3] = 255;
556 }
557 return rgba;
558 }
559
560 private static byte[] Convert24BitRgbToBgra(byte[] rgbBytes, int pixelCount)
561 {
562 byte[] rgba = new byte[pixelCount * 4];
563 int availablePixels = Math.Min(pixelCount, rgbBytes.Length / 3);
564 for (int i = 0, src = 0, dst = 0; i < availablePixels; i++, src += 3, dst += 4)
565 {
566 byte blue = rgbBytes[src];
567 byte green = rgbBytes[src + 1];
568 byte red = rgbBytes[src + 2];
569 rgba[dst] = blue;
570 rgba[dst + 1] = green;
571 rgba[dst + 2] = red;
572 rgba[dst + 3] = 255;
573 }
574 return rgba;
575 }
576
580 public bool HasTileTexture(TileIndex index)
581 {
582 return _textureCache.ContainsKey(index);
583 }
584
588 public void ReleaseTileTexture(TileIndex index)
589 {
590 if (_textureCache.TryGetValue(index, out int tex))
591 {
592 MakeCurrent();
593 GL.DeleteTexture(tex);
594 _textureCache.Remove(index);
595 }
596 }
597
601 public void ReleaseLevelTextures(int level)
602 {
603 if (!_glInitialized) return;
604
605 MakeCurrent();
606
607 var toRemove = _textureCache.Where(kvp => kvp.Key.Level == level).ToList();
608 foreach (var kvp in toRemove)
609 {
610 GL.DeleteTexture(kvp.Value);
611 _textureCache.Remove(kvp.Key);
612 }
613 }
614
618 public void ClearTextureCache()
619 {
620 if (!_glInitialized) return;
621
622 MakeCurrent();
623
624 foreach (var tex in _textureCache.Values)
625 GL.DeleteTexture(tex);
626
627 _textureCache.Clear();
628 }
629
630 private void EvictOldestTextures(int count)
631 {
632 // Simple FIFO eviction — could be improved with LRU tracking
633 var toRemove = _textureCache.Take(count).ToList();
634 foreach (var kvp in toRemove)
635 {
636 GL.DeleteTexture(kvp.Value);
637 _textureCache.Remove(kvp.Key);
638 }
639 }
640
641 // ============================================================================
642 // Public API
643 // ============================================================================
644
648 public void RequestRedraw()
649 {
650 NeedsRedraw = true;
651 Invalidate();
652 }
653
654 public void SetOverlayBitmap(System.Drawing.Bitmap bitmap)
655 {
656 if (bitmap == null)
657 {
658 _overlayPixels = null;
659 _overlayWidth = 0;
660 _overlayHeight = 0;
661 _overlayDirty = true;
663 return;
664 }
665
666 var rect = new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height);
667 var data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
668
669 try
670 {
671 int rowBytes = bitmap.Width * 4;
672 byte[] buffer = new byte[rowBytes * bitmap.Height];
673 int stride = data.Stride;
674 int absStride = Math.Abs(stride);
675
676 for (int y = 0; y < bitmap.Height; y++)
677 {
678 IntPtr srcRow = stride >= 0
679 ? IntPtr.Add(data.Scan0, y * stride)
680 : IntPtr.Add(data.Scan0, (bitmap.Height - 1 - y) * absStride);
681 Marshal.Copy(srcRow, buffer, y * rowBytes, rowBytes);
682 }
683
684 _overlayPixels = buffer;
685 _overlayWidth = bitmap.Width;
686 _overlayHeight = bitmap.Height;
687 _overlayDirty = true;
689 }
690 finally
691 {
692 bitmap.UnlockBits(data);
693 }
694 }
695
696 public void ClearOverlayBitmap()
697 {
698 _overlayPixels = null;
699 _overlayWidth = 0;
700 _overlayHeight = 0;
701 _overlayDirty = true;
703 }
704
708 public void SetTilesToRender(IEnumerable<TileRenderInfo> tiles)
709 {
710 TilesToRender.Clear();
711 TilesToRender.AddRange(tiles);
712 }
713
714 public int CachedTextureCount => _textureCache.Count;
715
720 public byte[] ReadPixels()
721 {
722 if (!_glInitialized) return null;
723
724 MakeCurrent();
725
726 int width = Width;
727 int height = Height;
728
729 byte[] pixels = new byte[width * height * 4];
730 GL.ReadPixels(0, 0, width, height, PixelFormat.Rgba, PixelType.UnsignedByte, pixels);
731
732 return pixels;
733 }
734 }
735
736 // ============================================================================
737 // Supporting Types
738 // ============================================================================
739
743 public struct TileRenderInfo
744 {
745 public TileIndex Index;
746
747 // Screen-space position and size in pixels
748 public float ScreenX;
749 public float ScreenY;
750 public float ScreenWidth;
751 public float ScreenHeight;
752
753 public TileRenderInfo(TileIndex index, float x, float y, float w, float h)
754 {
755 Index = index;
756 ScreenX = x;
757 ScreenY = y;
758 ScreenWidth = w;
759 ScreenHeight = h;
760 }
761 }
762}
AForge.PixelFormat PixelFormat
void ReleaseLevelTextures(int level)
Release all textures for a specific pyramid level.
void ClearTextureCache()
Clear all cached textures.
bool IsGLReady
True after the GL context has been fully initialized and is ready for texture uploads and rendering.
List< TileRenderInfo > TilesToRender
EventHandler GLReady
Fires exactly once after the GL context has been successfully initialized. Subscribe to this from Ima...
void SetTilesToRender(IEnumerable< TileRenderInfo > tiles)
Prepare tiles for rendering. Call before RequestRedraw().
void ReleaseTileTexture(TileIndex index)
Release a specific tile texture.
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 SetOverlayBitmap(System.Drawing.Bitmap bitmap)
byte[] ReadPixels()
Read pixels from the current framebuffer (for export/save operations). This is slow — only use for ex...
void RequestRedraw()
Request a redraw of the GLArea.
bool HasTileTexture(TileIndex index)
Check if a tile texture is already in the GPU cache.
Information needed to render a single tile.
TileRenderInfo(TileIndex index, float x, float y, float w, float h)