20 private bool _glInitialized =
false;
23 private int _shaderProgram;
28 private int _locViewportSize;
35 private Dictionary<TileIndex, int> _textureCache =
new();
36 private const int MAX_CACHED_TEXTURES = 500;
38 private int _overlayTexture;
39 private int _overlayWidth;
40 private int _overlayHeight;
41 private byte[] _overlayPixels;
42 private bool _overlayDirty;
61 private bool _glReadyFired =
false;
73 private const string VertexShaderSource =
@"
76layout(location=0) in vec2 aPos;
77layout(location=1) in vec2 aUV;
79uniform vec2 pos; // pixel-space top-left of tile
80uniform vec2 size; // pixel-space size of tile
81uniform vec2 viewportSize; // (width, height) in pixels
87 // aPos is in [0,1] x [0,1]
88 vec2 pixelPos = aPos * size + pos;
90 // Convert from pixel coords to NDC (-1..1), with Y flipped for GL
92 ndc.x = (pixelPos.x / viewportSize.x) * 2.0 - 1.0;
93 ndc.y = 1.0 - (pixelPos.y / viewportSize.y) * 2.0;
95 gl_Position = vec4(ndc, 0.0, 1.0);
100 private const string FragmentShaderSource =
@"
105uniform sampler2D tex;
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));
123 Disposed += OnDisposed;
127 private void OnDisposed(
object? sender, EventArgs e)
136 private void OnPaint(
object? sender, EventArgs e)
145 if (_glInitialized && !_glReadyFired)
147 _glReadyFired =
true;
148 Console.WriteLine(
"[SlideGLArea] GL initialized. Firing GLReady event.");
149 GLReady?.Invoke(
this, EventArgs.Empty);
158 private void OnResized(
object? sender, EventArgs e)
160 if (!_glInitialized)
return;
169 private void InitializeGL()
171 if (_glInitialized)
return;
176 int vertexShader = CompileShader(ShaderType.VertexShader, VertexShaderSource);
177 int fragmentShader = CompileShader(ShaderType.FragmentShader, FragmentShaderSource);
179 _shaderProgram = GL.CreateProgram();
180 GL.AttachShader(_shaderProgram, vertexShader);
181 GL.AttachShader(_shaderProgram, fragmentShader);
182 GL.LinkProgram(_shaderProgram);
184 GL.GetProgram(_shaderProgram, GetProgramParameterName.LinkStatus, out
int linkStatus);
187 string infoLog = GL.GetProgramInfoLog(_shaderProgram);
188 Console.WriteLine($
"[SlideGLArea] Shader link FAILED: {infoLog}");
192 GL.DeleteShader(vertexShader);
193 GL.DeleteShader(fragmentShader);
196 _locPos = GL.GetUniformLocation(_shaderProgram,
"pos");
197 _locSize = GL.GetUniformLocation(_shaderProgram,
"size");
198 _locViewportSize = GL.GetUniformLocation(_shaderProgram,
"viewportSize");
199 _locTex = GL.GetUniformLocation(_shaderProgram,
"tex");
201 Console.WriteLine($
"[SlideGLArea] Uniform locations: pos={_locPos} size={_locSize} viewport={_locViewportSize} tex={_locTex}");
204 float[] quadVertices =
216 _vao = GL.GenVertexArray();
217 _vbo = GL.GenBuffer();
219 GL.BindVertexArray(_vao);
220 GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
221 GL.BufferData(BufferTarget.ArrayBuffer, quadVertices.Length *
sizeof(
float),
222 quadVertices, BufferUsageHint.StaticDraw);
225 GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 0);
226 GL.EnableVertexAttribArray(0);
229 GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 2 *
sizeof(
float));
230 GL.EnableVertexAttribArray(1);
232 GL.BindVertexArray(0);
235 var err = GL.GetError();
236 if (err != ErrorCode.NoError)
237 Console.WriteLine($
"[SlideGLArea] GL error after init: {err}");
239 _glInitialized =
true;
240 Console.WriteLine($
"[SlideGLArea] GL initialization complete. VAO={_vao} VBO={_vbo} Program={_shaderProgram}");
244 Console.WriteLine($
"[SlideGLArea] GL initialization FAILED: {ex.Message}");
248 private int CompileShader(ShaderType type,
string source)
250 int shader = GL.CreateShader(type);
251 GL.ShaderSource(shader, source);
252 GL.CompileShader(shader);
254 GL.GetShader(shader, ShaderParameter.CompileStatus, out
int status);
257 string infoLog = GL.GetShaderInfoLog(shader);
258 Console.WriteLine($
"[SlideGLArea] Shader compile FAILED ({type}): {infoLog}");
259 throw new Exception($
"Shader compilation failed ({type}): {infoLog}");
262 Console.WriteLine($
"[SlideGLArea] Shader compiled OK ({type})");
266 private void CleanupGL()
268 if (_shaderProgram != 0)
270 GL.DeleteProgram(_shaderProgram);
276 GL.DeleteBuffer(_vbo);
282 GL.DeleteVertexArray(_vao);
286 if (_overlayTexture != 0)
288 GL.DeleteTexture(_overlayTexture);
293 _glInitialized =
false;
300 private void RenderFrame()
304 if (width <= 0 || height <= 0)
return;
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);
312 GL.Enable(EnableCap.Blend);
313 GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
315 GL.UseProgram(_shaderProgram);
316 GL.BindVertexArray(_vao);
319 GL.Uniform2(_locViewportSize, (
float)width, (
float)height);
320 GL.Uniform1(_locTex, 0);
321 GL.ActiveTexture(TextureUnit.Texture0);
329 if (!_textureCache.TryGetValue(tile.Index, out
int texId))
335 GL.Uniform2(_locPos, tile.ScreenX, tile.ScreenY);
336 GL.Uniform2(_locSize, tile.ScreenWidth, tile.ScreenHeight);
338 GL.BindTexture(TextureTarget.Texture2D, texId);
339 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
344 UploadOverlayTextureIfNeeded();
345 if (_overlayTexture != 0)
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);
353 GL.BindVertexArray(0);
355 GL.Disable(EnableCap.Blend);
358 private void UploadOverlayTextureIfNeeded()
363 _overlayDirty =
false;
380 if (_overlayTexture != 0)
382 GL.DeleteTexture(_overlayTexture);
386 if (_overlayPixels ==
null || _overlayPixels.Length == 0 || _overlayWidth <= 0 || _overlayHeight <= 0)
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);
395 var glErr = GL.GetError();
396 if (glErr != ErrorCode.NoError)
398 GL.DeleteTexture(_overlayTexture);
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);
420 if (pixelData ==
null || pixelData.Length == 0)
424 if (!IsHandleCreated)
447 if (_textureCache.ContainsKey(index))
451 if (_textureCache.Count >= MAX_CACHED_TEXTURES)
452 EvictOldestTextures(MAX_CACHED_TEXTURES / 4);
454 int expectedPixels = tileWidth * tileHeight;
455 if (expectedPixels <= 0)
458 byte[] uploadData = pixelData;
460 int expectedBytes = expectedPixels * 4;
462 if (pixelData.Length == expectedBytes)
466 else if (pixelData.Length == expectedPixels * 3)
468 uploadData = Convert24BitRgbToBgra(pixelData, expectedPixels);
470 else if (pixelData.Length >= expectedPixels * 2)
472 uploadData = Convert16BitGrayToBgra(pixelData, expectedPixels);
474 else if (pixelData.Length >= expectedPixels)
476 uploadData = Convert8BitGrayToBgra(pixelData, expectedPixels);
484 int tex = GL.GenTexture();
485 GL.BindTexture(TextureTarget.Texture2D, tex);
486 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
490 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
491 tileWidth, tileHeight, 0, uploadFormat, PixelType.UnsignedByte, uploadData);
493 var glErr = GL.GetError();
494 if (glErr != ErrorCode.NoError)
496 GL.DeleteTexture(tex);
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);
505 GL.BindTexture(TextureTarget.Texture2D, 0);
506 _textureCache[index] = tex;
510 private static byte[] Convert8BitGrayToBgra(
byte[] grayBytes,
int pixelCount)
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)
516 byte value = grayBytes[src];
518 rgba[dst + 1] = value;
519 rgba[dst + 2] = value;
525 private static byte[] Convert16BitGrayToBgra(
byte[] grayBytes,
int pixelCount)
527 byte[] rgba =
new byte[pixelCount * 4];
528 int availablePixels = Math.Min(pixelCount, grayBytes.Length / 2);
530 if (availablePixels <= 0)
533 ushort
min = ushort.MaxValue;
534 ushort
max = ushort.MinValue;
535 for (
int i = 0, src = 0; i < availablePixels; i++, src += 2)
537 ushort value = (ushort)(grayBytes[src] | (grayBytes[src + 1] << 8));
548 for (
int i = 0, src = 0, dst = 0; i < availablePixels; i++, src += 2, dst += 4)
550 ushort value = (ushort)(grayBytes[src] | (grayBytes[src + 1] << 8));
551 byte scaled = (byte)(((value - min) * 255) / range);
553 rgba[dst + 1] = scaled;
554 rgba[dst + 2] = scaled;
560 private static byte[] Convert24BitRgbToBgra(
byte[] rgbBytes,
int pixelCount)
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)
566 byte blue = rgbBytes[src];
567 byte green = rgbBytes[src + 1];
568 byte red = rgbBytes[src + 2];
570 rgba[dst + 1] = green;
582 return _textureCache.ContainsKey(index);
590 if (_textureCache.TryGetValue(index, out
int tex))
593 GL.DeleteTexture(tex);
594 _textureCache.Remove(index);
603 if (!_glInitialized)
return;
607 var toRemove = _textureCache.Where(kvp => kvp.Key.Level == level).ToList();
608 foreach (var kvp
in toRemove)
610 GL.DeleteTexture(kvp.Value);
611 _textureCache.Remove(kvp.Key);
620 if (!_glInitialized)
return;
624 foreach (var tex
in _textureCache.Values)
625 GL.DeleteTexture(tex);
627 _textureCache.Clear();
630 private void EvictOldestTextures(
int count)
633 var toRemove = _textureCache.Take(count).ToList();
634 foreach (var kvp
in toRemove)
636 GL.DeleteTexture(kvp.Value);
637 _textureCache.Remove(kvp.Key);
658 _overlayPixels =
null;
661 _overlayDirty =
true;
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);
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);
676 for (
int y = 0; y < bitmap.Height; y++)
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);
684 _overlayPixels = buffer;
685 _overlayWidth = bitmap.Width;
686 _overlayHeight = bitmap.Height;
687 _overlayDirty =
true;
692 bitmap.UnlockBits(data);
698 _overlayPixels =
null;
701 _overlayDirty =
true;
722 if (!_glInitialized)
return null;
729 byte[] pixels =
new byte[width * height * 4];
730 GL.ReadPixels(0, 0, width, height,
PixelFormat.Rgba, PixelType.UnsignedByte, pixels);