21 private const bool VerboseLogging =
false;
22 private static readonly
bool DiagnosticLogging =
true;
27 private bool _glInitialized;
30 private int _shaderProgram;
31 private int _completeFrameTexture;
32 private int _completeFrameWidth;
33 private int _completeFrameHeight;
34 private bool _tileFrameComplete;
39 private int _locViewportSize;
41 private int _locUvMin;
42 private int _locUvMax;
48 private Dictionary<TileIndex, int> _textureCache =
new();
49 private Dictionary<TileIndex, (
int w,
int h)> _textureSizes =
new();
50 private readonly HashSet<TileIndex> _protectedTileIndices =
new();
51 private long _textureBytes;
52 private const int MAX_CACHED_TEXTURES = 128;
53 private const long MAX_CACHED_TEXTURE_BYTES = 96L * 1024 * 1024;
59 private GRContext _grContext;
60 private GRBackendRenderTarget _renderTarget;
61 private SKSurface _skSurface;
67 public List<TileRenderInfo> TilesToRender {
get; } =
new();
68 public bool NeedsRedraw {
get;
set; } =
true;
69 public bool PreservePreviousFrameWhileIncomplete {
get;
set; } =
false;
70 private Action<byte[], int, int> _pendingWholeImageCapture;
75 public float ImageScreenX {
get;
set; } = -1;
76 public float ImageScreenY {
get;
set; } = -1;
77 public float ImageScreenW {
get;
set; } = -1;
78 public float ImageScreenH {
get;
set; } = -1;
89 private const string VertexShaderSource =
@"
92layout(location=0) in vec2 aPos;
93layout(location=1) in vec2 aUV;
95uniform vec2 pos; // pixel-space top-left of tile
96uniform vec2 size; // pixel-space size of tile
97uniform vec2 viewportSize; // (width, height) in pixels
98uniform vec2 uvMin; // UV sub-region min (for boundary clipping)
99uniform vec2 uvMax; // UV sub-region max (for boundary clipping)
105 // aPos is in [0,1] x [0,1]
106 vec2 pixelPos = aPos * size + pos;
108 // Convert from pixel coords to NDC (-1..1), with Y flipped for GL
110 ndc.x = (pixelPos.x / viewportSize.x) * 2.0 - 1.0;
111 ndc.y = 1.0 - (pixelPos.y / viewportSize.y) * 2.0;
113 gl_Position = vec4(ndc, 0.0, 1.0);
115 // Map aUV [0,1] to the sub-region [uvMin, uvMax]
116 uv = uvMin + aUV * (uvMax - uvMin);
120 private const string FragmentShaderSource =
@"
125uniform sampler2D tex;
129 // Fix: Removed '1.0 - uv.y'.
130 // Image data is usually Top-Down. OpenGL Textures are Bottom-Up.
131 // This implicit flip means UV(0,0) (Top-Left Quad) maps to Texture Bottom (Image Top).
132 // This is the correct orientation for Top-Left origin rendering.
133 FragColor = texture(tex, vec2(uv.x, uv.y));
143 HasDepthBuffer =
false;
144 HasStencilBuffer =
false;
152 Realized += OnRealized;
161 private void OnRealized(
object sender, EventArgs e)
167 if (VerboseLogging) Console.WriteLine($
"GLArea error on realize.");
174 _glInitialized =
true;
175 Gtk.Application.Invoke((s, a) => App.viewer?.RequestDeferredRender());
178 private void OnResized(
object sender, EventArgs e)
180 if (!_glInitialized)
return;
189 Gtk.Application.Invoke((s, a) => App.viewer?.RequestDeferredRender());
196 private void InitializeGL()
199 int vertexShader = CompileShader(ShaderType.VertexShader, VertexShaderSource);
200 int fragmentShader = CompileShader(ShaderType.FragmentShader, FragmentShaderSource);
202 _shaderProgram = GL.CreateProgram();
203 GL.AttachShader(_shaderProgram, vertexShader);
204 GL.AttachShader(_shaderProgram, fragmentShader);
205 GL.LinkProgram(_shaderProgram);
207 GL.GetProgram(_shaderProgram, GetProgramParameterName.LinkStatus, out
int linkStatus);
210 string infoLog = GL.GetProgramInfoLog(_shaderProgram);
211 throw new Exception($
"Shader link failed: {infoLog}");
214 GL.DeleteShader(vertexShader);
215 GL.DeleteShader(fragmentShader);
218 _locPos = GL.GetUniformLocation(_shaderProgram,
"pos");
219 _locSize = GL.GetUniformLocation(_shaderProgram,
"size");
220 _locViewportSize = GL.GetUniformLocation(_shaderProgram,
"viewportSize");
221 _locTex = GL.GetUniformLocation(_shaderProgram,
"tex");
222 _locUvMin = GL.GetUniformLocation(_shaderProgram,
"uvMin");
223 _locUvMax = GL.GetUniformLocation(_shaderProgram,
"uvMax");
226 float[] quadVertices =
238 _vao = GL.GenVertexArray();
239 _vbo = GL.GenBuffer();
241 GL.BindVertexArray(_vao);
242 GL.BindBuffer(BufferTarget.ArrayBuffer, _vbo);
243 GL.BufferData(BufferTarget.ArrayBuffer, quadVertices.Length *
sizeof(
float),
244 quadVertices, BufferUsageHint.StaticDraw);
247 GL.VertexAttribPointer(0, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 0);
248 GL.EnableVertexAttribArray(0);
251 GL.VertexAttribPointer(1, 2, VertexAttribPointerType.Float,
false, 4 *
sizeof(
float), 2 *
sizeof(
float));
252 GL.EnableVertexAttribArray(1);
254 GL.BindVertexArray(0);
257 private int CompileShader(ShaderType type,
string source)
259 int shader = GL.CreateShader(type);
260 GL.ShaderSource(shader, source);
261 GL.CompileShader(shader);
263 GL.GetShader(shader, ShaderParameter.CompileStatus, out
int status);
266 string infoLog = GL.GetShaderInfoLog(shader);
267 throw new Exception($
"Shader compilation failed ({type}): {infoLog}");
273 private void CleanupGL()
275 if (_shaderProgram != 0)
277 GL.DeleteProgram(_shaderProgram);
283 GL.DeleteBuffer(_vbo);
289 GL.DeleteVertexArray(_vao);
294 DiscardCompleteFrame();
301 private void InitializeSkia(
int framebuffer = -1)
306 GL.GetInteger(GetPName.FramebufferBinding, out framebuffer);
311 _grContext ??= GRContext.CreateGl();
315 _skSurface?.Dispose();
317 _renderTarget?.Dispose();
318 _renderTarget =
null;
320 int targetWidth = AllocatedWidth * ScaleFactor;
321 int targetHeight = AllocatedHeight * ScaleFactor;
322 if (_grContext ==
null || targetWidth <= 0 || targetHeight <= 0)
326 var framebufferInfo =
new GRGlFramebufferInfo((uint)framebuffer, 0x8058);
328 _renderTarget =
new GRBackendRenderTarget(
331 0, 8, framebufferInfo);
333 _skSurface = SKSurface.Create(_grContext, _renderTarget, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888);
336 private void CleanupSkia()
338 _skSurface?.Dispose();
341 _renderTarget?.Dispose();
342 _renderTarget =
null;
344 _grContext?.Dispose();
352 private void OnRender(
object sender, RenderArgs args)
361 GL.GetInteger(GetPName.FramebufferBinding, out
int presentationFramebuffer);
363 int width = AllocatedWidth;
364 int height = AllocatedHeight;
366 if (width <= 0 || height <= 0)
373 RenderTiles(width, height);
374 if (_tileFrameComplete)
375 CaptureCompleteTileFrame(width, height);
380 CaptureWholeImageFrameIfRequested(width, height);
385 RenderSkiaOverlay(width, height, presentationFramebuffer);
389 Console.WriteLine($
"Skia overlay render error: {ex.Message}");
396 GL.BindFramebuffer(FramebufferTarget.Framebuffer, presentationFramebuffer);
397 ForceOpaqueFramebuffer();
402 private static void ForceOpaqueFramebuffer()
404 GL.Disable(EnableCap.ScissorTest);
405 GL.ColorMask(
false,
false,
false,
true);
406 GL.ClearColor(0f, 0f, 0f, 1f);
407 GL.Clear(ClearBufferMask.ColorBufferBit);
408 GL.ColorMask(
true,
true,
true,
true);
411 private void RenderTiles(
int width,
int height)
413 _tileFrameComplete =
false;
414 int scale = ScaleFactor;
415 GL.Viewport(0, 0, width * scale, height * scale);
422 GL.Disable(EnableCap.ScissorTest);
423 GL.ColorMask(
true,
true,
true,
true);
424 GL.ClearColor(0.2f, 0.2f, 0.2f, 1f);
425 GL.Clear(ClearBufferMask.ColorBufferBit);
427 if (TilesToRender.Count == 0)
429 if (PreservePreviousFrameWhileIncomplete)
430 RenderCompleteFrameFallback(width, height);
431 LogDiag($
"[RenderTiles] no tiles to render viewport={width}x{height} imageScreen=({ImageScreenX},{ImageScreenY},{ImageScreenW},{ImageScreenH})");
435 bool hasAllRenderableTextures = TilesToRender.All(tile => _textureCache.ContainsKey(tile.Index));
436 if (!hasAllRenderableTextures)
442 if (_completeFrameTexture != 0)
443 RenderCompleteFrameFallback(width, height);
444 LogDiag($
"[RenderTiles] waiting for atomic tile frame viewport={width}x{height} tileCount={TilesToRender.Count}");
448 LogDiag($
"[RenderTiles] tiles={TilesToRender.Count} viewport={width}x{height} imageScreen=({ImageScreenX},{ImageScreenY},{ImageScreenW},{ImageScreenH})");
450 GL.Enable(EnableCap.Blend);
451 GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
455 if (ImageScreenW > 0 && ImageScreenH > 0)
457 GL.Enable(EnableCap.ScissorTest);
460 int sx = (int)(Math.Max(0, ImageScreenX) * scale);
461 int sy = (int)(Math.Max(0, (height - ImageScreenY - ImageScreenH)) * scale);
462 int sw = (int)(Math.Min(ImageScreenW, width - ImageScreenX) * scale);
463 int sh = (int)(Math.Min(ImageScreenH, height - ImageScreenY) * scale);
464 GL.Scissor(sx, sy, Math.Max(0, sw), Math.Max(0, sh));
467 GL.UseProgram(_shaderProgram);
468 GL.BindVertexArray(_vao);
470 GL.Uniform2(_locViewportSize, (
float)width, (
float)height);
471 GL.Uniform1(_locTex, 0);
472 GL.ActiveTexture(TextureUnit.Texture0);
474 int renderedCount = 0;
475 foreach (var tile
in TilesToRender)
477 if (!_textureCache.TryGetValue(tile.Index, out
int texId))
479 LogDiag($
"[RenderTiles] missing texture tile={tile.Index}");
483 GL.Uniform2(_locPos, tile.ScreenX, tile.ScreenY);
484 GL.Uniform2(_locSize, tile.ScreenWidth, tile.ScreenHeight);
485 GL.Uniform2(_locUvMin, tile.U0, tile.V0);
486 GL.Uniform2(_locUvMax, tile.U1, tile.V1);
488 LogDiag($
"[RenderTiles] draw tile={tile.Index} pos=({tile.ScreenX:F1},{tile.ScreenY:F1}) size=({tile.ScreenWidth:F1},{tile.ScreenHeight:F1}) tex={texId}");
490 GL.BindTexture(TextureTarget.Texture2D, texId);
491 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
495 LogDiag($
"[RenderTiles] rendered={renderedCount}");
497 GL.Disable(EnableCap.ScissorTest);
498 GL.BindVertexArray(0);
500 GL.Disable(EnableCap.Blend);
501 _tileFrameComplete =
true;
504 private void RenderCompleteFrameFallback(
int width,
int height)
506 if (_completeFrameTexture == 0 || _completeFrameWidth <= 0 || _completeFrameHeight <= 0)
509 GL.Disable(EnableCap.ScissorTest);
510 GL.Disable(EnableCap.Blend);
511 GL.UseProgram(_shaderProgram);
512 GL.BindVertexArray(_vao);
513 GL.Uniform2(_locViewportSize, (
float)width, (
float)height);
514 GL.Uniform1(_locTex, 0);
515 GL.Uniform2(_locPos, 0f, 0f);
516 GL.Uniform2(_locSize, (
float)width, (
float)height);
519 GL.Uniform2(_locUvMin, 0f, 1f);
520 GL.Uniform2(_locUvMax, 1f, 0f);
521 GL.ActiveTexture(TextureUnit.Texture0);
522 GL.BindTexture(TextureTarget.Texture2D, _completeFrameTexture);
523 GL.DrawArrays(PrimitiveType.Triangles, 0, 6);
524 GL.BindTexture(TextureTarget.Texture2D, 0);
525 GL.BindVertexArray(0);
529 private void CaptureCompleteTileFrame(
int width,
int height)
531 int physicalWidth = width * Math.Max(1, ScaleFactor);
532 int physicalHeight = height * Math.Max(1, ScaleFactor);
533 if (physicalWidth <= 0 || physicalHeight <= 0)
536 if (_completeFrameTexture == 0)
537 _completeFrameTexture = GL.GenTexture();
538 if (_completeFrameTexture == 0)
541 GL.BindTexture(TextureTarget.Texture2D, _completeFrameTexture);
542 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (
int)TextureMinFilter.Linear);
543 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (
int)TextureMagFilter.Linear);
544 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (
int)TextureWrapMode.ClampToEdge);
545 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (
int)TextureWrapMode.ClampToEdge);
547 if (_completeFrameWidth != physicalWidth || _completeFrameHeight != physicalHeight)
549 GL.CopyTexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba,
550 0, 0, physicalWidth, physicalHeight, 0);
551 _completeFrameWidth = physicalWidth;
552 _completeFrameHeight = physicalHeight;
556 GL.CopyTexSubImage2D(TextureTarget.Texture2D, 0, 0, 0,
557 0, 0, physicalWidth, physicalHeight);
559 GL.BindTexture(TextureTarget.Texture2D, 0);
562 public void DiscardCompleteFrame()
564 if (_completeFrameTexture != 0 && _glInitialized)
567 GL.DeleteTexture(_completeFrameTexture);
569 _completeFrameTexture = 0;
570 _completeFrameWidth = 0;
571 _completeFrameHeight = 0;
574 private void CaptureWholeImageFrameIfRequested(
int width,
int height)
576 var callback = _pendingWholeImageCapture;
577 if (callback ==
null || TilesToRender.Count == 0 ||
578 !TilesToRender.All(tile => _textureCache.ContainsKey(tile.Index)))
583 const float tolerance = 1.5f;
584 if (ImageScreenW <= 0 || ImageScreenH <= 0 ||
585 ImageScreenX < -tolerance || ImageScreenY < -tolerance ||
586 ImageScreenX + ImageScreenW > width + tolerance ||
587 ImageScreenY + ImageScreenH > height + tolerance)
590 int scale = Math.Max(1, ScaleFactor);
591 int framebufferWidth = width * scale;
592 int framebufferHeight = height * scale;
593 int left = Math.Clamp((
int)Math.Round(ImageScreenX * scale), 0, framebufferWidth - 1);
594 int top = Math.Clamp((
int)Math.Round(ImageScreenY * scale), 0, framebufferHeight - 1);
595 int captureWidth = Math.Clamp((
int)Math.Round(ImageScreenW * scale), 1, framebufferWidth - left);
596 int captureHeight = Math.Clamp((
int)Math.Round(ImageScreenH * scale), 1, framebufferHeight - top);
597 int glBottom = framebufferHeight - top - captureHeight;
599 byte[] rgbaBottomUp =
new byte[captureWidth * captureHeight * 4];
600 GL.ReadPixels(left, glBottom, captureWidth, captureHeight,
601 PixelFormat.Rgba, PixelType.UnsignedByte, rgbaBottomUp);
604 byte[] bgraTopDown =
new byte[rgbaBottomUp.Length];
605 int rowBytes = captureWidth * 4;
606 for (
int y = 0; y < captureHeight; y++)
608 int sourceRow = (captureHeight - 1 - y) * rowBytes;
609 int destinationRow = y * rowBytes;
610 for (
int x = 0; x < captureWidth; x++)
612 int source = sourceRow + x * 4;
613 int destination = destinationRow + x * 4;
614 bgraTopDown[destination] = rgbaBottomUp[source + 2];
615 bgraTopDown[destination + 1] = rgbaBottomUp[source + 1];
616 bgraTopDown[destination + 2] = rgbaBottomUp[source];
617 bgraTopDown[destination + 3] = 255;
621 _pendingWholeImageCapture =
null;
622 callback(bgraTopDown, captureWidth, captureHeight);
625 private void RenderSkiaOverlay(
int width,
int height,
int presentationFramebuffer)
628 InitializeSkia(presentationFramebuffer);
629 if (_skSurface ==
null)
635 _grContext.ResetContext(GRGlBackendState.All);
636 var canvas = _skSurface.Canvas;
640 _grContext.Submit(
true);
654 LogDiag($
"[UploadTileTexture] gl-not-initialized tile={index}");
658 if (pixelData ==
null || pixelData.Length == 0)
660 LogDiag($
"[UploadTileTexture] empty pixel data tile={index}");
669 if (tileWidth <= 0 || tileHeight <= 0)
671 LogDiag($
"[UploadTileTexture] invalid dimensions tile={index} size={tileWidth}x{tileHeight}");
675 int expectedBytes = tileWidth * tileHeight * 4;
676 if (pixelData.Length < expectedBytes)
678 LogDiag($
"[UploadTileTexture] buffer too small tile={index} need={expectedBytes} got={pixelData.Length}");
685 if (_textureCache.ContainsKey(index))
687 LogDiag($
"[UploadTileTexture] already cached tile={index}");
691 long newTextureBytes = (long)tileWidth * tileHeight * 4;
695 while (_textureCache.Count > 0 &&
696 (_textureCache.Count >= MAX_CACHED_TEXTURES ||
697 _textureBytes + newTextureBytes > MAX_CACHED_TEXTURE_BYTES))
699 EvictOldestTextures(1);
706 int tex = GL.GenTexture();
709 Console.WriteLine($
"OpenGL failed to allocate texture for tile {index}.");
712 GL.BindTexture(TextureTarget.Texture2D, tex);
713 GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
715 var handle = System.Runtime.InteropServices.GCHandle.Alloc(pixelData, System.Runtime.InteropServices.GCHandleType.Pinned);
718 GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba,
719 tileWidth, tileHeight, 0,
720 PixelFormat.Bgra, PixelType.UnsignedByte,
721 handle.AddrOfPinnedObject());
728 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (
int)TextureMinFilter.Linear);
729 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (
int)TextureMagFilter.Linear);
730 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (
int)TextureWrapMode.ClampToEdge);
731 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (
int)TextureWrapMode.ClampToEdge);
733 GL.BindTexture(TextureTarget.Texture2D, 0);
735 _textureCache[index] = tex;
736 _textureSizes[index] = (tileWidth, tileHeight);
737 _textureBytes += newTextureBytes;
738 LogDiag($
"[UploadTileTexture] uploaded tile={index} tex={tex} size={tileWidth}x{tileHeight} bytes={pixelData.Length}");
747 if (_textureSizes.TryGetValue(index, out var sz))
748 { w = sz.w; h = sz.h; }
758 return _textureCache.ContainsKey(index);
766 if (_textureCache.TryGetValue(index, out
int tex))
769 GL.DeleteTexture(tex);
770 _textureCache.Remove(index);
771 if (_textureSizes.TryGetValue(index, out var sz))
773 _textureBytes -= (long)sz.w * sz.h * 4;
774 _textureSizes.Remove(index);
784 if (!_glInitialized)
return;
788 var toRemove = _textureCache.Where(kvp => kvp.Key.Level == level).ToList();
789 foreach (var kvp
in toRemove)
791 GL.DeleteTexture(kvp.Value);
792 if (_textureSizes.TryGetValue(kvp.Key, out var sz))
794 _textureBytes -= (long)sz.w * sz.h * 4;
795 _textureSizes.Remove(kvp.Key);
797 _textureCache.Remove(kvp.Key);
806 if (!_glInitialized)
return;
810 foreach (var tex
in _textureCache.Values)
812 GL.DeleteTexture(tex);
814 _textureCache.Clear();
815 _textureSizes.Clear();
816 _protectedTileIndices.Clear();
820 private void EvictOldestTextures(
int count)
825 var toRemove = _textureCache
826 .Where(kvp => !_protectedTileIndices.Contains(kvp.Key))
831 if (toRemove.Count < count)
833 var selected =
new HashSet<TileIndex>(toRemove.Select(kvp => kvp.Key));
834 toRemove.AddRange(_textureCache
835 .Where(kvp => !selected.Contains(kvp.Key))
836 .Take(count - toRemove.Count));
838 foreach (var kvp
in toRemove)
840 GL.DeleteTexture(kvp.Value);
841 if (_textureSizes.TryGetValue(kvp.Key, out var sz))
843 _textureBytes -= (long)sz.w * sz.h * 4;
844 _textureSizes.Remove(kvp.Key);
846 _textureCache.Remove(kvp.Key);
870 _pendingWholeImageCapture = callback;
874 public void CancelWholeImageFrameCapture(Action<
byte[],
int,
int> callback)
876 if (ReferenceEquals(_pendingWholeImageCapture, callback))
877 _pendingWholeImageCapture =
null;
885 TilesToRender.Clear();
886 TilesToRender.AddRange(tiles);
887 LogDiag($
"[SetTilesToRender] count={TilesToRender.Count}");
897 _protectedTileIndices.Clear();
898 foreach (var index
in indices)
899 _protectedTileIndices.Add(index);
900 foreach (var tile
in TilesToRender)
901 _protectedTileIndices.Add(tile.Index);
904 public int CachedTextureCount => _textureCache.Count;
912 if (!_glInitialized)
return null;
916 int width = AllocatedWidth;
917 int height = AllocatedHeight;
919 byte[] pixels =
new byte[width * height * 4];
920 GL.ReadPixels(0, 0, width, height, PixelFormat.Rgba, PixelType.UnsignedByte, pixels);
925 private static void LogDiag(
string message)
927 if (!DiagnosticLogging)
930 AppLog.Append(message);