diff --git a/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs b/osu.Framework.Tests/Shaders/TestSceneShaderDisposal.cs index 3b44a403fe..78ecb5e99f 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(), globalUniformBuffer, 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, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, globalUniformBuffer, compilationStore) { } diff --git a/osu.Framework.Tests/Shaders/TestShaderLoading.cs b/osu.Framework.Tests/Shaders/TestShaderLoading.cs index f8299917c9..e8a95c08f7 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(), globalUniformBuffer, 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, IUniformBuffer globalUniformBuffer, ShaderCompilationStore compilationStore) + : base(renderer, name, parts, globalUniformBuffer, 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..9a83dc605b 100644 --- a/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs +++ b/osu.Framework/Graphics/OpenGL/Shaders/GLShader.cs @@ -1,12 +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 System.Text; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Shaders; @@ -42,14 +40,14 @@ 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; 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); @@ -59,9 +57,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 +156,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 +174,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; @@ -226,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 80be4026f8..2b4cf48cef 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; @@ -31,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; @@ -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)) { @@ -123,10 +121,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..128f821763 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/ComputeProgramCompilation.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 Veldrid.SPIRV; + +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. + /// + 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..185539f839 --- /dev/null +++ b/osu.Framework/Graphics/Shaders/ShaderCompilationStore.cs @@ -0,0 +1,139 @@ +// 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)) + { + existing.WasCached = true; + 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)) + { + existing.WasCached = true; + 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..92a30f61ac --- /dev/null +++ b/osu.Framework/Graphics/Shaders/VertexFragmentShaderCompilation.cs @@ -0,0 +1,40 @@ +// 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 + { + /// + /// Whether this compilation was retrieved from cache. + /// + public bool WasCached { get; set; } + + /// + /// 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..baba02044a 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. @@ -108,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); @@ -117,6 +117,8 @@ private void compile() try { + bool cached = true; + vertexShaderDescription = new ShaderDescription( ShaderStages.Vertex, Array.Empty(), @@ -128,19 +130,21 @@ 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); + cached &= compilation.WasCached; + 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 +156,21 @@ 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); + + cached &= platformCompilation.WasCached; } - 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; @@ -201,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) { 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);