diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index ecfeed1e..e22005af 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Threading; +using Editor; using FModel.Framework; using FModel.Services; using FModel.Settings; @@ -116,15 +117,14 @@ public partial class App #else var filePath = Path.Combine(UserSettings.Default.OutputDirectory, "Logs", $"FModel-Log-{DateTime.Now:yyyy-MM-dd}.log"); #endif - const string template1 = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Enriched}: {Message:lj}{NewLine}{Exception}"; + const string template1 = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}"; const string template2 = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] [{ClassName}] {ObjectPath}: {Message:lj}{NewLine}{Exception}"; Log.Logger = new LoggerConfiguration() #if DEBUG - .Enrich.With() .MinimumLevel.Verbose() -#else - .Enrich.With() #endif + .Enrich.FromLogContext() + .Enrich.WithProperty("SourceContext", "FModel") .WriteTo.Logger(lc => lc .Filter.ByExcluding(IsConversionLibrary) .WriteTo.Console(outputTemplate: template1, theme: AnsiConsoleTheme.Literate) @@ -133,7 +133,8 @@ public partial class App .Filter.ByIncludingOnly(IsConversionLibrary) .WriteTo.Console(outputTemplate: template2, theme: AnsiConsoleTheme.Literate) .WriteTo.File(outputTemplate: template2, path: filePath, shared: true)) - // .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose).WriteTo.Sink(ImGuiSink.Instance) + .MinimumLevel.Override("CUE4Parse_Conversion", LogEventLevel.Verbose) + .WriteTo.Sink(ImGuiSink.Instance) .CreateLogger(); CacheManager.MigrateLegacyFiles(); diff --git a/FModel/Framework/SnooperHost.cs b/FModel/Framework/SnooperHost.cs new file mode 100644 index 00000000..b96c475d --- /dev/null +++ b/FModel/Framework/SnooperHost.cs @@ -0,0 +1,107 @@ +using System; +using System.Threading; +using Editor; +using FModel.Views.Resources.Controls; +using OpenTK.Windowing.Desktop; +using Serilog; +using Serilog.Context; + +namespace FModel.Framework; + +/// +/// this basically opens Snooper, survives it crashing, and opens it again without taking FModel down with it. +/// clauded but fixes real problems I had with simpler implementations +/// +/// The simple version (a lazy window on a throwaway thread) does not work. A crash left FModel unable to open its +/// own menu items, and Snooper could not be started again without restarting FModel. Both come from the thread +/// dying: GLFW and the GL driver keep per-thread state there. So the thread Snooper is created on never dies until +/// FModel itself closes, it just loops back to waiting for the next window to build. +/// +public sealed class SnooperHost(Func build) : IDisposable +{ + private readonly Lock _lock = new(); + private readonly SemaphoreSlim _wanted = new(0); + private readonly ManualResetEventSlim _ready = new(); + + private volatile EditorWindow? _window; + private Exception? _failure; + private Thread? _thread; + + public EditorWindow Window + { + get + { + lock (_lock) + { + if (_window is { } current) return current; + + _thread ??= StartThread(); + _ready.Reset(); + _wanted.Release(); + _ready.Wait(); + + return _window ?? throw new InvalidOperationException("Snooper failed to start", _failure); + } + } + } + + private Thread StartThread() + { + // this thread must never die until fmodel closes + // if it dies first, opening any menu hangs the ui thread inside UiaReturnRawElementProvider, which is + // wpf building the popup, and exiting hangs too (dead thread leaves com state behind???) + var thread = new Thread(RenderLoop) { IsBackground = true, Name = "Snooper" }; + thread.Start(); + + return thread; + } + + private void RenderLoop() + { + using var _ = LogContext.PushProperty("SourceContext", "Snooper"); + GLFWProvider.CheckForMainThread = false; + + while (true) + { + _wanted.Wait(); + + try + { + _failure = null; + _window = build(); + } + catch (Exception e) + { + _failure = e; + continue; + } + finally + { + _ready.Set(); + } + + try + { + _window?.Run(); + } + catch (Exception e) + { + Log.Fatal(e, "Crashed"); + FLogger.Append(e); + } + finally + { + _window?.Dispose(); + _window = null; // the next caller asks for a new one, on this same thread + } + } + } + + public void Dispose() + { + if (_window is not { } window) return; + + window.Shutdown(); + SpinWait.SpinUntil(() => _window == null, TimeSpan.FromSeconds(10)); + } +} diff --git a/FModel/ViewModels/ExportSessionViewModel.cs b/FModel/ViewModels/ExportSessionViewModel.cs index f77b5a7b..8fefa595 100644 --- a/FModel/ViewModels/ExportSessionViewModel.cs +++ b/FModel/ViewModels/ExportSessionViewModel.cs @@ -13,6 +13,7 @@ using System.Windows.Threading; using CUE4Parse_Conversion; using CUE4Parse_Conversion.Options; using CUE4Parse.Utils; +using Editor; using FModel.Extensions; using FModel.Framework; using FModel.Settings; @@ -144,7 +145,7 @@ public class ExportSessionViewModel : ViewModel private ExportSessionViewModel() { - // ImGuiSink.Instance.OnExporterLogEvent += OnLogEvent; + ImGuiSink.Instance.OnExporterLogEvent += OnLogEvent; } private void OnLogEvent(LogEvent log) diff --git a/FModel/ViewModels/SnooperViewModel.cs b/FModel/ViewModels/SnooperViewModel.cs index 94579486..51869cd5 100644 --- a/FModel/ViewModels/SnooperViewModel.cs +++ b/FModel/ViewModels/SnooperViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Numerics; using System.Runtime.InteropServices; -using System.Threading; using System.Windows; using System.Windows.Forms; using CUE4Parse.UE4.Assets.Exports; @@ -11,8 +10,6 @@ using CUE4Parse.UE4.Objects.Engine; using Editor; using FModel.Framework; using FModel.Services; -using OpenTK.Windowing.Desktop; -using Serilog; using Snooper.Rendering.Actors; using Snooper.Rendering.Components; using Snooper.Rendering.Components.Light; @@ -25,99 +22,65 @@ public class SnooperViewModel : ViewModel, IDisposable { public static SnooperViewModel Instance { get; } = new(); - private readonly Lazy _editor; + private readonly SnooperHost _host; private SnooperViewModel() { var scale = GetDpiScale(); var htz = GetMaxRefreshFrequency(); - var width = Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .75 * scale); + var width = Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenWidth * .9 * scale); var height = Convert.ToInt32(SystemParameters.MaximizedPrimaryScreenHeight * .85 * scale); - _editor = new Lazy(() => StartEditor(htz, width, height)); - } - - private static EditorWindow StartEditor(int htz, int width, int height) - { - GLFWProvider.CheckForMainThread = false; - - var ready = new ManualResetEventSlim(); - EditorWindow? editor = null; - Exception? failure = null; - - new Thread(() => - { - try - { - editor = new EditorWindow(htz, width, height, ApplicationService.ApplicationView.CUE4Parse.Provider, false, true); - } - catch (Exception e) - { - failure = e; - return; - } - finally - { - ready.Set(); - } - - try - { - editor.Run(); - } - catch (Exception e) - { - Log.Error(e, "Snooper crashed"); - } - }) { IsBackground = true, Name = "Snooper" }.Start(); - - ready.Wait(); - return editor ?? throw new InvalidOperationException("Snooper failed to start", failure); + _host = new SnooperHost(() => new EditorWindow(htz, width, height, ApplicationService.ApplicationView.CUE4Parse.Provider, false, true)); } public void Load(UObject? obj) { - var scene = new Actor("Example Scene"); + Actor? actor = obj switch + { + UStaticMesh sm => new MeshActor(sm), + USkeletalMesh sk => new MeshActor(sk), + UWorld w => new WorldActor(w), + _ => null + }; + + var editor = _host.Window; + editor.Invoke(() => + { + if (editor.Manager.RootActor == null) + editor.Manager.LoadScene(CreateScene(obj is UWorld)); + + if (actor != null) + editor.Manager.LoadScene(actor); + + editor.Show(); + }); + } + + private Actor CreateScene(bool transparentGrid) + { + var scene = new Actor("Scene"); scene.Components.Add(new BoxComponent(Vector3.Zero, Vector3.One)); + var grid = new Actor("Grid"); + grid.Components.Add(transparentGrid ? new GridComponent() : new OpaqueGridComponent()); + scene.Children.Add(grid); + var camera = new CameraActor("Camera"); camera.CameraComponent.LocalTransform.Position = new Vector3(1, 2, -0.5f); camera.CameraComponent.LocalTransform.Rotation = new Quaternion(0, -1, 0, 1); scene.Children.Add(camera); var sun = new Actor("Sun Light"); - sun.Components.Add(new DirectionalLightComponent(MathF.PI, new Vector3(1.0f, 0.87f, 0.72f), new Transform(new Quaternion(new Vector3(0.5f, -0.5f, 0.0f), 1.0f)), "Directional Light")); + sun.Components.Add(new DirectionalLightComponent(MathF.PI, new Vector3(1.0f, 0.87f, 0.72f), new Transform(new Vector3(16.5f, 0, 0), new Quaternion(new Vector3(0.5f, -0.5f, 0.0f), 1.0f)), "Directional Light")); scene.Children.Add(sun); - GridComponent gridComponent = new OpaqueGridComponent(); - switch (obj) - { - case UStaticMesh sm: - scene.Children.Add(new MeshActor(sm)); - break; - case USkeletalMesh sk: - scene.Children.Add(new MeshActor(sk)); - break; - case UWorld w: - gridComponent = new GridComponent(); - scene.Children.Add(new WorldActor(w)); - break; - } - var grid = new Actor("Grid"); - grid.Components.Add(gridComponent); - scene.Children.Insert(0, grid); - - var editor = _editor.Value; - editor.Invoke(() => - { - editor.Manager.LoadScene(scene); - editor.Show(); - }); + return scene; } public void Run() { - var editor = _editor.Value; + var editor = _host.Window; editor.Invoke(editor.Show); } @@ -189,9 +152,5 @@ public class SnooperViewModel : ViewModel, IDisposable return rf; } - public void Dispose() - { - if (!_editor.IsValueCreated) return; - _editor.Value.Dispose(); - } + public void Dispose() => _host.Dispose(); } diff --git a/Snooper b/Snooper index b6ce5045..5c3e4fcc 160000 --- a/Snooper +++ b/Snooper @@ -1 +1 @@ -Subproject commit b6ce5045131c972a9ab6bd7fd658beb46e306114 +Subproject commit 5c3e4fcc89f0ab3f2e68790a84b29625cd214f0b