Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement texture mipmap generation via compute pipelines #5757

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2232cfa
Add basic mipmap generation compute shader
frenzibyte Apr 20, 2023
94c7599
Separate renderer setup methods for post-initialisation resources
frenzibyte Apr 20, 2023
9f933ef
Use compute shader to generate mipmaps
frenzibyte Apr 20, 2023
16715ce
Fix `glGetProgram` checking against wrong name
frenzibyte Apr 20, 2023
4c6627a
Fix compute-based generate mipmaps not actually binding the shader
frenzibyte Apr 20, 2023
9982423
Use `OpenGL` namespace for non-embedded platforms
frenzibyte Apr 20, 2023
b5b8fcf
Add memory barriers against texture/image fetches for synchronisation
frenzibyte Apr 20, 2023
45d9981
Restore previous shader state if one was bound
frenzibyte Apr 20, 2023
ff18add
Fix wrong for loop conditions
frenzibyte Apr 20, 2023
34dbc7c
Remove unused using directives and disable inspection
frenzibyte Apr 21, 2023
e0f0f5e
Update mipmap shader to use regular texture type for reading
frenzibyte Apr 21, 2023
13be7b0
Remove unnecessary sampler resource
frenzibyte Apr 22, 2023
e85a7c3
Use structured buffer for `image2D`s to respect D3D11 Shader Model 4
frenzibyte Apr 22, 2023
c476f92
Simplify mipmap compute shader using bilinear sampler
frenzibyte Apr 23, 2023
52d5f93
Simplify parameters buffer and only use for D3D11
frenzibyte Apr 23, 2023
62681b2
Emphasise on matching threadgroup sizes between pipeline and shader
frenzibyte Apr 23, 2023
b173987
Update legacy renderer inline with shader changes
frenzibyte Apr 23, 2023
b06615d
Add visual & benchmark tests for texture mipmaps
frenzibyte Apr 24, 2023
fed2aae
Improve test scene and fix profiling
frenzibyte Apr 24, 2023
26c3e92
Ignore test on headless runs
frenzibyte Apr 24, 2023
e17f927
Remove unused resolved property
frenzibyte Apr 24, 2023
aeefa45
Only use compute shader on non-ancient Direct3D hardware
frenzibyte Apr 24, 2023
4176ff2
Support partial mipmap generation for better performance
frenzibyte Apr 26, 2023
f020adb
Fix OpenGL not binding texture before modifying LOD parameters
frenzibyte Apr 26, 2023
bca0a9b
Centralise compute shader support conditions and handle old OpenGL ve…
frenzibyte Apr 26, 2023
1d8b8f3
Increase thread group size to 32x32
frenzibyte Apr 26, 2023
bf8d3b8
Cleanup regions storage in mipmap generation queue
frenzibyte Apr 26, 2023
b06b0c7
Move parameters struct to separate file
frenzibyte Apr 26, 2023
e72432e
Clarify GL compile exceptions
frenzibyte Apr 26, 2023
3711eb8
Minor cleanup on test scene
frenzibyte Apr 26, 2023
164d92d
Separate ternary condition to new lines
frenzibyte Apr 26, 2023
5524f68
Add xmldocs
frenzibyte Apr 26, 2023
141db51
Remove comment on using `gl_NumWorkGroups`
frenzibyte Apr 26, 2023
7dfd0c4
Trim left rectangle from overlapping with right rectangle
frenzibyte Apr 26, 2023
bf639e4
Fix Veldrid potentially creating out-of-bounds texture views
frenzibyte Apr 26, 2023
b32aa63
Merge branch 'master' into compute-mipmap-generation
frenzibyte Apr 26, 2023
ce2ef67
Merge branch 'master' into compute-mipmap-generation
frenzibyte Jul 27, 2023
ca52b00
Fix post-merge conflict issues
frenzibyte Jul 27, 2023
86f0c84
Update GL compute shader flow
frenzibyte Jul 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
339 changes: 339 additions & 0 deletions osu.Framework.Tests/Visual/Sprites/TestSceneTextureMipmaps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Veldrid;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Framework.Timing;
using osuTK;
using osuTK.Graphics.ES30;

