using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using DynamicData; using Intromat.Graphics; using Intromat.Model.Compiler; using Intromat.Nodes.Code; using Intromat.PersistentModel; using Intromat.ViewModels; using Intromat.ViewModels.Editors; using Intromat.Views; using NodeNetwork.ViewModels; using ReactiveUI; using SharpDX; using SharpDX.D3DCompiler; using SharpDX.Direct3D; using SharpDX.Direct3D11; using Splat; using Buffer = SharpDX.Direct3D11.Buffer; namespace Intromat.Nodes.Textures { public class ShaderTextureNode : DxTextureNodeBase { private readonly List _constantBuffers = new(); private ComputeShader? _cs; private byte[]? _byteCode; private TextureValue[]? _customOutputs; private readonly List<(NodeInputViewModel, InputDesc)> _customInputs = new(); private readonly EndpointGroup _group; private readonly Dictionary _inputValues = new(); public record InputDesc(int ConstantBufferIndex, int Offset, Type VariableType, string Name); static ShaderTextureNode() { Locator.CurrentMutable.Register(() => new CodeGenNodeView(), typeof(IViewFor)); } public ShaderTextureNode() { Name = "Shader Texture"; _group = new EndpointGroup("Shader Texture"); Inputs.Add(Source = new CodeGenInputViewModel>(EPortType.String) { Name = "Source", Group = _group, Editor = SourceEditor }); var dxHost = Locator.Current.GetService()!; Source.ValueChanged .ObserveOn(dxHost.RenderDispatcher) .Subscribe(_ => { _resourcesCreated = false; UpdateFrame(dxHost.Device, dxHost.Device.ImmediateContext); }); } public override NodeModelBase CreateModel() { return new ShaderTextureModel(); } public override void SaveModel(NodeModelBase model) { base.SaveModel(model); var shaderTexture = (ShaderTextureModel)model; shaderTexture.Source = SourceEditor.CreateModel(); shaderTexture.InputValues = _customInputs .Where(i => i.Item1.Editor != null) .Select(i => new LiteralModelEntry() { Key = i.Item1.Name, Literal = CreateLiteralModel((dynamic)i.Item1.Editor) }) .ToArray(); } private TModel CreateLiteralModel(ExpressionEditorBaseViewModel editor) where TValue : LiteralValueBase, new() where TModel : LiteralModelBase, new() { return editor.CreateModel(); } public override void LoadModel(NodeModelBase model) { base.LoadModel(model); var shaderTexture = (ShaderTextureModel)model; SourceEditor.LoadModel(shaderTexture.Source); foreach (var inputValue in shaderTexture.InputValues) { _inputValues[inputValue.Key] = inputValue.Literal; } var dxHost = Locator.Current.GetService()!; _resourcesCreated = true; CreateDeviceResources(dxHost.Device); } protected override void CreateDeviceResources(Device device) { foreach (var cb in _constantBuffers) cb.Dispose(); _constantBuffers.Clear(); var flags = ShaderFlags.None; #if DEBUG flags |= ShaderFlags.Debug; #endif var errors = new List(); var shader = Source.Value.Evaluate(); if (string.IsNullOrEmpty(shader)) { errors.Add("Shader is empty"); } else { try { var compilationResult = ShaderBytecode.Compile(shader, "main", "cs_5_0", flags); if (compilationResult.HasErrors) { errors.Add(compilationResult.Message); } else { _byteCode = compilationResult.Bytecode.Data; var reflection = new ShaderReflection(_byteCode); var newInputs = new List(); var newOutputs = new List(); for (int i = 0; i < reflection.Description.BoundResources; ++i) { var resBindingDesc = reflection.GetResourceBindingDescription(i); switch (resBindingDesc.Type) { case ShaderInputType.ConstantBuffer: { var cb = reflection.GetConstantBuffer(resBindingDesc.Name); var cbIndex = resBindingDesc.BindPoint; var cbSize = cb.Description.Size; var bufferDesc = new BufferDescription(cbSize, ResourceUsage.Dynamic, BindFlags.ConstantBuffer, CpuAccessFlags.Write, ResourceOptionFlags.None, 0); _constantBuffers.Add(new Buffer(device, bufferDesc)); for (int j = 0; j < cb.Description.VariableCount; ++j) { var variable = cb.GetVariable(j); var varTypeDesc = variable.GetVariableType() .Description; var type = varTypeDesc.Type switch { ShaderVariableType.Float => typeof(float), ShaderVariableType.Int => typeof(int), ShaderVariableType.Bool => typeof(bool), _ => null }; if (type == null) { errors.Add($"Constant buffer {resBindingDesc.Name} at slot {cbIndex} has a variable {variable.Description.Name} with unsupported type {varTypeDesc.Type}"); continue; } for (int k = 0; k < varTypeDesc.ColumnCount; ++k) { var suffix = "" + 'X' + k; var inputName = varTypeDesc.ColumnCount == 1 ? variable.Description.Name : $"{variable.Description.Name}.{suffix}"; newInputs.Add(new(cbIndex, variable.Description.StartOffset + k * variable.Description.Size, type, inputName)); } } break; } case ShaderInputType.Texture: { var texName = resBindingDesc.Name; newInputs.Add(new(-1, resBindingDesc.BindPoint, typeof(TextureValue), texName)); break; } case ShaderInputType.Sampler: { var texName = resBindingDesc.Name; newInputs.Add(new(-1, resBindingDesc.BindPoint, typeof(SamplerLiteralValue), texName)); break; } case ShaderInputType.UnorderedAccessViewRWTyped: { var texName = resBindingDesc.Name; if (resBindingDesc.Dimension != ShaderResourceViewDimension.Texture2D) { errors.Add($"UAV {texName} at slot {resBindingDesc.BindPoint} has unsupported dimension {resBindingDesc.Dimension}"); break; } newOutputs.Add(new(-1, resBindingDesc.BindPoint, typeof(TextureValue), texName)); break; } default: { errors.Add($"Resource {resBindingDesc.Name} at slot {resBindingDesc.BindPoint} has unsupported type {resBindingDesc.Type}"); break; } } } if (newOutputs.Count == 0) { errors.Add("The shader doesn't contain any UAV suitable for the texture output"); } Inputs.Edit(inputs => { var maxSamplerStateIndex = 0; for (var i = _customInputs.Count - 1; i >= 0; --i) { var existingInput = _customInputs[i]; if (newInputs.All(inputDesc => inputDesc.Name != existingInput.Item1.Name)) { inputs.Remove(existingInput.Item1); _customInputs.RemoveAt(i); } } foreach (var newInputDesc in newInputs) { if (_customInputs.Any(i => i.Item2 == newInputDesc)) continue; NodeInputViewModel newInput; if (newInputDesc.VariableType == typeof(float)) { newInput = new CodeGenInputViewModel>(EPortType.Float) { Name = newInputDesc.Name, Group = _group, Editor = new FloatExpressionEditorViewModel { MaxValue = 1 } }; if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is FloatLiteralModel model) ((FloatExpressionEditorViewModel)newInput.Editor).LoadModel(model); } else if (newInputDesc.VariableType == typeof(int)) { newInput = new CodeGenInputViewModel>(EPortType.Integer) { Name = newInputDesc.Name, Group = _group, Editor = new IntegerExpressionEditorViewModel { MaxValue = 100 } }; if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is IntLiteralModel model) ((IntegerExpressionEditorViewModel)newInput.Editor).LoadModel(model); } else if (newInputDesc.VariableType == typeof(bool)) { newInput = new CodeGenInputViewModel>(EPortType.Boolean) { Name = newInputDesc.Name, Group = _group, Editor = new BooleanExpressionEditorViewModel() }; if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is BooleanLiteralModel model) ((BooleanExpressionEditorViewModel)newInput.Editor).LoadModel(model); } else if (newInputDesc.VariableType == typeof(TextureValue)) { newInput = new CodeGenInputViewModel(EPortType.Texture) { Name = newInputDesc.Name }; if (newInputDesc.Offset == 0) { var inputObservable = ((CodeGenInputViewModel)newInput).ValueChanged; WidthEditor.InputValueProvider = inputObservable.Select(input => input?.Width ?? 0); HeightEditor.InputValueProvider = inputObservable.Select(input => input?.Height ?? 0); } } else if (newInputDesc.VariableType == typeof(SamplerLiteralValue)) { maxSamplerStateIndex = Math.Max(maxSamplerStateIndex, newInputDesc.Offset); newInput = new CodeGenInputViewModel>(EPortType.None) { Name = newInputDesc.Name, Group = _group, Editor = new SamplerEditorViewModel { CustomValue = new SamplerDesc() } }; if (_inputValues.TryGetValue(newInputDesc.Name, out var inputValue) && inputValue is SamplerLiteralModel model) ((SamplerEditorViewModel)newInput.Editor).LoadModel(model); } else { continue; } _customInputs.Add((newInput, newInputDesc)); inputs.Add(newInput); } }); Outputs.Edit(outputs => { if (outputs.Count > newOutputs.Count) { if (newOutputs.Count == 0) { if (outputs.Count > 1) outputs.RemoveRange(1, outputs.Count - 1); outputs[0] .Name = "Output"; } else { outputs.RemoveRange(newOutputs.Count, outputs.Count - newOutputs.Count); } } var outputIndex = 0; if (outputIndex < newOutputs.Count) for (; outputIndex < outputs.Count; ++outputIndex) outputs[outputIndex] .Name = newOutputs[outputIndex] .Name; for (; outputIndex < newOutputs.Count; ++outputIndex) { var newOutputDesc = newOutputs[outputIndex]; NodeOutputViewModel newOutput; if (newOutputDesc.VariableType == typeof(TextureValue)) newOutput = new CodeGenOutputViewModel(EPortType.Texture) { Name = newOutputDesc.Name }; else continue; outputs.Add(newOutput); } _customOutputs = new TextureValue[outputs.Count]; }); } } catch (CompilationException e) { errors.Add(e.Message); } } _cs?.Dispose(); _cs = errors.Count == 0 ? new ComputeShader(device, _byteCode) : null; Errors.Edit(e => { e.Clear(); e.AddRange(errors); }); } protected override unsafe void UpdateFrame(Device device, DeviceContext context) { if (!_resourcesCreated) { _resourcesCreated = true; var dxHost = Locator.Current.GetService()!; dxHost.MainDispatcher!.Invoke(() => CreateDeviceResources(device)); } EnsureBuffers(device); if (_customOutputs == null || _cs == null) return; _customOutputs[0] = _output; for (int i = 1; i < Outputs.Count; ++i) { EnsureTexture(device, ref _customOutputs![i]); } var cbData = _constantBuffers.Select(cb => { context.MapSubresource(cb, 0, MapMode.WriteDiscard, MapFlags.None, out var stream); return stream.DataPointer; }).ToArray(); int maxSrvIndex = 0; foreach (var input in _customInputs) { var desc = input.Item2; if (desc.ConstantBufferIndex >= 0) { var dataPtr = cbData[desc.ConstantBufferIndex] + desc.Offset; if (desc.VariableType == typeof(float)) { _inputValues[input.Item1.Name] = ((FloatExpressionEditorViewModel)input.Item1.Editor).CreateModel(); *(float*)dataPtr = ((CodeGenInputViewModel>)input.Item1).Value.Evaluate(); } else if (desc.VariableType == typeof(int)) { _inputValues[input.Item1.Name] = ((IntegerExpressionEditorViewModel)input.Item1.Editor).CreateModel(); *(int*)dataPtr = ((CodeGenInputViewModel>)input.Item1).Value.Evaluate(); } else if (desc.VariableType == typeof(bool)) { _inputValues[input.Item1.Name] = ((BooleanExpressionEditorViewModel)input.Item1.Editor).CreateModel(); *(bool*)dataPtr = ((CodeGenInputViewModel>)input.Item1).Value.Evaluate(); } } else { if (desc.VariableType == typeof(TextureValue)) { maxSrvIndex = Math.Max(maxSrvIndex, desc.Offset); context.ComputeShader.SetShaderResource(desc.Offset, ((CodeGenInputViewModel)input.Item1).Value?.ShaderResourceView); } else if (desc.VariableType == typeof(SamplerLiteralValue)) { _inputValues[input.Item1.Name] = ((SamplerEditorViewModel)input.Item1.Editor).CreateModel(); var samplerValue = (SamplerLiteralValue)((CodeGenInputViewModel>)input.Item1).Value; EnsureSamplerState(device, ref samplerValue); context.ComputeShader.SetSampler(desc.Offset, samplerValue.SamplerState); } } } for (var i = 0; i < _customOutputs!.Length; i++) { var output = _customOutputs![i]; context.ComputeShader.SetUnorderedAccessView(i, output.UnorderedAccessView); } for (int i = 0; i < _constantBuffers.Count; ++i) { context.UnmapSubresource(_constantBuffers[i], 0); context.ComputeShader.SetConstantBuffer(i, _constantBuffers[i]); } context.ComputeShader.SetShader(_cs, null, 0); context.Dispatch(_texDesc.Width / 16, _texDesc.Height / 16, 1); for (var i = 0; i < _customOutputs!.Length; i++) { context.ComputeShader.SetUnorderedAccessView(i, null); } for (var i = 0; i <= maxSrvIndex; i++) { context.ComputeShader.SetShaderResource(i, null); } } private void EnsureSamplerState(Device device, ref SamplerLiteralValue samplerLiteralValue) { if (samplerLiteralValue.SamplerState == null || samplerLiteralValue.SamplerState.Description.AddressU != samplerLiteralValue.Value.Address || samplerLiteralValue.SamplerState.Description.Filter != samplerLiteralValue.Value.Filter) { var desc = SamplerStateDescription.Default(); desc.AddressU = desc.AddressV = desc.AddressW = samplerLiteralValue.Value.Address; desc.Filter = samplerLiteralValue.Value.Filter; samplerLiteralValue.SamplerState = new SamplerState(device, desc); } } public ISourceList Errors { get; } = new SourceList(); public CodeGenInputViewModel> Source { get; } public StringExpressionEditorViewModel SourceEditor { get; } = new() { CustomValue = @" cbuffer cb { float f; } Texture2D t0; SamplerState ss; RWTexture2D o; [numthreads(16,16,1)] void main(uint3 i : SV_DispatchThreadID) { float2 s; o.GetDimensions(s.x, s.y); float2 uv = i.xy/s; float4 c = t0.SampleLevel(ss, uv, 0); o[i.xy] = float4(c.xyz, 1); }" }; } }