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:
Tyler Young
2020-02-28 00:27:35 -05:00
committed by GitHub
parent 3e62e6d1f6
commit 38aeca2342
8 changed files with 231 additions and 18 deletions

View File

@@ -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

View File

@@ -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");

View File

@@ -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

View File

@@ -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);

View File

@@ -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()

View File

@@ -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)
{
}

View File

@@ -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>();

View File

@@ -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);
}
}