A game framework written with osu! in mind.
at master 550 lines 23 kB view raw
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}