From 0c0d6074909e2f66085aa670bcaef95a3a37ec96 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 06:05:48 -0500 Subject: [PATCH 01/19] Disable source alpha multiplication --- .../Visual/Drawables/TestSceneComplexBlending.cs | 2 +- osu.Framework/Graphics/BlendingParameters.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs index 883c59538c..31c17d3169 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneComplexBlending.cs @@ -220,7 +220,7 @@ public TestSceneComplexBlending() colourEquation.Current.Value = foregroundContainer.Blending.RGBEquation; alphaEquation.Current.Value = foregroundContainer.Blending.AlphaEquation; - blendingSrcDropdown.Current.Value = BlendingType.SrcAlpha; + blendingSrcDropdown.Current.Value = BlendingType.One; blendingDestDropdown.Current.Value = BlendingType.OneMinusSrcAlpha; blendingAlphaSrcDropdown.Current.Value = BlendingType.One; blendingAlphaDestDropdown.Current.Value = BlendingType.One; diff --git a/osu.Framework/Graphics/BlendingParameters.cs b/osu.Framework/Graphics/BlendingParameters.cs index 1721dab3ac..c1542b3f60 100644 --- a/osu.Framework/Graphics/BlendingParameters.cs +++ b/osu.Framework/Graphics/BlendingParameters.cs @@ -72,7 +72,7 @@ public struct BlendingParameters : IEquatable public static BlendingParameters Mixture => new BlendingParameters { - Source = BlendingType.SrcAlpha, + Source = BlendingType.One, Destination = BlendingType.OneMinusSrcAlpha, SourceAlpha = BlendingType.One, DestinationAlpha = BlendingType.One, @@ -82,7 +82,7 @@ public struct BlendingParameters : IEquatable public static BlendingParameters Additive => new BlendingParameters { - Source = BlendingType.SrcAlpha, + Source = BlendingType.One, Destination = BlendingType.One, SourceAlpha = BlendingType.One, DestinationAlpha = BlendingType.One, @@ -123,7 +123,7 @@ public void CopyFromParent(BlendingParameters parent) public void ApplyDefaultToInherited() { if (Source == BlendingType.Inherit) - Source = BlendingType.SrcAlpha; + Source = BlendingType.One; if (Destination == BlendingType.Inherit) Destination = BlendingType.OneMinusSrcAlpha; From 8237df87d3057647b9410dd7e9fccff61bcb055b Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 06:14:54 -0500 Subject: [PATCH 02/19] Update raw-caching glyph store to output premultiplied --- osu.Framework/IO/Stores/RawCachingGlyphStore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs index c8a1bdaa85..3a5f7150e4 100644 --- a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs +++ b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs @@ -162,7 +162,8 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) for (int x = 0; x < character.Width; x++) { - span[x] = new Rgba32(255, 255, 255, x < readableWidth && y < readableHeight ? readBuffer[readOffset + x] : (byte)0); + byte val = x < readableWidth && y < readableHeight ? readBuffer[readOffset + x] : (byte)0; + span[x] = new Rgba32(val, val, val, val); } } From bf0d5d4135a07999261ced95716127c1274c417c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 06:34:29 -0500 Subject: [PATCH 03/19] Update texture atlas smoothing to output premultiplied --- .../Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs index 67e80627ec..80e262dd75 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs @@ -215,10 +215,10 @@ private void uploadCornerPadding(ReadOnlySpan upload, RectangleI middleB [MethodImpl(MethodImplOptions.AggressiveInlining)] private void transferBorderPixel(ref Rgba32 dest, Rgba32 source, bool fillOpaque) { - dest.R = source.R; - dest.G = source.G; - dest.B = source.B; dest.A = fillOpaque ? source.A : (byte)0; + dest.R = (byte)(source.R * (dest.A / 255.0)); + dest.G = (byte)(source.G * (dest.A / 255.0)); + dest.B = (byte)(source.B * (dest.A / 255.0)); } /// From 9a6149cca0859adf27c56df81b12614855345a9a Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 08:17:07 -0500 Subject: [PATCH 04/19] Update smooth path texture generation to output premultiplied --- osu.Framework/Graphics/Lines/SmoothPath.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Graphics/Lines/SmoothPath.cs b/osu.Framework/Graphics/Lines/SmoothPath.cs index e012020326..f591da7c23 100644 --- a/osu.Framework/Graphics/Lines/SmoothPath.cs +++ b/osu.Framework/Graphics/Lines/SmoothPath.cs @@ -75,13 +75,12 @@ private void validateTexture() float progress = (float)i / (textureWidth - 1); var colour = ColourAt(progress); - raw[i, 0] = new Rgba32(colour.R, colour.G, colour.B, colour.A * Math.Min(progress / aa_portion, 1)); + float alpha = Math.Min(progress / aa_portion, 1); + raw[i, 0] = new Rgba32(colour.R * alpha, colour.G * alpha, colour.B * alpha, colour.A * alpha); } if (Texture?.Width == textureWidth) - { Texture.SetData(new TextureUpload(raw)); - } else { var texture = new DisposableTexture(renderer.CreateTexture(textureWidth, 1, true)); From 130cf69611042e4504eed70521aaa61d2b4e42ee Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Dec 2024 12:11:35 -0500 Subject: [PATCH 05/19] Introduce `PremultipliedImage` and update texture loading processes --- .../Visual/Input/TestSceneInputResampler.cs | 5 +- .../Visual/Platform/TestSceneClipboard.cs | 2 +- .../Visual/Sprites/TestSceneScreenshot.cs | 2 +- .../UserInterface/TestSceneCircularBlob.cs | 13 ++-- .../TestSceneCircularProgress.cs | 13 ++-- .../UserInterface/TestSceneDrawablePath.cs | 5 +- osu.Framework/Graphics/Lines/SmoothPath.cs | 5 +- .../Performance/FrameStatisticsDisplay.cs | 7 +- .../Graphics/Textures/PremultipliedImage.cs | 67 +++++++++++++++++++ .../Graphics/Textures/TextureAtlas.cs | 5 +- .../Graphics/Textures/TextureLoaderStore.cs | 7 +- .../Graphics/Textures/TextureUpload.cs | 23 ++++--- osu.Framework/IO/Stores/GlyphStore.cs | 7 +- .../IO/Stores/RawCachingGlyphStore.cs | 4 +- 14 files changed, 111 insertions(+), 54 deletions(-) create mode 100644 osu.Framework/Graphics/Textures/PremultipliedImage.cs diff --git a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs index 174afbf555..cbace0425d 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs @@ -15,7 +15,6 @@ using osu.Framework.Testing; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.Input @@ -33,12 +32,12 @@ private void load(IRenderer renderer) { const int width = 2; Texture gradientTexture = renderer.CreateTexture(width, 1, true); - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); for (int i = 0; i < width; ++i) { byte brightnessByte = (byte)((float)i / (width - 1) * 255); - image[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte); + image.Premultiplied[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte); } gradientTexture.SetData(new TextureUpload(image)); diff --git a/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs b/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs index 148a490f39..82bf74a932 100644 --- a/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs +++ b/osu.Framework.Tests/Visual/Platform/TestSceneClipboard.cs @@ -73,7 +73,7 @@ public void TestImage() clipboardImage = image!.Clone(); var texture = renderer.CreateTexture(image.Width, image.Height); - texture.SetData(new TextureUpload(image)); + texture.SetData(new TextureUpload(PremultipliedImage.FromStraight(image))); Child = new Sprite { diff --git a/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs b/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs index c03a9c04d9..60e9b1899b 100644 --- a/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs +++ b/osu.Framework.Tests/Visual/Sprites/TestSceneScreenshot.cs @@ -78,7 +78,7 @@ private void takeScreenshot() var image = t.GetResultSafely(); var tex = renderer.CreateTexture(image.Width, image.Height); - tex.SetData(new TextureUpload(image)); + tex.SetData(new TextureUpload(PremultipliedImage.FromStraight(image))); display.Texture = tex; background.Show(); diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs index b12ecb5c22..07898065c8 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface @@ -32,31 +31,31 @@ private void load() { const int width = 128; - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); gradientTextureHorizontal = renderer.CreateTexture(width, 1, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); } gradientTextureHorizontal.SetData(new TextureUpload(image)); - image = new Image(width, 1); + image = new PremultipliedImage(width, 1); gradientTextureVertical = renderer.CreateTexture(1, width, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); } gradientTextureVertical.SetData(new TextureUpload(image)); - image = new Image(width, width); + image = new PremultipliedImage(width, width); gradientTextureBoth = renderer.CreateTexture(width, width, true); @@ -66,7 +65,7 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image[i, j] = new Rgba32( + image.Premultiplied[i, j] = new Rgba32( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs index 299b77fece..19170c7929 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface @@ -38,31 +37,31 @@ private void load() { const int width = 128; - var image = new Image(width, 1); + var image = new PremultipliedImage(width, 1); gradientTextureHorizontal = renderer.CreateTexture(width, 1, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); } gradientTextureHorizontal.SetData(new TextureUpload(image)); - image = new Image(width, 1); + image = new PremultipliedImage(width, 1); gradientTextureVertical = renderer.CreateTexture(1, width, true); for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); } gradientTextureVertical.SetData(new TextureUpload(image)); - image = new Image(width, width); + image = new PremultipliedImage(width, width); gradientTextureBoth = renderer.CreateTexture(width, width, true); @@ -72,7 +71,7 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image[i, j] = new Rgba32( + image.Premultiplied[i, j] = new Rgba32( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs index 42c15e0ffe..d5cefd2154 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs @@ -15,7 +15,6 @@ using osu.Framework.Utils; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface @@ -29,12 +28,12 @@ public partial class TestSceneDrawablePath : FrameworkTestScene [BackgroundDependencyLoader] private void load(IRenderer renderer) { - var image = new Image(texture_width, 1); + var image = new PremultipliedImage(texture_width, 1); for (int i = 0; i < texture_width; ++i) { byte brightnessByte = (byte)((float)i / (texture_width - 1) * 255); - image[i, 0] = new Rgba32(255, 255, 255, brightnessByte); + image.Premultiplied[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte, brightnessByte); } gradientTexture = renderer.CreateTexture(texture_width, 1, true); diff --git a/osu.Framework/Graphics/Lines/SmoothPath.cs b/osu.Framework/Graphics/Lines/SmoothPath.cs index f591da7c23..a5e80d3450 100644 --- a/osu.Framework/Graphics/Lines/SmoothPath.cs +++ b/osu.Framework/Graphics/Lines/SmoothPath.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osuTK.Graphics; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Lines @@ -66,7 +65,7 @@ private void validateTexture() int textureWidth = (int)PathRadius * 2; //initialise background - var raw = new Image(textureWidth, 1); + var raw = new PremultipliedImage(textureWidth, 1); const float aa_portion = 0.02f; @@ -76,7 +75,7 @@ private void validateTexture() var colour = ColourAt(progress); float alpha = Math.Min(progress / aa_portion, 1); - raw[i, 0] = new Rgba32(colour.R * alpha, colour.G * alpha, colour.B * alpha, colour.A * alpha); + raw.Premultiplied[i, 0] = new Rgba32(colour.R * alpha, colour.G * alpha, colour.B * alpha, colour.A * alpha); } if (Texture?.Width == textureWidth) diff --git a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs index f752c88c51..aad004fdc3 100644 --- a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs +++ b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs @@ -23,7 +23,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Platform; using osuTK; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using WindowState = osu.Framework.Platform.WindowState; @@ -255,14 +254,14 @@ private void load(IRenderer renderer) { //initialise background var columnUpload = new ArrayPoolTextureUpload(1, HEIGHT); - var fullBackground = new Image(WIDTH, HEIGHT); + var fullBackground = new PremultipliedImage(WIDTH, HEIGHT); addArea(null, null, HEIGHT, amount_ms_steps, columnUpload); for (int i = 0; i < HEIGHT; i++) { for (int k = 0; k < WIDTH; k++) - fullBackground[k, i] = columnUpload.RawData[i]; + fullBackground.Premultiplied[k, i] = columnUpload.RawData[i]; } addArea(null, null, HEIGHT, amount_count_steps, columnUpload); @@ -506,7 +505,7 @@ private int addArea(FrameStatistics frame, PerformanceCollectionType? frameTimeT else if (acceptableRange) brightnessAdjust *= 0.8f; - columnUpload.RawData[i] = new Rgba32(col.R * brightnessAdjust, col.G * brightnessAdjust, col.B * brightnessAdjust, col.A); + columnUpload.RawData[i] = new Rgba32(col.R * col.A * brightnessAdjust, col.G * col.A * brightnessAdjust, col.B * col.A * brightnessAdjust, col.A); currentHeight--; } diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs new file mode 100644 index 0000000000..3d550bf465 --- /dev/null +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Colour; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace osu.Framework.Graphics.Textures +{ + public class PremultipliedImage : IDisposable + { + /// + /// The underlying image in form. + /// + public readonly Image Premultiplied; + + private PremultipliedImage(Image premultiplied) + { + Premultiplied = premultiplied; + } + + public PremultipliedImage(int width, int height) + : this(new Image(width, height)) + { + } + + public PremultipliedImage(int width, int height, SRGBColour colour) // todo: colour will be called PremultipliedColour + : this(new Image(width, height, new Rgba32(colour.SRGB.R, colour.SRGB.G, colour.SRGB.B, colour.SRGB.A))) + { + } + + public PremultipliedImage Clone() => FromPremultiplied(Premultiplied.Clone()); + + public void Dispose() + { + Premultiplied.Dispose(); + } + + /// + /// Creates a from a non-premultiplied source by converting colours to premultiplied. + /// + /// The non-premultiplied source image. + public static PremultipliedImage FromStraight(Image image) + { + var premultiplied = image.Clone(); + premultiplied.Mutate(p => p.ProcessPixelRowsAsVector4(r => + { + foreach (ref var pixel in r) + { + pixel.X *= pixel.W; + pixel.Y *= pixel.W; + pixel.Z *= pixel.W; + } + })); + + return new PremultipliedImage(premultiplied); + } + + /// + /// Creates a from a premultiplied source . No conversion is done in this method. + /// + /// The premultiplied source image. + public static PremultipliedImage FromPremultiplied(Image image) => new PremultipliedImage(image); + } +} diff --git a/osu.Framework/Graphics/Textures/TextureAtlas.cs b/osu.Framework/Graphics/Textures/TextureAtlas.cs index a379dc577e..6f7264fa99 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas.cs @@ -4,13 +4,10 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Numerics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Logging; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Textures { @@ -84,7 +81,7 @@ public void Reset() using (var whiteTex = new TextureRegion(atlasTexture, bounds, WrapMode.Repeat, WrapMode.Repeat)) // Generate white padding as if the white texture was wrapped, even though it isn't - whiteTex.SetData(new TextureUpload(new Image(SixLabors.ImageSharp.Configuration.Default, whiteTex.Width, whiteTex.Height, new Rgba32(Vector4.One)))); + whiteTex.SetData(new TextureUpload(new PremultipliedImage(whiteTex.Width, whiteTex.Height, Colour4.White))); currentPosition = new Vector2I(PADDING + WHITE_PIXEL_SIZE, PADDING); } diff --git a/osu.Framework/Graphics/Textures/TextureLoaderStore.cs b/osu.Framework/Graphics/Textures/TextureLoaderStore.cs index d719402d5b..53d38647cc 100644 --- a/osu.Framework/Graphics/Textures/TextureLoaderStore.cs +++ b/osu.Framework/Graphics/Textures/TextureLoaderStore.cs @@ -9,8 +9,6 @@ using System.Threading; using System.Threading.Tasks; using osu.Framework.IO.Stores; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Textures { @@ -38,7 +36,7 @@ public TextureUpload Get(string name) using (var stream = store.GetStream(name)) { if (stream != null) - return new TextureUpload(ImageFromStream(stream)); + return new TextureUpload(ImageFromStream(stream)); } } catch @@ -50,8 +48,7 @@ public TextureUpload Get(string name) public Stream GetStream(string name) => store.GetStream(name); - protected virtual Image ImageFromStream(Stream stream) where TPixel : unmanaged, IPixel - => TextureUpload.LoadFromStream(stream); + protected virtual PremultipliedImage ImageFromStream(Stream stream) => TextureUpload.LoadFromStream(stream); public IEnumerable GetAvailableResources() => store.GetAvailableResources(); diff --git a/osu.Framework/Graphics/Textures/TextureUpload.cs b/osu.Framework/Graphics/Textures/TextureUpload.cs index 096e7ed783..78d68dfc5b 100644 --- a/osu.Framework/Graphics/Textures/TextureUpload.cs +++ b/osu.Framework/Graphics/Textures/TextureUpload.cs @@ -38,16 +38,19 @@ public class TextureUpload : ITextureUpload /// public RectangleI Bounds { get; set; } + /// + /// Overview of the texture upload's pixels content. The pixel colours are alpha-premultiplied. + /// public ReadOnlySpan Data => pixelMemory.Span; - public int Width => image?.Width ?? 0; + public int Width => image?.Premultiplied.Width ?? 0; - public int Height => image?.Height ?? 0; + public int Height => image?.Premultiplied.Height ?? 0; /// /// The backing texture. A handle is kept to avoid early GC. /// - private readonly Image image; + private readonly PremultipliedImage image; private ReadOnlyPixelMemory pixelMemory; @@ -55,10 +58,10 @@ public class TextureUpload : ITextureUpload /// Create an upload from a . This is the preferred method. /// /// The texture to upload. - public TextureUpload(Image image) + public TextureUpload(PremultipliedImage image) { this.image = image; - pixelMemory = image.CreateReadOnlyPixelMemory(); + pixelMemory = image.Premultiplied.CreateReadOnlyPixelMemory(); } /// @@ -68,16 +71,16 @@ public TextureUpload(Image image) /// /// The image content. public TextureUpload(Stream stream) - : this(LoadFromStream(stream)) + : this(LoadFromStream(stream)) { } private static bool stbiNotFound; - internal static Image LoadFromStream(Stream stream) where TPixel : unmanaged, IPixel + internal static PremultipliedImage LoadFromStream(Stream stream) { if (stbiNotFound) - return Image.Load(stream); + return PremultipliedImage.FromStraight(Image.Load(stream)); long initialPos = stream.Position; @@ -88,7 +91,7 @@ internal static Image LoadFromStream(Stream stream) where TPixel stream.ReadExactly(buffer.Memory.Span); using (var stbiImage = Stbi.LoadFromMemory(buffer.Memory.Span, 4)) - return Image.LoadPixelData(MemoryMarshal.Cast(stbiImage.Data), stbiImage.Width, stbiImage.Height); + return PremultipliedImage.FromStraight(Image.LoadPixelData(MemoryMarshal.Cast(stbiImage.Data), stbiImage.Width, stbiImage.Height)); } } catch (Exception e) @@ -98,7 +101,7 @@ internal static Image LoadFromStream(Stream stream) where TPixel Logger.Log($"Texture could not be loaded via STB; falling back to ImageSharp: {e.Message}"); stream.Position = initialPos; - return Image.Load(stream); + return PremultipliedImage.FromStraight(Image.Load(stream)); } } diff --git a/osu.Framework/IO/Stores/GlyphStore.cs b/osu.Framework/IO/Stores/GlyphStore.cs index 3688f8b52c..8c10a28fd7 100644 --- a/osu.Framework/IO/Stores/GlyphStore.cs +++ b/osu.Framework/IO/Stores/GlyphStore.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Text; using SharpFNT; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; @@ -166,7 +165,7 @@ protected virtual TextureUpload LoadCharacter(Character character) var page = GetPageImage(character.Page); LoadedGlyphCount++; - var image = new Image(SixLabors.ImageSharp.Configuration.Default, character.Width, character.Height); + var image = new PremultipliedImage(character.Width, character.Height); var source = page.Data; // the spritesheet may have unused pixels trimmed @@ -175,11 +174,11 @@ protected virtual TextureUpload LoadCharacter(Character character) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.DangerousGetPixelRowMemory(y); + var pixelRowMemory = image.Premultiplied.DangerousGetPixelRowMemory(y); int readOffset = (character.Y + y) * page.Width + character.X; for (int x = 0; x < character.Width; x++) - pixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(255, 255, 255, 0); + pixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(0, 0, 0, 0); } return new TextureUpload(image); diff --git a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs index 3a5f7150e4..a53c018ce1 100644 --- a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs +++ b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs @@ -141,7 +141,7 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) try { - var image = new Image(SixLabors.ImageSharp.Configuration.Default, character.Width, character.Height); + var image = new PremultipliedImage(character.Width, character.Height); if (!pageStreamHandles.TryGetValue(page.Filename, out var source)) source = pageStreamHandles[page.Filename] = CacheStorage.GetStream(page.Filename); @@ -156,7 +156,7 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.DangerousGetPixelRowMemory(y); + var pixelRowMemory = image.Premultiplied.DangerousGetPixelRowMemory(y); var span = pixelRowMemory.Span; int readOffset = y * pageWidth + character.X; From 89d6c25199324f23e6ecaeff148af7352d3dd0da Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Dec 2024 12:15:40 -0500 Subject: [PATCH 06/19] Update iOS texture loading process to load without conversion --- .../Graphics/Textures/IOSTextureLoaderStore.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs index def073c20a..843e81f118 100644 --- a/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs +++ b/osu.Framework.iOS/Graphics/Textures/IOSTextureLoaderStore.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; using UIKit; namespace osu.Framework.iOS.Graphics.Textures @@ -19,7 +20,7 @@ public IOSTextureLoaderStore(IResourceStore store) { } - protected override Image ImageFromStream(Stream stream) + protected override PremultipliedImage ImageFromStream(Stream stream) { using (var nativeData = NSData.FromStream(stream)) { @@ -39,9 +40,11 @@ protected override Image ImageFromStream(Stream stream) using (CGBitmapContext textureContext = new CGBitmapContext(data, width, height, 8, width * 4, CGColorSpace.CreateDeviceRGB(), CGImageAlphaInfo.PremultipliedLast)) textureContext.DrawImage(new CGRect(0, 0, width, height), uiImage.CGImage); - var image = Image.LoadPixelData(data, width, height); + var image = Image.LoadPixelData(data, width, height); - return image; + // UIImage/CGBitmapContext already produce images in premultiplied form, + // so we can directly wrap the image without performing any conversion. + return PremultipliedImage.FromPremultiplied(image); } } } From 0db36b79a29d26e303414060f983fb3d74f7a9e4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Dec 2024 14:57:52 -0500 Subject: [PATCH 07/19] Introduce `PremultipliedColour` and use in vertex specifications --- .../Color4Extensions/Color4Extensions.cs | 8 ++++ .../Graphics/Colour/PremultipliedColour.cs | 44 +++++++++++++++++++ osu.Framework/Graphics/Colour4.cs | 7 +++ osu.Framework/Graphics/Lines/Path_DrawNode.cs | 31 ++++++------- .../Graphics/Rendering/RendererExtensions.cs | 17 +++---- .../Rendering/Vertices/ParticleVertex2D.cs | 4 +- .../Rendering/Vertices/TexturedVertex2D.cs | 4 +- .../Rendering/Vertices/TexturedVertex3D.cs | 4 +- .../Vertices/TimedTexturedVertex2D.cs | 4 +- .../Graphics/Rendering/Vertices/Vertex2D.cs | 4 +- osu.Framework/Graphics/Shapes/FastCircle.cs | 9 ++-- .../Graphics/Textures/PremultipliedImage.cs | 4 +- .../Graphics/Textures/TextureAtlas.cs | 2 +- 13 files changed, 102 insertions(+), 40 deletions(-) create mode 100644 osu.Framework/Graphics/Colour/PremultipliedColour.cs diff --git a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs index 4c1c2d8382..0f83a6ccec 100644 --- a/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs +++ b/osu.Framework/Extensions/Color4Extensions/Color4Extensions.cs @@ -4,6 +4,7 @@ using osuTK.Graphics; using System; using System.Globalization; +using osu.Framework.Graphics.Colour; namespace osu.Framework.Extensions.Color4Extensions { @@ -288,5 +289,12 @@ public static (float h, float s, float v) ToHSV(this Color4 colour) return (h, s, v); } + + /// + /// Converts a to premultiplied alpha and returns a resultant . + /// The input colour is assumed to be in straight-alpha form. + /// + /// The colour as straight-alpha. + public static PremultipliedColour ToPremultiplied(this Color4 colour) => PremultipliedColour.FromStraight(colour); } } diff --git a/osu.Framework/Graphics/Colour/PremultipliedColour.cs b/osu.Framework/Graphics/Colour/PremultipliedColour.cs new file mode 100644 index 0000000000..a17936143f --- /dev/null +++ b/osu.Framework/Graphics/Colour/PremultipliedColour.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osuTK.Graphics; + +namespace osu.Framework.Graphics.Colour +{ + /// + /// Represents a provided in premultiplied-alpha form. + /// + public readonly struct PremultipliedColour : IEquatable + { + /// + /// The after alpha multiplication. + /// + public readonly Color4 Premultiplied; + + private PremultipliedColour(Color4 premultiplied) + { + Premultiplied = premultiplied; + } + + /// + /// Creates a from a straight-alpha colour. + /// + /// The straight-alpha colour. + public static PremultipliedColour FromStraight(Color4 colour) + { + colour.R *= colour.A; + colour.G *= colour.A; + colour.B *= colour.A; + return new PremultipliedColour(colour); + } + + /// + /// Creates a from a premultiplied-alpha colour. + /// + /// The premultiplied-alpha colour. + public static PremultipliedColour FromPremultiplied(Color4 colour) => new PremultipliedColour(colour); + + public bool Equals(PremultipliedColour other) => Premultiplied.Equals(other.Premultiplied); + } +} diff --git a/osu.Framework/Graphics/Colour4.cs b/osu.Framework/Graphics/Colour4.cs index 8809dbe71e..3a29283f86 100644 --- a/osu.Framework/Graphics/Colour4.cs +++ b/osu.Framework/Graphics/Colour4.cs @@ -6,6 +6,7 @@ using System; using System.Globalization; using System.Numerics; +using osu.Framework.Graphics.Colour; using osuTK.Graphics; namespace osu.Framework.Graphics @@ -547,6 +548,12 @@ public Vector4 ToHSL() return new Vector4(hue, saturation, lightness, A); } + /// + /// Converts this to premultiplied alpha and returns a resultant . + /// This colour is assumed to be in straight-alpha form. + /// + public PremultipliedColour ToPremultiplied() => PremultipliedColour.FromStraight(this); + private const double gamma = 2.4; private static float toLinear(float color) diff --git a/osu.Framework/Graphics/Lines/Path_DrawNode.cs b/osu.Framework/Graphics/Lines/Path_DrawNode.cs index 3ff49ec497..814b7c1151 100644 --- a/osu.Framework/Graphics/Lines/Path_DrawNode.cs +++ b/osu.Framework/Graphics/Lines/Path_DrawNode.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using System.Diagnostics; +using osu.Framework.Extensions.Color4Extensions; namespace osu.Framework.Graphics.Lines { @@ -105,19 +106,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = new Vector3(segmentRight.EndPoint.X, segmentRight.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.EndPoint) + Colour = colourAt(segmentRight.EndPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentRight.StartPoint.X, segmentRight.StartPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.StartPoint) + Colour = colourAt(segmentRight.StartPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); // Outer quad, triangle 2 @@ -125,19 +126,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = secondMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour + Colour = secondMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentRight.EndPoint.X, segmentRight.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentRight.EndPoint) + Colour = colourAt(segmentRight.EndPoint).ToPremultiplied() }); // Inner quad, triangle 1 @@ -145,19 +146,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = secondMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = secondMiddleColour + Colour = secondMiddleColour.ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentLeft.EndPoint.X, segmentLeft.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.EndPoint) + Colour = colourAt(segmentLeft.EndPoint).ToPremultiplied() }); // Inner quad, triangle 2 @@ -165,19 +166,19 @@ private void addSegmentQuads(Line segment, Line segmentLeft, Line segmentRight, { Position = new Vector3(segmentLeft.EndPoint.X, segmentLeft.EndPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.EndPoint) + Colour = colourAt(segmentLeft.EndPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = new Vector3(segmentLeft.StartPoint.X, segmentLeft.StartPoint.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = colourAt(segmentLeft.StartPoint) + Colour = colourAt(segmentLeft.StartPoint).ToPremultiplied() }); triangleBatch.Add(new TexturedVertex3D { Position = firstMiddlePoint, TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = firstMiddleColour + Colour = firstMiddleColour.ToPremultiplied() }); } @@ -214,7 +215,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(origin.X, origin.Y, 1), TexturePosition = new Vector2(texRect.Right, texRect.Centre.Y), - Colour = originColour + Colour = originColour.ToPremultiplied() }); // First outer point @@ -222,7 +223,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(current.X, current.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour + Colour = currentColour.ToPremultiplied() }); current = i < stepCount ? origin + pointOnCircle(theta0 + i * thetaStep) * radius : end; @@ -233,7 +234,7 @@ private void addSegmentCaps(float thetaDiff, Line segmentLeft, Line segmentRight { Position = new Vector3(current.X, current.Y, 0), TexturePosition = new Vector2(texRect.Left, texRect.Centre.Y), - Colour = currentColour + Colour = currentColour.ToPremultiplied() }); } } diff --git a/osu.Framework/Graphics/Rendering/RendererExtensions.cs b/osu.Framework/Graphics/Rendering/RendererExtensions.cs index 848db8f702..e1c758202c 100644 --- a/osu.Framework/Graphics/Rendering/RendererExtensions.cs +++ b/osu.Framework/Graphics/Rendering/RendererExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Primitives; @@ -70,7 +71,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = topColour.SRGB, + Colour = topColour.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -78,7 +79,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = drawColour.BottomLeft.SRGB, + Colour = drawColour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -86,7 +87,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2((inflatedCoordRect.Left + inflatedCoordRect.Right) / 2, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = bottomColour.SRGB, + Colour = bottomColour.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -94,7 +95,7 @@ public static void DrawTriangle(this IRenderer renderer, Texture texture, Triang TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = inflationAmount, - Colour = drawColour.BottomRight.SRGB, + Colour = drawColour.BottomRight.SRGB.ToPremultiplied(), }); long area = (long)vertexTriangle.Area; @@ -154,7 +155,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.BottomLeft.SRGB, + Colour = drawColour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -162,7 +163,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Bottom), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.BottomRight.SRGB, + Colour = drawColour.BottomRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -170,7 +171,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Right, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.TopRight.SRGB, + Colour = drawColour.TopRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -178,7 +179,7 @@ public static void DrawQuad(this IRenderer renderer, Texture texture, Quad verte TexturePosition = new Vector2(inflatedCoordRect.Left, inflatedCoordRect.Top), TextureRect = new Vector4(texRect.Left, texRect.Top, texRect.Right, texRect.Bottom), BlendRange = blendRange, - Colour = drawColour.TopLeft.SRGB, + Colour = drawColour.TopLeft.SRGB.ToPremultiplied(), }); long area = (long)vertexQuad.Area; diff --git a/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs index c352a2b949..7d2021eaeb 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/ParticleVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct ParticleVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs index 808d46314b..6a98358af7 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TexturedVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs index 34bcbac314..9834bbc9d9 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TexturedVertex3D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TexturedVertex3D : IEquatable, IVertex public Vector3 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs index 2ce3663f7f..851759f19f 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/TimedTexturedVertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct TimedTexturedVertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; [VertexMember(2, VertexAttribPointerType.Float)] public Vector2 TexturePosition; diff --git a/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs b/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs index d530fb2b47..8e9d95eb6c 100644 --- a/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs +++ b/osu.Framework/Graphics/Rendering/Vertices/Vertex2D.cs @@ -3,8 +3,8 @@ using System; using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; using osuTK; -using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Graphics.Rendering.Vertices @@ -16,7 +16,7 @@ public struct Vertex2D : IEquatable, IVertex public Vector2 Position; [VertexMember(4, VertexAttribPointerType.Float)] - public Color4 Colour; + public PremultipliedColour Colour; public readonly bool Equals(Vertex2D other) => Position.Equals(other.Position) && Colour.Equals(other.Colour); } diff --git a/osu.Framework/Graphics/Shapes/FastCircle.cs b/osu.Framework/Graphics/Shapes/FastCircle.cs index 660239ad45..ed976a30a5 100644 --- a/osu.Framework/Graphics/Shapes/FastCircle.cs +++ b/osu.Framework/Graphics/Shapes/FastCircle.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; @@ -97,7 +98,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(0, drawRectangle.W), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.BottomLeft.SRGB, + Colour = DrawColourInfo.Colour.BottomLeft.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -105,7 +106,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(drawRectangle.Z, drawRectangle.W), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.BottomRight.SRGB, + Colour = DrawColourInfo.Colour.BottomRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -113,7 +114,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = new Vector2(drawRectangle.Z, 0), TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.TopRight.SRGB, + Colour = DrawColourInfo.Colour.TopRight.SRGB.ToPremultiplied(), }); vertexAction(new TexturedVertex2D(renderer) { @@ -121,7 +122,7 @@ protected override void Draw(IRenderer renderer) TexturePosition = Vector2.Zero, TextureRect = drawRectangle, BlendRange = blend, - Colour = DrawColourInfo.Colour.TopLeft.SRGB, + Colour = DrawColourInfo.Colour.TopLeft.SRGB.ToPremultiplied(), }); shader.Unbind(); diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs index 3d550bf465..3f69a29e3f 100644 --- a/osu.Framework/Graphics/Textures/PremultipliedImage.cs +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -26,8 +26,8 @@ public PremultipliedImage(int width, int height) { } - public PremultipliedImage(int width, int height, SRGBColour colour) // todo: colour will be called PremultipliedColour - : this(new Image(width, height, new Rgba32(colour.SRGB.R, colour.SRGB.G, colour.SRGB.B, colour.SRGB.A))) + public PremultipliedImage(int width, int height, PremultipliedColour colour) + : this(new Image(width, height, new Rgba32(colour.Premultiplied.R, colour.Premultiplied.G, colour.Premultiplied.B, colour.Premultiplied.A))) { } diff --git a/osu.Framework/Graphics/Textures/TextureAtlas.cs b/osu.Framework/Graphics/Textures/TextureAtlas.cs index 6f7264fa99..149bd7fc4f 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas.cs @@ -81,7 +81,7 @@ public void Reset() using (var whiteTex = new TextureRegion(atlasTexture, bounds, WrapMode.Repeat, WrapMode.Repeat)) // Generate white padding as if the white texture was wrapped, even though it isn't - whiteTex.SetData(new TextureUpload(new PremultipliedImage(whiteTex.Width, whiteTex.Height, Colour4.White))); + whiteTex.SetData(new TextureUpload(new PremultipliedImage(whiteTex.Width, whiteTex.Height, Colour4.White.ToPremultiplied()))); currentPosition = new Vector2I(PADDING + WHITE_PIXEL_SIZE, PADDING); } From 21f500d65848570fd72567f0a55fa1f8d6b05f13 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Dec 2024 14:58:40 -0500 Subject: [PATCH 08/19] Intrdouce `UniformColour` for premultiplied colours in uniforms Also adds `UniformColourInfo` for border colour uniform, instead of the gigantic definition that was in `Renderer`. --- .../TestSceneShaderStorageBufferObject.cs | 8 +++-- .../Graphics/Rendering/GlobalUniformData.cs | 2 +- osu.Framework/Graphics/Rendering/Renderer.cs | 30 ++++------------- .../Graphics/Shaders/Types/UniformColour.cs | 32 +++++++++++++++++++ .../Shaders/Types/UniformColourInfo.cs | 30 +++++++++++++++++ 5 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 osu.Framework/Graphics/Shaders/Types/UniformColour.cs create mode 100644 osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs diff --git a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs index 6d0b7693a3..4c1681a37c 100644 --- a/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs +++ b/osu.Framework.Tests/Visual/Graphics/TestSceneShaderStorageBufferObject.cs @@ -7,12 +7,14 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders.Types; using osuTK; +using osuTK.Graphics; using osuTK.Graphics.ES30; namespace osu.Framework.Tests.Visual.Graphics @@ -116,7 +118,7 @@ protected override void Draw(IRenderer renderer) var rng = new Random(1337); for (int i = 0; i < colourBuffer.Size; i++) - colourBuffer[i] = new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }; + colourBuffer[i] = new ColourData { Colour = PremultipliedColour.FromStraight(new Color4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1)) }; } // Bind the custom shader and SSBO. @@ -212,7 +214,7 @@ protected override void Draw(IRenderer renderer) for (int i = 0; i < areas.Count; i++) { - int colourIndex = colourBuffer.Push(new ColourData { Colour = new Vector4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1) }); + int colourIndex = colourBuffer.Push(new ColourData { Colour = PremultipliedColour.FromStraight(new Color4(rng.NextSingle(), rng.NextSingle(), rng.NextSingle(), 1)) }); // Bind the SSBO. This may change between iterations if a buffer transition happens via the above push. shader.BindUniformBlock("g_ColourBuffer", colourBuffer.CurrentBuffer); @@ -260,7 +262,7 @@ protected override void Dispose(bool isDisposing) [StructLayout(LayoutKind.Sequential, Pack = 1)] private record struct ColourData { - public UniformVector4 Colour; + public UniformColour Colour; } [StructLayout(LayoutKind.Sequential)] diff --git a/osu.Framework/Graphics/Rendering/GlobalUniformData.cs b/osu.Framework/Graphics/Rendering/GlobalUniformData.cs index 51ba8b7e13..aa1ccec0ad 100644 --- a/osu.Framework/Graphics/Rendering/GlobalUniformData.cs +++ b/osu.Framework/Graphics/Rendering/GlobalUniformData.cs @@ -26,7 +26,7 @@ public record struct GlobalUniformData public UniformFloat BorderThickness; private readonly UniformPadding12 pad3; - public UniformMatrix4 BorderColour; + public UniformColourInfo BorderColour; public UniformFloat MaskingBlendRange; public UniformFloat AlphaExponent; public UniformVector2 EdgeOffset; diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 01a8a1f39a..8759b6ae31 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -975,27 +975,7 @@ public void DrawVertices(PrimitiveTopology topology, int vertexStart, int vertic currentMaskingInfo.MaskingRect.Bottom), BorderThickness = currentMaskingInfo.BorderThickness / currentMaskingInfo.BlendRange, BorderColour = currentMaskingInfo.BorderThickness > 0 - ? new Matrix4( - // TopLeft - currentMaskingInfo.BorderColour.TopLeft.SRGB.R, - currentMaskingInfo.BorderColour.TopLeft.SRGB.G, - currentMaskingInfo.BorderColour.TopLeft.SRGB.B, - currentMaskingInfo.BorderColour.TopLeft.SRGB.A, - // BottomLeft - currentMaskingInfo.BorderColour.BottomLeft.SRGB.R, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.G, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.B, - currentMaskingInfo.BorderColour.BottomLeft.SRGB.A, - // TopRight - currentMaskingInfo.BorderColour.TopRight.SRGB.R, - currentMaskingInfo.BorderColour.TopRight.SRGB.G, - currentMaskingInfo.BorderColour.TopRight.SRGB.B, - currentMaskingInfo.BorderColour.TopRight.SRGB.A, - // BottomRight - currentMaskingInfo.BorderColour.BottomRight.SRGB.R, - currentMaskingInfo.BorderColour.BottomRight.SRGB.G, - currentMaskingInfo.BorderColour.BottomRight.SRGB.B, - currentMaskingInfo.BorderColour.BottomRight.SRGB.A) + ? currentMaskingInfo.BorderColour : globalUniformBuffer.Data.BorderColour, MaskingBlendRange = currentMaskingInfo.BlendRange, AlphaExponent = currentMaskingInfo.AlphaExponent, @@ -1281,7 +1261,9 @@ private void validateUniformLayout() if (field.FieldType == typeof(UniformMatrix3) || field.FieldType == typeof(UniformMatrix4) || field.FieldType == typeof(UniformVector3) - || field.FieldType == typeof(UniformVector4)) + || field.FieldType == typeof(UniformVector4) + || field.FieldType == typeof(UniformColour) + || field.FieldType == typeof(UniformColourInfo)) { checkAlignment(field, offset, 16); } @@ -1310,8 +1292,10 @@ static void checkValidType(FieldInfo field) || field.FieldType == typeof(UniformPadding8) || field.FieldType == typeof(UniformPadding12) || field.FieldType == typeof(UniformVector2) + || field.FieldType == typeof(UniformVector3) || field.FieldType == typeof(UniformVector4) - || field.FieldType == typeof(UniformVector4)) + || field.FieldType == typeof(UniformColour) + || field.FieldType == typeof(UniformColourInfo)) { return; } diff --git a/osu.Framework/Graphics/Shaders/Types/UniformColour.cs b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs new file mode 100644 index 0000000000..c03ad9f902 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs @@ -0,0 +1,32 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; +using osu.Framework.Graphics.Colour; +using osuTK.Graphics; + +namespace osu.Framework.Graphics.Shaders.Types +{ + /// + /// Must be aligned to a 16-byte boundary. Enforces colours to be in premultiplied-alpha form. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 16)] + public record struct UniformColour + { + public UniformVector4 Value; + + public static implicit operator PremultipliedColour(UniformColour value) + => PremultipliedColour.FromPremultiplied(new Color4(value.Value.X, value.Value.Y, value.Value.Z, value.Value.W)); + + public static implicit operator UniformColour(PremultipliedColour value) => new UniformColour + { + Value = + { + X = value.Premultiplied.R, + Y = value.Premultiplied.G, + Z = value.Premultiplied.B, + W = value.Premultiplied.A, + } + }; + } +} diff --git a/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs b/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs new file mode 100644 index 0000000000..27a639b9f8 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/Types/UniformColourInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Runtime.InteropServices; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics.Colour; + +namespace osu.Framework.Graphics.Shaders.Types +{ + /// + /// Must be aligned to a 16-byte boundary. Wraps in a structure + /// with the rows containing top-left, bottom-left, top-right, bottom-right respectively. Alpha pre-multiplication is applied. + /// + [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 64)] + public record struct UniformColourInfo + { + public UniformMatrix4 Value; + + public static implicit operator UniformColourInfo(ColourInfo value) => new UniformColourInfo + { + Value = + { + Row0 = ((UniformColour)value.TopLeft.SRGB.ToPremultiplied()).Value, + Row1 = ((UniformColour)value.BottomLeft.SRGB.ToPremultiplied()).Value, + Row2 = ((UniformColour)value.TopRight.SRGB.ToPremultiplied()).Value, + Row3 = ((UniformColour)value.BottomRight.SRGB.ToPremultiplied()).Value, + } + }; + } +} From 5c9e39d7a1ec29c16cd6d8e057ea51e39bd5c30c Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 08:17:27 -0500 Subject: [PATCH 09/19] Update renderer clear info to use `PremultipliedColour` --- osu.Framework/Graphics/BufferedDrawNode.cs | 3 ++- osu.Framework/Graphics/OpenGL/GLRenderer.cs | 4 ++-- osu.Framework/Graphics/Rendering/ClearInfo.cs | 6 +++--- osu.Framework/Graphics/Rendering/Renderer.cs | 3 ++- .../Graphics/Veldrid/Pipelines/GraphicsPipeline.cs | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/osu.Framework/Graphics/BufferedDrawNode.cs b/osu.Framework/Graphics/BufferedDrawNode.cs index f250b96411..7de61d5c41 100644 --- a/osu.Framework/Graphics/BufferedDrawNode.cs +++ b/osu.Framework/Graphics/BufferedDrawNode.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering; using osu.Framework.Statistics; @@ -99,7 +100,7 @@ protected sealed override void Draw(IRenderer renderer) // We need to draw children as if they were zero-based to the top-left of the texture. // We can do this by adding a translation component to our (orthogonal) projection matrix. renderer.PushOrtho(screenSpaceDrawRectangle); - renderer.Clear(new ClearInfo(backgroundColour)); + renderer.Clear(new ClearInfo(backgroundColour.ToPremultiplied())); DrawOther(Child, renderer); diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 338cff27f3..a887e1198e 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -238,8 +238,8 @@ protected override void DeleteFrameBufferImplementation(IFrameBuffer frameBuffer protected override void ClearImplementation(ClearInfo clearInfo) { - if (clearInfo.Colour != CurrentClearInfo.Colour) - GL.ClearColor(clearInfo.Colour); + if (!clearInfo.Colour.Equals(CurrentClearInfo.Colour)) + GL.ClearColor(clearInfo.Colour.Premultiplied); if (clearInfo.Depth != CurrentClearInfo.Depth) { diff --git a/osu.Framework/Graphics/Rendering/ClearInfo.cs b/osu.Framework/Graphics/Rendering/ClearInfo.cs index c9f19ba327..c94f91ff7f 100644 --- a/osu.Framework/Graphics/Rendering/ClearInfo.cs +++ b/osu.Framework/Graphics/Rendering/ClearInfo.cs @@ -1,7 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osuTK.Graphics; +using osu.Framework.Graphics.Colour; namespace osu.Framework.Graphics.Rendering { @@ -18,7 +18,7 @@ public readonly struct ClearInfo /// /// The colour to write to the frame buffer. /// - public readonly Color4 Colour; + public readonly PremultipliedColour Colour; /// /// The depth to write to the frame buffer. @@ -30,7 +30,7 @@ public readonly struct ClearInfo /// public readonly int Stencil; - public ClearInfo(Color4 colour = default, double depth = 1f, int stencil = 0) + public ClearInfo(PremultipliedColour colour = default, double depth = 1f, int stencil = 0) { Colour = colour; Depth = depth; diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index 8759b6ae31..98ce4ee9ad 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Runtime.InteropServices; using osu.Framework.Development; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Rendering.Vertices; @@ -265,7 +266,7 @@ protected internal virtual void BeginFrame(Vector2 windowSize) PushDepthInfo(DepthInfo.Default); PushStencilInfo(StencilInfo.Default); - Clear(new ClearInfo(Color4.Black)); + Clear(new ClearInfo(Color4.Black.ToPremultiplied())); freeUnusedVertexBuffers(); vboInUse.Value = vertexBuffersInUse.Count; diff --git a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs index 76ff428298..931613f404 100644 --- a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs +++ b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs @@ -63,7 +63,7 @@ public override void Begin() /// The clearing parameters. public void Clear(ClearInfo clearInfo) { - Commands.ClearColorTarget(0, clearInfo.Colour.ToRgbaFloat()); + Commands.ClearColorTarget(0, clearInfo.Colour.Premultiplied.ToRgbaFloat()); var framebuffer = currentFrameBuffer?.Framebuffer ?? Device.SwapchainFramebuffer; if (framebuffer.DepthTarget != null) From f8f3b3105db1d7688dcc6eee71456b960bdf83e6 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Fri, 13 Dec 2024 07:38:36 -0500 Subject: [PATCH 10/19] Update all shaders to interpret colours as premultiplied --- osu.Framework/Resources/Shaders/sh_CircularBlob.fs | 2 +- osu.Framework/Resources/Shaders/sh_CircularProgress.fs | 2 +- osu.Framework/Resources/Shaders/sh_FastCircle.fs | 2 +- osu.Framework/Resources/Shaders/sh_Masking.h | 8 ++++---- osu.Framework/Resources/Shaders/sh_Particle.vs | 2 +- osu.Framework/Resources/Shaders/sh_Utils.h | 10 +--------- 6 files changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Framework/Resources/Shaders/sh_CircularBlob.fs b/osu.Framework/Resources/Shaders/sh_CircularBlob.fs index fa5f027085..a4899f4c96 100644 --- a/osu.Framework/Resources/Shaders/sh_CircularBlob.fs +++ b/osu.Framework/Resources/Shaders/sh_CircularBlob.fs @@ -33,7 +33,7 @@ void main(void) highp vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); lowp vec4 textureColour = getRoundedColor(wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9), wrappedCoord); - o_Colour = vec4(textureColour.rgb, textureColour.a * blobAlphaAt(pixelPos, innerRadius, texelSize, frequency, amplitude, noisePosition)); + o_Colour = textureColour.rgba * blobAlphaAt(pixelPos, innerRadius, texelSize, frequency, amplitude, noisePosition); } #endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_CircularProgress.fs b/osu.Framework/Resources/Shaders/sh_CircularProgress.fs index 559a064831..bf5f4b8a10 100644 --- a/osu.Framework/Resources/Shaders/sh_CircularProgress.fs +++ b/osu.Framework/Resources/Shaders/sh_CircularProgress.fs @@ -32,7 +32,7 @@ void main(void) highp vec2 wrappedCoord = wrap(v_TexCoord, v_TexRect); lowp vec4 textureColour = getRoundedColor(wrappedSampler(wrappedCoord, v_TexRect, m_Texture, m_Sampler, -0.9), wrappedCoord); - o_Colour = vec4(textureColour.rgb, textureColour.a * progressAlphaAt(pixelPos, progress, innerRadius, roundedCaps, texelSize)); + o_Colour = textureColour.rgba * progressAlphaAt(pixelPos, progress, innerRadius, roundedCaps, texelSize); } #endif \ No newline at end of file diff --git a/osu.Framework/Resources/Shaders/sh_FastCircle.fs b/osu.Framework/Resources/Shaders/sh_FastCircle.fs index 307f00a74c..be60600f41 100644 --- a/osu.Framework/Resources/Shaders/sh_FastCircle.fs +++ b/osu.Framework/Resources/Shaders/sh_FastCircle.fs @@ -20,7 +20,7 @@ void main(void) highp float alpha = v_BlendRange.x == 0.0 ? float(dst < radius) : (clamp(radius - dst, 0.0, v_BlendRange.x) / v_BlendRange.x); - o_Colour = getRoundedColor(vec4(vec3(1.0), alpha), vec2(0.0)); + o_Colour = getRoundedColor(vec4(alpha), vec2(0.0)); } #endif diff --git a/osu.Framework/Resources/Shaders/sh_Masking.h b/osu.Framework/Resources/Shaders/sh_Masking.h index 3fdc6c8846..e9e0223052 100644 --- a/osu.Framework/Resources/Shaders/sh_Masking.h +++ b/osu.Framework/Resources/Shaders/sh_Masking.h @@ -116,15 +116,15 @@ lowp vec4 getRoundedColor(lowp vec4 texel, mediump vec2 texCoord) lowp vec4 contentColour = v_Colour * texel; if (colourWeight == 1.0) - return vec4(contentColour.rgb, contentColour.a * alphaFactor); + return contentColour.rgba * alphaFactor; lowp vec4 borderColour = getBorderColour(); if (colourWeight <= 0.0) - return vec4(borderColour.rgb, borderColour.a * alphaFactor); + return borderColour.rgba * alphaFactor; - contentColour.a *= alphaFactor; - borderColour.a *= 1.0 - colourWeight; + contentColour.rgba *= alphaFactor; + borderColour.rgba *= 1.0 - colourWeight; return blend(borderColour, contentColour); } diff --git a/osu.Framework/Resources/Shaders/sh_Particle.vs b/osu.Framework/Resources/Shaders/sh_Particle.vs index 8da3d9ed64..9a8781f19e 100644 --- a/osu.Framework/Resources/Shaders/sh_Particle.vs +++ b/osu.Framework/Resources/Shaders/sh_Particle.vs @@ -23,7 +23,7 @@ void main(void) vec2(0.0, g_Gravity * g_FadeClock * g_FadeClock / 1000000.0); gl_Position = g_ProjMatrix * vec4(targetPosition, 1.0, 1.0); - v_Colour = vec4(1.0, 1.0, 1.0, 1.0 - clamp(g_FadeClock / m_Time, 0.0, 1.0)); + v_Colour = vec4(1.0 - clamp(g_FadeClock / m_Time, 0.0, 1.0)); v_TexCoord = m_TexCoord; } diff --git a/osu.Framework/Resources/Shaders/sh_Utils.h b/osu.Framework/Resources/Shaders/sh_Utils.h index 6e2e010c25..ffeb972298 100644 --- a/osu.Framework/Resources/Shaders/sh_Utils.h +++ b/osu.Framework/Resources/Shaders/sh_Utils.h @@ -8,15 +8,7 @@ // see http://apoorvaj.io/alpha-compositing-opengl-blending-and-premultiplied-alpha.html lowp vec4 blend(lowp vec4 src, lowp vec4 dst) { - lowp float finalAlpha = src.a + dst.a * (1.0 - src.a); - - if (finalAlpha == 0.0) - return vec4(0); - - return vec4( - (src.rgb * src.a + dst.rgb * dst.a * (1.0 - src.a)) / finalAlpha, - finalAlpha - ); + return src.rgba + dst.rgba * (1.0 - src.a); } // http://lolengine.net/blog/2013/07/27/rgb-to-hsv-in-glsl From 6f10e23e151b43483bf3b32cc3cbca5696fbba14 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sat, 14 Dec 2024 16:44:52 -0500 Subject: [PATCH 11/19] Update android texture loader store --- .../Graphics/Textures/AndroidTextureLoaderStore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs b/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs index 1f1730db4c..29611e72f0 100644 --- a/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs +++ b/osu.Framework.Android/Graphics/Textures/AndroidTextureLoaderStore.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Android.Graphics.Textures { @@ -17,7 +18,7 @@ public AndroidTextureLoaderStore(IResourceStore store) { } - protected override Image ImageFromStream(Stream stream) + protected override PremultipliedImage ImageFromStream(Stream stream) { using (var bitmap = BitmapFactory.DecodeStream(stream)) { @@ -34,7 +35,7 @@ protected override Image ImageFromStream(Stream stream) } bitmap.Recycle(); - return Image.LoadPixelData(result, bitmap.Width, bitmap.Height); + return PremultipliedImage.FromPremultiplied(Image.LoadPixelData(result, bitmap.Width, bitmap.Height)); } } } From 8a4abb37bc885a212f3e9bed220eca0955f3d4e4 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Wed, 18 Dec 2024 13:17:00 -0500 Subject: [PATCH 12/19] Fix `SRGBColour` not clamping alpha on multiplication Fixes the volume meters issue in https://github.com/ppy/osu/pull/31129#issue-2740154976 --- osu.Framework/Graphics/Colour/SRGBColour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/Colour/SRGBColour.cs b/osu.Framework/Graphics/Colour/SRGBColour.cs index d5c994200e..9b6aee1896 100644 --- a/osu.Framework/Graphics/Colour/SRGBColour.cs +++ b/osu.Framework/Graphics/Colour/SRGBColour.cs @@ -123,7 +123,7 @@ public struct SRGBColour : IEquatable /// Multiplies the alpha value of this colour by the given alpha factor. /// /// The alpha factor to multiply with. - public void MultiplyAlpha(float alpha) => SRGB.A *= alpha; + public void MultiplyAlpha(float alpha) => SRGB.A = Math.Min(1, SRGB.A * alpha); private static bool isWhite(SRGBColour colour) => colour.SRGB.R == 1 && colour.SRGB.G == 1 && colour.SRGB.B == 1; From 1ff2b65fd3ea98190fb8ca8d329663d8179edb2d Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 19 Dec 2024 12:00:48 -0500 Subject: [PATCH 13/19] Add test scene for blending correctness --- .../Textures/figma-blending-additive.png | Bin 0 -> 14541 bytes .../Textures/figma-blending-mixture.png | Bin 0 -> 14293 bytes .../Drawables/TestSceneBlendingCorrectness.cs | 363 ++++++++++++++++++ 3 files changed, 363 insertions(+) create mode 100644 osu.Framework.Tests/Resources/Textures/figma-blending-additive.png create mode 100644 osu.Framework.Tests/Resources/Textures/figma-blending-mixture.png create mode 100644 osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs diff --git a/osu.Framework.Tests/Resources/Textures/figma-blending-additive.png b/osu.Framework.Tests/Resources/Textures/figma-blending-additive.png new file mode 100644 index 0000000000000000000000000000000000000000..308ebd3a3d08b995c540d9b8c8d1a66425efdd8a GIT binary patch literal 14541 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. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; +using osuTK.Graphics; + +namespace osu.Framework.Tests.Visual.Drawables +{ + public partial class TestSceneBlendingCorrectness : FrameworkTestScene + { + [Resolved] + private TextureStore textures { get; set; } = null!; + + [Test] + public void TestMixture() + { + AddStep("setup", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20f), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f + } + } + } + } + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f, + } + } + } + } + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Container", + X = -150 + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Buffered", + X = 150 + }, + } + }, + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("figma-blending-mixture") + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Figma design", + }, + } + }; + }); + } + + [Test] + public void TestAdditive() + { + AddStep("setup", () => + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20f), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreRight, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f + } + } + } + } + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.CentreLeft, + Size = new Vector2(100, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Blue) + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Colour = Color4.Cyan, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Colour = Color4.Magenta, + Alpha = 0.5f, + } + } + } + } + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Container", + X = -150 + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Buffered", + X = 150 + }, + } + }, + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get("figma-blending-additive") + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Figma design", + }, + } + }; + }); + } + + ///

+ /// There is no reference source for this one. This can be used to test whether or is more correct + /// to use as the blending type for . + /// + [Test] + public void TestAdditiveAlpha() + { + AddStep("setup", () => + { + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 200), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Blue, + Alpha = 0.5f, + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.25f, + } + } + } + } + } + }; + + AddRange(new[] + { + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Container", + X = -150 + }, + new SpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Buffered", + X = 150 + }, + }); + }); + } + } +} From dc00efedc26e2b388ddb36dd73ff6e78cf55fa3e Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 19 Dec 2024 12:01:41 -0500 Subject: [PATCH 14/19] Fix mixture blending not blending alpha Read https://github.com/ppy/osu-framework/pull/6456#discussion_r1891381206 --- osu.Framework/Graphics/BlendingParameters.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Framework/Graphics/BlendingParameters.cs b/osu.Framework/Graphics/BlendingParameters.cs index c1542b3f60..533d216ceb 100644 --- a/osu.Framework/Graphics/BlendingParameters.cs +++ b/osu.Framework/Graphics/BlendingParameters.cs @@ -75,7 +75,7 @@ public struct BlendingParameters : IEquatable Source = BlendingType.One, Destination = BlendingType.OneMinusSrcAlpha, SourceAlpha = BlendingType.One, - DestinationAlpha = BlendingType.One, + DestinationAlpha = BlendingType.OneMinusSrcAlpha, RGBEquation = BlendingEquation.Add, AlphaEquation = BlendingEquation.Add, }; @@ -132,7 +132,7 @@ public void ApplyDefaultToInherited() SourceAlpha = BlendingType.One; if (DestinationAlpha == BlendingType.Inherit) - DestinationAlpha = BlendingType.One; + DestinationAlpha = BlendingType.OneMinusSrcAlpha; if (RGBEquation == BlendingEquation.Inherit) RGBEquation = BlendingEquation.Add; From 7be757a810ceabb2fb9eda1fceefd9631a8e11a2 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Thu, 19 Dec 2024 12:16:40 -0500 Subject: [PATCH 15/19] Fix last test case --- .../Drawables/TestSceneBlendingCorrectness.cs | 85 +++++++------------ 1 file changed, 32 insertions(+), 53 deletions(-) diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs index fc8a7975a0..2f97dc6235 100644 --- a/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs +++ b/osu.Framework.Tests/Visual/Drawables/TestSceneBlendingCorrectness.cs @@ -294,69 +294,48 @@ public void TestAdditiveAlpha() { AddStep("setup", () => { - Children = new Drawable[] + Child = new Container { - new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 200), + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(200, 200), - Children = new Drawable[] + new Box { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Blue, - Alpha = 0.5f, - }, - new BufferedContainer + RelativeSizeAxes = Axes.Both, + Colour = Color4.Blue, + Alpha = 0.5f, + }, + new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + new Box { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 1), - Blending = BlendingParameters.Additive, - Colour = Color4.White, - Alpha = 0.5f, - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.25f, 1), - Blending = BlendingParameters.Additive, - Colour = Color4.White, - Alpha = 0.25f, - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.5f, + }, + new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.25f, 1), + Blending = BlendingParameters.Additive, + Colour = Color4.White, + Alpha = 0.25f, } } } } }; - - AddRange(new[] - { - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Container", - X = -150 - }, - new SpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Buffered", - X = 150 - }, - }); }); } } From 39c888b28165a5c87fd9bafa43b90b02ef5396bc Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 08:18:18 -0500 Subject: [PATCH 16/19] Decompose `PremultipliedColour` structure into RGBA fields --- .../Graphics/Colour/PremultipliedColour.cs | 56 +++++++++++++------ osu.Framework/Graphics/OpenGL/GLRenderer.cs | 2 +- .../Graphics/Shaders/Types/UniformColour.cs | 11 ++-- .../Graphics/Textures/PremultipliedImage.cs | 2 +- .../Veldrid/Pipelines/GraphicsPipeline.cs | 2 +- .../Graphics/Veldrid/VeldridExtensions.cs | 5 +- 6 files changed, 49 insertions(+), 29 deletions(-) diff --git a/osu.Framework/Graphics/Colour/PremultipliedColour.cs b/osu.Framework/Graphics/Colour/PremultipliedColour.cs index a17936143f..abc93a0871 100644 --- a/osu.Framework/Graphics/Colour/PremultipliedColour.cs +++ b/osu.Framework/Graphics/Colour/PremultipliedColour.cs @@ -3,42 +3,62 @@ using System; using osuTK.Graphics; +using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Colour { /// - /// Represents a provided in premultiplied-alpha form. + /// Represents a structure for containing a "premultiplied colour". /// public readonly struct PremultipliedColour : IEquatable { /// - /// The after alpha multiplication. + /// The red component of this colour multiplied by the value of . /// - public readonly Color4 Premultiplied; + public readonly float PremultipliedR; - private PremultipliedColour(Color4 premultiplied) - { - Premultiplied = premultiplied; - } + /// + /// The green component of this colour multiplied by the value of . + /// + public readonly float PremultipliedG; /// - /// Creates a from a straight-alpha colour. + /// The blue component of this colour multiplied by the value of . /// - /// The straight-alpha colour. - public static PremultipliedColour FromStraight(Color4 colour) + public readonly float PremultipliedB; + + /// + /// The alpha component of this colour, often referred to as "occlusion" instead of "opacity" in the context of premultiplied colours. + /// + public readonly float Occlusion; + + public PremultipliedColour(float premultipliedR, float premultipliedG, float premultipliedB, float occlusion) { - colour.R *= colour.A; - colour.G *= colour.A; - colour.B *= colour.A; - return new PremultipliedColour(colour); + PremultipliedR = premultipliedR; + PremultipliedG = premultipliedG; + PremultipliedB = premultipliedB; + Occlusion = occlusion; } /// - /// Creates a from a premultiplied-alpha colour. + /// Creates a containing the premultiplied components of this colour. /// - /// The premultiplied-alpha colour. - public static PremultipliedColour FromPremultiplied(Color4 colour) => new PremultipliedColour(colour); + public Rgba32 ToPremultipliedRgba32() => new Rgba32(PremultipliedR, PremultipliedG, PremultipliedB, Occlusion); + + /// + /// Creates a from a straight-alpha colour. + /// + /// The straight-alpha colour. + public static PremultipliedColour FromStraight(Color4 colour) => new PremultipliedColour( + colour.R * colour.A, + colour.G * colour.A, + colour.B * colour.A, + colour.A); - public bool Equals(PremultipliedColour other) => Premultiplied.Equals(other.Premultiplied); + public bool Equals(PremultipliedColour other) + => PremultipliedR == other.PremultipliedR && + PremultipliedG == other.PremultipliedG && + PremultipliedB == other.PremultipliedB && + Occlusion == other.Occlusion; } } diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index a887e1198e..8af0e879f9 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -239,7 +239,7 @@ protected override void DeleteFrameBufferImplementation(IFrameBuffer frameBuffer protected override void ClearImplementation(ClearInfo clearInfo) { if (!clearInfo.Colour.Equals(CurrentClearInfo.Colour)) - GL.ClearColor(clearInfo.Colour.Premultiplied); + GL.ClearColor(new Color4(clearInfo.Colour.PremultipliedR, clearInfo.Colour.PremultipliedG, clearInfo.Colour.PremultipliedB, clearInfo.Colour.Occlusion)); if (clearInfo.Depth != CurrentClearInfo.Depth) { diff --git a/osu.Framework/Graphics/Shaders/Types/UniformColour.cs b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs index c03ad9f902..f666bfed2b 100644 --- a/osu.Framework/Graphics/Shaders/Types/UniformColour.cs +++ b/osu.Framework/Graphics/Shaders/Types/UniformColour.cs @@ -3,7 +3,6 @@ using System.Runtime.InteropServices; using osu.Framework.Graphics.Colour; -using osuTK.Graphics; namespace osu.Framework.Graphics.Shaders.Types { @@ -16,16 +15,16 @@ public record struct UniformColour public UniformVector4 Value; public static implicit operator PremultipliedColour(UniformColour value) - => PremultipliedColour.FromPremultiplied(new Color4(value.Value.X, value.Value.Y, value.Value.Z, value.Value.W)); + => new PremultipliedColour(value.Value.X, value.Value.Y, value.Value.Z, value.Value.W); public static implicit operator UniformColour(PremultipliedColour value) => new UniformColour { Value = { - X = value.Premultiplied.R, - Y = value.Premultiplied.G, - Z = value.Premultiplied.B, - W = value.Premultiplied.A, + X = value.PremultipliedR, + Y = value.PremultipliedG, + Z = value.PremultipliedB, + W = value.Occlusion, } }; } diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs index 3f69a29e3f..94437c8448 100644 --- a/osu.Framework/Graphics/Textures/PremultipliedImage.cs +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -27,7 +27,7 @@ public PremultipliedImage(int width, int height) } public PremultipliedImage(int width, int height, PremultipliedColour colour) - : this(new Image(width, height, new Rgba32(colour.Premultiplied.R, colour.Premultiplied.G, colour.Premultiplied.B, colour.Premultiplied.A))) + : this(new Image(width, height, colour.ToPremultipliedRgba32())) { } diff --git a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs index 931613f404..fafc082667 100644 --- a/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs +++ b/osu.Framework/Graphics/Veldrid/Pipelines/GraphicsPipeline.cs @@ -63,7 +63,7 @@ public override void Begin() /// The clearing parameters. public void Clear(ClearInfo clearInfo) { - Commands.ClearColorTarget(0, clearInfo.Colour.Premultiplied.ToRgbaFloat()); + Commands.ClearColorTarget(0, clearInfo.Colour.ToPremultipliedRgbaFloat()); var framebuffer = currentFrameBuffer?.Framebuffer ?? Device.SwapchainFramebuffer; if (framebuffer.DepthTarget != null) diff --git a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs index 272452a14d..ec572ed281 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridExtensions.cs @@ -7,10 +7,10 @@ using System.Runtime.InteropServices; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; -using osuTK.Graphics; using SharpGen.Runtime; using Veldrid; using Veldrid.MetalBindings; @@ -27,7 +27,8 @@ namespace osu.Framework.Graphics.Veldrid { internal static class VeldridExtensions { - public static RgbaFloat ToRgbaFloat(this Color4 colour) => new RgbaFloat(colour.R, colour.G, colour.B, colour.A); + public static RgbaFloat ToPremultipliedRgbaFloat(this PremultipliedColour colour) + => new RgbaFloat(colour.PremultipliedR, colour.PremultipliedG, colour.PremultipliedB, colour.Occlusion); public static BlendFactor ToBlendFactor(this BlendingType type) { From df532384f126c67a718c0d29b77fdf00f8e8fa6f Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 08:45:19 -0500 Subject: [PATCH 17/19] Add indexer for image pixels that accepts `PremultipliedColour`s --- .../Visual/Input/TestSceneInputResampler.cs | 4 ++-- .../Visual/UserInterface/TestSceneCircularBlob.cs | 10 +++++----- .../UserInterface/TestSceneCircularProgress.cs | 10 +++++----- .../Visual/UserInterface/TestSceneDrawablePath.cs | 4 ++-- .../Graphics/Colour/PremultipliedColour.cs | 10 ++++++++++ osu.Framework/Graphics/Lines/SmoothPath.cs | 4 +--- .../Graphics/Textures/PremultipliedImage.cs | 13 ++++++++++++- 7 files changed, 37 insertions(+), 18 deletions(-) diff --git a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs index cbace0425d..af38b62dfb 100644 --- a/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs +++ b/osu.Framework.Tests/Visual/Input/TestSceneInputResampler.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Rendering; @@ -15,7 +16,6 @@ using osu.Framework.Testing; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.Input { @@ -37,7 +37,7 @@ private void load(IRenderer renderer) for (int i = 0; i < width; ++i) { byte brightnessByte = (byte)((float)i / (width - 1) * 255); - image.Premultiplied[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte); + image[i, 0] = new Color4(brightnessByte, brightnessByte, brightnessByte, 255).ToPremultiplied(); } gradientTexture.SetData(new TextureUpload(image)); diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs index 07898065c8..7e39e6229a 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularBlob.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -11,7 +12,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -38,7 +38,7 @@ private void load() for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureHorizontal.SetData(new TextureUpload(image)); @@ -50,7 +50,7 @@ private void load() for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureVertical.SetData(new TextureUpload(image)); @@ -65,11 +65,11 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image.Premultiplied[i, j] = new Rgba32( + image[i, j] = new Color4( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), - 255); + 255).ToPremultiplied(); } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs index 19170c7929..0503cfaa53 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneCircularProgress.cs @@ -4,6 +4,7 @@ #nullable disable using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -13,7 +14,6 @@ using osu.Framework.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -44,7 +44,7 @@ private void load() for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureHorizontal.SetData(new TextureUpload(image)); @@ -56,7 +56,7 @@ private void load() for (int i = 0; i < width; ++i) { float brightness = (float)i / (width - 1); - image.Premultiplied[i, 0] = new Rgba32((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255); + image[i, 0] = new Color4((byte)(128 + (1 - brightness) * 127), (byte)(128 + brightness * 127), 128, 255).ToPremultiplied(); } gradientTextureVertical.SetData(new TextureUpload(image)); @@ -71,11 +71,11 @@ private void load() { float brightness = (float)i / (width - 1); float brightness2 = (float)j / (width - 1); - image.Premultiplied[i, j] = new Rgba32( + image[i, j] = new Color4( (byte)(128 + (1 + brightness - brightness2) / 2 * 127), (byte)(128 + (1 + brightness2 - brightness) / 2 * 127), (byte)(128 + (brightness + brightness2) / 2 * 127), - 255); + 255).ToPremultiplied(); } } diff --git a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs index d5cefd2154..a211ed9c7c 100644 --- a/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs +++ b/osu.Framework.Tests/Visual/UserInterface/TestSceneDrawablePath.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Lines; @@ -15,7 +16,6 @@ using osu.Framework.Utils; using osuTK; using osuTK.Graphics; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Tests.Visual.UserInterface { @@ -33,7 +33,7 @@ private void load(IRenderer renderer) for (int i = 0; i < texture_width; ++i) { byte brightnessByte = (byte)((float)i / (texture_width - 1) * 255); - image.Premultiplied[i, 0] = new Rgba32(brightnessByte, brightnessByte, brightnessByte, brightnessByte); + image[i, 0] = new Color4(255, 255, 255, brightnessByte).ToPremultiplied(); } gradientTexture = renderer.CreateTexture(texture_width, 1, true); diff --git a/osu.Framework/Graphics/Colour/PremultipliedColour.cs b/osu.Framework/Graphics/Colour/PremultipliedColour.cs index abc93a0871..02f8e87676 100644 --- a/osu.Framework/Graphics/Colour/PremultipliedColour.cs +++ b/osu.Framework/Graphics/Colour/PremultipliedColour.cs @@ -55,6 +55,16 @@ public PremultipliedColour(float premultipliedR, float premultipliedG, float pre colour.B * colour.A, colour.A); + /// + /// Creates a from a premultiplied-alpha colour. + /// + /// The premultiplied-alpha colour. + public static PremultipliedColour FromPremultiplied(Rgba32 premultipliedColour) + { + var premultipliedVector = premultipliedColour.ToVector4(); + return new PremultipliedColour(premultipliedVector.X, premultipliedVector.Y, premultipliedVector.Z, premultipliedVector.W); + } + public bool Equals(PremultipliedColour other) => PremultipliedR == other.PremultipliedR && PremultipliedG == other.PremultipliedG && diff --git a/osu.Framework/Graphics/Lines/SmoothPath.cs b/osu.Framework/Graphics/Lines/SmoothPath.cs index a5e80d3450..a121d384b8 100644 --- a/osu.Framework/Graphics/Lines/SmoothPath.cs +++ b/osu.Framework/Graphics/Lines/SmoothPath.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Textures; using osuTK.Graphics; -using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.Graphics.Lines { @@ -74,8 +73,7 @@ private void validateTexture() float progress = (float)i / (textureWidth - 1); var colour = ColourAt(progress); - float alpha = Math.Min(progress / aa_portion, 1); - raw.Premultiplied[i, 0] = new Rgba32(colour.R * alpha, colour.G * alpha, colour.B * alpha, colour.A * alpha); + raw[i, 0] = colour.Opacity(colour.A * Math.Min(progress / aa_portion, 1)).ToPremultiplied(); } if (Texture?.Width == textureWidth) diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs index 94437c8448..49edc4a5de 100644 --- a/osu.Framework/Graphics/Textures/PremultipliedImage.cs +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -12,7 +12,7 @@ namespace osu.Framework.Graphics.Textures public class PremultipliedImage : IDisposable { /// - /// The underlying image in form. + /// The underlying image in form. /// public readonly Image Premultiplied; @@ -33,6 +33,17 @@ public PremultipliedImage(int width, int height, PremultipliedColour colour) public PremultipliedImage Clone() => FromPremultiplied(Premultiplied.Clone()); + /// + /// Gets or sets the pixel at the specified position. + /// + /// The x-coordinate of the pixel. + /// The y-coordinate of the pixel. + public PremultipliedColour this[int x, int y] + { + get => PremultipliedColour.FromPremultiplied(Premultiplied[x, y]); + set => Premultiplied[x, y] = value.ToPremultipliedRgba32(); + } + public void Dispose() { Premultiplied.Dispose(); From ccd0269f2d9c273f3cf69d74060f802a1cae4c91 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 09:00:28 -0500 Subject: [PATCH 18/19] Mimic image API in `PremultipliedAlpha` and remove access to underlying image --- .../BenchmarkFontLoading.cs | 2 +- .../TestSceneTextureUploadPerformance.cs | 2 +- .../Graphics/OpenGL/Textures/GLTexture.cs | 2 +- .../Performance/FrameStatisticsDisplay.cs | 2 +- .../Textures/ArrayPoolTextureUpload.cs | 2 +- .../Graphics/Textures/ITextureUpload.cs | 2 +- .../Textures/MemoryAllocatorTextureUpload.cs | 2 +- .../Graphics/Textures/PremultipliedImage.cs | 59 ++++++++++++++++--- osu.Framework/Graphics/Textures/Texture.cs | 6 +- .../TextureAtlas_BackingAtlasTexture.cs | 4 +- .../Graphics/Textures/TextureUpload.cs | 14 ++--- .../Veldrid/Textures/VeldridTexture.cs | 4 +- .../Graphics/Video/VideoTextureUpload.cs | 2 +- osu.Framework/IO/Stores/GlyphStore.cs | 7 +-- .../IO/Stores/RawCachingGlyphStore.cs | 7 +-- 15 files changed, 78 insertions(+), 39 deletions(-) diff --git a/osu.Framework.Benchmarks/BenchmarkFontLoading.cs b/osu.Framework.Benchmarks/BenchmarkFontLoading.cs index 696be54804..d36a34eb0c 100644 --- a/osu.Framework.Benchmarks/BenchmarkFontLoading.cs +++ b/osu.Framework.Benchmarks/BenchmarkFontLoading.cs @@ -102,7 +102,7 @@ private void runFor(GlyphStore store) var icon = (IconUsage)propValue; using (var upload = store.Get(icon.Icon.ToString())) - Trace.Assert(upload.Data != null); + Trace.Assert(upload.PremultipliedData != null); if (remainingCount-- == 0) return; diff --git a/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs b/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs index 27ae435d53..67b4590233 100644 --- a/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs +++ b/osu.Framework.Tests/Visual/Performance/TestSceneTextureUploadPerformance.cs @@ -93,7 +93,7 @@ private class ReusableTextureUpload : ITextureUpload { private readonly ITextureUpload upload; - public ReadOnlySpan Data => upload.Data; + public ReadOnlySpan PremultipliedData => upload.PremultipliedData; public int Level => upload.Level; diff --git a/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs b/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs index a5ba2c5919..74863d80ac 100644 --- a/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs +++ b/osu.Framework/Graphics/OpenGL/Textures/GLTexture.cs @@ -213,7 +213,7 @@ public unsafe bool Upload() { using (upload) { - fixed (Rgba32* ptr = upload.Data) + fixed (Rgba32* ptr = upload.PremultipliedData) DoUpload(upload, (IntPtr)ptr); uploadedRegions.Add(upload.Bounds); diff --git a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs index aad004fdc3..ad2f54b5aa 100644 --- a/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs +++ b/osu.Framework/Graphics/Performance/FrameStatisticsDisplay.cs @@ -261,7 +261,7 @@ private void load(IRenderer renderer) for (int i = 0; i < HEIGHT; i++) { for (int k = 0; k < WIDTH; k++) - fullBackground.Premultiplied[k, i] = columnUpload.RawData[i]; + fullBackground.SetPremultipliedRgba32(k, i, columnUpload.RawData[i]); } addArea(null, null, HEIGHT, amount_count_steps, columnUpload); diff --git a/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs b/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs index 683cbfc256..163581e816 100644 --- a/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs +++ b/osu.Framework/Graphics/Textures/ArrayPoolTextureUpload.cs @@ -36,7 +36,7 @@ public void Dispose() public Span RawData => data; - public ReadOnlySpan Data => data; + public ReadOnlySpan PremultipliedData => data; public int Level { get; set; } diff --git a/osu.Framework/Graphics/Textures/ITextureUpload.cs b/osu.Framework/Graphics/Textures/ITextureUpload.cs index 2f78284710..3180367373 100644 --- a/osu.Framework/Graphics/Textures/ITextureUpload.cs +++ b/osu.Framework/Graphics/Textures/ITextureUpload.cs @@ -13,7 +13,7 @@ public interface ITextureUpload : IDisposable /// /// The raw data to be uploaded. /// - ReadOnlySpan Data { get; } + ReadOnlySpan PremultipliedData { get; } /// /// The target mipmap level to upload into. diff --git a/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs b/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs index f0a9560c53..43b4173619 100644 --- a/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs +++ b/osu.Framework/Graphics/Textures/MemoryAllocatorTextureUpload.cs @@ -16,7 +16,7 @@ public class MemoryAllocatorTextureUpload : ITextureUpload { public Span RawData => memoryOwner.Memory.Span; - public ReadOnlySpan Data => RawData; + public ReadOnlySpan PremultipliedData => RawData; private readonly IMemoryOwner memoryOwner; diff --git a/osu.Framework/Graphics/Textures/PremultipliedImage.cs b/osu.Framework/Graphics/Textures/PremultipliedImage.cs index 49edc4a5de..794cf94339 100644 --- a/osu.Framework/Graphics/Textures/PremultipliedImage.cs +++ b/osu.Framework/Graphics/Textures/PremultipliedImage.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Extensions.ImageExtensions; using osu.Framework.Graphics.Colour; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -12,13 +14,20 @@ namespace osu.Framework.Graphics.Textures public class PremultipliedImage : IDisposable { /// - /// The underlying image in form. + /// Gets the image width in px units. /// - public readonly Image Premultiplied; + public int Width => premultipliedImage.Width; - private PremultipliedImage(Image premultiplied) + /// + /// Gets the image height in px units. + /// + public int Height => premultipliedImage.Height; + + private readonly Image premultipliedImage; + + private PremultipliedImage(Image premultipliedImage) { - Premultiplied = premultiplied; + this.premultipliedImage = premultipliedImage; } public PremultipliedImage(int width, int height) @@ -31,8 +40,6 @@ public PremultipliedImage(int width, int height, PremultipliedColour colour) { } - public PremultipliedImage Clone() => FromPremultiplied(Premultiplied.Clone()); - /// /// Gets or sets the pixel at the specified position. /// @@ -40,13 +47,47 @@ public PremultipliedImage(int width, int height, PremultipliedColour colour) /// The y-coordinate of the pixel. public PremultipliedColour this[int x, int y] { - get => PremultipliedColour.FromPremultiplied(Premultiplied[x, y]); - set => Premultiplied[x, y] = value.ToPremultipliedRgba32(); + get => PremultipliedColour.FromPremultiplied(premultipliedImage[x, y]); + set => premultipliedImage[x, y] = value.ToPremultipliedRgba32(); } + /// + /// Creates a contiguous and read-only memory from the pixels of a . + /// Useful for retrieving unmanaged pointers to the entire pixel data of the for marshalling. + /// + /// + /// The returned must be disposed when usage is finished. + /// + /// The . + public ReadOnlyPixelMemory CreateReadOnlyPremultipliedPixelMemory() + => premultipliedImage.CreateReadOnlyPixelMemory(); + + /// + /// Gets the representation of the premultiplied pixels as of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The row. + /// The + public Memory DangerousGetPremultipliedPixelRowMemory(int rowIndex) + => premultipliedImage.DangerousGetPixelRowMemory(rowIndex); + + /// + /// Sets a pixel at the specified position with a premultiplied colour. + /// + /// The x-coordinate of the pixel. + /// The y-coordinate of the pixel. + /// The premultiplied colour. + public void SetPremultipliedRgba32(int x, int y, Rgba32 rgba32) => premultipliedImage[x, y] = rgba32; + + /// + /// Clones the current image. + /// + /// Returns a new image with all the same metadata as the original. + public PremultipliedImage Clone() => FromPremultiplied(premultipliedImage.Clone()); + public void Dispose() { - Premultiplied.Dispose(); + premultipliedImage.Dispose(); } /// diff --git a/osu.Framework/Graphics/Textures/Texture.cs b/osu.Framework/Graphics/Textures/Texture.cs index fb93dcf0ce..1283997108 100644 --- a/osu.Framework/Graphics/Textures/Texture.cs +++ b/osu.Framework/Graphics/Textures/Texture.cs @@ -202,14 +202,14 @@ internal virtual void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMod if (upload.Bounds.Width > NativeTexture.MaxSize || upload.Bounds.Height > NativeTexture.MaxSize) throw new TextureTooLargeForGLException(); - if (upload.Bounds.IsEmpty && upload.Data.Length > 0) + if (upload.Bounds.IsEmpty && upload.PremultipliedData.Length > 0) { upload.Bounds = new RectangleI(0, 0, Width, Height); - if (upload.Bounds.Width * upload.Bounds.Height > upload.Data.Length) + if (upload.Bounds.Width * upload.Bounds.Height > upload.PremultipliedData.Length) { throw new InvalidOperationException( - $"Size of texture upload ({upload.Bounds.Width}x{upload.Bounds.Height}) does not contain enough data ({upload.Data.Length} < {upload.Bounds.Width * upload.Bounds.Height})"); + $"Size of texture upload ({upload.Bounds.Width}x{upload.Bounds.Height}) does not contain enough data ({upload.PremultipliedData.Length} < {upload.Bounds.Width * upload.Bounds.Height})"); } } diff --git a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs index 80e262dd75..a33dfbebc4 100644 --- a/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs +++ b/osu.Framework/Graphics/Textures/TextureAtlas_BackingAtlasTexture.cs @@ -51,7 +51,7 @@ internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMo // Can only perform padding when the bounds are a sub-part of the texture RectangleI middleBounds = upload.Bounds; - if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.Data.Length) + if (middleBounds.IsEmpty || middleBounds.Width * middleBounds.Height > upload.PremultipliedData.Length) { // For a texture atlas, we don't care about opacity, so we avoid // any computations related to it by assuming it to be mixed. @@ -61,7 +61,7 @@ internal override void SetData(ITextureUpload upload, WrapMode wrapModeS, WrapMo int actualPadding = padding / (1 << upload.Level); - var data = upload.Data; + var data = upload.PremultipliedData; uploadCornerPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None && wrapModeT != WrapMode.None); uploadHorizontalPadding(data, middleBounds, actualPadding, wrapModeS != WrapMode.None); diff --git a/osu.Framework/Graphics/Textures/TextureUpload.cs b/osu.Framework/Graphics/Textures/TextureUpload.cs index 78d68dfc5b..0c0b4af451 100644 --- a/osu.Framework/Graphics/Textures/TextureUpload.cs +++ b/osu.Framework/Graphics/Textures/TextureUpload.cs @@ -39,20 +39,20 @@ public class TextureUpload : ITextureUpload public RectangleI Bounds { get; set; } /// - /// Overview of the texture upload's pixels content. The pixel colours are alpha-premultiplied. + /// Overview of the texture upload's pixels content. The pixel colours are premultiplied. /// - public ReadOnlySpan Data => pixelMemory.Span; + public ReadOnlySpan PremultipliedData => premultipliedPixelMemory.Span; - public int Width => image?.Premultiplied.Width ?? 0; + public int Width => image?.Width ?? 0; - public int Height => image?.Premultiplied.Height ?? 0; + public int Height => image?.Height ?? 0; /// /// The backing texture. A handle is kept to avoid early GC. /// private readonly PremultipliedImage image; - private ReadOnlyPixelMemory pixelMemory; + private ReadOnlyPixelMemory premultipliedPixelMemory; /// /// Create an upload from a . This is the preferred method. @@ -61,7 +61,7 @@ public class TextureUpload : ITextureUpload public TextureUpload(PremultipliedImage image) { this.image = image; - pixelMemory = image.Premultiplied.CreateReadOnlyPixelMemory(); + premultipliedPixelMemory = image.CreateReadOnlyPremultipliedPixelMemory(); } /// @@ -128,7 +128,7 @@ protected virtual void Dispose(bool isDisposing) return; image?.Dispose(); - pixelMemory.Dispose(); + premultipliedPixelMemory.Dispose(); disposed = true; } diff --git a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs index 8e50c8f5cb..e245db72e2 100644 --- a/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs +++ b/osu.Framework/Graphics/Veldrid/Textures/VeldridTexture.cs @@ -470,7 +470,7 @@ protected virtual void DoUpload(ITextureUpload upload) int lastMaximumUploadedLod = maximumUploadedLod; - if (!upload.Data.IsEmpty) + if (!upload.PremultipliedData.IsEmpty) { // ensure all mip levels up to the target level are initialised. // generally we always upload at level 0, so this won't run. @@ -483,7 +483,7 @@ protected virtual void DoUpload(ITextureUpload upload) } Renderer.UpdateTexture(texture, upload.Bounds.X >> upload.Level, upload.Bounds.Y >> upload.Level, upload.Bounds.Width >> upload.Level, upload.Bounds.Height >> upload.Level, - upload.Level, upload.Data); + upload.Level, upload.PremultipliedData); } if (sampler == null || maximumUploadedLod > lastMaximumUploadedLod) diff --git a/osu.Framework/Graphics/Video/VideoTextureUpload.cs b/osu.Framework/Graphics/Video/VideoTextureUpload.cs index ee6442cca5..209d8df520 100644 --- a/osu.Framework/Graphics/Video/VideoTextureUpload.cs +++ b/osu.Framework/Graphics/Video/VideoTextureUpload.cs @@ -24,7 +24,7 @@ public int GetPlaneHeight(uint plane) return (plane == 0) ? Frame->height : (Frame->height + 1) / 2; } - public ReadOnlySpan Data => ReadOnlySpan.Empty; + public ReadOnlySpan PremultipliedData => ReadOnlySpan.Empty; public int Level => 0; diff --git a/osu.Framework/IO/Stores/GlyphStore.cs b/osu.Framework/IO/Stores/GlyphStore.cs index 8c10a28fd7..459dd99f1d 100644 --- a/osu.Framework/IO/Stores/GlyphStore.cs +++ b/osu.Framework/IO/Stores/GlyphStore.cs @@ -17,7 +17,6 @@ using osu.Framework.Logging; using osu.Framework.Text; using SharpFNT; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.IO.Stores @@ -166,7 +165,7 @@ protected virtual TextureUpload LoadCharacter(Character character) LoadedGlyphCount++; var image = new PremultipliedImage(character.Width, character.Height); - var source = page.Data; + var source = page.PremultipliedData; // the spritesheet may have unused pixels trimmed int readableHeight = Math.Min(character.Height, page.Height - character.Y); @@ -174,11 +173,11 @@ protected virtual TextureUpload LoadCharacter(Character character) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.Premultiplied.DangerousGetPixelRowMemory(y); + var premultipliedPixelRowMemory = image.DangerousGetPremultipliedPixelRowMemory(y); int readOffset = (character.Y + y) * page.Width + character.X; for (int x = 0; x < character.Width; x++) - pixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(0, 0, 0, 0); + premultipliedPixelRowMemory.Span[x] = x < readableWidth && y < readableHeight ? source[readOffset + x] : new Rgba32(0, 0, 0, 0); } return new TextureUpload(image); diff --git a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs index a53c018ce1..a6324cbe2d 100644 --- a/osu.Framework/IO/Stores/RawCachingGlyphStore.cs +++ b/osu.Framework/IO/Stores/RawCachingGlyphStore.cs @@ -14,7 +14,6 @@ using osu.Framework.Platform; using SharpFNT; using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; namespace osu.Framework.IO.Stores @@ -107,7 +106,7 @@ private PageInfo createCachedPageInfo(int page) using (var buffer = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate(convert.Width * convert.Height)) { var output = buffer.Memory.Span; - var source = convert.Data; + var source = convert.PremultipliedData; for (int i = 0; i < output.Length; i++) output[i] = source[i].A; @@ -156,8 +155,8 @@ private TextureUpload createTextureUpload(Character character, PageInfo page) for (int y = 0; y < character.Height; y++) { - var pixelRowMemory = image.Premultiplied.DangerousGetPixelRowMemory(y); - var span = pixelRowMemory.Span; + var premultipliedPixelRowMemory = image.DangerousGetPremultipliedPixelRowMemory(y); + var span = premultipliedPixelRowMemory.Span; int readOffset = y * pageWidth + character.X; for (int x = 0; x < character.Width; x++) From 59493e39b3bf8d1e43a7d272353d76a791bfe110 Mon Sep 17 00:00:00 2001 From: Salman Alshamrani Date: Sun, 22 Dec 2024 10:54:14 -0500 Subject: [PATCH 19/19] Revert "Fix `SRGBColour` not clamping alpha on multiplication" This reverts commit 8a4abb37bc885a212f3e9bed220eca0955f3d4e4. --- osu.Framework/Graphics/Colour/SRGBColour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Framework/Graphics/Colour/SRGBColour.cs b/osu.Framework/Graphics/Colour/SRGBColour.cs index 9b6aee1896..d5c994200e 100644 --- a/osu.Framework/Graphics/Colour/SRGBColour.cs +++ b/osu.Framework/Graphics/Colour/SRGBColour.cs @@ -123,7 +123,7 @@ public struct SRGBColour : IEquatable /// Multiplies the alpha value of this colour by the given alpha factor. /// /// The alpha factor to multiply with. - public void MultiplyAlpha(float alpha) => SRGB.A = Math.Min(1, SRGB.A * alpha); + public void MultiplyAlpha(float alpha) => SRGB.A *= alpha; private static bool isWhite(SRGBColour colour) => colour.SRGB.R == 1 && colour.SRGB.G == 1 && colour.SRGB.B == 1;