// NOTE: Enabling this will require Windows Graphics Tools feature to be enabled // This will prevent the player from running on most Windows systems. //#define FORCE_D3D_DEBUG using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Linq; using System.Text; using CommandLine; using CommandLine.Text; using ManagedBass; using Newtonsoft.Json; using SharpDX.Direct3D; using SharpDX.Direct3D11; using SharpDX.DXGI; using T3.Core.Animation; using T3.Core.Audio; using T3.Core.Compilation; using T3.Core.DataTypes.Vector; using T3.Core.IO; using T3.Core.Logging; using T3.Core.Model; using T3.Core.Operator; using T3.Core.Operator.Slots; using T3.Core.Settings; using T3.Core.Resource; using T3.Core.SystemUi; using Device = SharpDX.Direct3D11.Device; using Resource = SharpDX.Direct3D11.Resource; using SharpDX.Windows; using SilkWindows; using T3.Core.Resource.ShaderCompiling; using T3.Core.Utils; using T3.Serialization; using DeviceContext = SharpDX.Direct3D11.DeviceContext; using Factory = SharpDX.DXGI.Factory; using FillMode = SharpDX.Direct3D11.FillMode; using ResourceManager = T3.Core.Resource.ResourceManager; using VertexShader = T3.Core.DataTypes.VertexShader; using PixelShader = T3.Core.DataTypes.PixelShader; using ShaderCompiler = T3.Core.Resource.ShaderCompiling.ShaderCompiler; using Texture2D = T3.Core.DataTypes.Texture2D; namespace T3.Player; /// /// Bootstraps the standalone player, loads exported content, and starts the render loop. /// internal static partial class Program { /// /// Defines command line switches for exported playback. /// private class Options { [Option(Default = false, Required = false, HelpText = "Disable vsync")] public bool NoVsync { get; set; } [Option(Default = 1920, Required = false, HelpText = "Defines the width")] public int Width { get; set; } [Option(Default = 1080, Required = false, HelpText = "Defines the height")] public int Height { get; set; } [Option(Default = false, Required = false, HelpText = "Run in windowed mode")] public bool Windowed { get; set; } [Option(Default = false, Required = false, HelpText = "Loops the demo")] public bool Loop { get; set; } [Option(Default = true, Required = false, HelpText = "Show log messages.")] public bool Logging { get; set; } } [STAThread] private static void Main(string[] args) { // Must run before any code that may trigger assembly resolution. T3.Core.Diagnostics.AssemblyLoadDiagnostics.Install(); CoreUi.Instance = new MsForms.MsForms(); BlockingWindow.Instance = new SilkWindowProvider(); var settingsPath = Path.Combine(FileLocations.StartFolder, "exportSettings.json"); if (!JsonUtils.TryLoadingJson(settingsPath, out ExportSettings exportSettings)) { var message = $"Failed to load export settings from \"{settingsPath}\". Exiting!"; Log.Error(message); BlockingWindow.Instance.ShowMessageBox(message); return; } CoreSettings.Config = exportSettings!.ConfigData; var logDirectory = Path.Combine(Core.Settings.FileLocations.SettingsDirectory, "Player" , exportSettings.Author, exportSettings.ApplicationTitle); var fileWriter = FileWriter.CreateDefault(logDirectory, out var logPath); try { Log.AddWriter(new ConsoleWriter()); Log.AddWriter(fileWriter); if (!TryResolveOptions(args, exportSettings!, out _resolvedOptions)) return; Log.Info($"Starting {exportSettings.ApplicationTitle} with id {exportSettings.OperatorId} by {exportSettings.Author}."); Log.Info($"Build: {exportSettings.BuildId}, Editor: {exportSettings.EditorVersion}"); ShaderCompiler.ShaderCacheSubdirectory = Path.Combine("Player", exportSettings.EditorVersion, exportSettings.Author, exportSettings.ApplicationTitle, exportSettings.OperatorId.ToString(), exportSettings.BuildId.ToString()); var resolution = new Int2(_resolvedOptions.Width, _resolvedOptions.Height); _vsyncInterval = Convert.ToInt16(!_resolvedOptions.NoVsync); Log.Debug($": {_vsyncInterval}, windowed: {_resolvedOptions.Windowed}, size: {resolution}, loop: {_resolvedOptions.Loop}, logging: {_resolvedOptions.Logging}"); var iconPath = Path.Combine(SharedResources.EditorResourcesDirectory, SharedResources.EditorResourcesDirectory,"images", "t3.ico"); var gotIcon = File.Exists(iconPath); Icon icon; if (!gotIcon) { Log.Warning("Failed to load icon from " + iconPath); icon = null; } else { icon = new Icon(iconPath); } _renderForm = new RenderForm(exportSettings!.ApplicationTitle) { ClientSize = new Size(resolution.X, resolution.Y), AllowUserResizing = false, Icon = icon, }; var windowHandle = _renderForm.Handle; // SwapChain description var desc = new SwapChainDescription { BufferCount = 3, ModeDescription = new ModeDescription(resolution.Width, resolution.Height, new Rational(60, 1), Format.R8G8B8A8_UNorm), IsWindowed = _resolvedOptions.Windowed, OutputHandle = windowHandle, SampleDescription = new SampleDescription(1, 0), SwapEffect = SwapEffect.FlipDiscard, Flags = SwapChainFlags.AllowModeSwitch, Usage = Usage.RenderTargetOutput, }; //Try to load 11.1 if possible, revert to 11.0 auto FeatureLevel[] levels = { FeatureLevel.Level_11_1, FeatureLevel.Level_11_0, }; // Create Device and SwapChain #if DEBUG || FORCE_D3D_DEBUG var deviceCreationFlags = DeviceCreationFlags.Debug | DeviceCreationFlags.BgraSupport; #else var deviceCreationFlags = DeviceCreationFlags.None; #endif Device.CreateWithSwapChain(DriverType.Hardware, deviceCreationFlags, desc, out _device, out _swapChain); ResourceManager.Init(_device); _deviceContext = _device.ImmediateContext; var cursor = CoreUi.Instance.Cursor; if (_swapChain.IsFullScreen) { cursor.SetVisible(false); } // Ign ore all windows events var factory = _swapChain.GetParent(); factory.MakeWindowAssociation(_renderForm.Handle, WindowAssociationFlags.IgnoreAll); InitializeInput(_renderForm); // New RenderTargetView from the backbuffer _backBuffer = Resource.FromSwapChain(_swapChain, 0); _renderView = new RenderTargetView(_device, _backBuffer); var shaderCompiler = new DX11ShaderCompiler { Device = _device }; ShaderCompiler.Instance = shaderCompiler; SharedResources.Initialize(); _fullScreenPixelShaderResource = SharedResources.FullScreenPixelShaderResource; _fullScreenVertexShaderResource = SharedResources.FullScreenVertexShaderResource; LoadOperators(); if(!SymbolRegistry.TryGetSymbol(exportSettings.OperatorId, out var demoSymbol)) { CloseApplication(true, $"Failed to find [{exportSettings.ApplicationTitle}] with id {exportSettings.OperatorId}"); return; } Log.Debug($"Try to load playback settings for {demoSymbol}"); var playbackSettings = demoSymbol.CompositionSettings; if (playbackSettings != null) { Log.Debug("Playback settings: " + JsonConvert.SerializeObject( playbackSettings, Formatting.Indented )); } else { Log.Warning($"No playback settings defined"); } _playback = new Playback { Settings = playbackSettings }; // Create instance of project op, all children are create automatically if (!demoSymbol.TryGetParentlessInstance(out _project)) { CloseApplication(true, $"Failed to create instance of project op {demoSymbol}"); return; } _evalContext = new EvaluationContext(); var prerenderRequired = false; _resolution = new Int2(_resolvedOptions.Width, _resolvedOptions.Height); // Init wasapi input if required if (playbackSettings is { Playback.AudioSource: CompositionSettings.AudioSources.ProjectSoundTrack }) { // Cache handles for every clip with a valid AssetPath so the per-frame // render loop can register them all. The first IsMainSoundtrack=true clip // is also stored in _soundtrackHandle for end-of-timeline / preload semantics. foreach (var clip in playbackSettings.Playback.AudioClips) { if (string.IsNullOrEmpty(clip.AssetPath)) continue; var handle = new AudioClipResourceHandle(clip, _project); _allSoundtrackHandles.Add(handle); if (clip.IsMainSoundtrack && _soundtrackHandle == null) _soundtrackHandle = handle; } if (_soundtrackHandle != null) { if (_soundtrackHandle.TryGetFileResource(out var file)) { // BPM lives on Playback now; the settings loader migrated any legacy // per-clip BPM into playbackSettings.Playback.Bpm. _playback.Bpm = playbackSettings.Playback.Bpm; // Pre-register every clip so all streams load before the first frame. foreach (var h in _allSoundtrackHandles) AudioEngine.UseSoundtrackClip(h, 0); AudioEngine.CompleteFrame(_playback, Playback.LastFrameDuration); // Initialize prerenderRequired = true; } else { Log.Warning($"Can't find soundtrack {_soundtrackHandle.Clip.AssetPath}"); _soundtrackHandle = null; } } } var rasterizerDesc = new RasterizerStateDescription { FillMode = FillMode.Solid, CullMode = CullMode.None, IsScissorEnabled = false, IsDepthClipEnabled = false }; _rasterizerState = new RasterizerState(_device, rasterizerDesc); foreach (var output in _project.Outputs) { if (output is Slot textureSlot) { if (_textureOutput == null) _textureOutput = textureSlot; else { var message = "Multiple texture outputs found. Only the first one will be used."; Log.Warning(message); break; } } } if (_textureOutput == null) { var sb = new StringBuilder(); var slots = _project.Outputs.Where(x => x is not null).ToArray(); sb.AppendLine("Found the following outputs:"); foreach (var slot in slots) { sb.AppendLine($"{slot.GetType()} | {slot.ValueType} ({slot.ValueType.Assembly.ToString()}\n"); } sb.AppendLine(); sb.AppendLine("Expected:"); sb.Append($"{typeof(Slot).FullName} | {typeof(Texture2D).FullName} ({typeof(Texture2D).Assembly.ToString()}\n"); var message = $"Failed to find texture output. \n{sb}"; CloseApplication(true, message); return; } // TODO - implement proper shader pre-compilation as an option to instance instantiation // move this to core? // Sample some frames to preload all shaders and resources if (prerenderRequired) { PreloadShadersAndResources(_soundtrackHandle.Clip.LengthInSeconds, _resolution, _playback, _deviceContext, _evalContext, _textureOutput, _swapChain, _renderView); } // Start playback _playback.Update(); _playback.TimeInBars = 0; _playback.PlaybackSpeed = 1.0; try { // Main loop RenderLoop.Run(_renderForm, RenderCallback); } catch (TimelineEndedException) { Log.Info($"Program ended at the end of the timeline: {_playback.TimeInSecs:0.00}s / {_playback.TimeInBars:0.00} bars"); CloseApplication(false, null); } catch (Exception e) { var errorMessage = "Exception in main loop:\n" + e; CloseApplication(true, errorMessage); Log.Error(errorMessage); fileWriter.Dispose(); // flush and close BlockingWindow.Instance.ShowMessageBox(errorMessage); } } catch (Exception e) { CloseApplication(true, "Exception in initialization:\n" + e); } return; void CloseApplication(bool error, string message) { CoreUi.Instance.Cursor.SetVisible(true); ShaderCompiler.Shutdown(); bool openLogs = false; if (!string.IsNullOrWhiteSpace(message)) { if (error) Log.Error(message); else Log.Info(message); const int maxLines = 10; message = StringUtils.TrimStringToLineCount(message, maxLines).ToString(); if (error) { message += "\n\nDo you want to open the log file?"; var result = BlockingWindow.Instance.ShowMessageBox(message, $"{exportSettings.ApplicationTitle} crashed /:", "Yes", "No"); openLogs = result == "Yes"; } } fileWriter.Dispose(); // flush and close // Release all resources try { _renderView?.Dispose(); _backBuffer?.Dispose(); _deviceContext?.ClearState(); _deviceContext?.Flush(); _device?.Dispose(); _deviceContext?.Dispose(); } catch (Exception e) { Log.Error($"Failed to dispose of resources: {e}"); } if (openLogs) { CoreUi.Instance.OpenWithDefaultApplication(logPath); } CoreUi.Instance.ExitApplication(); } } private static void RebuildBackBuffer(RenderForm form, Device device, ref RenderTargetView rtv, ref SharpDX.Direct3D11.Texture2D buffer, SwapChain swapChain) { rtv.Dispose(); buffer.Dispose(); swapChain.ResizeBuffers(3, form.ClientSize.Width, form.ClientSize.Height, Format.Unknown, SwapChainFlags.AllowModeSwitch); buffer = Resource.FromSwapChain(swapChain, 0); rtv = new RenderTargetView(device, buffer); } private static bool TryResolveOptions(string[] args, ExportSettings exportSettings, out Options resolvedOptions) { var parser = new Parser(config => { config.HelpWriter = null; config.AutoVersion = false; }); var parserResult = parser.ParseArguments(args); var helpText = HelpText.AutoBuild(parserResult, h => { h.AdditionalNewLineAfterOption = false; // Todo: This should use information from the main operator h.Heading = exportSettings.ApplicationTitle; h.Copyright = exportSettings.Author; h.AutoVersion = false; return h; }, e => e); Options parsedOptions = null; parserResult.WithParsed(o => { parsedOptions = o; }) .WithNotParsed(_ => { Log.Debug(helpText); }); resolvedOptions = parsedOptions; if (resolvedOptions == null) return false; // use windowed status _only_ when explicitly set, the Options struct doesn't know about this if (!args.Any(s => "--windowed".Contains(s))) { parsedOptions.Windowed = exportSettings.WindowMode == WindowMode.Windowed; } return true; } private readonly struct PackageLoadInfo( PlayerSymbolPackage package, List newlyLoadedSymbols) { public readonly PlayerSymbolPackage Package = package; public readonly List NewlyLoadedSymbols = newlyLoadedSymbols; } // Private static bool _inResize; private static int _vsyncInterval; private static SwapChain _swapChain; private static RenderTargetView _renderView; private static SharpDX.Direct3D11.Texture2D _backBuffer; private static Instance _project; private static EvaluationContext _evalContext; private static Playback _playback; private static AudioClipResourceHandle _soundtrackHandle; // All clips registered with the engine each frame so multiple clips play simultaneously. // _soundtrackHandle above remains the first IsMainSoundtrack=true entry for end-of-timeline // and preload semantics. private static readonly List _allSoundtrackHandles = new(); private static DeviceContext _deviceContext; private static Options _resolvedOptions; private static RenderForm _renderForm; private static Texture2D _outputTexture; private static ShaderResourceView _outputTextureSrv; private static RasterizerState _rasterizerState; private static Resource _fullScreenVertexShaderResource; private static Resource _fullScreenPixelShaderResource; private static Device _device; private static Int2 _resolution; private static Slot _textureOutput; }