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;
55 private bool _glReadyFired =
false;
67 private const string VertexShaderSource =
@"
70layout(location=0) in vec2 aPos;
71layout(location=1) in vec2 aUV;
73uniform vec2 pos; // pixel-space top-left of tile
74uniform vec2 size; // pixel-space size of tile
75uniform vec2 viewportSize; // (width, height) in pixels
81 // aPos is in [0,1] x [0,1]
82 vec2 pixelPos = aPos * size + pos;
84 // Convert from pixel coords to NDC (-1..1), with Y flipped for GL
86 ndc.x = (pixelPos.x / viewportSize.x) * 2.0 - 1.0;
87 ndc.y = 1.0 - (pixelPos.y / viewportSize.y) * 2.0;
89 gl_Position = vec4(ndc, 0.0, 1.0);
94 private const string FragmentShaderSource =
@"
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));
117 Disposed += OnDisposed;
121 private void OnDisposed(
object? sender, EventArgs e)
130 private void OnPaint(
object? sender, EventArgs e)
139 if (_glInitialized && !_glReadyFired)
141 _glReadyFired =
true;
142 Console.WriteLine(
"[SlideGLArea] GL initialized. Firing GLReady event.");
143 GLReady?.Invoke(
this, EventArgs.Empty);
152 private void OnResized(
object? sender, EventArgs e)
154 if (!_glInitialized)
return;
163 private void InitializeGL()
165 if (_glInitialized)
return;
170 int vertexShader = CompileShader(ShaderType.VertexShader, VertexShaderSource);
171 int fragmentShader = CompileShader(ShaderType.FragmentShader, FragmentShaderSource);
173 _shaderProgram = GL.CreateProgram();
174 GL.AttachShader(_shaderProgram, vertexShader);
175 GL.AttachShader(_shaderProgram, fragmentShader);
176 GL.LinkProgram(_shaderProgram);
178 GL.GetProgram(_shaderProgram, GetProgramParameterName.LinkStatus, out
int linkStatus);
181 string infoLog = GL.GetProgramInfoLog(_shaderProgram);
182 Console.WriteLine($
"[SlideGLArea] Shader link FAILED: {infoLog}");
186 GL.DeleteShader(vertexShader);
187 GL.DeleteShader(fragmentShader);
190 _locPos = GL.GetUniformLocation(_shaderProgram,
"pos");
191 _locSize = GL.GetUniformLocation(_shaderProgram,
"size");
192 _locViewportSize = GL.GetUniformLocation(_shaderProgram,
"viewportSize");
193 _locTex = GL.GetUniformLocation(_shaderProgram,
"tex");
195 Console.WriteLine($
"[SlideGLArea] Uniform locations: pos={_locPos} size={_locSize} viewport={_locViewportSize} tex={_locTex}");
198 float[] quadVertices =
210 _vao = GL.GenVertexArray();
211 _vbo = GL.GenBuffer();
213 GL.BindVertexArray(_vao);
214 GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
215 GL.BufferData(BufferTarget.ArrayBuffer, quadVertices.Length *
sizeof(
float),
216 quadVertices, BufferUsageHint.StaticDraw);
219 GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 0);
220 GL.EnableVertexAttribArray(0);
223 GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 2 *
sizeof(
float));
224 GL.EnableVertexAttribArray(1);
226 GL.BindVertexArray(0);
229 var err = GL.GetError();
230 if (err != ErrorCode.NoError)
231 Console.WriteLine($
"[SlideGLArea] GL error after init: {err}");
233 _glInitialized =
true;
234 Console.WriteLine($
"[SlideGLArea] GL initialization complete. VAO={_vao} VBO={_vbo} Program={_shaderProgram}");
238 Console.WriteLine($
"[SlideGLArea] GL initialization FAILED: {ex.Message}");
242 private int CompileShader(ShaderType type,
string source)
244 int shader = GL.CreateShader(type);
245 GL.ShaderSource(shader, source);
246 GL.CompileShader(shader);
248 GL.GetShader(shader, ShaderParameter.CompileStatus, out
int status);
251 string infoLog = GL.GetShaderInfoLog(shader);
252 Console.WriteLine($
"[SlideGLArea] Shader compile FAILED ({type}): {infoLog}");
253 throw new Exception($
"Shader compilation failed ({type}): {infoLog}");
256 Console.WriteLine($
"[SlideGLArea] Shader compiled OK ({type})");
260 private void CleanupGL()
262 if (_shaderProgram != 0)
264 GL.DeleteProgram(_shaderProgram);
270 GL.DeleteBuffer(_vbo);
276 GL.DeleteVertexArray(_vao);
281 _glInitialized =
false;
288 private void RenderFrame()
292 if (width <= 0 || height <= 0)
return;
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);
303 GL.Enable(EnableCap.Blend);
304 GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
306 GL.UseProgram(_shaderProgram);
307 GL.BindVertexArray(_vao);
310 GL.Uniform2(_locViewportSize, (
float)width, (
float)height);
311 GL.Uniform1(_locTex, 0);
312 GL.ActiveTexture(TextureUnit.Texture0);
318 if (!_textureCache.TryGetValue(tile.Index, out
int texId))
324 GL.Uniform2(_locPos, tile.ScreenX, tile.ScreenY);
325 GL.Uniform2(_locSize, tile.ScreenWidth, tile.ScreenHeight);
327 GL.BindTexture(TextureTarget.Texture2D, texId);
328 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
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}.");
338 GL.BindVertexArray(0);
340 GL.Disable(EnableCap.Blend);
353 if (pixelData ==
null || pixelData.Length == 0)
357 if (!IsHandleCreated)
359 Console.WriteLine($
"[SlideGLArea] UploadTileTexture skipped — control handle not created yet.");
369 Console.WriteLine($
"[SlideGLArea] MakeCurrent failed in UploadTileTexture: {ex.Message}");
378 Console.WriteLine($
"[SlideGLArea] UploadTileTexture skipped — GL init failed.");
383 if (_textureCache.ContainsKey(index))
387 if (_textureCache.Count >= MAX_CACHED_TEXTURES)
388 EvictOldestTextures(MAX_CACHED_TEXTURES / 4);
391 int expectedBytes = tileWidth * tileHeight * 4;
392 if (pixelData.Length < expectedBytes)
394 Console.WriteLine($
"[SlideGLArea] UploadTileTexture: pixel data too small. Expected {expectedBytes} bytes for {tileWidth}x{tileHeight}, got {pixelData.Length}.");
399 int tex = GL.GenTexture();
400 GL.BindTexture(TextureTarget.Texture2D, tex);
401 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1);
403 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
404 tileWidth, tileHeight, 0,
PixelFormat.Rgba, PixelType.UnsignedByte, pixelData);
406 var glErr = GL.GetError();
407 if (glErr != ErrorCode.NoError)
409 Console.WriteLine($
"[SlideGLArea] GL error after TexImage2D ({tileWidth}x{tileHeight}): {glErr}");
410 GL.DeleteTexture(tex);
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);
419 GL.BindTexture(TextureTarget.Texture2D, 0);
420 _textureCache[index] = tex;
429 return _textureCache.ContainsKey(index);
437 if (_textureCache.TryGetValue(index, out
int tex))
440 GL.DeleteTexture(tex);
441 _textureCache.Remove(index);
450 if (!_glInitialized)
return;
454 var toRemove = _textureCache.Where(kvp => kvp.Key.Level == level).ToList();
455 foreach (var kvp
in toRemove)
457 GL.DeleteTexture(kvp.Value);
458 _textureCache.Remove(kvp.Key);
467 if (!_glInitialized)
return;
471 foreach (var tex
in _textureCache.Values)
472 GL.DeleteTexture(tex);
474 _textureCache.Clear();
477 private void EvictOldestTextures(
int count)
480 var toRemove = _textureCache.Take(count).ToList();
481 foreach (var kvp
in toRemove)
483 GL.DeleteTexture(kvp.Value);
484 _textureCache.Remove(kvp.Key);
516 if (!_glInitialized)
return null;
523 byte[] pixels =
new byte[width * height * 4];
524 GL.ReadPixels(0, 0, width, height,
PixelFormat.Rgba, PixelType.UnsignedByte, pixels);