// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Development; using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shaders; using osu.Framework.Threading; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; using osu.Framework.Statistics; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Platform; using osu.Framework.Timing; using static osu.Framework.Threading.ScheduledDelegate; namespace osu.Framework.Graphics.OpenGL { public static class GLWrapper { /// /// Maximum number of s a can draw with. /// This is a carefully-chosen number to enable the update and draw threads to work concurrently without causing unnecessary load. /// public const int MAX_DRAW_NODES = 3; /// /// The interval (in frames) before checking whether VBOs should be freed. /// VBOs may remain unused for at most double this length before they are recycled. /// private const int vbo_free_check_interval = 300; /// /// The amount of times has been invoked. /// internal static ulong ResetId { get; private set; } public static ref readonly MaskingInfo CurrentMaskingInfo => ref currentMaskingInfo; private static MaskingInfo currentMaskingInfo; public static RectangleI Viewport { get; private set; } public static RectangleF Ortho { get; private set; } public static RectangleI Scissor { get; private set; } public static Vector2I ScissorOffset { get; private set; } public static Matrix4 ProjectionMatrix { get; set; } public static DepthInfo CurrentDepthInfo { get; private set; } public static float BackbufferDrawDepth { get; private set; } public static bool UsingBackbuffer => frame_buffer_stack.Peek() == DefaultFrameBuffer; public static int DefaultFrameBuffer; public static bool IsEmbedded { get; internal set; } /// /// Check whether we have an initialised and non-disposed GL context. /// public static bool HasContext => GraphicsContext.CurrentContext != null; public static int MaxTextureSize { get; private set; } = 4096; // default value is to allow roughly normal flow in cases we don't have a GL context, like headless CI. public static int MaxRenderBufferSize { get; private set; } = 4096; // default value is to allow roughly normal flow in cases we don't have a GL context, like headless CI. /// /// The maximum number of texture uploads to dequeue and upload per frame. /// Defaults to 32. /// public static int MaxTexturesUploadedPerFrame { get; set; } = 32; /// /// The maximum number of pixels to upload per frame. /// Defaults to 2 megapixels (8mb alloc). /// public static int MaxPixelsUploadedPerFrame { get; set; } = 1024 * 1024 * 2; private static readonly Scheduler reset_scheduler = new Scheduler(() => ThreadSafety.IsDrawThread, new StopwatchClock(true)); // force no thread set until we are actually on the draw thread. /// /// A queue from which a maximum of one operation is invoked per draw frame. /// private static readonly ConcurrentQueue expensive_operation_queue = new ConcurrentQueue(); private static readonly ConcurrentQueue texture_upload_queue = new ConcurrentQueue(); private static readonly List batch_reset_list = new List(); private static readonly List vertex_buffers_in_use = new List(); public static bool IsInitialized { get; private set; } private static WeakReference host; internal static void Initialize(GameHost host) { if (IsInitialized) return; if (host.Window is OsuTKWindow win) IsEmbedded = win.IsEmbedded; GLWrapper.host = new WeakReference(host); MaxTextureSize = GL.GetInteger(GetPName.MaxTextureSize); MaxRenderBufferSize = GL.GetInteger(GetPName.MaxRenderbufferSize); GL.Disable(EnableCap.StencilTest); GL.Enable(EnableCap.Blend); IsInitialized = true; reset_scheduler.AddDelayed(checkPendingDisposals, 0, true); } private static readonly GLDisposalQueue disposal_queue = new GLDisposalQueue(); internal static void ScheduleDisposal(Action disposalAction) { if (host != null && host.TryGetTarget(out _)) disposal_queue.ScheduleDisposal(disposalAction); else disposalAction.Invoke(); } private static void checkPendingDisposals() { disposal_queue.CheckPendingDisposals(); } private static readonly GlobalStatistic stat_expensive_operations_queued = GlobalStatistics.Get(nameof(GLWrapper), "Expensive operation queue length"); private static readonly GlobalStatistic stat_texture_uploads_queued = GlobalStatistics.Get(nameof(GLWrapper), "Texture upload queue length"); private static readonly GlobalStatistic stat_texture_uploads_dequeued = GlobalStatistics.Get(nameof(GLWrapper), "Texture uploads dequeued"); private static readonly GlobalStatistic stat_texture_uploads_performed = GlobalStatistics.Get(nameof(GLWrapper), "Texture uploads performed"); internal static void Reset(Vector2 size) { ResetId++; Trace.Assert(shader_stack.Count == 0); reset_scheduler.Update(); stat_expensive_operations_queued.Value = expensive_operation_queue.Count; while (expensive_operation_queue.TryDequeue(out ScheduledDelegate operation)) { if (operation.State == RunState.Waiting) { operation.RunTask(); break; } } lastActiveBatch = null; lastBlendingParameters = new BlendingParameters(); lastBlendingEnabledState = null; foreach (var b in batch_reset_list) b.ResetCounters(); batch_reset_list.Clear(); viewport_stack.Clear(); ortho_stack.Clear(); masking_stack.Clear(); scissor_rect_stack.Clear(); frame_buffer_stack.Clear(); depth_stack.Clear(); scissor_state_stack.Clear(); scissor_offset_stack.Clear(); BindFrameBuffer(DefaultFrameBuffer); Scissor = RectangleI.Empty; ScissorOffset = Vector2I.Zero; Viewport = RectangleI.Empty; Ortho = RectangleF.Empty; PushScissorState(true); PushViewport(new RectangleI(0, 0, (int)size.X, (int)size.Y)); PushScissor(new RectangleI(0, 0, (int)size.X, (int)size.Y)); PushScissorOffset(Vector2I.Zero); PushMaskingInfo(new MaskingInfo { ScreenSpaceAABB = new RectangleI(0, 0, (int)size.X, (int)size.Y), MaskingRect = new RectangleF(0, 0, size.X, size.Y), ToMaskingSpace = Matrix3.Identity, BlendRange = 1, AlphaExponent = 1, CornerExponent = 2.5f, }, true); PushDepthInfo(DepthInfo.Default); Clear(new ClearInfo(Color4.Black)); freeUnusedVertexBuffers(); stat_texture_uploads_queued.Value = texture_upload_queue.Count; stat_texture_uploads_dequeued.Value = 0; stat_texture_uploads_performed.Value = 0; // increase the number of items processed with the queue length to ensure it doesn't get out of hand. int targetUploads = Math.Clamp(texture_upload_queue.Count / 2, 1, MaxTexturesUploadedPerFrame); int uploads = 0; int uploadedPixels = 0; // continue attempting to upload textures until enough uploads have been performed. while (texture_upload_queue.TryDequeue(out TextureGL texture)) { stat_texture_uploads_dequeued.Value++; texture.IsQueuedForUpload = false; if (!texture.Upload()) continue; stat_texture_uploads_performed.Value++; if (++uploads >= targetUploads) break; if ((uploadedPixels += texture.Width * texture.Height) > MaxPixelsUploadedPerFrame) break; } last_bound_texture.AsSpan().Clear(); last_bound_texture_is_atlas.AsSpan().Clear(); last_bound_buffers.AsSpan().Clear(); } private static ClearInfo currentClearInfo; public static void Clear(ClearInfo clearInfo) { PushDepthInfo(new DepthInfo(writeDepth: true)); PushScissorState(false); if (clearInfo.Colour != currentClearInfo.Colour) GL.ClearColor(clearInfo.Colour); if (clearInfo.Depth != currentClearInfo.Depth) { if (IsEmbedded) { // GL ES only supports glClearDepthf // See: https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glClearDepthf.xhtml GL.ClearDepth((float)clearInfo.Depth); } else { // Older desktop platforms don't support glClearDepthf, so standard GL's double version is used instead // See: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glClearDepth.xhtml osuTK.Graphics.OpenGL.GL.ClearDepth(clearInfo.Depth); } } if (clearInfo.Stencil != currentClearInfo.Stencil) GL.ClearStencil(clearInfo.Stencil); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit); currentClearInfo = clearInfo; PopScissorState(); PopDepthInfo(); } private static readonly Stack scissor_state_stack = new Stack(); private static bool currentScissorState; public static void PushScissorState(bool enabled) { scissor_state_stack.Push(enabled); setScissorState(enabled); } public static void PopScissorState() { Trace.Assert(scissor_state_stack.Count > 1); scissor_state_stack.Pop(); setScissorState(scissor_state_stack.Peek()); } private static void setScissorState(bool enabled) { if (enabled == currentScissorState) return; currentScissorState = enabled; if (enabled) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest); } /// /// Enqueues a texture to be uploaded in the next frame. /// /// The texture to be uploaded. public static void EnqueueTextureUpload(TextureGL texture) { if (texture.IsQueuedForUpload) return; if (host != null) { texture.IsQueuedForUpload = true; texture_upload_queue.Enqueue(texture); } } /// /// Schedules an expensive operation to a queue from which a maximum of one operation is performed per frame. /// /// The operation to schedule. public static void ScheduleExpensiveOperation(ScheduledDelegate operation) { if (host != null) expensive_operation_queue.Enqueue(operation); } private static readonly int[] last_bound_buffers = new int[2]; /// /// Bind an OpenGL buffer object. /// /// The buffer type to bind. /// The buffer ID to bind. /// Whether an actual bind call was necessary. This value is false when repeatedly binding the same buffer. public static bool BindBuffer(BufferTarget target, int buffer) { int bufferIndex = target - BufferTarget.ArrayBuffer; if (last_bound_buffers[bufferIndex] == buffer) return false; last_bound_buffers[bufferIndex] = buffer; GL.BindBuffer(target, buffer); FrameStatistics.Increment(StatisticsCounterType.VBufBinds); return true; } private static IVertexBatch lastActiveBatch; /// /// Sets the last vertex batch used for drawing. /// /// This is done so that various methods that change GL state can force-draw the batch /// before continuing with the state change. /// /// /// The batch. internal static void SetActiveBatch(IVertexBatch batch) { if (lastActiveBatch == batch) return; batch_reset_list.Add(batch); FlushCurrentBatch(); lastActiveBatch = batch; } /// /// Notifies that a has begun being used. /// /// The in use. internal static void RegisterVertexBufferUse(IVertexBuffer buffer) => vertex_buffers_in_use.Add(buffer); private static void freeUnusedVertexBuffers() { if (ResetId % vbo_free_check_interval != 0) return; foreach (var buf in vertex_buffers_in_use) { if (buf.InUse && ResetId - buf.LastUseResetId > vbo_free_check_interval) buf.Free(); } vertex_buffers_in_use.RemoveAll(b => !b.InUse); } private static readonly int[] last_bound_texture = new int[16]; private static readonly bool[] last_bound_texture_is_atlas = new bool[16]; internal static int GetTextureUnitId(TextureUnit unit) => (int)unit - (int)TextureUnit.Texture0; internal static bool AtlasTextureIsBound(TextureUnit unit) => last_bound_texture_is_atlas[GetTextureUnitId(unit)]; /// /// Binds a texture to draw with. /// /// The texture to bind. /// The texture unit to bind it to. /// The texture wrap mode in horizontal direction. /// The texture wrap mode in vertical direction. /// true if the provided texture was not already bound (causing a binding change). public static bool BindTexture(TextureGL texture, TextureUnit unit = TextureUnit.Texture0, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) { bool didBind = BindTexture(texture?.TextureId ?? 0, unit, wrapModeS, wrapModeT); last_bound_texture_is_atlas[GetTextureUnitId(unit)] = texture is TextureGLAtlas; return didBind; } internal static WrapMode CurrentWrapModeS; internal static WrapMode CurrentWrapModeT; /// /// Binds a texture to draw with. /// /// The texture to bind. /// The texture unit to bind it to. /// The texture wrap mode in horizontal direction. /// The texture wrap mode in vertical direction. /// true if the provided texture was not already bound (causing a binding change). public static bool BindTexture(int textureId, TextureUnit unit = TextureUnit.Texture0, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None) { var index = GetTextureUnitId(unit); if (wrapModeS != CurrentWrapModeS) { GlobalPropertyManager.Set(GlobalProperty.WrapModeS, (int)wrapModeS); CurrentWrapModeS = wrapModeS; } if (wrapModeT != CurrentWrapModeT) { GlobalPropertyManager.Set(GlobalProperty.WrapModeT, (int)wrapModeT); CurrentWrapModeT = wrapModeT; } if (last_bound_texture[index] == textureId) return false; FlushCurrentBatch(); GL.ActiveTexture(unit); GL.BindTexture(TextureTarget.Texture2D, textureId); last_bound_texture[index] = textureId; last_bound_texture_is_atlas[GetTextureUnitId(unit)] = false; FrameStatistics.Increment(StatisticsCounterType.TextureBinds); return true; } private static BlendingParameters lastBlendingParameters; private static bool? lastBlendingEnabledState; /// /// Sets the blending function to draw with. /// /// The info we should use to update the active state. public static void SetBlend(BlendingParameters blendingParameters) { if (lastBlendingParameters == blendingParameters) return; FlushCurrentBatch(); if (blendingParameters.IsDisabled) { if (!lastBlendingEnabledState.HasValue || lastBlendingEnabledState.Value) GL.Disable(EnableCap.Blend); lastBlendingEnabledState = false; } else { if (!lastBlendingEnabledState.HasValue || !lastBlendingEnabledState.Value) GL.Enable(EnableCap.Blend); lastBlendingEnabledState = true; GL.BlendEquationSeparate(blendingParameters.RGBEquationMode, blendingParameters.AlphaEquationMode); GL.BlendFuncSeparate(blendingParameters.SourceBlendingFactor, blendingParameters.DestinationBlendingFactor, blendingParameters.SourceAlphaBlendingFactor, blendingParameters.DestinationAlphaBlendingFactor); } lastBlendingParameters = blendingParameters; } private static readonly Stack viewport_stack = new Stack(); /// /// Applies a new viewport rectangle. /// /// The viewport rectangle. public static void PushViewport(RectangleI viewport) { var actualRect = viewport; if (actualRect.Width < 0) { actualRect.X += viewport.Width; actualRect.Width = -viewport.Width; } if (actualRect.Height < 0) { actualRect.Y += viewport.Height; actualRect.Height = -viewport.Height; } PushOrtho(viewport); viewport_stack.Push(actualRect); if (Viewport == actualRect) return; Viewport = actualRect; GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); } /// /// Applies the last viewport rectangle. /// public static void PopViewport() { Trace.Assert(viewport_stack.Count > 1); PopOrtho(); viewport_stack.Pop(); RectangleI actualRect = viewport_stack.Peek(); if (Viewport == actualRect) return; Viewport = actualRect; GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height); } /// /// Applies a new scissor rectangle. /// /// The scissor rectangle. public static void PushScissor(RectangleI scissor) { FlushCurrentBatch(); scissor_rect_stack.Push(scissor); if (Scissor == scissor) return; Scissor = scissor; setScissor(scissor); } /// /// Applies the last scissor rectangle. /// public static void PopScissor() { Trace.Assert(scissor_rect_stack.Count > 1); FlushCurrentBatch(); scissor_rect_stack.Pop(); RectangleI scissor = scissor_rect_stack.Peek(); if (Scissor == scissor) return; Scissor = scissor; setScissor(scissor); } private static void setScissor(RectangleI scissor) { if (scissor.Width < 0) { scissor.X += scissor.Width; scissor.Width = -scissor.Width; } if (scissor.Height < 0) { scissor.Y += scissor.Height; scissor.Height = -scissor.Height; } GL.Scissor(scissor.X, Viewport.Height - scissor.Bottom, scissor.Width, scissor.Height); } private static readonly Stack scissor_offset_stack = new Stack(); /// /// Applies an offset to the scissor rectangle. /// /// The offset. public static void PushScissorOffset(Vector2I offset) { FlushCurrentBatch(); scissor_offset_stack.Push(offset); if (ScissorOffset == offset) return; ScissorOffset = offset; } /// /// Applies the last scissor rectangle offset. /// public static void PopScissorOffset() { Trace.Assert(scissor_offset_stack.Count > 1); FlushCurrentBatch(); scissor_offset_stack.Pop(); Vector2I offset = scissor_offset_stack.Peek(); if (ScissorOffset == offset) return; ScissorOffset = offset; } private static readonly Stack ortho_stack = new Stack(); /// /// Applies a new orthographic projection rectangle. /// /// The orthographic projection rectangle. public static void PushOrtho(RectangleF ortho) { FlushCurrentBatch(); ortho_stack.Push(ortho); if (Ortho == ortho) return; Ortho = ortho; ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); GlobalPropertyManager.Set(GlobalProperty.ProjMatrix, ProjectionMatrix); } /// /// Applies the last orthographic projection rectangle. /// public static void PopOrtho() { Trace.Assert(ortho_stack.Count > 1); FlushCurrentBatch(); ortho_stack.Pop(); RectangleF actualRect = ortho_stack.Peek(); if (Ortho == actualRect) return; Ortho = actualRect; ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1); GlobalPropertyManager.Set(GlobalProperty.ProjMatrix, ProjectionMatrix); } private static readonly Stack masking_stack = new Stack(); private static readonly Stack scissor_rect_stack = new Stack(); private static readonly Stack frame_buffer_stack = new Stack(); private static readonly Stack depth_stack = new Stack(); private static void setMaskingInfo(MaskingInfo maskingInfo, bool isPushing, bool overwritePreviousScissor) { FlushCurrentBatch(); GlobalPropertyManager.Set(GlobalProperty.MaskingRect, new Vector4( maskingInfo.MaskingRect.Left, maskingInfo.MaskingRect.Top, maskingInfo.MaskingRect.Right, maskingInfo.MaskingRect.Bottom)); GlobalPropertyManager.Set(GlobalProperty.ToMaskingSpace, maskingInfo.ToMaskingSpace); GlobalPropertyManager.Set(GlobalProperty.CornerRadius, maskingInfo.CornerRadius); GlobalPropertyManager.Set(GlobalProperty.CornerExponent, maskingInfo.CornerExponent); GlobalPropertyManager.Set(GlobalProperty.BorderThickness, maskingInfo.BorderThickness / maskingInfo.BlendRange); if (maskingInfo.BorderThickness > 0) { GlobalPropertyManager.Set(GlobalProperty.BorderColour, new Vector4( maskingInfo.BorderColour.Linear.R, maskingInfo.BorderColour.Linear.G, maskingInfo.BorderColour.Linear.B, maskingInfo.BorderColour.Linear.A)); } GlobalPropertyManager.Set(GlobalProperty.MaskingBlendRange, maskingInfo.BlendRange); GlobalPropertyManager.Set(GlobalProperty.AlphaExponent, maskingInfo.AlphaExponent); GlobalPropertyManager.Set(GlobalProperty.EdgeOffset, maskingInfo.EdgeOffset); GlobalPropertyManager.Set(GlobalProperty.DiscardInner, maskingInfo.Hollow); if (maskingInfo.Hollow) GlobalPropertyManager.Set(GlobalProperty.InnerCornerRadius, maskingInfo.HollowCornerRadius); if (isPushing) { // When drawing to a viewport that doesn't match the projection size (e.g. via framebuffers), the resultant image will be scaled Vector2 viewportScale = Vector2.Divide(Viewport.Size, Ortho.Size); Vector2 location = (maskingInfo.ScreenSpaceAABB.Location - ScissorOffset) * viewportScale; Vector2 size = maskingInfo.ScreenSpaceAABB.Size * viewportScale; RectangleI actualRect = new RectangleI( (int)Math.Floor(location.X), (int)Math.Floor(location.Y), (int)Math.Ceiling(size.X), (int)Math.Ceiling(size.Y)); PushScissor(overwritePreviousScissor ? actualRect : RectangleI.Intersect(scissor_rect_stack.Peek(), actualRect)); } else PopScissor(); } internal static void FlushCurrentBatch() { lastActiveBatch?.Draw(); } public static bool IsMaskingActive => masking_stack.Count > 1; /// /// Applies a new scissor rectangle. /// /// The masking info. /// Whether or not to shrink an existing scissor rectangle. public static void PushMaskingInfo(in MaskingInfo maskingInfo, bool overwritePreviousScissor = false) { masking_stack.Push(maskingInfo); if (CurrentMaskingInfo == maskingInfo) return; currentMaskingInfo = maskingInfo; setMaskingInfo(CurrentMaskingInfo, true, overwritePreviousScissor); } /// /// Applies the last scissor rectangle. /// public static void PopMaskingInfo() { Trace.Assert(masking_stack.Count > 1); masking_stack.Pop(); MaskingInfo maskingInfo = masking_stack.Peek(); if (CurrentMaskingInfo == maskingInfo) return; currentMaskingInfo = maskingInfo; setMaskingInfo(CurrentMaskingInfo, false, true); } /// /// Applies a new depth information. /// /// The depth information. public static void PushDepthInfo(DepthInfo depthInfo) { depth_stack.Push(depthInfo); if (CurrentDepthInfo.Equals(depthInfo)) return; CurrentDepthInfo = depthInfo; setDepthInfo(CurrentDepthInfo); } /// /// Applies the last depth information. /// public static void PopDepthInfo() { Trace.Assert(depth_stack.Count > 1); depth_stack.Pop(); DepthInfo depthInfo = depth_stack.Peek(); if (CurrentDepthInfo.Equals(depthInfo)) return; CurrentDepthInfo = depthInfo; setDepthInfo(CurrentDepthInfo); } private static void setDepthInfo(DepthInfo depthInfo) { FlushCurrentBatch(); if (depthInfo.DepthTest) { GL.Enable(EnableCap.DepthTest); GL.DepthFunc(depthInfo.Function); } else GL.Disable(EnableCap.DepthTest); GL.DepthMask(depthInfo.WriteDepth); } /// /// Sets the current draw depth. /// The draw depth is written to every vertex added to s. /// /// The draw depth. internal static void SetDrawDepth(float drawDepth) => BackbufferDrawDepth = drawDepth; /// /// Binds a framebuffer. /// /// The framebuffer to bind. public static void BindFrameBuffer(int frameBuffer) { if (frameBuffer == -1) return; bool alreadyBound = frame_buffer_stack.Count > 0 && frame_buffer_stack.Peek() == frameBuffer; frame_buffer_stack.Push(frameBuffer); if (!alreadyBound) { FlushCurrentBatch(); GL.BindFramebuffer(FramebufferTarget.Framebuffer, frameBuffer); GlobalPropertyManager.Set(GlobalProperty.BackbufferDraw, UsingBackbuffer); } GlobalPropertyManager.Set(GlobalProperty.GammaCorrection, UsingBackbuffer); } /// /// Binds a framebuffer. /// /// The framebuffer to bind. public static void UnbindFrameBuffer(int frameBuffer) { if (frameBuffer == -1) return; if (frame_buffer_stack.Peek() != frameBuffer) return; frame_buffer_stack.Pop(); FlushCurrentBatch(); GL.BindFramebuffer(FramebufferTarget.Framebuffer, frame_buffer_stack.Peek()); GlobalPropertyManager.Set(GlobalProperty.BackbufferDraw, UsingBackbuffer); GlobalPropertyManager.Set(GlobalProperty.GammaCorrection, UsingBackbuffer); } /// /// Deletes a frame buffer. /// /// The frame buffer to delete. internal static void DeleteFrameBuffer(int frameBuffer) { if (frameBuffer == -1) return; while (frame_buffer_stack.Peek() == frameBuffer) UnbindFrameBuffer(frameBuffer); ScheduleDisposal(() => { GL.DeleteFramebuffer(frameBuffer); }); } private static int currentShader; private static readonly Stack shader_stack = new Stack(); public static void UseProgram(int? shader) { ThreadSafety.EnsureDrawThread(); if (shader != null) { shader_stack.Push(shader.Value); } else { shader_stack.Pop(); //check if the stack is empty, and if so don't restore the previous shader. if (shader_stack.Count == 0) return; } int s = shader ?? shader_stack.Peek(); if (currentShader == s) return; FrameStatistics.Increment(StatisticsCounterType.ShaderBinds); FlushCurrentBatch(); GL.UseProgram(s); currentShader = s; } internal static void SetUniform(IUniformWithValue uniform) where T : struct, IEquatable { if (uniform.Owner == currentShader) FlushCurrentBatch(); switch (uniform) { case IUniformWithValue b: GL.Uniform1(uniform.Location, b.GetValue() ? 1 : 0); break; case IUniformWithValue i: GL.Uniform1(uniform.Location, i.GetValue()); break; case IUniformWithValue f: GL.Uniform1(uniform.Location, f.GetValue()); break; case IUniformWithValue v2: GL.Uniform2(uniform.Location, ref v2.GetValueByRef()); break; case IUniformWithValue v3: GL.Uniform3(uniform.Location, ref v3.GetValueByRef()); break; case IUniformWithValue v4: GL.Uniform4(uniform.Location, ref v4.GetValueByRef()); break; case IUniformWithValue m2: GL.UniformMatrix2(uniform.Location, false, ref m2.GetValueByRef()); break; case IUniformWithValue m3: GL.UniformMatrix3(uniform.Location, false, ref m3.GetValueByRef()); break; case IUniformWithValue m4: GL.UniformMatrix4(uniform.Location, false, ref m4.GetValueByRef()); break; } } } public struct MaskingInfo : IEquatable { public RectangleI ScreenSpaceAABB; public RectangleF MaskingRect; public Quad ConservativeScreenSpaceQuad; /// /// This matrix transforms screen space coordinates to masking space (likely the parent /// space of the container doing the masking). /// It is used by a shader to determine which pixels to discard. /// public Matrix3 ToMaskingSpace; public float CornerRadius; public float CornerExponent; public float BorderThickness; public SRGBColour BorderColour; public float BlendRange; public float AlphaExponent; public Vector2 EdgeOffset; public bool Hollow; public float HollowCornerRadius; public readonly bool Equals(MaskingInfo other) => this == other; public static bool operator ==(in MaskingInfo left, in MaskingInfo right) => left.ScreenSpaceAABB == right.ScreenSpaceAABB && left.MaskingRect == right.MaskingRect && left.ToMaskingSpace == right.ToMaskingSpace && left.CornerRadius == right.CornerRadius && left.CornerExponent == right.CornerExponent && left.BorderThickness == right.BorderThickness && left.BorderColour.Equals(right.BorderColour) && left.BlendRange == right.BlendRange && left.AlphaExponent == right.AlphaExponent && left.EdgeOffset == right.EdgeOffset && left.Hollow == right.Hollow && left.HollowCornerRadius == right.HollowCornerRadius; public static bool operator !=(in MaskingInfo left, in MaskingInfo right) => !(left == right); public override readonly bool Equals(object obj) => obj is MaskingInfo other && this == other; public override readonly int GetHashCode() => 0; // Shouldn't be used; simplifying implementation here. } }