namespace osu.Framework.Tests.Visual.Sprites
{
[Ignore("This test cannot be run in headless mode (a renderer is required).")]
public partial class TestSceneTextureMipmaps : FrameworkTestScene
{
[Resolved]
private GameHost host { get; set; } = null!;

[Resolved]
private Game game { get; set; } = null!;

private readonly List<string?> availableFontResources = new List<string?>();

private FontStore? fonts;

private double timeSpent;

[SetUp]
public void SetUp() => Schedule(() =>
{
Scheduler.CancelDelayedTasks();

fonts?.Dispose();
fonts = new FontStore(host.Renderer, new GlyphStore(game.Resources, @"Fonts/FontAwesome5/FontAwesome-Solid", host.CreateTextureLoaderStore(game.Resources)), useAtlas: true);

availableFontResources.Clear();
availableFontResources.AddRange(fonts.GetAvailableResources());

// fetch any glyph for a texture atlas to be generated.
getNextGlyph();

Debug.Assert(fonts.Atlas.AtlasTexture != null);

Children = new[]
{
new FillFlowContainer
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Margin = new MarginPadding { Left = 15f, Top = 30f },
Spacing = new Vector2(0f, 5f),
Children = new Drawable[]
{
new SpriteText
{
Text = "Mipmaps",
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new[]
{
new MipmapSprite(0)
{
Texture = fonts.Atlas.AtlasTexture,
Size = new Vector2(512, 512),
},
new MipmapSprite(1)
{
X = 512,
Texture = fonts.Atlas.AtlasTexture,
Size = new Vector2(256, 256),
},
new MipmapSprite(2)
{
X = 512,
Y = 256,
Texture = fonts.Atlas.AtlasTexture,
Size = new Vector2(128, 128),
},
new MipmapSprite(3)
{
X = 512,
Y = 384,
Texture = fonts.Atlas.AtlasTexture,
Size = new Vector2(64, 64),
},
}
}
},
}
};
});

[Test]
public void TestAddOnce()
{
AddStep("add one", () =>
{
timeSpent = 0;

getNextGlyph();
uploadAndReport();
});
}

[Test]
public void TestAddGradually()
{
int fetchedGlyphs = 0;
int? count;
ScheduledDelegate? glyphDelegate = null;

AddStep("add gradually", () =>
{
count = availableFontResources.Count;
fetchedGlyphs = 0;

timeSpent = 0;

glyphDelegate?.Cancel();
glyphDelegate = Scheduler.AddDelayed(() =>
{
if (fetchedGlyphs == count)
{
if (!pendingMipmapGeneration)
stop();

return;
}

getNextGlyph();
upload();
fetchedGlyphs++;
}, 1, true);
});

AddStep("stop", stop);

void stop()
{
// ReSharper disable once AccessToModifiedClosure
glyphDelegate?.Cancel();
report($"Generating mipmaps spent {timeSpent / (fetchedGlyphs):N3}ms on average");
}
}

[Test]
public void TestAddHalf()
{
AddStep("add half", () =>
{
timeSpent = 0;

int count = availableFontResources.Count / 2;
for (int i = 0; i < count; i++)
getNextGlyph();

uploadAndReport();
});
}

[Test]
public void TestAddAll()
{
AddStep("add all", () =>
{
timeSpent = 0;

int count = availableFontResources.Count;
for (int i = 0; i < count; i++)
getNextGlyph();

uploadAndReport();
});
}

private readonly StopwatchClock stopwatchClock = new StopwatchClock(true);

private bool pendingMipmapGeneration;

private void upload()
{
if (pendingMipmapGeneration)
return;

host.Renderer.ScheduleExpensiveOperation(new ScheduledDelegate(() =>
{
// ensure atlas texture is uploaded first before profiling mipmap generation.
Debug.Assert(fonts != null);

fonts.Atlas.AtlasTexture!.NativeTexture.Upload();

switch (host.Renderer)
{
case GLRenderer gl:
uploadOpenGL(gl);
break;

case VeldridRenderer veldrid:
uploadVeldrid(veldrid);
break;

default:
throw new InvalidOperationException("Unsupported renderer.");
}

pendingMipmapGeneration = false;
}));

pendingMipmapGeneration = true;
}

private void uploadOpenGL(GLRenderer gl)
{
Debug.Assert(fonts != null);

double timeBefore = stopwatchClock.CurrentTime;

fonts.Atlas.AtlasTexture!.NativeTexture.GenerateMipmaps();
GL.Finish();

timeSpent += stopwatchClock.CurrentTime - timeBefore;
}

private void uploadVeldrid(VeldridRenderer veldrid)
{
Debug.Assert(fonts != null);

double timeBefore = stopwatchClock.CurrentTime;

veldrid.MipmapGenerationCommands.Begin();
veldrid.BypassMipmapGenerationQueue = true;

fonts.Atlas.AtlasTexture!.NativeTexture.GenerateMipmaps();

veldrid.BypassMipmapGenerationQueue = false;

veldrid.BufferUpdateCommands.End();
veldrid.Device.SubmitCommands(veldrid.BufferUpdateCommands);

veldrid.MipmapGenerationCommands.End();
veldrid.Device.SubmitCommands(veldrid.MipmapGenerationCommands);
veldrid.Device.WaitForIdle();

timeSpent += stopwatchClock.CurrentTime - timeBefore;

veldrid.BufferUpdateCommands.Begin();
}

private void uploadAndReport()
{
upload();

ScheduledDelegate reportDelegate = null!;

reportDelegate = Scheduler.AddDelayed(() =>
{
for (int i = 10; i >= 0; --i)
{
if (pendingMipmapGeneration)
continue;

report($"Generating mipmaps spent {timeSpent:N3}ms");
// ReSharper disable once AccessToModifiedClosure
reportDelegate.Cancel();
break;
}
}, 0, true);
}

private SpriteText? reportText;

private void report(string message)
{
if (reportText != null)
Remove(reportText, true);

Add(reportText = new SpriteText
{
Position = new Vector2(15f, 5f),
Text = message
});
}

private void getNextGlyph()
{
if (availableFontResources.Count == 0)
return;

Debug.Assert(fonts != null);

fonts.Get(availableFontResources[0]);
availableFontResources.RemoveAt(0);
}

private partial class MipmapSprite : Sprite
{
private readonly int level;

public MipmapSprite(int level)
{
this.level = level;
}

protected override DrawNode CreateDrawNode() => new SingleMipmapSpriteDrawNode(this, level);

private class SingleMipmapSpriteDrawNode : SpriteDrawNode
{
private readonly int level;

public SingleMipmapSpriteDrawNode(Sprite source, int level)
: base(source)
{
this.level = level;
}

protected override void Blit(IRenderer renderer)
{
Texture.MipLevel = level;
base.Blit(renderer);
renderer.FlushCurrentBatch(null);
Texture.MipLevel = null;
}
}
}
}
}
Loading