From dee306b0c37aed41d30e5f41ea57050d46097c16 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Jun 2023 20:36:01 +0900 Subject: [PATCH 1/5] Add shader compilation cache --- .../Shaders/TestSceneShaderDisposal.cs | 8 +- .../Shaders/TestShaderLoading.cs | 8 +- osu.Framework/Graphics/OpenGL/GLRenderer.cs | 4 +- .../Graphics/OpenGL/Shaders/GLShader.cs | 17 ++- .../Graphics/OpenGL/Shaders/GLShaderPart.cs | 6 +- .../Graphics/Rendering/Dummy/DummyRenderer.cs | 2 + osu.Framework/Graphics/Rendering/IRenderer.cs | 5 + osu.Framework/Graphics/Rendering/Renderer.cs | 18 ++- .../Shaders/ComputeProgramCompilation.cs | 25 ++++ .../Shaders/ShaderCompilationStore.cs | 133 ++++++++++++++++++ .../VertexFragmentShaderCompilation.cs | 35 +++++ .../Graphics/Veldrid/Shaders/VeldridShader.cs | 30 ++-- .../Veldrid/Shaders/VeldridShaderPart.cs | 6 +- .../Graphics/Veldrid/VeldridRenderer.cs | 4 +- osu.Framework/Platform/GameHost.cs | 1 + 15 files changed, 258 insertions(+), 44 deletions(-) create mode 100644 osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs create mode 100644 osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs create mode 100644 osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs diff --git a/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs b/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs index 3b44a403fe..bddfcf64f9 100644 --- a/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs +++ b/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs @@ -60,13 +60,13 @@ public void TestShadersLoseReferencesOnManagerDisposal() private class TestGLRenderer : GLRenderer { - protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer) - => new TestGLShader(this, name, parts.Cast().ToArray()); + protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + => new TestGLShader(this, name, parts.Cast().ToArray(), compilationStore); private class TestGLShader : GLShader { - internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts) - : base(renderer, name, parts, null) + internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, null, compilationStore) { } diff --git a/osu.Framework.Tests/Shaders/TestShaderLoading.cs b/osu.Framework.Tests/Shaders/TestShaderLoading.cs index f8299917c9..fc9b842448 100644 --- a/osu.Framework.Tests/Shaders/TestShaderLoading.cs +++ b/osu.Framework.Tests/Shaders/TestShaderLoading.cs @@ -40,13 +40,13 @@ public void TestFetchExistentShader() private class TestGLRenderer : GLRenderer { - protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer) - => new TestGLShader(this, name, parts.Cast().ToArray()); + protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + => new TestGLShader(this, name, parts.Cast().ToArray(), compilationStore); private class TestGLShader : GLShader { - internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts) - : base(renderer, name, parts, null) + internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, null, compilationStore) { } diff --git a/osu.Framework/Graphics/OpenGL/GLRenderer.cs b/osu.Framework/Graphics/OpenGL/GLRenderer.cs index 25f5ebca6a..82b639d200 100644 --- a/osu.Framework/Graphics/OpenGL/GLRenderer.cs +++ b/osu.Framework/Graphics/OpenGL/GLRenderer.cs @@ -376,8 +376,8 @@ protected override IShaderPart CreateShaderPart(IShaderStore store, string name, return new GLShaderPart(this, name, rawData, glType, store); } - protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer) - => new GLShader(this, name, parts.Cast().ToArray(), globalUniformBuffer); + protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + => new GLShader(this, name, parts.Cast().ToArray(), globalUniformBuffer, compilationStore); public override IFrameBuffer CreateFrameBuffer(RenderBufferFormat[]? renderBufferFormats = null, TextureFilteringMode filteringMode = TextureFilteringMode.Linear) { diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs index bac09fe3b4..96fe85ebe2 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; @@ -42,9 +41,9 @@ internal class GLShader : IShader private readonly GLShaderPart vertexPart; private readonly GLShaderPart fragmentPart; - private readonly VertexFragmentCompilationResult crossCompileResult; + private readonly VertexFragmentShaderCompilation compilation; - internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer globalUniformBuffer) + internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) { this.renderer = renderer; this.name = name; @@ -59,9 +58,9 @@ internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUnifo try { // Shaders are in "Vulkan GLSL" format. They need to be cross-compiled to GLSL. - crossCompileResult = SpirvCompilation.CompileVertexFragment( - Encoding.UTF8.GetBytes(vertexPart.GetRawText()), - Encoding.UTF8.GetBytes(fragmentPart.GetRawText()), + compilation = compilationStore.CompileVertexFragment( + vertexPart.GetRawText(), + fragmentPart.GetRawText(), renderer.IsEmbedded ? CrossCompileTarget.ESSL : CrossCompileTarget.GLSL); } catch (Exception e) @@ -158,8 +157,8 @@ public virtual void BindUniformBlock(string blockName, IUniformBuffer buffer) private protected virtual bool CompileInternal() { - vertexPart.Compile(crossCompileResult.VertexShader); - fragmentPart.Compile(crossCompileResult.FragmentShader); + vertexPart.Compile(compilation.VertexText); + fragmentPart.Compile(compilation.FragmentText); foreach (GLShaderPart p in parts) GL.AttachShader(this, p); @@ -176,7 +175,7 @@ private protected virtual bool CompileInternal() int blockBindingIndex = 0; int textureIndex = 0; - foreach (ResourceLayoutDescription layout in crossCompileResult.Reflection.ResourceLayouts) + foreach (ResourceLayoutDescription layout in compilation.Reflection.ResourceLayouts) { if (layout.Elements.Length == 0) continue; diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs index 80be4026f8..abc610e91f 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs @@ -123,10 +123,10 @@ private string loadFile(byte[] bytes, bool mainFile) if (!string.IsNullOrEmpty(backbufferCode)) { - string realMainName = "real_main_" + Guid.NewGuid().ToString("N"); + const string real_main_name = "__internal_real_main"; - backbufferCode = backbufferCode.Replace("{{ real_main }}", realMainName); - code = Regex.Replace(code, @"void main\((.*)\)", $"void {realMainName}()") + backbufferCode + '\n'; + backbufferCode = backbufferCode.Replace("{{ real_main }}", real_main_name); + code = Regex.Replace(code, @"void main\((.*)\)", $"void {real_main_name}()") + backbufferCode + '\n'; } } } diff --git a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs index a8f4284098..4d2ebe878b 100644 --- a/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs +++ b/osu.Framework/Graphics/Rendering/Dummy/DummyRenderer.cs @@ -57,6 +57,8 @@ public DummyRenderer() bool IRenderer.AllowTearing { get; set; } + Storage? IRenderer.CacheStorage { set { } } + void IRenderer.Initialise(IGraphicsSurface graphicsSurface) { IsInitialised = true; diff --git a/osu.Framework/Graphics/Rendering/IRenderer.cs b/osu.Framework/Graphics/Rendering/IRenderer.cs index 455a5455ec..3525c2c1da 100644 --- a/osu.Framework/Graphics/Rendering/IRenderer.cs +++ b/osu.Framework/Graphics/Rendering/IRenderer.cs @@ -51,6 +51,11 @@ public interface IRenderer protected internal bool AllowTearing { get; set; } + /// + /// A that can be used to cache objects. + /// + protected internal Storage? CacheStorage { set; } + /// /// The maximum allowed texture size. /// diff --git a/osu.Framework/Graphics/Rendering/Renderer.cs b/osu.Framework/Graphics/Rendering/Renderer.cs index dae5797f80..360bf9d3a7 100644 --- a/osu.Framework/Graphics/Rendering/Renderer.cs +++ b/osu.Framework/Graphics/Rendering/Renderer.cs @@ -44,6 +44,11 @@ public abstract class Renderer : IRenderer protected internal abstract bool VerticalSync { get; set; } protected internal abstract bool AllowTearing { get; set; } + protected internal Storage? CacheStorage + { + set => shaderCompilationStore.CacheStorage = value; + } + public int MaxTextureSize { get; protected set; } = 4096; // default value is to allow roughly normal flow in cases we don't have graphics context, like headless CI. public int MaxTexturesUploadedPerFrame { get; set; } = 32; @@ -94,6 +99,8 @@ public abstract class Renderer : IRenderer /// protected IShader? Shader { get; private set; } + private readonly ShaderCompilationStore shaderCompilationStore = new ShaderCompilationStore(); + private readonly GlobalStatistic statExpensiveOperationsQueued; private readonly GlobalStatistic statTextureUploadsQueued; private readonly GlobalStatistic statTextureUploadsDequeued; @@ -1036,7 +1043,7 @@ internal void SetUniform(IUniformWithValue uniform) protected abstract IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType); /// - protected abstract IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer); + protected abstract IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore); private IShader? mipmapShader; @@ -1056,7 +1063,7 @@ internal IShader GetMipmapShader() { CreateShaderPart(store, "mipmap.vs", store.GetRawData("sh_mipmap.vs"), ShaderPartType.Vertex), CreateShaderPart(store, "mipmap.fs", store.GetRawData("sh_mipmap.fs"), ShaderPartType.Fragment), - }, globalUniformBuffer.AsNonNull()); + }, globalUniformBuffer.AsNonNull(), shaderCompilationStore); return mipmapShader; } @@ -1128,6 +1135,11 @@ bool IRenderer.AllowTearing set => AllowTearing = value; } + Storage? IRenderer.CacheStorage + { + set => CacheStorage = value; + } + IVertexBatch IRenderer.DefaultQuadBatch => DefaultQuadBatch; void IRenderer.BeginFrame(Vector2 windowSize) => BeginFrame(windowSize); void IRenderer.FinishFrame() => FinishFrame(); @@ -1143,7 +1155,7 @@ bool IRenderer.AllowTearing void IRenderer.PopQuadBatch() => PopQuadBatch(); Image IRenderer.TakeScreenshot() => TakeScreenshot(); IShaderPart IRenderer.CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType) => CreateShaderPart(store, name, rawData, partType); - IShader IRenderer.CreateShader(string name, IShaderPart[] parts) => CreateShader(name, parts, globalUniformBuffer!); + IShader IRenderer.CreateShader(string name, IShaderPart[] parts) => CreateShader(name, parts, globalUniformBuffer!, shaderCompilationStore); IVertexBatch IRenderer.CreateLinearBatch(int size, int maxBuffers, PrimitiveTopology topology) { diff --git a/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs b/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs new file mode 100644 index 0000000000..41e63ef6da --- /dev/null +++ b/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Veldrid.SPIRV; + +namespace osu.Framework.Graphics.Shaders +{ + public class ComputeProgramCompilation + { + /// + /// The SpirV bytes for the program. + /// + public byte[] ProgramBytes { get; set; } = null!; + + /// + /// The cross-compiled program text. + /// + public string ProgramText { get; set; } = null!; + + /// + /// A reflection of the shader program, describing the layout of resources. + /// + public SpirvReflection Reflection { get; set; } = null!; + } +} diff --git a/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs b/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs new file mode 100644 index 0000000000..c7f291f071 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs @@ -0,0 +1,133 @@ +// 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 System.Diagnostics.CodeAnalysis; +using System.IO; +using Newtonsoft.Json; +using osu.Framework.Extensions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using Veldrid; +using Veldrid.SPIRV; + +namespace osu.Framework.Graphics.Shaders +{ + public class ShaderCompilationStore + { + public Storage? CacheStorage { private get; set; } + + public VertexFragmentShaderCompilation CompileVertexFragment(string vertexText, string fragmentText, CrossCompileTarget target) + { + // vertexHash#fragmentHash#target + string filename = $"{vertexText.ComputeMD5Hash()}#{fragmentText.ComputeMD5Hash()}#{(int)target}"; + + if (tryGetCached(filename, out VertexFragmentShaderCompilation? existing)) + return existing; + + // Debug preserves names for reflection. + byte[] vertexBytes = SpirvCompilation.CompileGlslToSpirv(vertexText, null, ShaderStages.Vertex, new GlslCompileOptions(true)).SpirvBytes; + byte[] fragmentBytes = SpirvCompilation.CompileGlslToSpirv(fragmentText, null, ShaderStages.Fragment, new GlslCompileOptions(true)).SpirvBytes; + + VertexFragmentCompilationResult crossResult = SpirvCompilation.CompileVertexFragment(vertexBytes, fragmentBytes, target, new CrossCompileOptions()); + VertexFragmentShaderCompilation compilation = new VertexFragmentShaderCompilation + { + VertexBytes = vertexBytes, + FragmentBytes = fragmentBytes, + VertexText = crossResult.VertexShader, + FragmentText = crossResult.FragmentShader, + Reflection = crossResult.Reflection + }; + + saveToCache(filename, compilation); + + return compilation; + } + + public ComputeProgramCompilation CompileCompute(string programText, CrossCompileTarget target) + { + // programHash#target + string filename = $"{programText.ComputeMD5Hash()}#{(int)target}"; + + if (tryGetCached(filename, out ComputeProgramCompilation? existing)) + return existing; + + // Debug preserves names for reflection. + byte[] programBytes = SpirvCompilation.CompileGlslToSpirv(programText, null, ShaderStages.Compute, new GlslCompileOptions(true)).SpirvBytes; + + ComputeCompilationResult crossResult = SpirvCompilation.CompileCompute(programBytes, target, new CrossCompileOptions()); + ComputeProgramCompilation compilation = new ComputeProgramCompilation + { + ProgramBytes = programBytes, + ProgramText = crossResult.ComputeShader, + Reflection = crossResult.Reflection + }; + + saveToCache(filename, compilation); + + return compilation; + } + + private bool tryGetCached(string filename, [NotNullWhen(true)] out T? compilation) + where T : class + { + compilation = null; + + try + { + if (CacheStorage == null) + return false; + + if (!CacheStorage.Exists(filename)) + return false; + + using var stream = CacheStorage.GetStream(filename); + using var br = new BinaryReader(stream); + + string checksum = br.ReadString(); + string data = br.ReadString(); + + if (data.ComputeMD5Hash() != checksum) + { + // Data corrupted.. + Logger.Log("Cached shader data is corrupted - recompiling."); + return false; + } + + compilation = JsonConvert.DeserializeObject(data)!; + return true; + } + catch (Exception e) + { + Logger.Error(e, "Failed to read cached shader compilation - recompiling."); + } + + return false; + } + + private void saveToCache(string filename, object compilation) + { + if (CacheStorage == null) + return; + + try + { + // ensure any stale cached versions are deleted. + CacheStorage.Delete(filename); + + using var stream = CacheStorage.CreateFileSafely(filename); + using var bw = new BinaryWriter(stream); + + string data = JsonConvert.SerializeObject(compilation); + string checksum = data.ComputeMD5Hash(); + + bw.Write(checksum); + bw.Write(data); + } + catch (Exception ex) + { + Logger.Error(ex, "Failed to save shader to cache."); + } + } + } +} diff --git a/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs b/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs new file mode 100644 index 0000000000..2945c5032b --- /dev/null +++ b/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs @@ -0,0 +1,35 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using Veldrid.SPIRV; + +namespace osu.Framework.Graphics.Shaders +{ + public class VertexFragmentShaderCompilation + { + /// + /// The SpirV bytes for the vertex shader. + /// + public byte[] VertexBytes { get; set; } = null!; + + /// + /// The SpirV bytes for the fragment shader. + /// + public byte[] FragmentBytes { get; set; } = null!; + + /// + /// The cross-compiled vertex shader text. + /// + public string VertexText { get; set; } = null!; + + /// + /// The cross-compiled fragment shader text. + /// + public string FragmentText { get; set; } = null!; + + /// + /// A reflection of the shader program, describing the layout of resources. + /// + public SpirvReflection Reflection { get; set; } = null!; + } +} diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs index d0066df6f9..ff1912d8a6 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs @@ -23,6 +23,7 @@ internal class VeldridShader : IShader private readonly string name; private readonly VeldridShaderPart[] parts; private readonly IUniformBuffer globalUniformBuffer; + private readonly ShaderCompilationStore compilationStore; private readonly VeldridRenderer renderer; public Shader[]? Shaders; @@ -42,11 +43,12 @@ internal class VeldridShader : IShader private readonly Dictionary uniformLayouts = new Dictionary(); private readonly List textureLayouts = new List(); - public VeldridShader(VeldridRenderer renderer, string name, VeldridShaderPart[] parts, IUniformBuffer globalUniformBuffer) + public VeldridShader(VeldridRenderer renderer, string name, VeldridShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) { this.name = name; this.parts = parts; this.globalUniformBuffer = globalUniformBuffer; + this.compilationStore = compilationStore; this.renderer = renderer; // This part of the compilation is quite CPU expensive. @@ -128,19 +130,19 @@ private void compile() renderer.Factory.BackendType == GraphicsBackend.Metal ? "main0" : "main"); // GLSL cross compile is always performed for reflection, even though the cross-compiled shaders aren't used under other backends. - VertexFragmentCompilationResult crossCompileResult = SpirvCompilation.CompileVertexFragment( - Encoding.UTF8.GetBytes(vertex.GetRawText()), - Encoding.UTF8.GetBytes(fragment.GetRawText()), + VertexFragmentShaderCompilation compilation = compilationStore.CompileVertexFragment( + vertex.GetRawText(), + fragment.GetRawText(), RuntimeInfo.IsMobile ? CrossCompileTarget.ESSL : CrossCompileTarget.GLSL); if (renderer.SurfaceType == GraphicsSurfaceType.Vulkan) { - vertexShaderDescription.ShaderBytes = SpirvCompilation.CompileGlslToSpirv(vertex.GetRawText(), null, ShaderStages.Vertex, GlslCompileOptions.Default).SpirvBytes; - fragmentShaderDescription.ShaderBytes = SpirvCompilation.CompileGlslToSpirv(fragment.GetRawText(), null, ShaderStages.Fragment, GlslCompileOptions.Default).SpirvBytes; + vertexShaderDescription.ShaderBytes = compilation.VertexBytes; + fragmentShaderDescription.ShaderBytes = compilation.FragmentBytes; } else { - VertexFragmentCompilationResult platformCrossCompileResult = crossCompileResult; + VertexFragmentShaderCompilation platformCompilation = compilation; // If we don't have an OpenGL surface, we need to cross-compile once more for the correct platform. if (renderer.SurfaceType != GraphicsSurfaceType.OpenGL) @@ -152,19 +154,19 @@ private void compile() _ => throw new InvalidOperationException($"Unsupported surface type: {renderer.SurfaceType}.") }; - platformCrossCompileResult = SpirvCompilation.CompileVertexFragment( - Encoding.UTF8.GetBytes(vertex.GetRawText()), - Encoding.UTF8.GetBytes(fragment.GetRawText()), + platformCompilation = compilationStore.CompileVertexFragment( + vertex.GetRawText(), + fragment.GetRawText(), target); } - vertexShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCrossCompileResult.VertexShader); - fragmentShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCrossCompileResult.FragmentShader); + vertexShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCompilation.VertexText); + fragmentShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCompilation.FragmentText); } - for (int set = 0; set < crossCompileResult.Reflection.ResourceLayouts.Length; set++) + for (int set = 0; set < compilation.Reflection.ResourceLayouts.Length; set++) { - ResourceLayoutDescription layout = crossCompileResult.Reflection.ResourceLayouts[set]; + ResourceLayoutDescription layout = compilation.Reflection.ResourceLayouts[set]; if (layout.Elements.Length == 0) continue; diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs index 9e4b38586f..749df7c3f6 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShaderPart.cs @@ -115,10 +115,10 @@ private string loadFile(byte[]? bytes, bool mainFile) if (!string.IsNullOrEmpty(backbufferCode)) { - string realMainName = "real_main_" + Guid.NewGuid().ToString("N"); + const string real_main_name = "__internal_real_main"; - backbufferCode = backbufferCode.Replace("{{ real_main }}", realMainName); - code = Regex.Replace(code, @"void main\((.*)\)", $"void {realMainName}()") + backbufferCode + '\n'; + backbufferCode = backbufferCode.Replace("{{ real_main }}", real_main_name); + code = Regex.Replace(code, @"void main\((.*)\)", $"void {real_main_name}()") + backbufferCode + '\n'; } } } diff --git a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs index 0f1302a440..1319bddde1 100644 --- a/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs +++ b/osu.Framework/Graphics/Veldrid/VeldridRenderer.cs @@ -572,8 +572,8 @@ protected internal override unsafe Image TakeScreenshot() protected override IShaderPart CreateShaderPart(IShaderStore store, string name, byte[]? rawData, ShaderPartType partType) => new VeldridShaderPart(rawData, partType, store); - protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer) - => new VeldridShader(this, name, parts.Cast().ToArray(), globalUniformBuffer); + protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + => new VeldridShader(this, name, parts.Cast().ToArray(), globalUniformBuffer, compilationStore); public override IFrameBuffer CreateFrameBuffer(RenderBufferFormat[]? renderBufferFormats = null, TextureFilteringMode filteringMode = TextureFilteringMode.Linear) => new VeldridFrameBuffer(this, renderBufferFormats?.ToPixelFormats(), filteringMode.ToSamplerFilter()); diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index 26327e3f2c..d7499466dc 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -942,6 +942,7 @@ protected void SetupRendererAndWindow(IRenderer renderer, GraphicsSurfaceType su Logger.Log($"🖼️ Initialising \"{renderer.GetType().ReadableName().Replace("Renderer", "")}\" renderer with \"{surfaceType}\" surface"); Renderer = renderer; + Renderer.CacheStorage = CacheStorage.GetStorageForDirectory("shaders"); // Prepare window Window = CreateWindow(surfaceType); From cac31ce3dd23abbf2e676e7eb5451a48d55d472d Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Jun 2023 21:50:10 +0900 Subject: [PATCH 2/5] Nullable-annotate GLShader and GLShaderPart --- .../Graphics/OpenGL/Shaders/GLShader.cs | 20 +++++++++---------- .../Graphics/OpenGL/Shaders/GLShaderPart.cs | 8 +++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs index 96fe85ebe2..9a83dc605b 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs @@ -1,11 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; @@ -48,7 +47,7 @@ internal GLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUnifo this.renderer = renderer; this.name = name; this.globalUniformBuffer = globalUniformBuffer; - this.parts = parts.Where(p => p != null).ToArray(); + this.parts = parts; vertexPart = parts.Single(p => p.Type == ShaderType.VertexShader); fragmentPart = parts.Single(p => p.Type == ShaderType.FragmentShader); @@ -225,15 +224,16 @@ public void Dispose() protected virtual void Dispose(bool disposing) { - if (!IsDisposed) - { - IsDisposed = true; + if (IsDisposed) + return; - shaderCompileDelegate?.Cancel(); + IsDisposed = true; - if (programID != -1) - DeleteProgram(this); - } + if (shaderCompileDelegate.IsNotNull()) + shaderCompileDelegate.Cancel(); + + if (programID != -1) + DeleteProgram(this); } #endregion diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs index abc610e91f..3bfe223916 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Globalization; @@ -62,10 +60,10 @@ public GLShaderPart(IRenderer renderer, string name, byte[] data, ShaderType typ shaderCodes[i] = uniform_pattern.Replace(shaderCodes[i], match => $"{match.Groups[1].Value}set = {int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture) + 1}{match.Groups[3].Value}"); } - private string loadFile(byte[] bytes, bool mainFile) + private string loadFile(byte[]? bytes, bool mainFile) { if (bytes == null) - return null; + return string.Empty; using (MemoryStream ms = new MemoryStream(bytes)) using (StreamReader sr = new StreamReader(ms)) @@ -74,7 +72,7 @@ private string loadFile(byte[] bytes, bool mainFile) while (sr.Peek() != -1) { - string line = sr.ReadLine(); + string? line = sr.ReadLine(); if (string.IsNullOrEmpty(line)) { From 1ce6167626390fb4b8b351545ce53f3caaea077b Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Jun 2023 22:45:15 +0900 Subject: [PATCH 3/5] Fix CI inspections --- osu.Framework.Tests/Shaders/TestShaderLoading.cs | 6 +++--- osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Framework.Tests/Shaders/TestShaderLoading.cs b/osu.Framework.Tests/Shaders/TestShaderLoading.cs index fc9b842448..e8a95c08f7 100644 --- a/osu.Framework.Tests/Shaders/TestShaderLoading.cs +++ b/osu.Framework.Tests/Shaders/TestShaderLoading.cs @@ -41,12 +41,12 @@ public void TestFetchExistentShader() private class TestGLRenderer : GLRenderer { protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) - => new TestGLShader(this, name, parts.Cast().ToArray(), compilationStore); + => new TestGLShader(this, name, parts.Cast().ToArray(), globalUniformBuffer, compilationStore); private class TestGLShader : GLShader { - internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, ShaderCompilationStore compilationStore) - : base(renderer, name, parts, null, compilationStore) + internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, globalUniformBuffer, compilationStore) { } diff --git a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs index 3bfe223916..2b4cf48cef 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShaderPart.cs @@ -29,7 +29,7 @@ internal class GLShaderPart : IShaderPart private int partID = -1; - public GLShaderPart(IRenderer renderer, string name, byte[] data, ShaderType type, IShaderStore store) + public GLShaderPart(IRenderer renderer, string name, byte[]? data, ShaderType type, IShaderStore store) { this.renderer = renderer; this.store = store; From 6d2507641c9063fe9be0f49c1b5dbcced4fe1141 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 9 Jun 2023 22:57:59 +0900 Subject: [PATCH 4/5] Fix another CI inspection --- osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs b/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs index bddfcf64f9..78ecb5e99f 100644 --- a/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs +++ b/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs @@ -61,12 +61,12 @@ public void TestShadersLoseReferencesOnManagerDisposal() private class TestGLRenderer : GLRenderer { protected override IShader CreateShader(string name, IShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) - => new TestGLShader(this, name, parts.Cast().ToArray(), compilationStore); + => new TestGLShader(this, name, parts.Cast().ToArray(), globalUniformBuffer, compilationStore); private class TestGLShader : GLShader { - internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, ShaderCompilationStore compilationStore) - : base(renderer, name, parts, null, compilationStore) + internal TestGLShader(GLRenderer renderer, string name, GLShaderPart[] parts, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, globalUniformBuffer, compilationStore) { } From 7efde6b938535d1dd5619fa55622166f95ff4b21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 11 Jun 2023 14:05:06 +0900 Subject: [PATCH 5/5] Update log output to show whether a shader was compiled or cached The previous output was always implying that a compilation happened, even when it wasn't. I also removed the pre-compilation log output, as that was mostly there to gauge performance of shader compiles, which is less of an issue now. If we still want performance metrics in logs, I'd instead consider adding a `Stopwatch` and including the elapsed time in the "loaded" log line. But this is probably unnecessary. --- .../Graphics/Shaders/ComputeProgramCompilation.cs | 5 +++++ .../Graphics/Shaders/ShaderCompilationStore.cs | 6 ++++++ .../Shaders/VertexFragmentShaderCompilation.cs | 5 +++++ .../Graphics/Veldrid/Shaders/VeldridShader.cs | 12 +++++++++--- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs b/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs index 41e63ef6da..128f821763 100644 --- a/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs +++ b/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.cs @@ -7,6 +7,11 @@ namespace osu.Framework.Graphics.Shaders { public class ComputeProgramCompilation { + /// + /// Whether this compilation was retrieved from cache. + /// + public bool WasCached { get; set; } + /// /// The SpirV bytes for the program. /// diff --git a/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs b/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs index c7f291f071..185539f839 100644 --- a/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs +++ b/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs @@ -23,7 +23,10 @@ public VertexFragmentShaderCompilation CompileVertexFragment(string vertexText, string filename = $"{vertexText.ComputeMD5Hash()}#{fragmentText.ComputeMD5Hash()}#{(int)target}"; if (tryGetCached(filename, out VertexFragmentShaderCompilation? existing)) + { + existing.WasCached = true; return existing; + } // Debug preserves names for reflection. byte[] vertexBytes = SpirvCompilation.CompileGlslToSpirv(vertexText, null, ShaderStages.Vertex, new GlslCompileOptions(true)).SpirvBytes; @@ -50,7 +53,10 @@ public ComputeProgramCompilation CompileCompute(string programText, CrossCompile string filename = $"{programText.ComputeMD5Hash()}#{(int)target}"; if (tryGetCached(filename, out ComputeProgramCompilation? existing)) + { + existing.WasCached = true; return existing; + } // Debug preserves names for reflection. byte[] programBytes = SpirvCompilation.CompileGlslToSpirv(programText, null, ShaderStages.Compute, new GlslCompileOptions(true)).SpirvBytes; diff --git a/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs b/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs index 2945c5032b..92a30f61ac 100644 --- a/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs +++ b/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs @@ -7,6 +7,11 @@ namespace osu.Framework.Graphics.Shaders { public class VertexFragmentShaderCompilation { + /// + /// Whether this compilation was retrieved from cache. + /// + public bool WasCached { get; set; } + /// /// The SpirV bytes for the vertex shader. /// diff --git a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs index ff1912d8a6..baba02044a 100644 --- a/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs +++ b/osu.Framework/Graphics/Veldrid/Shaders/VeldridShader.cs @@ -110,8 +110,6 @@ public void BindUniformBlock(string blockName, IUniformBuffer buffer) private void compile() { - Logger.Log($"🖍️ Compiling shader {name}..."); - Debug.Assert(parts.Length == 2); VeldridShaderPart vertex = parts.Single(p => p.Type == ShaderPartType.Vertex); @@ -119,6 +117,8 @@ private void compile() try { + bool cached = true; + vertexShaderDescription = new ShaderDescription( ShaderStages.Vertex, Array.Empty(), @@ -135,6 +135,8 @@ private void compile() fragment.GetRawText(), RuntimeInfo.IsMobile ? CrossCompileTarget.ESSL : CrossCompileTarget.GLSL); + cached &= compilation.WasCached; + if (renderer.SurfaceType == GraphicsSurfaceType.Vulkan) { vertexShaderDescription.ShaderBytes = compilation.VertexBytes; @@ -158,6 +160,8 @@ private void compile() vertex.GetRawText(), fragment.GetRawText(), target); + + cached &= platformCompilation.WasCached; } vertexShaderDescription.ShaderBytes = Encoding.UTF8.GetBytes(platformCompilation.VertexText); @@ -203,7 +207,9 @@ private void compile() } } - Logger.Log($"🖍️ Shader {name} compiled!"); + Logger.Log(cached + ? $"🖍️ Shader {name} loaded from cache!" + : $"🖍️ Shader {name} compiled!"); } catch (SpirvCompilationException e) {