mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-14 19:29:36 +01:00
Automatic Partial Shader Reloading (#1005)
* merged in shader automatic reloading, remove internal shader reloading add support for includes to shader reloading, modified includes reload shaders that depends upon them * add some fault tolerance to allow playing with the file a bit * prevent multiple watch attempts * fix spacing * revert project change * add cancellation to Reload with a default of 30s fix case sensitivity comparison assume windows & mac are case insensitive
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Robust.Client.Input;
|
||||
using System.Threading;
|
||||
using Robust.Client.Interfaces;
|
||||
using Robust.Client.Interfaces.Console;
|
||||
using Robust.Client.Interfaces.Debugging;
|
||||
@@ -23,6 +25,7 @@ using Robust.Client.State.States;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.GameObjects.Components.Transform;
|
||||
using Robust.Shared.Interfaces.GameObjects;
|
||||
@@ -735,25 +738,178 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal class ReloadShaders : IConsoleCommand
|
||||
internal class ReloadShadersCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "rldshader";
|
||||
|
||||
public string Description => "Reloads all shaders";
|
||||
|
||||
public string Help => "rldshader";
|
||||
|
||||
public static Dictionary<string, FileSystemWatcher> _watchers;
|
||||
|
||||
public static ConcurrentDictionary<string, bool> _reloadShadersQueued = new ConcurrentDictionary<string, bool>();
|
||||
|
||||
public bool Execute(IDebugConsole console, params string[] args)
|
||||
{
|
||||
var resC = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
var paths = resC.GetAllResources<ShaderSourceResource>().Select(p => p.Key).ToList();
|
||||
|
||||
foreach (var path in paths)
|
||||
IResourceCache resC;
|
||||
if (args.Length == 1)
|
||||
{
|
||||
resC.ReloadResource<ShaderSourceResource>(path);
|
||||
if (args[0] == "+watch")
|
||||
{
|
||||
if (_watchers != null)
|
||||
{
|
||||
console.AddLine("Already watching.");
|
||||
return false;
|
||||
}
|
||||
resC = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
_watchers = new Dictionary<string, FileSystemWatcher>();
|
||||
|
||||
var stringComparer = PathHelpers.IsFileSystemCaseSensitive()
|
||||
? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase;
|
||||
|
||||
var reversePathResolution = new ConcurrentDictionary<string, HashSet<ResourcePath>>(stringComparer);
|
||||
|
||||
var syncCtx = SynchronizationContext.Current;
|
||||
|
||||
var shaderCount = 0;
|
||||
var created = 0;
|
||||
var dirs = new ConcurrentDictionary<string, SortedSet<string>>(stringComparer);
|
||||
foreach (var (path, src) in resC.GetAllResources<ShaderSourceResource>())
|
||||
{
|
||||
if (!resC.TryGetDiskFilePath(path, out var fullPath))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
reversePathResolution.GetOrAdd(fullPath, _ => new HashSet<ResourcePath>()).Add(path);
|
||||
|
||||
var dir = Path.GetDirectoryName(fullPath);
|
||||
var fileName = Path.GetFileName(fullPath);
|
||||
dirs.GetOrAdd(dir, _ => new SortedSet<string>(stringComparer))
|
||||
.Add(fileName);
|
||||
|
||||
foreach (var inc in src.ParsedShader.Includes)
|
||||
{
|
||||
if (!resC.TryGetDiskFilePath(inc, out var incFullPath))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
reversePathResolution.GetOrAdd(incFullPath, _ => new HashSet<ResourcePath>()).Add(path);
|
||||
|
||||
var incDir = Path.GetDirectoryName(incFullPath);
|
||||
var incFileName = Path.GetFileName(incFullPath);
|
||||
dirs.GetOrAdd(incDir, _ => new SortedSet<string>(stringComparer))
|
||||
.Add(incFileName);
|
||||
}
|
||||
|
||||
++shaderCount;
|
||||
}
|
||||
|
||||
foreach (var (dir, files) in dirs)
|
||||
{
|
||||
if (_watchers.TryGetValue(dir, out var watcher))
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
watcher = new FileSystemWatcher(dir);
|
||||
watcher.Changed += (_, ev) =>
|
||||
{
|
||||
if (_reloadShadersQueued.TryAdd(ev.FullPath, true))
|
||||
{
|
||||
syncCtx.Post(o =>
|
||||
{
|
||||
var changed = (FileSystemEventArgs) o;
|
||||
var resPaths = reversePathResolution[changed.FullPath];
|
||||
foreach (var resPath in resPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
IoCManager.Resolve<IResourceCache>()
|
||||
.ReloadResource<ShaderSourceResource>(resPath);
|
||||
console.AddLine($"Reloaded shader: {resPath}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
console.AddLine($"Failed to reload shader: {resPath}");
|
||||
}
|
||||
|
||||
_reloadShadersQueued.TryRemove(changed.FullPath, out var _);
|
||||
}
|
||||
}, ev);
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
watcher.Filters.Add(file);
|
||||
}
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
|
||||
_watchers.Add(dir, watcher);
|
||||
++created;
|
||||
}
|
||||
|
||||
console.AddLine($"Created {created} shader directory watchers for {shaderCount} shaders.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (args[0] == "-watch")
|
||||
{
|
||||
if (_watchers == null)
|
||||
{
|
||||
console.AddLine("No shader directory watchers active.");
|
||||
return false;
|
||||
}
|
||||
|
||||
var disposed = 0;
|
||||
foreach (var (_, watcher) in _watchers)
|
||||
{
|
||||
++disposed;
|
||||
watcher.Dispose();
|
||||
}
|
||||
|
||||
_watchers = null;
|
||||
|
||||
console.AddLine($"Disposed of {disposed} shader directory watchers.");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
console.AddLine("Not implemented.");
|
||||
return false;
|
||||
}
|
||||
|
||||
console.AddLine("Reloading content shader resources...");
|
||||
|
||||
resC = IoCManager.Resolve<IResourceCache>();
|
||||
|
||||
foreach (var (path, _) in resC.GetAllResources<ShaderSourceResource>())
|
||||
{
|
||||
try
|
||||
{
|
||||
resC.ReloadResource<ShaderSourceResource>(path);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
console.AddLine($"Failed to reload shader: {path}");
|
||||
}
|
||||
}
|
||||
|
||||
console.AddLine("Done.");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class ClydeDebugLayerCommand : IConsoleCommand
|
||||
|
||||
@@ -7,6 +7,7 @@ using Robust.Client.Graphics.ClientEye;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.Interfaces.Graphics.ClientEye;
|
||||
using Robust.Client.ResourceManagement.ResourceTypes;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Maths;
|
||||
using static Robust.Client.GameObjects.ClientOccluderComponent;
|
||||
@@ -172,10 +173,19 @@ namespace Robust.Client.Graphics.Clyde
|
||||
|
||||
ClydeHandle LoadShaderHandle(string path)
|
||||
{
|
||||
var shaderSource = _resourceCache.GetResource<ShaderSourceResource>(path);
|
||||
return shaderSource.ClydeHandle;
|
||||
try
|
||||
{
|
||||
var shaderSource = _resourceCache.GetResource<ShaderSourceResource>(path);
|
||||
return shaderSource.ClydeHandle;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning($"Can't load shader {path}\n{ex.GetType().Name}: {ex.Message}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
_lightShaderHandle = LoadShaderHandle("/Shaders/Internal/light.swsl");
|
||||
_fovShaderHandle = LoadShaderHandle("/Shaders/Internal/fov.swsl");
|
||||
_fovLightShaderHandle = LoadShaderHandle("/Shaders/Internal/fov-lighting.swsl");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.Shaders
|
||||
{
|
||||
@@ -9,7 +10,7 @@ namespace Robust.Client.Graphics.Shaders
|
||||
public ParsedShader(IReadOnlyDictionary<string, ShaderUniformDefinition> uniforms,
|
||||
IReadOnlyDictionary<string, ShaderVaryingDefinition> varyings,
|
||||
IReadOnlyDictionary<string, ShaderConstantDefinition> constants, IList<ShaderFunctionDefinition> functions,
|
||||
ShaderLightMode lightMode, ShaderBlendMode blendMode, ShaderPreset preset)
|
||||
ShaderLightMode lightMode, ShaderBlendMode blendMode, ShaderPreset preset, ICollection<ResourcePath> includes)
|
||||
{
|
||||
Uniforms = uniforms;
|
||||
Varyings = varyings;
|
||||
@@ -17,6 +18,7 @@ namespace Robust.Client.Graphics.Shaders
|
||||
LightMode = lightMode;
|
||||
BlendMode = blendMode;
|
||||
Preset = preset;
|
||||
Includes = includes;
|
||||
Constants = constants;
|
||||
}
|
||||
|
||||
@@ -27,6 +29,8 @@ namespace Robust.Client.Graphics.Shaders
|
||||
public ShaderLightMode LightMode { get; }
|
||||
public ShaderBlendMode BlendMode { get; }
|
||||
public ShaderPreset Preset { get; }
|
||||
public ICollection<ResourcePath> Includes { get; }
|
||||
|
||||
}
|
||||
|
||||
internal sealed class ShaderFunctionDefinition
|
||||
|
||||
@@ -138,8 +138,8 @@ namespace Robust.Client.Graphics.Shaders
|
||||
_currentParser.Take(); // Quote.
|
||||
|
||||
var pathString = new string(pathParsing.ToArray());
|
||||
|
||||
var path = new ResourcePath(pathString);
|
||||
_includes.AddLast(path);
|
||||
using var stream = _resManager.ContentFileRead(path);
|
||||
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Interfaces.Resources;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Client.Graphics.Shaders
|
||||
{
|
||||
@@ -18,6 +19,7 @@ namespace Robust.Client.Graphics.Shaders
|
||||
private readonly List<ShaderConstantDefinition> _constantsParsing = new List<ShaderConstantDefinition>();
|
||||
private readonly List<ShaderVaryingDefinition> _varyingsParsing = new List<ShaderVaryingDefinition>();
|
||||
private readonly List<ShaderFunctionDefinition> _functionsParsing = new List<ShaderFunctionDefinition>();
|
||||
private readonly LinkedList<ResourcePath> _includes = new LinkedList<ResourcePath>();
|
||||
|
||||
public static ParsedShader Parse(TextReader reader, IResourceManager resManager)
|
||||
{
|
||||
@@ -188,7 +190,8 @@ namespace Robust.Client.Graphics.Shaders
|
||||
_functionsParsing,
|
||||
lightMode ?? ShaderLightMode.Default,
|
||||
blendMode ?? ShaderBlendMode.Mix,
|
||||
preset ?? ShaderPreset.Default);
|
||||
preset ?? ShaderPreset.Default,
|
||||
_includes);
|
||||
}
|
||||
|
||||
private void _parseFunction()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -28,7 +29,7 @@ namespace Robust.Client.ResourceManagement
|
||||
/// <param name="path">Path of the resource requested on the VFS.</param>
|
||||
public abstract void Load(IResourceCache cache, ResourcePath path);
|
||||
|
||||
public virtual void Reload(IResourceCache cache, ResourcePath path)
|
||||
public virtual void Reload(IResourceCache cache, ResourcePath path, CancellationToken ct = default)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Graphics.Shaders;
|
||||
using Robust.Client.Interfaces.Graphics;
|
||||
using Robust.Client.Interfaces.ResourceManagement;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -28,12 +30,30 @@ namespace Robust.Client.ResourceManagement.ResourceTypes
|
||||
ClydeHandle = clyde.LoadShader(ParsedShader, path.ToString());
|
||||
}
|
||||
|
||||
public override void Reload(IResourceCache cache, ResourcePath path)
|
||||
public override void Reload(IResourceCache cache, ResourcePath path, CancellationToken ct = default)
|
||||
{
|
||||
using (var stream = cache.ContentFileRead(path))
|
||||
using (var reader = new StreamReader(stream, EncodingHelpers.UTF8))
|
||||
ct = ct != default ? ct : new CancellationTokenSource(30000).Token;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
ParsedShader = ShaderParser.Parse(reader, cache);
|
||||
try
|
||||
{
|
||||
using var stream = cache.ContentFileRead(path);
|
||||
using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
|
||||
ParsedShader = ShaderParser.Parse(reader, cache);
|
||||
break;
|
||||
}
|
||||
catch (IOException ioe)
|
||||
{
|
||||
if (!PathHelpers.IsFileInUse(ioe))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Thread.Sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
var clyde = IoCManager.Resolve<IClydeInternal>();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
{
|
||||
@@ -60,5 +61,23 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsFileInUse(IOException exception)
|
||||
{
|
||||
var errorCode = exception.HResult & 0xFFFF;
|
||||
return errorCode switch
|
||||
{
|
||||
// TODO: verify works on non-win systems
|
||||
32 => /* sharing not allowed */ true,
|
||||
33 => /* file is locked */ true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: gaf
|
||||
public static bool IsFileSystemCaseSensitive() =>
|
||||
!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
|
||||
&& !RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user