using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using Silk.NET.OpenGL; namespace AcDream.App.Platform; /// /// Executes one minimal, ownership-balanced call path for every OpenGL feature /// the mandatory renderer relies upon. Advertisement alone is insufficient: /// several Linux driver failures present an extension string while returning /// a missing entry point, invalid bindless handle, or unusable framebuffer. /// internal static unsafe class GraphicalGlFunctionProbe { private const string VertexSource = """ #version 430 core #extension GL_ARB_shader_draw_parameters : require void main() { float identity = float(gl_DrawIDARB + gl_BaseInstanceARB) * 0.0; gl_Position = vec4(identity, 0.0, 0.0, 1.0); } """; private const string FragmentSource = """ #version 430 core layout(location = 0) out vec4 outColor; void main() { outColor = vec4(1.0); } """; internal static GraphicalFunctionProbeResult Run( GL gl, GraphicalCapabilityRecord capabilities) { ArgumentNullException.ThrowIfNull(gl); ArgumentNullException.ThrowIfNull(capabilities); var failures = new List(); bool bindless = capabilities.HasBindlessTexture && Attempt( "bindless texture handle/residency", () => ProbeBindless(gl), failures); bool shaderDraw = capabilities.HasShaderDrawParameters && Attempt( "shader draw parameters and multi-draw indirect", () => ProbeShaderDrawAndMultiDraw(gl), failures); bool multiDraw = shaderDraw && capabilities.HasMultiDrawIndirect; bool shaderStorage = capabilities.HasShaderStorageBuffer && Attempt( "shader-storage buffer", () => ProbeShaderStorageBuffer(gl), failures); bool timer = capabilities.HasTimerQuery && Attempt( "timer query", () => ProbeTimerQuery(gl), failures); bool srgb = capabilities.Framebuffer.FramebufferSrgbApi && Attempt( "sRGB depth/stencil framebuffer", () => ProbeSrgbFramebuffer(gl), failures); bool? persistent = capabilities.HasBufferStorage ? Attempt( "persistent coherent buffer storage", () => ProbePersistentBufferStorage(gl), failures) : null; if (!capabilities.HasBindlessTexture) failures.Add("GL_ARB_bindless_texture was not advertised."); if (!capabilities.HasShaderDrawParameters) failures.Add("GL_ARB_shader_draw_parameters was not advertised."); if (!capabilities.HasMultiDrawIndirect) failures.Add("multi-draw indirect was not advertised."); if (!capabilities.HasShaderStorageBuffer) failures.Add("shader-storage buffers were not advertised."); if (!capabilities.HasTimerQuery) failures.Add("timer queries were not advertised."); if (!capabilities.Framebuffer.FramebufferSrgbApi) failures.Add("framebuffer sRGB was not advertised."); return new GraphicalFunctionProbeResult( bindless, shaderDraw, multiDraw, shaderStorage, timer, srgb, persistent, failures); } private static bool Attempt( string name, Action action, List failures) { try { action(); return true; } catch (Exception error) { failures.Add($"{name}: {error.GetType().Name}: {error.Message}"); return false; } } private static void ProbeBindless(GL gl) { if (!BindlessSupport.TryCreate(gl, out BindlessSupport? bindless) || bindless is null) { throw new NotSupportedException( "the ARB bindless extension object could not be loaded."); } uint texture = GlResourceCommand.CreateTexture( gl, "graphical capability bindless texture"); ulong handle = 0; try { byte* pixel = stackalloc byte[4] { 255, 255, 255, 255 }; GlResourceCommand.Execute( gl, "initialize graphical capability bindless texture", () => { gl.BindTexture(TextureTarget.Texture2D, texture); gl.TexImage2D( TextureTarget.Texture2D, 0, InternalFormat.Rgba8, 1, 1, 0, PixelFormat.Rgba, PixelType.UnsignedByte, pixel); gl.TexParameter( TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest); gl.TexParameter( TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest); }); handle = bindless.GetResidentHandle(texture); bindless.MakeNonResident(handle); handle = 0; } finally { if (handle != 0) bindless.MakeNonResident(handle); gl.BindTexture(TextureTarget.Texture2D, 0); GlResourceCommand.DeleteTexture( gl, texture, "delete graphical capability bindless texture"); } } private static void ProbeShaderDrawAndMultiDraw(GL gl) { uint program = ShaderProgramConstruction.Build( new GlShaderProgramBuildApi(gl), VertexSource, FragmentSource); uint vertexArray = 0; uint elementBuffer = 0; uint indirectBuffer = 0; try { vertexArray = GlResourceCommand.CreateName( gl, "graphical capability vertex array", gl.GenVertexArray, gl.DeleteVertexArray); elementBuffer = GlResourceCommand.CreateName( gl, "graphical capability element buffer", gl.GenBuffer, gl.DeleteBuffer); indirectBuffer = GlResourceCommand.CreateName( gl, "graphical capability indirect buffer", gl.GenBuffer, gl.DeleteBuffer); uint* index = stackalloc uint[1] { 0 }; uint* command = stackalloc uint[5] { 0, 1, 0, 0, 0, }; GLHelpers.ThrowOnResourceError( gl, "execute graphical capability multi-draw indirect call " + "(precondition)"); gl.UseProgram(program); gl.BindVertexArray(vertexArray); gl.BindBuffer( BufferTargetARB.ElementArrayBuffer, elementBuffer); gl.BufferData( BufferTargetARB.ElementArrayBuffer, (nuint)sizeof(uint), index, BufferUsageARB.StaticDraw); gl.BindBuffer( BufferTargetARB.DrawIndirectBuffer, indirectBuffer); gl.BufferData( BufferTargetARB.DrawIndirectBuffer, (nuint)(sizeof(uint) * 5), command, BufferUsageARB.StaticDraw); gl.MultiDrawElementsIndirect( PrimitiveType.Triangles, DrawElementsType.UnsignedInt, null, 1, 0); GLHelpers.ThrowOnResourceError( gl, "execute graphical capability multi-draw indirect call"); } finally { gl.UseProgram(0); gl.BindVertexArray(0); gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, 0); if (indirectBuffer != 0) { GlResourceCommand.DeleteBuffer( gl, indirectBuffer, "delete graphical capability indirect buffer"); } if (elementBuffer != 0) { GlResourceCommand.DeleteBuffer( gl, elementBuffer, "delete graphical capability element buffer"); } if (vertexArray != 0) { GlResourceCommand.DeleteVertexArray( gl, vertexArray, "delete graphical capability vertex array"); } GlResourceCommand.DeleteProgram( gl, program, "delete graphical capability shader program"); } } private static void ProbeShaderStorageBuffer(GL gl) { uint buffer = GlResourceCommand.CreateName( gl, "graphical capability shader-storage buffer", gl.GenBuffer, gl.DeleteBuffer); try { uint* value = stackalloc uint[1] { 0xACD0_0001u }; GLHelpers.ThrowOnResourceError( gl, "bind graphical capability shader-storage buffer " + "(precondition)"); gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, buffer); gl.BufferData( BufferTargetARB.ShaderStorageBuffer, (nuint)sizeof(uint), value, BufferUsageARB.StaticDraw); gl.BindBufferBase( BufferTargetARB.ShaderStorageBuffer, 0, buffer); gl.BindBufferBase( BufferTargetARB.ShaderStorageBuffer, 0, 0); GLHelpers.ThrowOnResourceError( gl, "bind graphical capability shader-storage buffer"); } finally { gl.BindBuffer(BufferTargetARB.ShaderStorageBuffer, 0); GlResourceCommand.DeleteBuffer( gl, buffer, "delete graphical capability shader-storage buffer"); } } private static void ProbeTimerQuery(GL gl) { uint query = GlResourceCommand.CreateName( gl, "graphical capability timer query", gl.GenQuery, gl.DeleteQuery); try { GlResourceCommand.Execute( gl, "execute graphical capability timer query", () => { gl.BeginQuery(QueryTarget.TimeElapsed, query); gl.EndQuery(QueryTarget.TimeElapsed); }); } finally { GlResourceCommand.Execute( gl, "delete graphical capability timer query", () => gl.DeleteQuery(query)); } } private static void ProbeSrgbFramebuffer(GL gl) { uint texture = 0; uint depthStencil = 0; uint framebuffer = 0; try { texture = GlResourceCommand.CreateTexture( gl, "graphical capability sRGB color texture"); depthStencil = GlResourceCommand.CreateName( gl, "graphical capability depth/stencil renderbuffer", gl.GenRenderbuffer, gl.DeleteRenderbuffer); framebuffer = GlResourceCommand.CreateName( gl, "graphical capability framebuffer", gl.GenFramebuffer, gl.DeleteFramebuffer); GlResourceCommand.Execute( gl, "configure graphical capability sRGB framebuffer", () => { gl.BindTexture(TextureTarget.Texture2D, texture); gl.TexImage2D( TextureTarget.Texture2D, 0, InternalFormat.Srgb8Alpha8, 2, 2, 0, PixelFormat.Rgba, PixelType.UnsignedByte, null); gl.BindRenderbuffer( RenderbufferTarget.Renderbuffer, depthStencil); gl.RenderbufferStorage( RenderbufferTarget.Renderbuffer, InternalFormat.Depth24Stencil8, 2, 2); gl.BindFramebuffer( FramebufferTarget.Framebuffer, framebuffer); gl.FramebufferTexture2D( FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, texture, 0); gl.FramebufferRenderbuffer( FramebufferTarget.Framebuffer, FramebufferAttachment.DepthStencilAttachment, RenderbufferTarget.Renderbuffer, depthStencil); if (gl.CheckFramebufferStatus( FramebufferTarget.Framebuffer) != GLEnum.FramebufferComplete) { throw new InvalidOperationException( "the sRGB depth/stencil framebuffer is incomplete."); } gl.Enable(EnableCap.FramebufferSrgb); gl.Clear( ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); gl.Disable(EnableCap.FramebufferSrgb); }); } finally { gl.Disable(EnableCap.FramebufferSrgb); gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0); gl.BindTexture(TextureTarget.Texture2D, 0); if (framebuffer != 0) { GlResourceCommand.Execute( gl, "delete graphical capability framebuffer", () => gl.DeleteFramebuffer(framebuffer)); } if (depthStencil != 0) { GlResourceCommand.Execute( gl, "delete graphical capability depth/stencil renderbuffer", () => gl.DeleteRenderbuffer(depthStencil)); } if (texture != 0) { GlResourceCommand.DeleteTexture( gl, texture, "delete graphical capability sRGB texture"); } } } private static void ProbePersistentBufferStorage(GL gl) { uint buffer = GlResourceCommand.CreateName( gl, "graphical capability persistent buffer", gl.GenBuffer, gl.DeleteBuffer); void* mapped = null; try { GlResourceCommand.Execute( gl, "allocate graphical capability persistent buffer", () => { gl.BindBuffer(BufferTargetARB.ArrayBuffer, buffer); gl.BufferStorage( GLEnum.ArrayBuffer, 64, null, (uint)( BufferStorageMask.MapWriteBit | BufferStorageMask.MapPersistentBit | BufferStorageMask.MapCoherentBit | BufferStorageMask.DynamicStorageBit)); mapped = gl.MapBufferRange( BufferTargetARB.ArrayBuffer, 0, 64, MapBufferAccessMask.WriteBit | MapBufferAccessMask.PersistentBit | MapBufferAccessMask.CoherentBit); if (mapped is null) { throw new InvalidOperationException( "persistent mapping returned a null pointer."); } *((byte*)mapped) = 0x5A; }); } finally { if (mapped is not null) { GlResourceCommand.Execute( gl, "unmap graphical capability persistent buffer", () => gl.UnmapBuffer(BufferTargetARB.ArrayBuffer)); } gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); GlResourceCommand.DeleteBuffer( gl, buffer, "delete graphical capability persistent buffer"); } } }