crash proof snooper

TODO: find and fix the various crashes that can happen (there's a lot)
This commit is contained in:
Asval
2026-08-15 20:01:45 +02:00
parent c21d6d6369
commit d8e2503af0
5 changed files with 151 additions and 83 deletions

View File

@@ -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<SourceEnricher>()
.MinimumLevel.Verbose()
#else
.Enrich.With<CallerEnricher>()
#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();

View File

@@ -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;
/// <summary>
/// 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.
/// </summary>
public sealed class SnooperHost(Func<EditorWindow> 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));
}
}

View File

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

View File

@@ -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<EditorWindow> _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<EditorWindow>(() => 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();
}

Submodule Snooper updated: b6ce504513...5c3e4fcc89