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.Linq;
6using osu.Framework.Graphics.Colour;
7using osu.Framework.Graphics.Containers;
8using osu.Framework.Graphics.Cursor;
9using osu.Framework.Graphics.OpenGL.Textures;
10using osu.Framework.Graphics.OpenGL.Vertices;
11using osu.Framework.Graphics.Shapes;
12using osu.Framework.Graphics.Sprites;
13using osu.Framework.Localisation;
14using osu.Framework.Utils;
15using osuTK;
16using osuTK.Graphics;
17
18namespace osu.Framework.Graphics.Visualisation
19{
20 internal class TextureVisualiser : ToolWindow
21 {
22 private readonly FillFlowContainer<TexturePanel> atlasFlow;
23 private readonly FillFlowContainer<TexturePanel> textureFlow;
24
25 public TextureVisualiser()
26 : base("Textures", "(Ctrl+F3 to toggle)")
27 {
28 ScrollContent.Child = new FillFlowContainer
29 {
30 RelativeSizeAxes = Axes.X,
31 AutoSizeAxes = Axes.Y,
32 Children = new Drawable[]
33 {
34 new SpriteText
35 {
36 Text = "Atlases",
37 Padding = new MarginPadding(5),
38 Font = FrameworkFont.Condensed.With(weight: "Bold")
39 },
40 atlasFlow = new FillFlowContainer<TexturePanel>
41 {
42 RelativeSizeAxes = Axes.X,
43 AutoSizeAxes = Axes.Y,
44 Spacing = new Vector2(22),
45 Padding = new MarginPadding(10),
46 },
47 new SpriteText
48 {
49 Text = "Textures",
50 Padding = new MarginPadding(5),
51 Font = FrameworkFont.Condensed.With(weight: "Bold")
52 },
53 textureFlow = new FillFlowContainer<TexturePanel>
54 {
55 RelativeSizeAxes = Axes.X,
56 AutoSizeAxes = Axes.Y,
57 Spacing = new Vector2(22),
58 Padding = new MarginPadding(10),
59 }
60 }
61 };
62 }
63
64 protected override void PopIn()
65 {
66 base.PopIn();
67
68 foreach (var tex in TextureGLSingle.GetAllTextures())
69 addTexture(tex);
70
71 TextureGLSingle.TextureCreated += addTexture;
72 }
73
74 protected override void PopOut()
75 {
76 base.PopOut();
77
78 atlasFlow.Clear();
79 textureFlow.Clear();
80
81 TextureGLSingle.TextureCreated -= addTexture;
82 }
83
84 private void addTexture(TextureGLSingle texture) => Schedule(() =>
85 {
86 var target = texture is TextureGLAtlas ? atlasFlow : textureFlow;
87
88 if (target.Any(p => p.Texture == texture))
89 return;
90
91 target.Add(new TexturePanel(texture));
92 });
93
94 private class TexturePanel : CompositeDrawable
95 {
96 private readonly WeakReference<TextureGLSingle> textureReference;
97
98 public TextureGLSingle Texture => textureReference.TryGetTarget(out var tex) ? tex : null;
99
100 private readonly SpriteText titleText;
101 private readonly SpriteText footerText;
102
103 private readonly UsageBackground usage;
104
105 public TexturePanel(TextureGLSingle texture)
106 {
107 textureReference = new WeakReference<TextureGLSingle>(texture);
108
109 Size = new Vector2(100, 132);
110
111 InternalChild = new FillFlowContainer
112 {
113 RelativeSizeAxes = Axes.X,
114 AutoSizeAxes = Axes.Y,
115 Direction = FillDirection.Vertical,
116 Children = new Drawable[]
117 {
118 titleText = new SpriteText
119 {
120 Anchor = Anchor.TopCentre,
121 Origin = Anchor.TopCentre,
122 Font = FrameworkFont.Regular.With(size: 16)
123 },
124 new Container
125 {
126 Anchor = Anchor.TopCentre,
127 Origin = Anchor.TopCentre,
128 RelativeSizeAxes = Axes.X,
129 AutoSizeAxes = Axes.Y,
130 Children = new Drawable[]
131 {
132 usage = new UsageBackground(textureReference)
133 {
134 Size = new Vector2(100)
135 },
136 },
137 },
138 footerText = new SpriteText
139 {
140 Anchor = Anchor.TopCentre,
141 Origin = Anchor.TopCentre,
142 Font = FrameworkFont.Regular.With(size: 16),
143 },
144 }
145 };
146 }
147
148 protected override void Update()
149 {
150 base.Update();
151
152 try
153 {
154 var texture = Texture;
155
156 if (texture?.Available != true)
157 {
158 Expire();
159 return;
160 }
161
162 titleText.Text = $"{texture.TextureId}. {texture.Width}x{texture.Height} ";
163 footerText.Text = Precision.AlmostBigger(usage.AverageUsagesPerFrame, 1) ? $"{usage.AverageUsagesPerFrame:N0} binds" : string.Empty;
164 }
165 catch { }
166 }
167 }
168
169 private class UsageBackground : Box, IHasTooltip
170 {
171 private readonly WeakReference<TextureGLSingle> textureReference;
172
173 private ulong lastBindCount;
174
175 public float AverageUsagesPerFrame { get; private set; }
176
177 public UsageBackground(WeakReference<TextureGLSingle> textureReference)
178 {
179 this.textureReference = textureReference;
180 }
181
182 protected override DrawNode CreateDrawNode() => new UsageBackgroundDrawNode(this);
183
184 private class UsageBackgroundDrawNode : SpriteDrawNode
185 {
186 protected new UsageBackground Source => (UsageBackground)base.Source;
187
188 private ColourInfo drawColour;
189
190 private WeakReference<TextureGLSingle> textureReference;
191
192 public UsageBackgroundDrawNode(Box source)
193 : base(source)
194 {
195 }
196
197 public override void ApplyState()
198 {
199 base.ApplyState();
200
201 textureReference = Source.textureReference;
202 }
203
204 public override void Draw(Action<TexturedVertex2D> vertexAction)
205 {
206 if (!textureReference.TryGetTarget(out var texture))
207 return;
208
209 if (!texture.Available)
210 return;
211
212 ulong delta = texture.BindCount - Source.lastBindCount;
213
214 Source.AverageUsagesPerFrame = Source.AverageUsagesPerFrame * 0.9f + delta * 0.1f;
215
216 drawColour = DrawColourInfo.Colour;
217 drawColour.ApplyChild(
218 Precision.AlmostBigger(Source.AverageUsagesPerFrame, 1)
219 ? Interpolation.ValueAt(Source.AverageUsagesPerFrame, Color4.DarkGray, Color4.Red, 0, 200)
220 : Color4.Transparent);
221
222 base.Draw(vertexAction);
223
224 // intentionally after draw to avoid counting our own bind.
225 Source.lastBindCount = texture.BindCount;
226 }
227
228 protected override void Blit(Action<TexturedVertex2D> vertexAction)
229 {
230 if (!textureReference.TryGetTarget(out var texture))
231 return;
232
233 const float border_width = 4;
234
235 // border
236 DrawQuad(Texture, ScreenSpaceDrawQuad, drawColour, null, vertexAction);
237
238 var shrunkenQuad = ScreenSpaceDrawQuad.AABBFloat.Shrink(border_width);
239
240 // background
241 DrawQuad(Texture, shrunkenQuad, Color4.Black, null, vertexAction);
242
243 float aspect = (float)texture.Width / texture.Height;
244
245 if (aspect > 1)
246 {
247 float newHeight = shrunkenQuad.Height / aspect;
248
249 shrunkenQuad.Y += (shrunkenQuad.Height - newHeight) / 2;
250 shrunkenQuad.Height = newHeight;
251 }
252 else if (aspect < 1)
253 {
254 float newWidth = shrunkenQuad.Width / (1 / aspect);
255
256 shrunkenQuad.X += (shrunkenQuad.Width - newWidth) / 2;
257 shrunkenQuad.Width = newWidth;
258 }
259
260 // texture
261 texture.Bind();
262 DrawQuad(texture, shrunkenQuad, Color4.White, null, vertexAction);
263 }
264
265 protected internal override bool CanDrawOpaqueInterior => false;
266 }
267
268 public LocalisableString TooltipText
269 {
270 get
271 {
272 if (!textureReference.TryGetTarget(out var texture))
273 return string.Empty;
274
275 return $"type: {texture.GetType().Name}, size: {(float)texture.GetByteSize() / 1024 / 1024:N2}mb";
276 }
277 }
278 }
279 }
280}