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.Generic;
6using System.Linq;
7using NUnit.Framework;
8using osu.Framework.Allocation;
9using osu.Framework.Graphics;
10using osu.Framework.Graphics.Containers;
11using osu.Framework.Graphics.OpenGL;
12using osu.Framework.Graphics.Shapes;
13using osuTK;
14using osuTK.Graphics;
15
16namespace osu.Framework.Tests.Visual.Drawables
17{
18 public class TestSceneDrawNodeDisposal : FrameworkTestScene
19 {
20 /// <summary>
21 /// Tests that all references are lost after a drawable is disposed.
22 /// </summary>
23 [Test]
24 public void TestBasicDrawNodeReferencesRemovedAfterDisposal() => performTest(new Box { RelativeSizeAxes = Axes.Both });
25
26 /// <summary>
27 /// Tests that all references are lost after a composite is disposed.
28 /// </summary>
29 [Test]
30 public void TestCompositeDrawNodeReferencesRemovedAfterDisposal() => performTest(new NonFlattenedContainer
31 {
32 RelativeSizeAxes = Axes.Both,
33 Children = new[]
34 {
35 new Box
36 {
37 RelativeSizeAxes = Axes.Both,
38 Width = 0.5f,
39 Colour = Color4.Blue
40 },
41 new Box
42 {
43 RelativeSizeAxes = Axes.Both,
44 X = 0.5f,
45 Width = 0.5f,
46 Colour = Color4.Blue
47 },
48 }
49 });
50
51 /// <summary>
52 /// Tests that all references are lost after a buffered container is disposed.
53 /// </summary>
54 [Test]
55 public void TestBufferedDrawNodeReferencesRemovedAfterDisposal() => performTest(new BufferedContainer
56 {
57 RelativeSizeAxes = Axes.Both,
58 Children = new[]
59 {
60 new Box
61 {
62 RelativeSizeAxes = Axes.Both,
63 Width = 0.5f,
64 Colour = Color4.Blue
65 },
66 new Box
67 {
68 RelativeSizeAxes = Axes.Both,
69 X = 0.5f,
70 Width = 0.5f,
71 Colour = Color4.Blue
72 },
73 }
74 });
75
76 private void performTest(Drawable child)
77 {
78 Container parentContainer = null;
79
80 var drawableRefs = new List<WeakReference>();
81
82 // Add the children to the hierarchy, and build weak-reference wrappers around them
83 AddStep("create hierarchy", () =>
84 {
85 drawableRefs.Clear();
86 buildReferencesRecursive(child);
87
88 Child = parentContainer = new NonFlattenedContainer
89 {
90 Size = new Vector2(200),
91 Child = child
92 };
93
94 void buildReferencesRecursive(Drawable target)
95 {
96 drawableRefs.Add(new WeakReference(target));
97
98 if (target is CompositeDrawable compositeTarget)
99 {
100 foreach (var c in compositeTarget.InternalChildren)
101 buildReferencesRecursive(c);
102 }
103 }
104 });
105
106 AddWaitStep("wait for some draw nodes", GLWrapper.MAX_DRAW_NODES);
107
108 // Clear the parent to ensure no references are held via drawables themselves,
109 // and remove the parent to ensure that the parent maintains references to the child draw nodes
110 AddStep("clear + remove parent container", () =>
111 {
112 parentContainer.Clear();
113 Remove(parentContainer);
114
115 // Lose last hard-reference to the child
116 child = null;
117 });
118
119 // Wait for all drawables to get disposed
120 DisposalMarker disposalMarker = null;
121 AddStep("add disposal marker", () => AsyncDisposalQueue.Enqueue(disposalMarker = new DisposalMarker()));
122 AddUntilStep("wait for drawables to dispose", () => disposalMarker.Disposed);
123
124 // Induce the collection of drawables
125 AddStep("invoke GC", () =>
126 {
127 GC.Collect();
128 GC.WaitForPendingFinalizers();
129 });
130
131 AddUntilStep("all drawable references lost", () => !drawableRefs.Any(r => r.IsAlive));
132 }
133
134 private class NonFlattenedContainer : Container
135 {
136 protected override bool CanBeFlattened => false;
137 }
138
139 private class DisposalMarker : IDisposable
140 {
141 public bool Disposed { get; private set; }
142
143 public void Dispose()
144 {
145 Disposed = true;
146 }
147 }
148 }
149}