using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; using Avalonia.Styling; using SkiaSharp; using SukiUI.Extensions; namespace SukiUI.Utilities.Effects { /// /// Represents an SKSL shader that SukiUI can handle and pass relevant uniforms into. /// Use the static methods and for creation. /// public class SukiEffect { // Basic uniforms passed into the shader from the CPU. private static readonly string[] Uniforms = { "uniform float iTime;", "uniform float iDark;", "uniform float iAlpha;", "uniform vec3 iResolution;", "uniform vec3 iPrimary;", "uniform vec3 iAccent;", "uniform vec3 iBase;" }; private static readonly List LoadedEffects = new(); private readonly string _rawShaderString; private readonly string _shaderString; /// /// The compiled that will actually be used in draw calls. /// public SKRuntimeEffect Effect { get; } private SukiEffect(string shaderString, string rawShaderString) { _shaderString = shaderString; _rawShaderString = rawShaderString; var compiledEffect = SKRuntimeEffect.Create(_shaderString, out var errors); Effect = compiledEffect ?? throw new ShaderCompilationException(errors); } static SukiEffect() { if (Application.Current.ApplicationLifetime is IControlledApplicationLifetime controlled) controlled.Exit += (_, _) => EnsureDisposed(); } /// /// Attempts to load and compile a ".sksl" shader file from the assembly. /// You don't need to provide the extension. /// The shader will be pre-compiled /// REMEMBER: For files to be discoverable in the assembly they should be marked as an embedded resource. /// /// Name of the shader to load, with or without extension. - MUST BE .sksl /// An instance of a SukiBackgroundShader with the loaded shader. public static SukiEffect FromEmbeddedResource(string shaderName) { shaderName = shaderName.ToLowerInvariant(); if (!shaderName.EndsWith(".sksl")) shaderName += ".sksl"; var assembly = Assembly.GetEntryAssembly(); var resName = assembly!.GetManifestResourceNames() .FirstOrDefault(x => x.ToLowerInvariant().Contains(shaderName)); if (resName is null) { assembly = Assembly.GetExecutingAssembly(); resName = assembly.GetManifestResourceNames() .FirstOrDefault(x => x.ToLowerInvariant().Contains(shaderName)); } if (resName is null) throw new FileNotFoundException( $"Unable to find a file with the name \"{shaderName}\" anywhere in the assembly."); using var tr = new StreamReader(assembly.GetManifestResourceStream(resName)!); return FromString(tr.ReadToEnd()); } /// /// Attempts to compile an sksl shader from a string. /// The shader will be pre-compiled and any errors will be thrown as an exception. /// REMEMBER: For files to be discoverable in the assembly they should be marked as an embedded resource. /// /// The shader code to be compiled. /// An instance of a SukiBackgroundShader with the loaded shader public static SukiEffect FromString(string shaderString) { var sb = new StringBuilder(); foreach (var uniform in Uniforms) sb.AppendLine(uniform); sb.Append(shaderString); var withUniforms = sb.ToString(); return new SukiEffect(withUniforms, shaderString); } private static bool _disposed; /// /// Necessary to make sure all the unmanaged effects are disposed. /// internal static void EnsureDisposed() { if (_disposed) throw new InvalidOperationException( "SukiEffects should only be disposed once at the app lifecycle end."); _disposed = true; foreach (var loaded in LoadedEffects) loaded.Effect.Dispose(); LoadedEffects.Clear(); } public override bool Equals(object obj) { if (obj is not SukiEffect effect) return false; return effect._shaderString == _shaderString; } private static readonly float[] White = { 0.95f, 0.95f, 0.95f }; private readonly float[] _backgroundAlloc = new float[3]; private readonly float[] _backgroundAccentAlloc = new float[3]; private readonly float[] _backgroundPrimaryAlloc = new float[3]; private readonly float[] _boundsAlloc = new float[3]; internal SKShader ToShaderWithUniforms(float timeSeconds, ThemeVariant activeVariant, Rect bounds, float animationScale, float alpha = 1f) { var suki = SukiTheme.GetInstance(); if(suki is null) throw new InvalidOperationException("No Suki Theme Instance is available."); if (suki.ActiveColorTheme is null) throw new InvalidOperationException("No ActiveColorTheme is available."); // Update allocated color arrays. suki.ActiveColorTheme.Background.ToFloatArrayNonAlloc(_backgroundAlloc); suki.ActiveColorTheme.BackgroundAccent.ToFloatArrayNonAlloc(_backgroundAccentAlloc); suki.ActiveColorTheme.BackgroundPrimary.ToFloatArrayNonAlloc(_backgroundPrimaryAlloc); _boundsAlloc[0] = (float)bounds.Width; _boundsAlloc[1] = (float)bounds.Height; var inputs = new SKRuntimeEffectUniforms(Effect) { { "iResolution", _boundsAlloc }, { "iTime", timeSeconds * animationScale }, { "iBase", activeVariant == ThemeVariant.Dark ? _backgroundAlloc : White }, { "iAccent", _backgroundAccentAlloc }, { "iPrimary", _backgroundPrimaryAlloc }, { "iDark", activeVariant == ThemeVariant.Dark ? 1f : 0f }, { "iAlpha", alpha } }; return Effect.ToShader(false, inputs); } internal SKShader ToShaderWithCustomUniforms(Func uniformFactory, float timeSeconds, Rect bounds, float animationScale, float alpha = 1f) { var uniforms = uniformFactory(Effect); uniforms.Add("iResolution", new[] { (float)bounds.Width, (float)bounds.Height, 0f }); uniforms.Add("iTime", timeSeconds * animationScale); uniforms.Add("iAlpha", alpha); return Effect.ToShader(false, uniforms); } /// /// Returns the pure shader string without uniforms. /// public override string ToString() { return _rawShaderString; } private class ShaderCompilationException : Exception { public ShaderCompilationException(string message) : base(message) { } } } }