#nullable enable using System; using T3.Core.DataTypes; using T3.Core.Operator.Interfaces; using T3.Core.Operator.Slots; using T3.Core.Resource; using T3.Core.Stats; using T3.Core.Utils; using ShaderCompiler = T3.Core.Resource.ShaderCompiling.ShaderCompiler; namespace T3.Core.Operator; /// /// An interface for shader operators (PixelShader, VertexShader, etc.) /// Shader updating can be complex, so this interface is used to provide the common functionality for all of them /// /// The type of shader (i.e. T3.Core.DataTypes.PixelShader) public interface IShaderCodeOperator where T : AbstractShader { public InputSlot Code { get; } public Slot ShaderSlot { get; } public InputSlot EntryPoint { get; } public InputSlot DebugName { get; } public void SetWarning(string message); void Initialize() { Action invalidateShader = context => ShaderSlot.DirtyFlag.Invalidate(); Code.UpdateAction += invalidateShader; EntryPoint.UpdateAction += invalidateShader; ShaderSlot.UpdateAction += UpdateShader; } private void UpdateShader(EvaluationContext context) { // cache interface values to avoid additional virtual method calls var sourceSlot = Code; var entryPointSlot = EntryPoint; var debugNameSlot = DebugName; var debugName = debugNameSlot.GetValue(context); var shaderSlot = ShaderSlot; var currentShader = shaderSlot.Value; if (currentShader != null) currentShader.Name = debugName ?? string.Empty; if (!sourceSlot.DirtyFlag.IsDirty && !entryPointSlot.DirtyFlag.IsDirty) { return; } var previousSource = Code.Value; var newSource = sourceSlot.GetValue(context); if(previousSource==newSource) return; var entryPoint = entryPointSlot.GetValue(context); var instance = sourceSlot.Parent; if (string.IsNullOrWhiteSpace(newSource)) { SetWarning("Shader code is empty"); return; } if (string.IsNullOrWhiteSpace(entryPoint)) { SetWarning("Entry point is empty"); return; } //Log.Debug($"Attempting to update shader \"{debugName}\" ({GetType().Name}) with entry point \"{entryPoint}\"."); if (string.IsNullOrWhiteSpace(debugName)) { debugName = $"{typeof(T).Name}"; } var compilationArgs = new ShaderCompiler.ShaderCompilationArgs( SourceCode: newSource, EntryPoint: entryPoint, Owner: instance, Name: debugName, OldBytecode: currentShader?.CompiledBytecode); var compiled = ShaderCompiler.TryCompileShaderFromSource(compilationArgs, true, true, out currentShader, out var errorMessage); shaderSlot.Value = currentShader!; shaderSlot.DirtyFlag.Clear(); if (!compiled) { instance.LogErrorState(errorMessage); } else { instance.ClearErrorState(); currentShader!.Name = debugName; } SetWarning(errorMessage); } } public interface IShaderOperator : IDescriptiveFilename where T : AbstractShader { public InputSlot Path { get; } InputSlot IDescriptiveFilename.SourcePathSlot => Path; public Slot ShaderSlot { get; } public InputSlot EntryPoint { get; } public InputSlot DebugName { get; } public void SetWarning(string message); protected string CachedEntryPoint { get; set; } void OnShaderUpdate(EvaluationContext context, T? shader); /// /// Outputs derived from the compiled shader (e.g. a compute shader's thread-group count) that must be /// re-invalidated when the shader file recompiles. A live recompile force-invalidates only the resource's /// dependent slots, so a second derived output must be listed here — otherwise downstream consumers keep /// the stale value until the graph is invalidated some other way (reconnect, restart). /// protected ISlot[]? ShaderDerivedOutputs => null; IResourceConsumer Instance => ShaderSlot.Parent; void Initialize() { var resource = ResourceManager.CreateShaderResource(Path, () => CachedEntryPoint); var shaderSlot = ShaderSlot; resource.AddDependentSlots(shaderSlot); var derivedOutputs = ShaderDerivedOutputs; if (derivedOutputs != null) resource.AddDependentSlots(derivedOutputs); shaderSlot.UpdateAction = context => { if (EntryPoint.DirtyFlag.IsDirty) { // we still need to recompile if the entrypoint changes resource.MarkFileAsChanged(); } CachedEntryPoint = EntryPoint.GetValue(context) ?? string.Empty; // This is a hack to invalidate the input so that the op doesn't stay dirtly _ = DebugName.GetValue(context); var shader = resource.GetValue(context); ShaderSlot.Value = shader!; OnShaderUpdate(context, shader); }; } }