using Microsoft.Extensions.Logging; using Silk.NET.Core.Native; using Silk.NET.OpenGL; using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace AcDream.App.Rendering.Wb { public static class GLHelpers { public static OpenGLGraphicsDevice? Device { get; set; } public static ILogger? Logger { get; set; } public static void Init(OpenGLGraphicsDevice device, ILogger logger) { Logger = logger; Device = device; } #if DEBUG private static bool _loggedVersion = false; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CheckErrors(GL gl, bool logErrors = false, [CallerMemberName] string callerName = "", [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) { var error = gl.GetError(); if (error != GLEnum.NoError) { if (!_loggedVersion) { _loggedVersion = true; var version = gl.GetStringS(GLEnum.Version); var vendor = gl.GetStringS(GLEnum.Vendor); var renderer = gl.GetStringS(GLEnum.Renderer); Logger?.LogInformation($"GL Version: {version}, Vendor: {vendor}, Renderer: {renderer}"); } string errorDetails = GetErrorDetails(error); string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}"; var program = (uint)gl.GetInteger(GLEnum.CurrentProgram); var vao = gl.GetInteger(GLEnum.VertexArrayBinding); var activeTex = gl.GetInteger(GLEnum.ActiveTexture); var threadId = System.Threading.Thread.CurrentThread.ManagedThreadId; string extraInfo = ""; if (program != 0) { bool isProgram = gl.IsProgram(program); gl.GetProgram(program, GLEnum.LinkStatus, out int linkStatus); gl.GetProgram(program, GLEnum.DeleteStatus, out int deleteStatus); gl.GetProgram(program, GLEnum.ValidateStatus, out int validateStatus); extraInfo = $", IsProg: {isProgram}, Link: {linkStatus}, Del: {deleteStatus}, Valid: {validateStatus}"; } string message = $"OpenGL Error: {error} ({errorDetails}) at {location}. Thread: {threadId}, Program: {program}{extraInfo}, VAO: {vao}, ActiveTex: {activeTex}"; Logger?.LogError(message); throw new Exception(message); } } #else [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CheckErrors(GL gl, bool logErrors = false, string callerName = "", string callerFile = "", int callerLine = 0) { } #endif public static string GetErrorDetails(GLEnum error) { return error switch { GLEnum.InvalidEnum => "Invalid enum - An unacceptable value is specified for an enumerated argument", GLEnum.InvalidValue => "Invalid value - A numeric argument is out of range", GLEnum.InvalidOperation => "Invalid operation - The specified operation is not allowed in the current state", GLEnum.StackOverflow => "Stack overflow - An operation would cause an internal stack to overflow", GLEnum.StackUnderflow => "Stack underflow - An operation would cause an internal stack to underflow", GLEnum.OutOfMemory => "Out of memory - There is not enough memory left to execute the command", GLEnum.InvalidFramebufferOperation => "Invalid framebuffer operation - The framebuffer object is not complete", GLEnum.ContextLost => "Context lost - The OpenGL context has been lost due to a graphics card reset", _ => "Unknown error" }; } #if DEBUG /// /// Checks for OpenGL errors and provides context-specific information /// public static void CheckErrorsWithContext(GL gl, string context, [CallerMemberName] string callerName = "", [CallerFilePath] string callerFile = "", [CallerLineNumber] int callerLine = 0) { var error = gl.GetError(); if (error != GLEnum.NoError) { string errorDetails = GetErrorDetails(error); string location = $"{System.IO.Path.GetFileName(callerFile)}::{callerName}:{callerLine}"; string message = $"OpenGL Error: {error} ({errorDetails})\nContext: {context}\nLocation: {location}"; Logger?.LogError(message); throw new Exception(message); } } #else [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void CheckErrorsWithContext(GL gl, string context, string callerName = "", string callerFile = "", int callerLine = 0) { } #endif /// /// Gets detailed information about the current texture state for debugging /// public static string GetTextureDebugInfo(GL gl, GLEnum target) { var info = new System.Text.StringBuilder(); info.AppendLine($"Texture Debug Info for {target}:"); try { gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0, GetTextureParameter.TextureWidth, out int width); gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0, GetTextureParameter.TextureHeight, out int height); gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0, GetTextureParameter.TextureDepthExt, out int depth); gl.GetTextureLevelParameter((uint)gl.GetInteger(GetPName.TextureBinding2DArray), 0, GetTextureParameter.TextureInternalFormat, out int format); info.AppendLine($" Dimensions: {width}x{height}x{depth}"); info.AppendLine($" Internal Format: {(InternalFormat)format}"); gl.GetTexParameter(target, GetTextureParameter.TextureMinFilter, out int minFilter); gl.GetTexParameter(target, GetTextureParameter.TextureMagFilter, out int magFilter); info.AppendLine($" Min Filter: {(TextureMinFilter)minFilter}"); info.AppendLine($" Mag Filter: {(TextureMagFilter)magFilter}"); // Get max mipmap level gl.GetTexParameter(target, GetTextureParameter.TextureMaxLevelSgis, out int maxLevel); info.AppendLine($" Max Level: {maxLevel}"); // Check completeness int maxMipLevel = (int)Math.Floor(Math.Log2(Math.Max(width, height))); info.AppendLine($" Calculated Max Mip Level: {maxMipLevel}"); } catch (Exception ex) { info.AppendLine($" Error getting texture info: {ex.Message}"); } return info.ToString(); } /// /// Validates texture completeness for mipmapping /// public static bool ValidateTextureMipmapStatus(GL gl, GLEnum target, out string errorMessage) { try { gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureWidth, out int width); gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureHeight, out int height); gl.GetTexLevelParameter(target, 0, GetTextureParameter.TextureInternalFormat, out int format); if (width == 0 || height == 0) { errorMessage = "Texture has zero dimensions"; return false; } // Check if format is valid for mipmap generation var internalFormat = (InternalFormat)format; if (IsCompressedFormat(internalFormat)) { errorMessage = $"Compressed format {internalFormat} does not support automatic mipmap generation"; return false; } errorMessage = String.Empty; return true; } catch (Exception ex) { errorMessage = $"Exception during validation: {ex.Message}"; return false; } } private static bool IsCompressedFormat(InternalFormat format) { return format == InternalFormat.CompressedRgbaS3TCDxt1Ext || format == InternalFormat.CompressedRgbaS3TCDxt3Ext || format == InternalFormat.CompressedRgbaS3TCDxt5Ext || format == InternalFormat.CompressedRgbS3TCDxt1Ext || format == InternalFormat.CompressedSrgbAlphaS3TCDxt1Ext || format == InternalFormat.CompressedSrgbAlphaS3TCDxt3Ext || format == InternalFormat.CompressedSrgbAlphaS3TCDxt5Ext; } /// /// Logs current OpenGL state for debugging /// public static void LogGLState(GL gl, string context = "") { var state = new System.Text.StringBuilder(); state.AppendLine($"=== OpenGL State ({context}) ==="); try { state.AppendLine( $"Active Texture Unit: GL_TEXTURE{gl.GetInteger(GetPName.ActiveTexture) - (int)GLEnum.Texture0}"); state.AppendLine($"Bound 2D Array Texture: {gl.GetInteger(GetPName.TextureBinding2DArray)}"); state.AppendLine($"Current Program: {gl.GetInteger(GetPName.CurrentProgram)}"); gl.GetInteger(GetPName.MaxTextureSize, out int maxTexSize); state.AppendLine($"Max Texture Size: {maxTexSize}"); gl.GetInteger(GetPName.Max3DTextureSize, out int max3DSize); state.AppendLine($"Max 3D Texture Size: {max3DSize}"); gl.GetInteger(GetPName.MaxArrayTextureLayers, out int maxLayers); state.AppendLine($"Max Array Texture Layers: {maxLayers}"); } catch (Exception ex) { state.AppendLine($"Error getting GL state: {ex.Message}"); } state.AppendLine("======================"); Logger?.LogInformation(state.ToString()); } /// /// Explicit defaults to prevent Avalonia state leakage into our custom rendering pipeline. /// Call this at the start of complex render cycles immediately inside a GLStateScope. /// public static void SetupDefaultRenderState(GL gl) { gl.BindSampler(0, 0); gl.BindSampler(1, 0); gl.BindSampler(2, 0); gl.ActiveTexture(TextureUnit.Texture1); gl.BindTexture(TextureTarget.Texture2D, 0); gl.ActiveTexture(TextureUnit.Texture2); gl.BindTexture(TextureTarget.Texture2D, 0); gl.ActiveTexture(TextureUnit.Texture0); // End on Texture0 gl.BindTexture(TextureTarget.Texture2D, 0); gl.BindVertexArray(0); gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0); gl.UseProgram(0); gl.PixelStore(PixelStoreParameter.UnpackAlignment, 1); gl.PixelStore(PixelStoreParameter.UnpackRowLength, 0); gl.PixelStore(PixelStoreParameter.UnpackSkipRows, 0); gl.PixelStore(PixelStoreParameter.UnpackSkipPixels, 0); gl.Disable(EnableCap.StencilTest); gl.BlendColor(0, 0, 0, 0); gl.PolygonMode(GLEnum.FrontAndBack, PolygonMode.Fill); // Disable Avalonia/Skia specific states gl.Disable(EnableCap.SampleAlphaToCoverage); gl.Disable(EnableCap.SampleAlphaToOne); gl.Disable(EnableCap.Multisample); gl.Disable((EnableCap)GLEnum.PrimitiveRestart); gl.LineWidth(1.0f); gl.PolygonOffset(0f, 0f); gl.Disable(EnableCap.PolygonOffsetFill); gl.Disable((EnableCap)GLEnum.ProgramPointSize); } } }