using System.Diagnostics; using System.Numerics; using System.Security.Cryptography; using System.Text.Json; using Microsoft.UI.Windowing; using Windows.Graphics; using XFEEDToolBox.Controls; using XFEEDToolBox.Core.Rendering; using XFEEDToolBox.Rendering; using XFEEDToolBox.Views; namespace XFEEDToolBox; public partial class App { private static async Task RunModelProbeAsync(string reportPath) { var output = Path.GetDirectoryName(Path.GetFullPath(reportPath))!; Directory.CreateDirectory(output); void Stage(string stage) => File.AppendAllText(reportPath + ".progress", DateTimeOffset.UtcNow.ToString("o") + " " + stage + Environment.NewLine); Stage("start"); object? rendering = null, refit = null; var models = new List(); var lifecycle = new List(); var resizing = new List(); string? failure = null; try { var executable = ViewModel.PluginGameExecutable; await Task.Run(() => { var reader = new EliteLocalModels(executable); foreach (var model in EliteLocalModels.Reference.Models) { var mesh = reader.Read(model.ShipType)!; var min = new Vector3(float.PositiveInfinity); var max = new Vector3(float.NegativeInfinity); foreach (var v in mesh.Vertices) { min = Vector3.Min(min, v.Position); max = Vector3.Max(max, v.Position); } models.Add(new { model.ShipType, model.ResourceName, model.ResourceHash, mesh.Triangles, mesh.Sha256, finite = mesh.Vertices.All(v => float.IsFinite(v.Position.LengthSquared()) && float.IsFinite(v.Normal.LengthSquared())), dimensions = new[] { max.X - min.X, max.Y - min.Y, max.Z - min.Z }, normalized = (max - min).Length() > 0 && Math.Abs(Math.Max(max.X - min.X, Math.Max(max.Y - min.Y, max.Z - min.Z)) - 2) < .001 }); } }); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); Stage("all-models-verified"); ((OverlappedPresenter)MainWindow.AppWindow.Presenter).Restore(false); MainWindow.AppWindow.Resize(new SizeInt32(1510, 980)); MainWindow.AppWindow.Move(new(60, 60)); AppShellPage.Current!.Navigate(typeof(MainPage)); MainWindow.Activate(); await Task.Delay(500); var main = (MainPage)AppShellPage.Current.CurrentPage!; var preview = (ShipModelView)main.FindName("ShipSchematic"); preview.StartBringIntoView(new BringIntoViewOptions { VerticalAlignmentRatio = .5, AnimationDesired = false }); await Task.Delay(300); await WaitForModelAsync(preview); Stage("home-rendering"); await Task.Delay(1800); var first = await preview.CaptureFrameAsync(); await SaveModelFrameAsync(first, Path.Combine(output, "home-model-first.png")); var frames = preview.Frames; var clock = Stopwatch.StartNew(); await Task.Delay(3000); var fps = (preview.Frames - frames) / clock.Elapsed.TotalSeconds; var timing = preview.MonitorTiming; var second = await preview.CaptureFrameAsync(); await SaveModelFrameAsync(second, Path.Combine(output, "home-model-rotated.png")); Stage("rotation-measured"); var changedPixels = first.Width == second.Width && first.Height == second.Height ? first.Pixels.Zip(second.Pixels).Count(p => p.First != p.Second) : -1; var presenter = (OverlappedPresenter)MainWindow.AppWindow.Presenter; Stage("minimizing"); presenter.Minimize(); Stage("minimized"); await Task.Delay(400); var before = preview.Frames; await Task.Delay(600); var paused = preview.Frames == before; presenter.Restore(false); Stage("restored"); await Task.Delay(400); var scroll = (ScrollViewer)main.Content; scroll.ChangeView(null, 0, null, true); await Task.Delay(400); var offscreenFrames = preview.Frames; await Task.Delay(600); var pausedWhenOffscreen = preview.Frames == offscreenFrames; preview.StartBringIntoView(new BringIntoViewOptions { VerticalAlignmentRatio = .5, AnimationDesired = false }); await Task.Delay(400); var resumedWhenVisible = preview.Frames > offscreenFrames; Stage("visibility-tested"); rendering = new { preview.ShipType, preview.ModelSource, preview.ModelHash, preview.Triangles, preview.Adapter, preview.GraphicsBackend, preview.FeatureEdges, first.Width, first.Height, monitor = timing.Monitor, displayModeHz = timing.ModeHz, desktopCompositionHz = timing.RefreshHz, measuredFps = fps, preview.AverageDrawMilliseconds, preview.AverageWaitMilliseconds, synchronized = timing.RefreshHz > 1 && Math.Abs(fps - timing.RefreshHz) / timing.RefreshHz < .12, slowRotation = second.AngleDegrees > first.AngleDegrees + 6 && second.AngleDegrees < first.AngleDegrees + 18, changedPixels, pixelsPresent = second.Pixels.Where((_, i) => i % 4 == 3).Count(a => a > 0), framing = MeasureHologramFrame(second), pausedWhenMinimized = paused, pausedWhenOffscreen, resumedWhenVisible, preview.RenderError }; foreach (var size in new[] { new SizeInt32(1900, 1100), new SizeInt32(2560, 1440), new SizeInt32(1510, 980) }) { MainWindow.AppWindow.Resize(size); await Task.Delay(300); preview.StartBringIntoView(new BringIntoViewOptions { VerticalAlignmentRatio = .5, AnimationDesired = false }); await Task.Delay(250); var resized = await preview.CaptureFrameAsync(); resizing.Add(new { windowWidth = size.Width, windowHeight = size.Height, resized.Width, resized.Height, framing = MeasureHologramFrame(resized), preview.RenderError }); } Stage("native-resizing-tested"); using (await preview.FreezeForXamlProbeAsync()) await CapturePageAsync(main, Path.Combine(output, "home-layout-with-model.png")); Stage("home-captured"); MainWindow.Activate(); await Task.Delay(200); AppShellPage.Current.Navigate(typeof(RefitPage)); await Task.Delay(400); Stage("refit-navigated"); var stoppedWhenUnloaded = !preview.IsRendering; var page = (RefitPage)AppShellPage.Current.CurrentPage!; var background = (ShipModelView)page.FindName("RefitShipModel"); await WaitForModelAsync(background); var initial = background.ShipType; Stage("refit-rendering"); // A stock Sidewinder hash generated independently by the upstream EDSY V8 oracle. ViewModel.RefitEdsyLink = "https://edsy.org/#/L=K100000H4C0S00,FBR00FBR00,,9p300A4Y00AKA00AZo00Ans00B1U00BH600BWQ00,,7Og0003w00mpU0nG0-0nF0-"; await ViewModel.ImportEdsyCommand.ExecuteAsync(null); await Task.Delay(500); await WaitForModelAsync(background); var image = await background.CaptureFrameAsync(); await SaveModelFrameAsync(image, Path.Combine(output, "refit-model.png")); refit = new { initial, selected = ViewModel.RefitShipType, background = background.ShipType, background.ModelSource, switched = background.ShipType == ViewModel.RefitShipType && background.ShipType != initial, sourceMatchesSelected = background.ModelSource.Contains("#" + EliteLocalModels.Reference.Models.Single(m => EliteLocalModels.Normalize(m.ShipType) == EliteLocalModels.Normalize(background.ShipType)).ResourceName), stoppedWhenUnloaded, framing = MeasureHologramFrame(image), background.GraphicsBackend, background.FeatureEdges, unmodifiedGradeEmpty = ViewModel.RefitGrade == 0 && ((XFEEDToolBox.Controls.EngineeringGradePicker)page.FindName("EngineeringGrade")).Value == -1, ViewModel.RefitEdsyStatus, documentShip = ViewModel.RefitDocument?.ShipType, background.RenderError }; using (await background.FreezeForXamlProbeAsync()) await CapturePageAsync(page, Path.Combine(output, "refit-layout-with-model.png")); await background.StopAsync(); Stage("refit-stopped"); for (var repeat = 0; repeat < 4; repeat++) { AppShellPage.Current.Navigate(typeof(MainPage)); await Task.Delay(300); main = (MainPage)AppShellPage.Current.CurrentPage!; preview = (ShipModelView)main.FindName("ShipSchematic"); preview.StartBringIntoView(new BringIntoViewOptions { VerticalAlignmentRatio = .5, AnimationDesired = false }); await WaitForModelAsync(preview); await Task.Delay(200); presenter.Minimize(); await Task.Delay(300); var count = preview.Frames; await Task.Delay(300); var halted = count == preview.Frames; presenter.Restore(false); await Task.Delay(350); var resumed = preview.Frames > count && preview.RenderError.Length == 0; AppShellPage.Current.Navigate(typeof(RefitPage)); await Task.Delay(400); var oldStopped = !preview.IsRendering; page = (RefitPage)AppShellPage.Current.CurrentPage!; background = (ShipModelView)page.FindName("RefitShipModel"); await WaitForModelAsync(background); lifecycle.Add(new { repeat, halted, resumed, oldStopped, error = preview.RenderError, refitError = background.RenderError }); if (!halted || !resumed || !oldStopped || background.RenderError.Length > 0) throw new InvalidOperationException("模型页面切换/最小化恢复检查失败。"); Stage("lifecycle-" + repeat); } await background.StopAsync(); } catch (Exception e) { failure = e.ToString(); } await File.WriteAllTextAsync(reportPath, JsonSerializer.Serialize(new { models, rendering, refit, lifecycle, resizing, failure, gameActionsInvoked = false, gameFilesChanged = false }, new JsonSerializerOptions { WriteIndented = true })); MainWindow.Close(); } private static object MeasureHologramFrame(ShipModelView.RenderedFrame frame) { var left = frame.Width; var top = frame.Height; var right = -1; var bottom = -1; var transparent = 0; var premultiplied = true; for (var y = 0; y < frame.Height; y++) for (var x = 0; x < frame.Width; x++) { var i = (y * frame.Width + x) * 4; var a = frame.Pixels[i + 3]; if (a == 0) transparent++; else { left = Math.Min(left, x); top = Math.Min(top, y); right = Math.Max(right, x); bottom = Math.Max(bottom, y); } premultiplied &= frame.Pixels[i] <= a + 1 && frame.Pixels[i + 1] <= a + 1 && frame.Pixels[i + 2] <= a + 1; } return new { left, top, right, bottom, transparentPixels = transparent, transparentFraction = (double)transparent / (frame.Width * frame.Height), premultiplied, marginOnEverySide = left > 4 && top > 4 && right < frame.Width - 5 && bottom < frame.Height - 5, centred = Math.Abs((left + right + 1) / 2.0 - frame.Width / 2.0) < frame.Width * .12 && Math.Abs((top + bottom + 1) / 2.0 - frame.Height / 2.0) < frame.Height * .12 }; } private static async Task WaitForModelAsync(ShipModelView view) { var clock = Stopwatch.StartNew(); while (view.Frames < 3 && view.RenderError.Length == 0 && clock.Elapsed < TimeSpan.FromSeconds(15)) await Task.Delay(100); if (view.RenderError.Length > 0 || view.Frames < 3) throw new InvalidOperationException("模型未开始渲染:" + view.ShipType + " · " + view.RenderError); } private static async Task SaveModelFrameAsync(ShipModelView.RenderedFrame frame, string path) { using var output = File.Open(path, FileMode.Create); var encoder = await Windows.Graphics.Imaging.BitmapEncoder.CreateAsync(Windows.Graphics.Imaging.BitmapEncoder.PngEncoderId, output.AsRandomAccessStream()); encoder.SetPixelData(Windows.Graphics.Imaging.BitmapPixelFormat.Bgra8, Windows.Graphics.Imaging.BitmapAlphaMode.Premultiplied, (uint)frame.Width, (uint)frame.Height, 96, 96, frame.Pixels); await encoder.FlushAsync(); } }