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 System.Runtime.InteropServices;
8using osu.Framework.Development;
9using osu.Framework.Extensions.ImageExtensions;
10using osu.Framework.Graphics.Batches;
11using osu.Framework.Graphics.Primitives;
12using osuTK.Graphics.ES30;
13using osu.Framework.Statistics;
14using osu.Framework.Graphics.Colour;
15using osu.Framework.Graphics.OpenGL.Vertices;
16using osu.Framework.Graphics.Textures;
17using osu.Framework.Lists;
18using osu.Framework.Platform;
19using osuTK;
20using SixLabors.ImageSharp;
21using SixLabors.ImageSharp.PixelFormats;
22using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
23
24namespace osu.Framework.Graphics.OpenGL.Textures
25{
26 internal class TextureGLSingle : TextureGL
27 {
28 /// <summary>
29 /// Contains all currently-active <see cref="TextureGLSingle"/>es.
30 /// </summary>
31 private static readonly LockedWeakList<TextureGLSingle> all_textures = new LockedWeakList<TextureGLSingle>();
32
33 public const int MAX_MIPMAP_LEVELS = 3;
34
35 private static readonly Action<TexturedVertex2D> default_quad_action = new QuadBatch<TexturedVertex2D>(100, 1000).AddAction;
36
37 private readonly Queue<ITextureUpload> uploadQueue = new Queue<ITextureUpload>();
38
39 /// <summary>
40 /// Invoked when a new <see cref="TextureGLAtlas"/> is created.
41 /// </summary>
42 /// <remarks>
43 /// Invocation from the draw or update thread cannot be assumed.
44 /// </remarks>
45 public static event Action<TextureGLSingle> TextureCreated;
46
47 private int internalWidth;
48 private int internalHeight;
49
50 private readonly All filteringMode;
51
52 private readonly Rgba32 initialisationColour;
53
54 /// <summary>
55 /// The total amount of times this <see cref="TextureGLSingle"/> was bound.
56 /// </summary>
57 public ulong BindCount { get; protected set; }
58
59 // ReSharper disable once InconsistentlySynchronizedField (no need to lock here. we don't really care if the value is stale).
60 public override bool Loaded => textureId > 0 || uploadQueue.Count > 0;
61
62 public override RectangleI Bounds => new RectangleI(0, 0, Width, Height);
63
64 /// <summary>
65 /// Creates a new <see cref="TextureGLSingle"/>.
66 /// </summary>
67 /// <param name="width">The width of the texture.</param>
68 /// <param name="height">The height of the texture.</param>
69 /// <param name="manualMipmaps">Whether manual mipmaps will be uploaded to the texture. If false, the texture will compute mipmaps automatically.</param>
70 /// <param name="filteringMode">The filtering mode.</param>
71 /// <param name="wrapModeS">The texture wrap mode in horizontal direction.</param>
72 /// <param name="wrapModeT">The texture wrap mode in vertical direction.</param>
73 /// <param name="initialisationColour">The colour to initialise texture levels with (in the case of sub region initial uploads).</param>
74 public TextureGLSingle(int width, int height, bool manualMipmaps = false, All filteringMode = All.Linear, WrapMode wrapModeS = WrapMode.None, WrapMode wrapModeT = WrapMode.None, Rgba32 initialisationColour = default)
75 : base(wrapModeS, wrapModeT)
76 {
77 Width = width;
78 Height = height;
79 this.manualMipmaps = manualMipmaps;
80 this.filteringMode = filteringMode;
81 this.initialisationColour = initialisationColour;
82
83 all_textures.Add(this);
84
85 TextureCreated?.Invoke(this);
86 }
87
88 /// <summary>
89 /// Retrieves all currently-active <see cref="TextureGLSingle"/>s.
90 /// </summary>
91 public static TextureGLSingle[] GetAllTextures() => all_textures.ToArray();
92
93 #region Disposal
94
95 ~TextureGLSingle()
96 {
97 Dispose(false);
98 }
99
100 protected override void Dispose(bool isDisposing)
101 {
102 base.Dispose(isDisposing);
103
104 all_textures.Remove(this);
105
106 while (tryGetNextUpload(out var upload))
107 upload.Dispose();
108
109 GLWrapper.ScheduleDisposal(unload);
110 }
111
112 /// <summary>
113 /// Removes texture from GL memory.
114 /// </summary>
115 private void unload()
116 {
117 int disposableId = textureId;
118
119 if (disposableId <= 0)
120 return;
121
122 GL.DeleteTextures(1, new[] { disposableId });
123
124 memoryLease?.Dispose();
125
126 textureId = 0;
127 }
128
129 #endregion
130
131 #region Memory Tracking
132
133 private List<long> levelMemoryUsage = new List<long>();
134
135 private NativeMemoryTracker.NativeMemoryLease memoryLease;
136
137 private void updateMemoryUsage(int level, long newUsage)
138 {
139 levelMemoryUsage ??= new List<long>();
140
141 while (level >= levelMemoryUsage.Count)
142 levelMemoryUsage.Add(0);
143
144 levelMemoryUsage[level] = newUsage;
145
146 memoryLease?.Dispose();
147 memoryLease = NativeMemoryTracker.AddMemory(this, getMemoryUsage());
148 }
149
150 private long getMemoryUsage()
151 {
152 long usage = 0;
153
154 for (int i = 0; i < levelMemoryUsage.Count; i++)
155 usage += levelMemoryUsage[i];
156
157 return usage;
158 }
159
160 #endregion
161
162 private int height;
163
164 public override TextureGL Native => this;
165
166 public override int Height
167 {
168 get => height;
169 set => height = value;
170 }
171
172 private int width;
173
174 public override int Width
175 {
176 get => width;
177 set => width = value;
178 }
179
180 private int textureId;
181
182 public override int TextureId
183 {
184 get
185 {
186 if (!Available)
187 throw new ObjectDisposedException(ToString(), "Can not obtain ID of a disposed texture.");
188
189 if (textureId == 0)
190 throw new InvalidOperationException("Can not obtain ID of a texture before uploading it.");
191
192 return textureId;
193 }
194 }
195
196 /// <summary>
197 /// Retrieves the size of this texture in bytes.
198 /// </summary>
199 public virtual int GetByteSize() => Width * Height * 4;
200
201 private static void rotateVector(ref Vector2 toRotate, float sin, float cos)
202 {
203 float oldX = toRotate.X;
204 toRotate.X = toRotate.X * cos - toRotate.Y * sin;
205 toRotate.Y = oldX * sin + toRotate.Y * cos;
206 }
207
208 public override RectangleF GetTextureRect(RectangleF? textureRect)
209 {
210 RectangleF texRect = textureRect != null
211 ? new RectangleF(textureRect.Value.X, textureRect.Value.Y, textureRect.Value.Width, textureRect.Value.Height)
212 : new RectangleF(0, 0, Width, Height);
213
214 texRect.X /= width;
215 texRect.Y /= height;
216 texRect.Width /= width;
217 texRect.Height /= height;
218
219 return texRect;
220 }
221
222 public const int VERTICES_PER_TRIANGLE = 4;
223
224 internal override void DrawTriangle(Triangle vertexTriangle, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null,
225 Vector2? inflationPercentage = null, RectangleF? textureCoords = null)
226 {
227 if (!Available)
228 throw new ObjectDisposedException(ToString(), "Can not draw a triangle with a disposed texture.");
229
230 RectangleF texRect = GetTextureRect(textureRect);
231 Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero;
232
233 // If clamp to edge is active, allow the texture coordinates to penetrate by half the repeated atlas margin width
234 if (GLWrapper.CurrentWrapModeS == WrapMode.ClampToEdge || GLWrapper.CurrentWrapModeT == WrapMode.ClampToEdge)
235 {
236 Vector2 inflationVector = Vector2.Zero;
237
238 const int mipmap_padding_requirement = (1 << MAX_MIPMAP_LEVELS) / 2;
239
240 if (GLWrapper.CurrentWrapModeS == WrapMode.ClampToEdge)
241 inflationVector.X = mipmap_padding_requirement / (float)width;
242 if (GLWrapper.CurrentWrapModeT == WrapMode.ClampToEdge)
243 inflationVector.Y = mipmap_padding_requirement / (float)height;
244 texRect = texRect.Inflate(inflationVector);
245 }
246
247 RectangleF coordRect = GetTextureRect(textureCoords ?? textureRect);
248 RectangleF inflatedCoordRect = coordRect.Inflate(inflationAmount);
249
250 vertexAction ??= default_quad_action;
251
252 // We split the triangle into two, such that we can obtain smooth edges with our
253 // texture coordinate trick. We might want to revert this to drawing a single
254 // triangle in case we ever need proper texturing, or if the additional vertices
255 // end up becoming an overhead (unlikely).
256 SRGBColour topColour = (drawColour.TopLeft + drawColour.TopRight) / 2;
257 SRGBColour bottomColour = (drawColour.BottomLeft + drawColour.BottomRight) / 2;
258
259 vertexAction(new TexturedVertex2D
260 {
261 Position = vertexTriangle.P0,
262 TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Top),
263 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
264 BlendRange = inflationAmount,
265 Colour = topColour.Linear,
266 });
267 vertexAction(new TexturedVertex2D
268 {
269 Position = vertexTriangle.P1,
270 TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom),
271 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
272 BlendRange = inflationAmount,
273 Colour = drawColour.BottomLeft.Linear,
274 });
275 vertexAction(new TexturedVertex2D
276 {
277 Position = (vertexTriangle.P1 + vertexTriangle.P2) / 2,
278 TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Bottom),
279 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
280 BlendRange = inflationAmount,
281 Colour = bottomColour.Linear,
282 });
283 vertexAction(new TexturedVertex2D
284 {
285 Position = vertexTriangle.P2,
286 TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom),
287 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
288 BlendRange = inflationAmount,
289 Colour = drawColour.BottomRight.Linear,
290 });
291
292 FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexTriangle.Area);
293 }
294
295 public const int VERTICES_PER_QUAD = 4;
296
297 internal override void DrawQuad(Quad vertexQuad, ColourInfo drawColour, RectangleF? textureRect = null, Action<TexturedVertex2D> vertexAction = null, Vector2? inflationPercentage = null,
298 Vector2? blendRangeOverride = null, RectangleF? textureCoords = null)
299 {
300 if (!Available)
301 throw new ObjectDisposedException(ToString(), "Can not draw a quad with a disposed texture.");
302
303 RectangleF texRect = GetTextureRect(textureRect);
304 Vector2 inflationAmount = inflationPercentage.HasValue ? new Vector2(inflationPercentage.Value.X * texRect.Width, inflationPercentage.Value.Y * texRect.Height) : Vector2.Zero;
305
306 // If clamp to edge is active, allow the texture coordinates to penetrate by half the repeated atlas margin width
307 if (GLWrapper.CurrentWrapModeS == WrapMode.ClampToEdge || GLWrapper.CurrentWrapModeT == WrapMode.ClampToEdge)
308 {
309 Vector2 inflationVector = Vector2.Zero;
310
311 const int mipmap_padding_requirement = (1 << MAX_MIPMAP_LEVELS) / 2;
312
313 if (GLWrapper.CurrentWrapModeS == WrapMode.ClampToEdge)
314 inflationVector.X = mipmap_padding_requirement / (float)width;
315 if (GLWrapper.CurrentWrapModeT == WrapMode.ClampToEdge)
316 inflationVector.Y = mipmap_padding_requirement / (float)height;
317 texRect = texRect.Inflate(inflationVector);
318 }
319
320 RectangleF coordRect = GetTextureRect(textureCoords ?? textureRect);
321 RectangleF inflatedCoordRect = coordRect.Inflate(inflationAmount);
322 Vector2 blendRange = blendRangeOverride ?? inflationAmount;
323
324 vertexAction ??= default_quad_action;
325
326 vertexAction(new TexturedVertex2D
327 {
328 Position = vertexQuad.BottomLeft,
329 TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom),
330 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
331 BlendRange = blendRange,
332 Colour = drawColour.BottomLeft.Linear,
333 });
334 vertexAction(new TexturedVertex2D
335 {
336 Position = vertexQuad.BottomRight,
337 TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom),
338 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
339 BlendRange = blendRange,
340 Colour = drawColour.BottomRight.Linear,
341 });
342 vertexAction(new TexturedVertex2D
343 {
344 Position = vertexQuad.TopRight,
345 TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Top),
346 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
347 BlendRange = blendRange,
348 Colour = drawColour.TopRight.Linear,
349 });
350 vertexAction(new TexturedVertex2D
351 {
352 Position = vertexQuad.TopLeft,
353 TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Top),
354 TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom),
355 BlendRange = blendRange,
356 Colour = drawColour.TopLeft.Linear,
357 });
358
359 FrameStatistics.Add(StatisticsCounterType.Pixels, (long)vertexQuad.Area);
360 }
361
362 internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMode wrapModeT, Opacity? uploadOpacity)
363 {
364 if (!Available)
365 throw new ObjectDisposedException(ToString(), "Can not set data of a disposed texture.");
366
367 if (upload.Bounds.IsEmpty && upload.Data.Length > 0)
368 {
369 upload.Bounds = Bounds;
370 if (width * height > upload.Data.Length)
371 throw new InvalidOperationException($"Size of texture upload ({width}x{height}) does not contain enough data ({upload.Data.Length} < {width * height})");
372 }
373
374 UpdateOpacity(upload, ref uploadOpacity);
375
376 lock (uploadQueue)
377 {
378 bool requireUpload = uploadQueue.Count == 0;
379 uploadQueue.Enqueue(upload);
380
381 if (requireUpload && !BypassTextureUploadQueueing)
382 GLWrapper.EnqueueTextureUpload(this);
383 }
384 }
385
386 internal override bool Bind(TextureUnit unit, WrapMode wrapModeS, WrapMode wrapModeT)
387 {
388 if (!Available)
389 throw new ObjectDisposedException(ToString(), "Can not bind a disposed texture.");
390
391 Upload();
392
393 if (textureId <= 0)
394 return false;
395
396 if (GLWrapper.BindTexture(this, unit, wrapModeS, wrapModeT))
397 BindCount++;
398
399 return true;
400 }
401
402 private bool manualMipmaps;
403
404 internal override unsafe bool Upload()
405 {
406 if (!Available)
407 return false;
408
409 // We should never run raw OGL calls on another thread than the main thread due to race conditions.
410 ThreadSafety.EnsureDrawThread();
411
412 bool didUpload = false;
413
414 while (tryGetNextUpload(out ITextureUpload upload))
415 {
416 using (upload)
417 {
418 fixed (Rgba32* ptr = upload.Data)
419 DoUpload(upload, (IntPtr)ptr);
420
421 didUpload = true;
422 }
423 }
424
425 if (didUpload && !manualMipmaps)
426 {
427 GL.Hint(HintTarget.GenerateMipmapHint, HintMode.Nicest);
428 GL.GenerateMipmap(TextureTarget.Texture2D);
429 }
430
431 return didUpload;
432 }
433
434 internal override void FlushUploads()
435 {
436 while (tryGetNextUpload(out var upload))
437 upload.Dispose();
438 }
439
440 private bool tryGetNextUpload(out ITextureUpload upload)
441 {
442 lock (uploadQueue)
443 {
444 if (uploadQueue.Count == 0)
445 {
446 upload = null;
447 return false;
448 }
449
450 upload = uploadQueue.Dequeue();
451 return true;
452 }
453 }
454
455 protected virtual void DoUpload(ITextureUpload upload, IntPtr dataPointer)
456 {
457 // Do we need to generate a new texture?
458 if (textureId <= 0 || internalWidth != width || internalHeight != height)
459 {
460 internalWidth = width;
461 internalHeight = height;
462
463 // We only need to generate a new texture if we don't have one already. Otherwise just re-use the current one.
464 if (textureId <= 0)
465 {
466 int[] textures = new int[1];
467 GL.GenTextures(1, textures);
468
469 textureId = textures[0];
470
471 GLWrapper.BindTexture(this);
472
473 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureBaseLevel, 0);
474 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, MAX_MIPMAP_LEVELS);
475
476 // These shouldn't be required, but on some older Intel drivers the MAX_LOD chosen by the shader isn't clamped to the MAX_LEVEL from above, resulting in disappearing textures.
477 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinLod, 0);
478 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLod, MAX_MIPMAP_LEVELS);
479
480 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter,
481 (int)(manualMipmaps ? filteringMode : filteringMode == All.Linear ? All.LinearMipmapLinear : All.Nearest));
482 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)filteringMode);
483
484 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
485 GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
486 }
487 else
488 GLWrapper.BindTexture(this);
489
490 if (width == upload.Bounds.Width && height == upload.Bounds.Height || dataPointer == IntPtr.Zero)
491 {
492 updateMemoryUsage(upload.Level, (long)width * height * 4);
493 GL.TexImage2D(TextureTarget2d.Texture2D, upload.Level, TextureComponentCount.Srgb8Alpha8, width, height, 0, upload.Format, PixelType.UnsignedByte, dataPointer);
494 }
495 else
496 {
497 initializeLevel(upload.Level, width, height);
498
499 GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X, upload.Bounds.Y, upload.Bounds.Width, upload.Bounds.Height, upload.Format,
500 PixelType.UnsignedByte, dataPointer);
501 }
502 }
503 // Just update content of the current texture
504 else if (dataPointer != IntPtr.Zero)
505 {
506 GLWrapper.BindTexture(this);
507
508 if (!manualMipmaps && upload.Level > 0)
509 {
510 //allocate mipmap levels
511 int level = 1;
512 int d = 2;
513
514 while (width / d > 0)
515 {
516 initializeLevel(level, width / d, height / d);
517 level++;
518 d *= 2;
519 }
520
521 manualMipmaps = true;
522 }
523
524 int div = (int)Math.Pow(2, upload.Level);
525
526 GL.TexSubImage2D(TextureTarget2d.Texture2D, upload.Level, upload.Bounds.X / div, upload.Bounds.Y / div, upload.Bounds.Width / div, upload.Bounds.Height / div,
527 upload.Format, PixelType.UnsignedByte, dataPointer);
528 }
529 }
530
531 private void initializeLevel(int level, int width, int height)
532 {
533 using (var image = createBackingImage(width, height))
534 using (var pixels = image.CreateReadOnlyPixelSpan())
535 {
536 updateMemoryUsage(level, (long)width * height * 4);
537 GL.TexImage2D(TextureTarget2d.Texture2D, level, TextureComponentCount.Srgb8Alpha8, width, height, 0, PixelFormat.Rgba, PixelType.UnsignedByte,
538 ref MemoryMarshal.GetReference(pixels.Span));
539 }
540 }
541
542 private Image<Rgba32> createBackingImage(int width, int height)
543 {
544 // it is faster to initialise without a background specification if transparent black is all that's required.
545 return initialisationColour == default
546 ? new Image<Rgba32>(width, height)
547 : new Image<Rgba32>(width, height, initialisationColour);
548 }
549 }
550}