A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Concurrent;
6using System.Collections.Generic;
7using System.Diagnostics;
8using osu.Framework.Development;
9using osu.Framework.Graphics.Batches;
10using osu.Framework.Graphics.OpenGL.Textures;
11using osu.Framework.Graphics.Shaders;
12using osu.Framework.Threading;
13using osuTK;
14using osuTK.Graphics;
15using osuTK.Graphics.ES30;
16using osu.Framework.Statistics;
17using osu.Framework.Graphics.Primitives;
18using osu.Framework.Graphics.Colour;
19using osu.Framework.Graphics.OpenGL.Buffers;
20using osu.Framework.Platform;
21using osu.Framework.Timing;
22using static osu.Framework.Threading.ScheduledDelegate;
23
24namespace osu.Framework.Graphics.OpenGL
25{
26 public static class GLWrapper
27 {
28 /// <summary>
29 /// Maximum number of <see cref="DrawNode"/>s a <see cref="Drawable"/> can draw with.
30 /// This is a carefully-chosen number to enable the update and draw threads to work concurrently without causing unnecessary load.
31 /// </summary>
32 public const int MAX_DRAW_NODES = 3;
33
34 /// <summary>
35 /// The interval (in frames) before checking whether VBOs should be freed.
36 /// VBOs may remain unused for at most double this length before they are recycled.
37 /// </summary>
38 private const int vbo_free_check_interval = 300;
39
40 /// <summary>
41 /// The amount of times <see cref="Reset"/> has been invoked.
42 /// </summary>
43 internal static ulong ResetId { get; private set; }
44
45 public static ref readonly MaskingInfo CurrentMaskingInfo => ref currentMaskingInfo;
46 private static MaskingInfo currentMaskingInfo;
47
48 public static RectangleI Viewport { get; private set; }
49 public static RectangleF Ortho { get; private set; }
50 public static RectangleI Scissor { get; private set; }
51 public static Vector2I ScissorOffset { get; private set; }
52 public static Matrix4 ProjectionMatrix { get; set; }
53 public static DepthInfo CurrentDepthInfo { get; private set; }
54
55 public static float BackbufferDrawDepth { get; private set; }
56
57 public static bool UsingBackbuffer => frame_buffer_stack.Peek() == DefaultFrameBuffer;
58
59 public static int DefaultFrameBuffer;
60
61 public static bool IsEmbedded { get; internal set; }
62
63 /// <summary>
64 /// Check whether we have an initialised and non-disposed GL context.
65 /// </summary>
66 public static bool HasContext => GraphicsContext.CurrentContext != null;
67
68 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.
69 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.
70
71 /// <summary>
72 /// The maximum number of texture uploads to dequeue and upload per frame.
73 /// Defaults to 32.
74 /// </summary>
75 public static int MaxTexturesUploadedPerFrame { get; set; } = 32;
76
77 /// <summary>
78 /// The maximum number of pixels to upload per frame.
79 /// Defaults to 2 megapixels (8mb alloc).
80 /// </summary>
81 public static int MaxPixelsUploadedPerFrame { get; set; } = 1024 * 1024 * 2;
82
83 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.
84
85 /// <summary>
86 /// A queue from which a maximum of one operation is invoked per draw frame.
87 /// </summary>
88 private static readonly ConcurrentQueue<ScheduledDelegate> expensive_operation_queue = new ConcurrentQueue<ScheduledDelegate>();
89
90 private static readonly ConcurrentQueue<TextureGL> texture_upload_queue = new ConcurrentQueue<TextureGL>();
91
92 private static readonly List<IVertexBatch> batch_reset_list = new List<IVertexBatch>();
93
94 private static readonly List<IVertexBuffer> vertex_buffers_in_use = new List<IVertexBuffer>();
95
96 public static bool IsInitialized { get; private set; }
97
98 private static WeakReference<GameHost> host;
99
100 internal static void Initialize(GameHost host)
101 {
102 if (IsInitialized) return;
103
104 if (host.Window is OsuTKWindow win)
105 IsEmbedded = win.IsEmbedded;
106
107 GLWrapper.host = new WeakReference<GameHost>(host);
108
109 MaxTextureSize = GL.GetInteger(GetPName.MaxTextureSize);
110 MaxRenderBufferSize = GL.GetInteger(GetPName.MaxRenderbufferSize);
111
112 GL.Disable(EnableCap.StencilTest);
113 GL.Enable(EnableCap.Blend);
114
115 IsInitialized = true;
116
117 reset_scheduler.AddDelayed(checkPendingDisposals, 0, true);
118 }
119
120 private static readonly GLDisposalQueue disposal_queue = new GLDisposalQueue();
121
122 internal static void ScheduleDisposal(Action disposalAction)
123 {
124 if (host != null && host.TryGetTarget(out _))
125 disposal_queue.ScheduleDisposal(disposalAction);
126 else
127 disposalAction.Invoke();
128 }
129
130 private static void checkPendingDisposals()
131 {
132 disposal_queue.CheckPendingDisposals();
133 }
134
135 private static readonly GlobalStatistic<int> stat_expensive_operations_queued = GlobalStatistics.Get<int>(nameof(GLWrapper), "Expensive operation queue length");
136 private static readonly GlobalStatistic<int> stat_texture_uploads_queued = GlobalStatistics.Get<int>(nameof(GLWrapper), "Texture upload queue length");
137 private static readonly GlobalStatistic<int> stat_texture_uploads_dequeued = GlobalStatistics.Get<int>(nameof(GLWrapper), "Texture uploads dequeued");
138 private static readonly GlobalStatistic<int> stat_texture_uploads_performed = GlobalStatistics.Get<int>(nameof(GLWrapper), "Texture uploads performed");
139
140 internal static void Reset(Vector2 size)
141 {
142 ResetId++;
143
144 Trace.Assert(shader_stack.Count == 0);
145
146 reset_scheduler.Update();
147
148 stat_expensive_operations_queued.Value = expensive_operation_queue.Count;
149
150 while (expensive_operation_queue.TryDequeue(out ScheduledDelegate operation))
151 {
152 if (operation.State == RunState.Waiting)
153 {
154 operation.RunTask();
155 break;
156 }
157 }
158
159 lastActiveBatch = null;
160 lastBlendingParameters = new BlendingParameters();
161 lastBlendingEnabledState = null;
162
163 foreach (var b in batch_reset_list)
164 b.ResetCounters();
165 batch_reset_list.Clear();
166
167 viewport_stack.Clear();
168 ortho_stack.Clear();
169 masking_stack.Clear();
170 scissor_rect_stack.Clear();
171 frame_buffer_stack.Clear();
172 depth_stack.Clear();
173 scissor_state_stack.Clear();
174 scissor_offset_stack.Clear();
175
176 BindFrameBuffer(DefaultFrameBuffer);
177
178 Scissor = RectangleI.Empty;
179 ScissorOffset = Vector2I.Zero;
180 Viewport = RectangleI.Empty;
181 Ortho = RectangleF.Empty;
182
183 PushScissorState(true);
184 PushViewport(new RectangleI(0, 0, (int)size.X, (int)size.Y));
185 PushScissor(new RectangleI(0, 0, (int)size.X, (int)size.Y));
186 PushScissorOffset(Vector2I.Zero);
187 PushMaskingInfo(new MaskingInfo
188 {
189 ScreenSpaceAABB = new RectangleI(0, 0, (int)size.X, (int)size.Y),
190 MaskingRect = new RectangleF(0, 0, size.X, size.Y),
191 ToMaskingSpace = Matrix3.Identity,
192 BlendRange = 1,
193 AlphaExponent = 1,
194 CornerExponent = 2.5f,
195 }, true);
196
197 PushDepthInfo(DepthInfo.Default);
198 Clear(new ClearInfo(Color4.Black));
199
200 freeUnusedVertexBuffers();
201
202 stat_texture_uploads_queued.Value = texture_upload_queue.Count;
203 stat_texture_uploads_dequeued.Value = 0;
204 stat_texture_uploads_performed.Value = 0;
205
206 // increase the number of items processed with the queue length to ensure it doesn't get out of hand.
207 int targetUploads = Math.Clamp(texture_upload_queue.Count / 2, 1, MaxTexturesUploadedPerFrame);
208 int uploads = 0;
209 int uploadedPixels = 0;
210
211 // continue attempting to upload textures until enough uploads have been performed.
212 while (texture_upload_queue.TryDequeue(out TextureGL texture))
213 {
214 stat_texture_uploads_dequeued.Value++;
215
216 texture.IsQueuedForUpload = false;
217
218 if (!texture.Upload())
219 continue;
220
221 stat_texture_uploads_performed.Value++;
222
223 if (++uploads >= targetUploads)
224 break;
225
226 if ((uploadedPixels += texture.Width * texture.Height) > MaxPixelsUploadedPerFrame)
227 break;
228 }
229
230 last_bound_texture.AsSpan().Clear();
231 last_bound_texture_is_atlas.AsSpan().Clear();
232 last_bound_buffers.AsSpan().Clear();
233 }
234
235 private static ClearInfo currentClearInfo;
236
237 public static void Clear(ClearInfo clearInfo)
238 {
239 PushDepthInfo(new DepthInfo(writeDepth: true));
240 PushScissorState(false);
241 if (clearInfo.Colour != currentClearInfo.Colour)
242 GL.ClearColor(clearInfo.Colour);
243
244 if (clearInfo.Depth != currentClearInfo.Depth)
245 {
246 if (IsEmbedded)
247 {
248 // GL ES only supports glClearDepthf
249 // See: https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glClearDepthf.xhtml
250 GL.ClearDepth((float)clearInfo.Depth);
251 }
252 else
253 {
254 // Older desktop platforms don't support glClearDepthf, so standard GL's double version is used instead
255 // See: https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/glClearDepth.xhtml
256 osuTK.Graphics.OpenGL.GL.ClearDepth(clearInfo.Depth);
257 }
258 }
259
260 if (clearInfo.Stencil != currentClearInfo.Stencil)
261 GL.ClearStencil(clearInfo.Stencil);
262
263 GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
264
265 currentClearInfo = clearInfo;
266
267 PopScissorState();
268 PopDepthInfo();
269 }
270
271 private static readonly Stack<bool> scissor_state_stack = new Stack<bool>();
272
273 private static bool currentScissorState;
274
275 public static void PushScissorState(bool enabled)
276 {
277 scissor_state_stack.Push(enabled);
278 setScissorState(enabled);
279 }
280
281 public static void PopScissorState()
282 {
283 Trace.Assert(scissor_state_stack.Count > 1);
284
285 scissor_state_stack.Pop();
286
287 setScissorState(scissor_state_stack.Peek());
288 }
289
290 private static void setScissorState(bool enabled)
291 {
292 if (enabled == currentScissorState)
293 return;
294
295 currentScissorState = enabled;
296
297 if (enabled)
298 GL.Enable(EnableCap.ScissorTest);
299 else
300 GL.Disable(EnableCap.ScissorTest);
301 }
302
303 /// <summary>
304 /// Enqueues a texture to be uploaded in the next frame.
305 /// </summary>
306 /// <param name="texture">The texture to be uploaded.</param>
307 public static void EnqueueTextureUpload(TextureGL texture)
308 {
309 if (texture.IsQueuedForUpload)
310 return;
311
312 if (host != null)
313 {
314 texture.IsQueuedForUpload = true;
315 texture_upload_queue.Enqueue(texture);
316 }
317 }
318
319 /// <summary>
320 /// Schedules an expensive operation to a queue from which a maximum of one operation is performed per frame.
321 /// </summary>
322 /// <param name="operation">The operation to schedule.</param>
323 public static void ScheduleExpensiveOperation(ScheduledDelegate operation)
324 {
325 if (host != null)
326 expensive_operation_queue.Enqueue(operation);
327 }
328
329 private static readonly int[] last_bound_buffers = new int[2];
330
331 /// <summary>
332 /// Bind an OpenGL buffer object.
333 /// </summary>
334 /// <param name="target">The buffer type to bind.</param>
335 /// <param name="buffer">The buffer ID to bind.</param>
336 /// <returns>Whether an actual bind call was necessary. This value is false when repeatedly binding the same buffer.</returns>
337 public static bool BindBuffer(BufferTarget target, int buffer)
338 {
339 int bufferIndex = target - BufferTarget.ArrayBuffer;
340 if (last_bound_buffers[bufferIndex] == buffer)
341 return false;
342
343 last_bound_buffers[bufferIndex] = buffer;
344 GL.BindBuffer(target, buffer);
345
346 FrameStatistics.Increment(StatisticsCounterType.VBufBinds);
347
348 return true;
349 }
350
351 private static IVertexBatch lastActiveBatch;
352
353 /// <summary>
354 /// Sets the last vertex batch used for drawing.
355 /// <para>
356 /// This is done so that various methods that change GL state can force-draw the batch
357 /// before continuing with the state change.
358 /// </para>
359 /// </summary>
360 /// <param name="batch">The batch.</param>
361 internal static void SetActiveBatch(IVertexBatch batch)
362 {
363 if (lastActiveBatch == batch)
364 return;
365
366 batch_reset_list.Add(batch);
367
368 FlushCurrentBatch();
369
370 lastActiveBatch = batch;
371 }
372
373 /// <summary>
374 /// Notifies that a <see cref="IVertexBuffer"/> has begun being used.
375 /// </summary>
376 /// <param name="buffer">The <see cref="IVertexBuffer"/> in use.</param>
377 internal static void RegisterVertexBufferUse(IVertexBuffer buffer) => vertex_buffers_in_use.Add(buffer);
378
379 private static void freeUnusedVertexBuffers()
380 {
381 if (ResetId % vbo_free_check_interval != 0)
382 return;
383
384 foreach (var buf in vertex_buffers_in_use)
385 {
386 if (buf.InUse && ResetId - buf.LastUseResetId > vbo_free_check_interval)
387 buf.Free();
388 }
389
390 vertex_buffers_in_use.RemoveAll(b => !b.InUse);
391 }
392
393 private static readonly int[] last_bound_texture = new int[16];
394 private static readonly bool[] last_bound_texture_is_atlas = new bool[16];
395
396 internal static int GetTextureUnitId(TextureUnit unit) => (int)unit - (int)TextureUnit.Texture0;
397 internal static bool AtlasTextureIsBound(TextureUnit unit) => last_bound_texture_is_atlas[GetTextureUnitId(unit)];
398
399 /// <summary>
400 /// Binds a texture to draw with.
401 /// </summary>
402 /// <param name="texture">The texture to bind.</param>
403 /// <param name="unit">The texture unit to bind it to.</param>
404 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
405 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
406 /// <returns>true if the provided texture was not already bound (causing a binding change).</returns>
407 public static bool BindTexture(TextureGL texture, TextureUnit unit = TextureUnit.Texture0, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
408 {
409 bool didBind = BindTexture(texture?.TextureId ?? 0, unit, wrapModeS, wrapModeT);
410 last_bound_texture_is_atlas[GetTextureUnitId(unit)] = texture is TextureGLAtlas;
411
412 return didBind;
413 }
414
415 internal static WrapMode CurrentWrapModeS;
416 internal static WrapMode CurrentWrapModeT;
417
418 /// <summary>
419 /// Binds a texture to draw with.
420 /// </summary>
421 /// <param name="textureId">The texture to bind.</param>
422 /// <param name="unit">The texture unit to bind it to.</param>
423 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
424 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
425 /// <returns>true if the provided texture was not already bound (causing a binding change).</returns>
426 public static bool BindTexture(int textureId, TextureUnit unit = TextureUnit.Texture0, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None)
427 {
428 var index = GetTextureUnitId(unit);
429
430 if (wrapModeS != CurrentWrapModeS)
431 {
432 GlobalPropertyManager.Set(GlobalProperty.WrapModeS, (int)wrapModeS);
433 CurrentWrapModeS = wrapModeS;
434 }
435
436 if (wrapModeT != CurrentWrapModeT)
437 {
438 GlobalPropertyManager.Set(GlobalProperty.WrapModeT, (int)wrapModeT);
439 CurrentWrapModeT = wrapModeT;
440 }
441
442 if (last_bound_texture[index] == textureId)
443 return false;
444
445 FlushCurrentBatch();
446
447 GL.ActiveTexture(unit);
448 GL.BindTexture(TextureTarget.Texture2D, textureId);
449
450 last_bound_texture[index] = textureId;
451 last_bound_texture_is_atlas[GetTextureUnitId(unit)] = false;
452
453 FrameStatistics.Increment(StatisticsCounterType.TextureBinds);
454 return true;
455 }
456
457 private static BlendingParameters lastBlendingParameters;
458 private static bool? lastBlendingEnabledState;
459
460 /// <summary>
461 /// Sets the blending function to draw with.
462 /// </summary>
463 /// <param name="blendingParameters">The info we should use to update the active state.</param>
464 public static void SetBlend(BlendingParameters blendingParameters)
465 {
466 if (lastBlendingParameters == blendingParameters)
467 return;
468
469 FlushCurrentBatch();
470
471 if (blendingParameters.IsDisabled)
472 {
473 if (!lastBlendingEnabledState.HasValue || lastBlendingEnabledState.Value)
474 GL.Disable(EnableCap.Blend);
475
476 lastBlendingEnabledState = false;
477 }
478 else
479 {
480 if (!lastBlendingEnabledState.HasValue || !lastBlendingEnabledState.Value)
481 GL.Enable(EnableCap.Blend);
482
483 lastBlendingEnabledState = true;
484
485 GL.BlendEquationSeparate(blendingParameters.RGBEquationMode, blendingParameters.AlphaEquationMode);
486 GL.BlendFuncSeparate(blendingParameters.SourceBlendingFactor, blendingParameters.DestinationBlendingFactor,
487 blendingParameters.SourceAlphaBlendingFactor, blendingParameters.DestinationAlphaBlendingFactor);
488 }
489
490 lastBlendingParameters = blendingParameters;
491 }
492
493 private static readonly Stack<RectangleI> viewport_stack = new Stack<RectangleI>();
494
495 /// <summary>
496 /// Applies a new viewport rectangle.
497 /// </summary>
498 /// <param name="viewport">The viewport rectangle.</param>
499 public static void PushViewport(RectangleI viewport)
500 {
501 var actualRect = viewport;
502
503 if (actualRect.Width < 0)
504 {
505 actualRect.X += viewport.Width;
506 actualRect.Width = -viewport.Width;
507 }
508
509 if (actualRect.Height < 0)
510 {
511 actualRect.Y += viewport.Height;
512 actualRect.Height = -viewport.Height;
513 }
514
515 PushOrtho(viewport);
516
517 viewport_stack.Push(actualRect);
518
519 if (Viewport == actualRect)
520 return;
521
522 Viewport = actualRect;
523
524 GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height);
525 }
526
527 /// <summary>
528 /// Applies the last viewport rectangle.
529 /// </summary>
530 public static void PopViewport()
531 {
532 Trace.Assert(viewport_stack.Count > 1);
533
534 PopOrtho();
535
536 viewport_stack.Pop();
537 RectangleI actualRect = viewport_stack.Peek();
538
539 if (Viewport == actualRect)
540 return;
541
542 Viewport = actualRect;
543
544 GL.Viewport(Viewport.Left, Viewport.Top, Viewport.Width, Viewport.Height);
545 }
546
547 /// <summary>
548 /// Applies a new scissor rectangle.
549 /// </summary>
550 /// <param name="scissor">The scissor rectangle.</param>
551 public static void PushScissor(RectangleI scissor)
552 {
553 FlushCurrentBatch();
554
555 scissor_rect_stack.Push(scissor);
556 if (Scissor == scissor)
557 return;
558
559 Scissor = scissor;
560 setScissor(scissor);
561 }
562
563 /// <summary>
564 /// Applies the last scissor rectangle.
565 /// </summary>
566 public static void PopScissor()
567 {
568 Trace.Assert(scissor_rect_stack.Count > 1);
569
570 FlushCurrentBatch();
571
572 scissor_rect_stack.Pop();
573 RectangleI scissor = scissor_rect_stack.Peek();
574
575 if (Scissor == scissor)
576 return;
577
578 Scissor = scissor;
579 setScissor(scissor);
580 }
581
582 private static void setScissor(RectangleI scissor)
583 {
584 if (scissor.Width < 0)
585 {
586 scissor.X += scissor.Width;
587 scissor.Width = -scissor.Width;
588 }
589
590 if (scissor.Height < 0)
591 {
592 scissor.Y += scissor.Height;
593 scissor.Height = -scissor.Height;
594 }
595
596 GL.Scissor(scissor.X, Viewport.Height - scissor.Bottom, scissor.Width, scissor.Height);
597 }
598
599 private static readonly Stack<Vector2I> scissor_offset_stack = new Stack<Vector2I>();
600
601 /// <summary>
602 /// Applies an offset to the scissor rectangle.
603 /// </summary>
604 /// <param name="offset">The offset.</param>
605 public static void PushScissorOffset(Vector2I offset)
606 {
607 FlushCurrentBatch();
608
609 scissor_offset_stack.Push(offset);
610 if (ScissorOffset == offset)
611 return;
612
613 ScissorOffset = offset;
614 }
615
616 /// <summary>
617 /// Applies the last scissor rectangle offset.
618 /// </summary>
619 public static void PopScissorOffset()
620 {
621 Trace.Assert(scissor_offset_stack.Count > 1);
622
623 FlushCurrentBatch();
624
625 scissor_offset_stack.Pop();
626 Vector2I offset = scissor_offset_stack.Peek();
627
628 if (ScissorOffset == offset)
629 return;
630
631 ScissorOffset = offset;
632 }
633
634 private static readonly Stack<RectangleF> ortho_stack = new Stack<RectangleF>();
635
636 /// <summary>
637 /// Applies a new orthographic projection rectangle.
638 /// </summary>
639 /// <param name="ortho">The orthographic projection rectangle.</param>
640 public static void PushOrtho(RectangleF ortho)
641 {
642 FlushCurrentBatch();
643
644 ortho_stack.Push(ortho);
645 if (Ortho == ortho)
646 return;
647
648 Ortho = ortho;
649
650 ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1);
651 GlobalPropertyManager.Set(GlobalProperty.ProjMatrix, ProjectionMatrix);
652 }
653
654 /// <summary>
655 /// Applies the last orthographic projection rectangle.
656 /// </summary>
657 public static void PopOrtho()
658 {
659 Trace.Assert(ortho_stack.Count > 1);
660
661 FlushCurrentBatch();
662
663 ortho_stack.Pop();
664 RectangleF actualRect = ortho_stack.Peek();
665
666 if (Ortho == actualRect)
667 return;
668
669 Ortho = actualRect;
670
671 ProjectionMatrix = Matrix4.CreateOrthographicOffCenter(Ortho.Left, Ortho.Right, Ortho.Bottom, Ortho.Top, -1, 1);
672 GlobalPropertyManager.Set(GlobalProperty.ProjMatrix, ProjectionMatrix);
673 }
674
675 private static readonly Stack<MaskingInfo> masking_stack = new Stack<MaskingInfo>();
676 private static readonly Stack<RectangleI> scissor_rect_stack = new Stack<RectangleI>();
677 private static readonly Stack<int> frame_buffer_stack = new Stack<int>();
678 private static readonly Stack<DepthInfo> depth_stack = new Stack<DepthInfo>();
679
680 private static void setMaskingInfo(MaskingInfo maskingInfo, bool isPushing, bool overwritePreviousScissor)
681 {
682 FlushCurrentBatch();
683
684 GlobalPropertyManager.Set(GlobalProperty.MaskingRect, new Vector4(
685 maskingInfo.MaskingRect.Left,
686 maskingInfo.MaskingRect.Top,
687 maskingInfo.MaskingRect.Right,
688 maskingInfo.MaskingRect.Bottom));
689
690 GlobalPropertyManager.Set(GlobalProperty.ToMaskingSpace, maskingInfo.ToMaskingSpace);
691
692 GlobalPropertyManager.Set(GlobalProperty.CornerRadius, maskingInfo.CornerRadius);
693 GlobalPropertyManager.Set(GlobalProperty.CornerExponent, maskingInfo.CornerExponent);
694
695 GlobalPropertyManager.Set(GlobalProperty.BorderThickness, maskingInfo.BorderThickness / maskingInfo.BlendRange);
696
697 if (maskingInfo.BorderThickness > 0)
698 {
699 GlobalPropertyManager.Set(GlobalProperty.BorderColour, new Vector4(
700 maskingInfo.BorderColour.Linear.R,
701 maskingInfo.BorderColour.Linear.G,
702 maskingInfo.BorderColour.Linear.B,
703 maskingInfo.BorderColour.Linear.A));
704 }
705
706 GlobalPropertyManager.Set(GlobalProperty.MaskingBlendRange, maskingInfo.BlendRange);
707 GlobalPropertyManager.Set(GlobalProperty.AlphaExponent, maskingInfo.AlphaExponent);
708
709 GlobalPropertyManager.Set(GlobalProperty.EdgeOffset, maskingInfo.EdgeOffset);
710
711 GlobalPropertyManager.Set(GlobalProperty.DiscardInner, maskingInfo.Hollow);
712 if (maskingInfo.Hollow)
713 GlobalPropertyManager.Set(GlobalProperty.InnerCornerRadius, maskingInfo.HollowCornerRadius);
714
715 if (isPushing)
716 {
717 // When drawing to a viewport that doesn't match the projection size (e.g. via framebuffers), the resultant image will be scaled
718 Vector2 viewportScale = Vector2.Divide(Viewport.Size, Ortho.Size);
719
720 Vector2 location = (maskingInfo.ScreenSpaceAABB.Location - ScissorOffset) * viewportScale;
721 Vector2 size = maskingInfo.ScreenSpaceAABB.Size * viewportScale;
722
723 RectangleI actualRect = new RectangleI(
724 (int)Math.Floor(location.X),
725 (int)Math.Floor(location.Y),
726 (int)Math.Ceiling(size.X),
727 (int)Math.Ceiling(size.Y));
728
729 PushScissor(overwritePreviousScissor ? actualRect : RectangleI.Intersect(scissor_rect_stack.Peek(), actualRect));
730 }
731 else
732 PopScissor();
733 }
734
735 internal static void FlushCurrentBatch()
736 {
737 lastActiveBatch?.Draw();
738 }
739
740 public static bool IsMaskingActive => masking_stack.Count > 1;
741
742 /// <summary>
743 /// Applies a new scissor rectangle.
744 /// </summary>
745 /// <param name="maskingInfo">The masking info.</param>
746 /// <param name="overwritePreviousScissor">Whether or not to shrink an existing scissor rectangle.</param>
747 public static void PushMaskingInfo(in MaskingInfo maskingInfo, bool overwritePreviousScissor = false)
748 {
749 masking_stack.Push(maskingInfo);
750 if (CurrentMaskingInfo == maskingInfo)
751 return;
752
753 currentMaskingInfo = maskingInfo;
754 setMaskingInfo(CurrentMaskingInfo, true, overwritePreviousScissor);
755 }
756
757 /// <summary>
758 /// Applies the last scissor rectangle.
759 /// </summary>
760 public static void PopMaskingInfo()
761 {
762 Trace.Assert(masking_stack.Count > 1);
763
764 masking_stack.Pop();
765 MaskingInfo maskingInfo = masking_stack.Peek();
766
767 if (CurrentMaskingInfo == maskingInfo)
768 return;
769
770 currentMaskingInfo = maskingInfo;
771 setMaskingInfo(CurrentMaskingInfo, false, true);
772 }
773
774 /// <summary>
775 /// Applies a new depth information.
776 /// </summary>
777 /// <param name="depthInfo">The depth information.</param>
778 public static void PushDepthInfo(DepthInfo depthInfo)
779 {
780 depth_stack.Push(depthInfo);
781
782 if (CurrentDepthInfo.Equals(depthInfo))
783 return;
784
785 CurrentDepthInfo = depthInfo;
786 setDepthInfo(CurrentDepthInfo);
787 }
788
789 /// <summary>
790 /// Applies the last depth information.
791 /// </summary>
792 public static void PopDepthInfo()
793 {
794 Trace.Assert(depth_stack.Count > 1);
795
796 depth_stack.Pop();
797 DepthInfo depthInfo = depth_stack.Peek();
798
799 if (CurrentDepthInfo.Equals(depthInfo))
800 return;
801
802 CurrentDepthInfo = depthInfo;
803 setDepthInfo(CurrentDepthInfo);
804 }
805
806 private static void setDepthInfo(DepthInfo depthInfo)
807 {
808 FlushCurrentBatch();
809
810 if (depthInfo.DepthTest)
811 {
812 GL.Enable(EnableCap.DepthTest);
813 GL.DepthFunc(depthInfo.Function);
814 }
815 else
816 GL.Disable(EnableCap.DepthTest);
817
818 GL.DepthMask(depthInfo.WriteDepth);
819 }
820
821 /// <summary>
822 /// Sets the current draw depth.
823 /// The draw depth is written to every vertex added to <see cref="VertexBuffer{T}"/>s.
824 /// </summary>
825 /// <param name="drawDepth">The draw depth.</param>
826 internal static void SetDrawDepth(float drawDepth) => BackbufferDrawDepth = drawDepth;
827
828 /// <summary>
829 /// Binds a framebuffer.
830 /// </summary>
831 /// <param name="frameBuffer">The framebuffer to bind.</param>
832 public static void BindFrameBuffer(int frameBuffer)
833 {
834 if (frameBuffer == -1) return;
835
836 bool alreadyBound = frame_buffer_stack.Count > 0 && frame_buffer_stack.Peek() == frameBuffer;
837
838 frame_buffer_stack.Push(frameBuffer);
839
840 if (!alreadyBound)
841 {
842 FlushCurrentBatch();
843 GL.BindFramebuffer(FramebufferTarget.Framebuffer, frameBuffer);
844
845 GlobalPropertyManager.Set(GlobalProperty.BackbufferDraw, UsingBackbuffer);
846 }
847
848 GlobalPropertyManager.Set(GlobalProperty.GammaCorrection, UsingBackbuffer);
849 }
850
851 /// <summary>
852 /// Binds a framebuffer.
853 /// </summary>
854 /// <param name="frameBuffer">The framebuffer to bind.</param>
855 public static void UnbindFrameBuffer(int frameBuffer)
856 {
857 if (frameBuffer == -1) return;
858
859 if (frame_buffer_stack.Peek() != frameBuffer)
860 return;
861
862 frame_buffer_stack.Pop();
863
864 FlushCurrentBatch();
865 GL.BindFramebuffer(FramebufferTarget.Framebuffer, frame_buffer_stack.Peek());
866
867 GlobalPropertyManager.Set(GlobalProperty.BackbufferDraw, UsingBackbuffer);
868 GlobalPropertyManager.Set(GlobalProperty.GammaCorrection, UsingBackbuffer);
869 }
870
871 /// <summary>
872 /// Deletes a frame buffer.
873 /// </summary>
874 /// <param name="frameBuffer">The frame buffer to delete.</param>
875 internal static void DeleteFrameBuffer(int frameBuffer)
876 {
877 if (frameBuffer == -1) return;
878
879 while (frame_buffer_stack.Peek() == frameBuffer)
880 UnbindFrameBuffer(frameBuffer);
881
882 ScheduleDisposal(() => { GL.DeleteFramebuffer(frameBuffer); });
883 }
884
885 private static int currentShader;
886
887 private static readonly Stack<int> shader_stack = new Stack<int>();
888
889 public static void UseProgram(int? shader)
890 {
891 ThreadSafety.EnsureDrawThread();
892
893 if (shader != null)
894 {
895 shader_stack.Push(shader.Value);
896 }
897 else
898 {
899 shader_stack.Pop();
900
901 //check if the stack is empty, and if so don't restore the previous shader.
902 if (shader_stack.Count == 0)
903 return;
904 }
905
906 int s = shader ?? shader_stack.Peek();
907
908 if (currentShader == s) return;
909
910 FrameStatistics.Increment(StatisticsCounterType.ShaderBinds);
911
912 FlushCurrentBatch();
913
914 GL.UseProgram(s);
915 currentShader = s;
916 }
917
918 internal static void SetUniform<T>(IUniformWithValue<T> uniform)
919 where T : struct, IEquatable<T>
920 {
921 if (uniform.Owner == currentShader)
922 FlushCurrentBatch();
923
924 switch (uniform)
925 {
926 case IUniformWithValue<bool> b:
927 GL.Uniform1(uniform.Location, b.GetValue() ? 1 : 0);
928 break;
929
930 case IUniformWithValue<int> i:
931 GL.Uniform1(uniform.Location, i.GetValue());
932 break;
933
934 case IUniformWithValue<float> f:
935 GL.Uniform1(uniform.Location, f.GetValue());
936 break;
937
938 case IUniformWithValue<Vector2> v2:
939 GL.Uniform2(uniform.Location, ref v2.GetValueByRef());
940 break;
941
942 case IUniformWithValue<Vector3> v3:
943 GL.Uniform3(uniform.Location, ref v3.GetValueByRef());
944 break;
945
946 case IUniformWithValue<Vector4> v4:
947 GL.Uniform4(uniform.Location, ref v4.GetValueByRef());
948 break;
949
950 case IUniformWithValue<Matrix2> m2:
951 GL.UniformMatrix2(uniform.Location, false, ref m2.GetValueByRef());
952 break;
953
954 case IUniformWithValue<Matrix3> m3:
955 GL.UniformMatrix3(uniform.Location, false, ref m3.GetValueByRef());
956 break;
957
958 case IUniformWithValue<Matrix4> m4:
959 GL.UniformMatrix4(uniform.Location, false, ref m4.GetValueByRef());
960 break;
961 }
962 }
963 }
964
965 public struct MaskingInfo : IEquatable<MaskingInfo>
966 {
967 public RectangleI ScreenSpaceAABB;
968 public RectangleF MaskingRect;
969
970 public Quad ConservativeScreenSpaceQuad;
971
972 /// <summary>
973 /// This matrix transforms screen space coordinates to masking space (likely the parent
974 /// space of the container doing the masking).
975 /// It is used by a shader to determine which pixels to discard.
976 /// </summary>
977 public Matrix3 ToMaskingSpace;
978
979 public float CornerRadius;
980 public float CornerExponent;
981
982 public float BorderThickness;
983 public SRGBColour BorderColour;
984
985 public float BlendRange;
986 public float AlphaExponent;
987
988 public Vector2 EdgeOffset;
989
990 public bool Hollow;
991 public float HollowCornerRadius;
992
993 public readonly bool Equals(MaskingInfo other) => this == other;
994
995 public static bool operator ==(in MaskingInfo left, in MaskingInfo right) =>
996 left.ScreenSpaceAABB == right.ScreenSpaceAABB &&
997 left.MaskingRect == right.MaskingRect &&
998 left.ToMaskingSpace == right.ToMaskingSpace &&
999 left.CornerRadius == right.CornerRadius &&
1000 left.CornerExponent == right.CornerExponent &&
1001 left.BorderThickness == right.BorderThickness &&
1002 left.BorderColour.Equals(right.BorderColour) &&
1003 left.BlendRange == right.BlendRange &&
1004 left.AlphaExponent == right.AlphaExponent &&
1005 left.EdgeOffset == right.EdgeOffset &&
1006 left.Hollow == right.Hollow &&
1007 left.HollowCornerRadius == right.HollowCornerRadius;
1008
1009 public static bool operator !=(in MaskingInfo left, in MaskingInfo right) => !(left == right);
1010
1011 public override readonly bool Equals(object obj) => obj is MaskingInfo other && this == other;
1012
1013 public override readonly int GetHashCode() => 0; // Shouldn't be used; simplifying implementation here.
1014 }
1015}