315 lines
12 KiB
C#
315 lines
12 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
|
|
// Compiles the FX / sprite / particle shaders used by gameplay during the preload
|
|
// screen so the first note (or first VFX / UI blur) does not pay the variant compile
|
|
// cost mid-gameplay.
|
|
//
|
|
// Progress: WarmupRoutine drives compilation across frames and reports a localized
|
|
// progress string ("正在编译着色器 xx%") through the onProgress callback so the caller
|
|
// (preload) can surface it on the loading overlay. When nothing needs compiling
|
|
// (cache valid) it reports the idle default ("加载中...").
|
|
//
|
|
// Persistence: after compiling, a validation manifest is written to a project-local
|
|
// ShaderCache folder (never Application.persistentDataPath / the C: AppData path). On
|
|
// the next load the manifest is compared against the current shader sources + GPU +
|
|
// build. If everything matches, compilation is skipped; if any shader source changed
|
|
// (or the GPU / build changed, invalidating previously compiled variants) the shaders
|
|
// are recompiled and the manifest refreshed.
|
|
public static class GlobalFxShaderWarmer
|
|
{
|
|
public const string IdleLoadingText = "加载中...";
|
|
private const string CompilingTextFormat = "正在编译着色器 {0}%";
|
|
|
|
[Serializable]
|
|
private class ShaderEntry
|
|
{
|
|
public string name;
|
|
public string sourceHash;
|
|
}
|
|
|
|
[Serializable]
|
|
private class WarmManifest
|
|
{
|
|
public string unityVersion;
|
|
public string appVersion;
|
|
public string graphicsDevice;
|
|
public string graphicsDeviceType;
|
|
public ShaderEntry[] shaders;
|
|
}
|
|
|
|
// A shader to warm plus its source path relative to Application.dataPath (the
|
|
// Assets folder). sourcePath may be null for engine/built-in shaders that have no
|
|
// file under Assets; those are still gated by the Unity/GPU/build fingerprint.
|
|
private readonly struct WarmTarget
|
|
{
|
|
public readonly string ShaderName;
|
|
public readonly string SourcePath;
|
|
|
|
public WarmTarget(string shaderName, string sourcePath)
|
|
{
|
|
ShaderName = shaderName;
|
|
SourcePath = sourcePath;
|
|
}
|
|
}
|
|
|
|
// Order is stable and used directly for manifest comparison.
|
|
private static readonly WarmTarget[] Targets =
|
|
{
|
|
// Global full-screen FX blit shaders (noteFunction effects).
|
|
new WarmTarget("Hidden/Bansonic/GlobalGlitch", "Shaders/GlobalGlitch.shader"),
|
|
new WarmTarget("Hidden/Bansonic/GlobalMonochrome", "Shaders/GlobalMonochrome.shader"),
|
|
new WarmTarget("Hidden/Bansonic/GlobalDistortion", "Shaders/GlobalDistortion.shader"),
|
|
new WarmTarget("Hidden/Bansonic/GlobalAfterimage", "Shaders/GlobalAfterimage.shader"),
|
|
|
|
// Explicitly requested particle shaders.
|
|
new WarmTarget("Cartoon FX/Remaster/Particle Ubershader",
|
|
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Ubershader.cfxrshader"),
|
|
new WarmTarget("Mobile/Particles/Additive", null),
|
|
|
|
// Other CFXR particle shaders used across gameplay VFX.
|
|
new WarmTarget("Cartoon FX/Remaster/Particle Procedural Glow",
|
|
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Glow.cfxrshader"),
|
|
new WarmTarget("Cartoon FX/Remaster/Particle Screen Distortion",
|
|
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Distortion.cfxrshader"),
|
|
new WarmTarget("Cartoon FX/Remaster/Particle Procedural Ring",
|
|
"JMO Assets/Cartoon FX Remaster/CFXR Assets/Shaders/CFXR Particle Procedural Ring.cfxrshader"),
|
|
|
|
// Legacy Cartoon FX particle shaders still referenced by prefabs.
|
|
new WarmTarget("Cartoon FX/Legacy/Particles Additive Alpha8",
|
|
"JMO Assets/Cartoon FX (legacy)/Shaders/CFXM_MobileParticleAdd_Alpha8.shader"),
|
|
new WarmTarget("Cartoon FX/Legacy/Particle Multiply Colored",
|
|
"JMO Assets/Cartoon FX (legacy)/Shaders/CFX3 Multiply Color.shader"),
|
|
|
|
// Custom sprite/UI shaders used by gameplay materials.
|
|
new WarmTarget("Bansonic/Sprite Glow", "Shaders/BansonicSpriteGlow.shader"),
|
|
new WarmTarget("Bansonic/Sprite Light Curtain", "Shaders/BansonicSpriteLightCurtain.shader"),
|
|
new WarmTarget("Bansonic/Sprite Vertical Glow Only", "Shaders/BansonicSpriteVerticalGlowOnly.shader"),
|
|
new WarmTarget("Sprites/Outline", "SpriteGlow/Resources/SpriteGlow/Shaders/SpriteOutline.shader"),
|
|
new WarmTarget("Custom/SphereGradientShader", "sphereShaders/SphereGradient.shader"),
|
|
new WarmTarget("UI/Soft Shadow", "Shaders/UI/UIShadow.shader"),
|
|
new WarmTarget("UI/UIGaussianBlur", "Shaders/UI/UIGaussianBlur.shader"),
|
|
new WarmTarget("UI/Bansonic/Blur Behind", "Shaders/UI/BansonicUIBlurBehind.shader")
|
|
};
|
|
|
|
private const string ManifestFolder = "ShaderCache";
|
|
private const string ManifestFileName = "fx_shader_warm_manifest.json";
|
|
|
|
private static bool s_warmed;
|
|
|
|
// Coroutine entry point: warms shaders across frames and reports progress. Safe to
|
|
// call once per process; subsequent calls are no-ops (guarded by s_warmed).
|
|
public static IEnumerator WarmupRoutine(Action<string> onProgress)
|
|
{
|
|
if (s_warmed)
|
|
{
|
|
onProgress?.Invoke(IdleLoadingText);
|
|
yield break;
|
|
}
|
|
s_warmed = true;
|
|
|
|
WarmManifest current = BuildCurrentManifest();
|
|
WarmManifest cached = TryLoadManifest();
|
|
|
|
if (cached != null && ManifestsMatch(cached, current))
|
|
{
|
|
// Shaders (and GPU/build) unchanged: the driver's persistent pipeline cache
|
|
// still holds valid binaries, so skip the redundant compile this process.
|
|
Debug.Log("[GlobalFxShaderWarmer] Cached warm manifest valid; skipping shader recompile.");
|
|
onProgress?.Invoke(IdleLoadingText);
|
|
yield break;
|
|
}
|
|
|
|
RenderTexture temp = null;
|
|
RenderTexture previousActive = RenderTexture.active;
|
|
int count = Targets.Length;
|
|
try
|
|
{
|
|
temp = RenderTexture.GetTemporary(4, 4, 0);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
int percent = Mathf.Clamp(Mathf.RoundToInt((float)i / count * 100f), 0, 100);
|
|
onProgress?.Invoke(string.Format(CompilingTextFormat, percent));
|
|
CompileOne(Targets[i].ShaderName, temp);
|
|
|
|
// Yield so the compile spikes are spread over frames and the progress
|
|
// text can refresh instead of blocking on one long frame.
|
|
yield return null;
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (temp != null)
|
|
RenderTexture.ReleaseTemporary(temp);
|
|
RenderTexture.active = previousActive;
|
|
}
|
|
|
|
SaveManifest(current);
|
|
onProgress?.Invoke(IdleLoadingText);
|
|
Debug.Log(cached == null
|
|
? "[GlobalFxShaderWarmer] No cache found; compiled shaders and wrote manifest."
|
|
: "[GlobalFxShaderWarmer] Shader source or device changed; recompiled shaders and refreshed manifest.");
|
|
}
|
|
|
|
private static void CompileOne(string shaderName, RenderTexture temp)
|
|
{
|
|
Shader shader = Shader.Find(shaderName);
|
|
if (shader == null)
|
|
{
|
|
Debug.LogWarning("[GlobalFxShaderWarmer] Shader not found: " + shaderName);
|
|
return;
|
|
}
|
|
|
|
Material material = CoreUtils.CreateEngineMaterial(shader);
|
|
if (material == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
// Blit pass 0 to force the shader's variant to compile. Even for sprite /
|
|
// particle shaders this triggers program compilation; the throwaway 4x4
|
|
// render is discarded. Use a real source texture so URP's Blit does not
|
|
// warn about a null source.
|
|
Graphics.Blit(Texture2D.blackTexture, temp, material, 0);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning("[GlobalFxShaderWarmer] Warmup blit failed for " + shaderName + ": " + ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
CoreUtils.Destroy(material);
|
|
}
|
|
}
|
|
|
|
private static WarmManifest BuildCurrentManifest()
|
|
{
|
|
var entries = new ShaderEntry[Targets.Length];
|
|
for (int i = 0; i < Targets.Length; i++)
|
|
{
|
|
entries[i] = new ShaderEntry
|
|
{
|
|
name = Targets[i].ShaderName,
|
|
sourceHash = ComputeSourceHash(Targets[i].SourcePath)
|
|
};
|
|
}
|
|
|
|
return new WarmManifest
|
|
{
|
|
unityVersion = Application.unityVersion,
|
|
appVersion = Application.version,
|
|
graphicsDevice = SystemInfo.graphicsDeviceName,
|
|
graphicsDeviceType = SystemInfo.graphicsDeviceType.ToString(),
|
|
shaders = entries
|
|
};
|
|
}
|
|
|
|
// Hashes the .shader source so an edit invalidates the cache. Engine/built-in
|
|
// shaders (sourcePath == null) or a stripped player where the file is absent hash
|
|
// to empty; validity there is gated by the build version + GPU fingerprint.
|
|
private static string ComputeSourceHash(string relativePath)
|
|
{
|
|
if (string.IsNullOrEmpty(relativePath))
|
|
return string.Empty;
|
|
|
|
try
|
|
{
|
|
string full = Path.Combine(Application.dataPath, relativePath);
|
|
if (!File.Exists(full))
|
|
return string.Empty;
|
|
|
|
byte[] bytes = File.ReadAllBytes(full);
|
|
using (var sha = SHA256.Create())
|
|
{
|
|
byte[] hash = sha.ComputeHash(bytes);
|
|
var sb = new StringBuilder(hash.Length * 2);
|
|
foreach (byte b in hash)
|
|
sb.Append(b.ToString("x2"));
|
|
return sb.ToString();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to hash shader source '" + relativePath + "': " + ex.Message);
|
|
return string.Empty;
|
|
}
|
|
}
|
|
|
|
private static bool ManifestsMatch(WarmManifest a, WarmManifest b)
|
|
{
|
|
if (a == null || b == null)
|
|
return false;
|
|
|
|
if (a.unityVersion != b.unityVersion
|
|
|| a.appVersion != b.appVersion
|
|
|| a.graphicsDevice != b.graphicsDevice
|
|
|| a.graphicsDeviceType != b.graphicsDeviceType)
|
|
return false;
|
|
|
|
if (a.shaders == null || b.shaders == null || a.shaders.Length != b.shaders.Length)
|
|
return false;
|
|
|
|
for (int i = 0; i < a.shaders.Length; i++)
|
|
{
|
|
ShaderEntry ea = a.shaders[i];
|
|
ShaderEntry eb = b.shaders[i];
|
|
if (ea == null || eb == null)
|
|
return false;
|
|
if (ea.name != eb.name || ea.sourceHash != eb.sourceHash)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private static string GetManifestPath()
|
|
{
|
|
// Project-local folder only. Deliberately not Application.persistentDataPath
|
|
// (that lives under C:/Users/.../AppData) per the requirement to stay off C:.
|
|
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
|
string dir = Path.Combine(projectRoot, ManifestFolder);
|
|
Directory.CreateDirectory(dir);
|
|
return Path.Combine(dir, ManifestFileName);
|
|
}
|
|
|
|
private static WarmManifest TryLoadManifest()
|
|
{
|
|
try
|
|
{
|
|
string path = GetManifestPath();
|
|
if (!File.Exists(path))
|
|
return null;
|
|
|
|
string json = File.ReadAllText(path);
|
|
if (string.IsNullOrEmpty(json))
|
|
return null;
|
|
|
|
return JsonUtility.FromJson<WarmManifest>(json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to read warm manifest: " + ex.Message);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static void SaveManifest(WarmManifest manifest)
|
|
{
|
|
try
|
|
{
|
|
string path = GetManifestPath();
|
|
string json = JsonUtility.ToJson(manifest, true);
|
|
File.WriteAllText(path, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning("[GlobalFxShaderWarmer] Failed to write warm manifest: " + ex.Message);
|
|
}
|
|
}
|
|
}
|