XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Diagnostics;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml.Hosting;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using XFEEDToolBox.Core.Rendering;
using XFEEDToolBox.Rendering;

namespace XFEEDToolBox.Controls;

/// <summary>Transparent GPU composition preview. Rendering is independent of data bindings.</summary>
public sealed class ShipModelView : UserControl
{
    public sealed record RenderedFrame(int Width, int Height, byte[] Pixels, double AngleDegrees);
    public static readonly DependencyProperty ShipTypeProperty = DependencyProperty.Register(nameof(ShipType), typeof(string), typeof(ShipModelView), new PropertyMetadata("", Changed));
    public string ShipType { get => (string)GetValue(ShipTypeProperty); set => SetValue(ShipTypeProperty, value); }
    public static readonly DependencyProperty ShowPedestalProperty=DependencyProperty.Register(nameof(ShowPedestal),typeof(bool),typeof(ShipModelView),new PropertyMetadata(false,Changed));
    public static readonly DependencyProperty HologramColorProperty=DependencyProperty.Register(nameof(HologramColor),typeof(Windows.UI.Color),typeof(ShipModelView),new PropertyMetadata(Windows.UI.Color.FromArgb(255,255,128,0),Changed));
    public bool ShowPedestal{get=>(bool)GetValue(ShowPedestalProperty);set=>SetValue(ShowPedestalProperty,value);}
    public Windows.UI.Color HologramColor{get=>(Windows.UI.Color)GetValue(HologramColorProperty);set=>SetValue(HologramColorProperty,value);}
    public static readonly DependencyProperty FitToViewportProperty=DependencyProperty.Register(nameof(FitToViewport),typeof(bool),typeof(ShipModelView),new PropertyMetadata(false,Changed));
    public bool FitToViewport{get=>(bool)GetValue(FitToViewportProperty);set=>SetValue(FitToViewportProperty,value);}
    private readonly Grid panel = new();
    private SpriteVisual? modelVisual;
    private CompositionSurfaceBrush? modelBrush;
    private XamlRoot? observedRoot;
    private readonly Image snapshot = new() { Visibility = Visibility.Collapsed, Stretch = Stretch.Fill };
    private readonly TextBlock message = new() { Text = "正在读取飞船模型…", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, FontSize = 12, Foreground = new SolidColorBrush(Windows.UI.Color.FromArgb(255, 139, 163, 183)), TextWrapping = TextWrapping.Wrap, Margin = new(16) };
    private readonly SemaphoreSlim switching = new(1);
    private CancellationTokenSource? stop;
    private Task? worker;
    private ShipGraphics? graphics;
    private int version, pixelWidth = 1, pixelHeight = 1;
    private bool loaded;
    private bool pageActive = true;
    private volatile bool inViewport = true;
    private volatile bool frozenForProbe;
    private sealed record FrameCapture(TaskCompletionSource<RenderedFrame> Completion,double? Seconds);
    private FrameCapture? capture;
    private long drawTicks, waitTicks, renderSamples;
    public string ModelSource { get; private set; } = "";
    public string ModelHash { get; private set; } = "";
    public string RenderError { get; private set; } = "";
    public int Triangles { get; private set; }
    public long Frames => graphics?.Frames ?? 0;
    public double MeasuredFps => graphics?.MeasuredFps ?? 0;
    public double AngleDegrees => graphics?.AngleDegrees ?? 0;
    public string Adapter => graphics?.Adapter ?? "";
    public int FeatureEdges => graphics?.FeatureEdges ?? 0;
    public string GraphicsBackend => "Direct3D11 / CompositionDrawingSurface";
    public (string Monitor, double RefreshHz, double ModeHz) MonitorTiming => DisplayTiming.Read(WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow));
    public bool IsRendering => worker is { IsCompleted: false };
    public double AverageDrawMilliseconds => renderSamples > 0 ? Interlocked.Read(ref drawTicks) * 1000.0 / Stopwatch.Frequency / Interlocked.Read(ref renderSamples) : 0;
    public double AverageWaitMilliseconds => renderSamples > 0 ? Interlocked.Read(ref waitTicks) * 1000.0 / Stopwatch.Frequency / Interlocked.Read(ref renderSamples) : 0;
    private static void Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) => ((ShipModelView)d).RefreshModel();
    public ShipModelView()
    {
        var root = new Grid(); root.Children.Add(panel); root.Children.Add(snapshot); root.Children.Add(message); Content = root;
        IsHitTestVisible = false;
        Loaded += (_, _) => { loaded = true; observedRoot = XamlRoot; if (observedRoot is not null) observedRoot.Changed += RootChanged; Resize(); RefreshModel(); };
        Unloaded += async (_, _) => { loaded = false; if (observedRoot is not null) observedRoot.Changed -= RootChanged; observedRoot = null; Interlocked.Increment(ref version); await StopAsync(); };
        SizeChanged += (_, _) => Resize();
        EffectiveViewportChanged += (_, e) =>
        {
            var viewport = e.EffectiveViewport;
            inViewport = viewport.Width > 0 && viewport.Height > 0 && viewport.Right > 0 && viewport.Bottom > 0 && viewport.X < ActualWidth && viewport.Y < ActualHeight;
        };
    }
    private void RootChanged(XamlRoot sender, XamlRootChangedEventArgs args) => Resize();
    private void Resize()
    {
        var scale = XamlRoot?.RasterizationScale ?? 1;
        Volatile.Write(ref pixelWidth, Math.Clamp((int)Math.Ceiling(ActualWidth * scale), 1, 7680));
        Volatile.Write(ref pixelHeight, Math.Clamp((int)Math.Ceiling(ActualHeight * scale), 1, 4320));
    }
    public async void RefreshModel()
    {
        var current = Interlocked.Increment(ref version);
        if (!loaded || !pageActive) return;
        var type = ShipType;var showPedestal=ShowPedestal;
        await switching.WaitAsync();
        try
        {
            if (!loaded || !pageActive || current != version) return;
            await StopCoreAsync();
            ModelSource = ModelHash = RenderError = ""; Triangles = 0;
            message.Visibility = Visibility.Visible; message.Text = "正在读取飞船模型…";
            var mesh = await Task.Run(() => LocalShipModels.Load(type));
            if (!loaded || !pageActive || current != version) return;
            if (mesh is null) { message.Text = "该船型的本地模型尚未导入"; return; }
            var scene = await Task.Run(() => new HologramScene(mesh,showPedestal,false));
            if (!loaded || !pageActive || current != version) return;
            Start(mesh, scene);
        }
        catch (Exception e)
        {
            RenderError = e.Message; message.Text = "飞船模型暂不可用";
            message.Visibility = Visibility.Visible; App.Runtime.Log("飞船模型", e.Message);
            await StopCoreAsync();
        }
        finally { switching.Release(); }
    }
    private void Start(ShipMesh mesh, HologramScene scene)
    {
        var compositor = ElementCompositionPreview.GetElementVisual(panel).Compositor;
        var colour=HologramColor;
        graphics = new(mesh, scene, Volatile.Read(ref pixelWidth), Volatile.Read(ref pixelHeight), compositor,new(colour.R/255f,colour.G/255f,colour.B/255f,1),FitToViewport);
        modelBrush = compositor.CreateSurfaceBrush(graphics.Surface); modelBrush.Stretch = CompositionStretch.Fill;
        modelVisual = compositor.CreateSpriteVisual(); modelVisual.RelativeSizeAdjustment = Vector2.One; modelVisual.Brush = modelBrush;
        ElementCompositionPreview.SetElementChildVisual(panel, modelVisual);
        ModelSource = mesh.Source; ModelHash = mesh.Sha256; Triangles = mesh.Triangles;
        message.Visibility = Visibility.Collapsed;
        stop = new(); var token = stop.Token; var renderer = graphics;
        drawTicks = waitTicks = renderSamples = 0;
        var window = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
        worker = Task.Factory.StartNew(() =>
        {
            var apartment = RoInitialize(1);
            try
            {
                var clock = Stopwatch.StartNew();
                while (!token.IsCancellationRequested)
                {
                    if (frozenForProbe || !inViewport || DisplayTiming.IsMinimized(window)) { token.WaitHandle.WaitOne(100); continue; }
                    var waiting = Stopwatch.GetTimestamp();
                    Marshal.ThrowExceptionForHR(DwmFlush());
                    var drawing = Stopwatch.GetTimestamp();
                    if (token.IsCancellationRequested) break;
                    renderer.Resize(Volatile.Read(ref pixelWidth), Volatile.Read(ref pixelHeight));
                    var request = Interlocked.Exchange(ref capture, null);
                    if (request is null) renderer.Draw(clock.Elapsed.TotalSeconds);
                    else
                    {
                        try
                        {
                            renderer.Draw(request.Seconds??clock.Elapsed.TotalSeconds, false);
                            var frame = new RenderedFrame(renderer.Width, renderer.Height, renderer.ReadPixels(), renderer.AngleDegrees);
                            renderer.Present();
                            request.Completion.TrySetResult(frame);
                        }
                        catch (Exception e) { request.Completion.TrySetException(e); throw; }
                    }
                    Interlocked.Add(ref waitTicks, drawing - waiting);
                    Interlocked.Add(ref drawTicks, Stopwatch.GetTimestamp() - drawing);
                    Interlocked.Increment(ref renderSamples);
                }
            }
            catch (Exception e)
            {
                if (token.IsCancellationRequested) return;
                var diagnostic = renderer.LastOperation + Environment.NewLine + e;
                App.Runtime.Log("飞船渲染", diagnostic);
                DispatcherQueue.TryEnqueue(() => { if (!ReferenceEquals(graphics, renderer)) return; RenderError = diagnostic; message.Text = "飞船模型暂不可用"; message.Visibility = Visibility.Visible; });
            }
            finally { if (apartment >= 0) RoUninitialize(); }
        }, token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
    }
    public async Task StopAsync()
    {
        await switching.WaitAsync();
        try { await StopCoreAsync(); } finally { switching.Release(); }
    }
    public Task SuspendPageAsync() { pageActive = false; Interlocked.Increment(ref version); return StopAsync(); }
    public void ResumePage() { pageActive = true; RefreshModel(); }
    private async Task StopCoreAsync()
    {
        stop?.Cancel();
        Interlocked.Exchange(ref capture, null)?.Completion.TrySetCanceled();
        if (worker is not null) { try { await worker; } catch (OperationCanceledException) { } worker = null; }
        if (graphics is not null)
        {
            ElementCompositionPreview.SetElementChildVisual(panel, null);
            modelVisual?.Dispose(); modelVisual = null; modelBrush?.Dispose(); modelBrush = null;
            graphics.Dispose(); graphics = null;
        }
        stop?.Dispose(); stop = null;
    }
    public Task<RenderedFrame> CaptureFrameAsync()=>CaptureFrameCore(null);
    internal Task<RenderedFrame> CaptureFrameAtAsync(double seconds)=>CaptureFrameCore(seconds);
    private Task<RenderedFrame> CaptureFrameCore(double? seconds)
    {
        if(seconds is {} time&&(!double.IsFinite(time)||time<0))throw new ArgumentOutOfRangeException(nameof(seconds));
        if (!IsRendering) throw new InvalidOperationException("模型尚未开始渲染。" + RenderError);
        var request = new TaskCompletionSource<RenderedFrame>(TaskCreationOptions.RunContinuationsAsynchronously);
        if (Interlocked.CompareExchange(ref capture, new(request,seconds), null) is not null) throw new InvalidOperationException("已有模型帧读取请求。");
        return request.Task.WaitAsync(TimeSpan.FromSeconds(8));
    }
    // RenderTargetBitmap omits native composition content. A probe may show the exact GPU frame
    // as a XAML bitmap to verify layering with the rest of this app; this never captures the OS desktop.
    public async Task<IDisposable> FreezeForXamlProbeAsync(double? seconds=null)
    {
        var frame = await CaptureFrameCore(seconds);
        var bitmap = new WriteableBitmap(frame.Width, frame.Height);
        using (var stream = bitmap.PixelBuffer.AsStream()) await stream.WriteAsync(frame.Pixels);
        bitmap.Invalidate(); snapshot.Source = bitmap; frozenForProbe = true;
        snapshot.Visibility = Visibility.Visible; panel.Visibility = Visibility.Collapsed;
        return new ProbeFrameScope(this);
    }
    private sealed class ProbeFrameScope(ShipModelView owner) : IDisposable
    {
        public void Dispose()
        {
            owner.panel.Visibility = Visibility.Visible; owner.snapshot.Visibility = Visibility.Collapsed;
            owner.snapshot.Source = null; owner.frozenForProbe = false;
        }
    }
    [DllImport("dwmapi.dll")] private static extern int DwmFlush();
    [DllImport("combase.dll")] private static extern int RoInitialize(uint mode);
    [DllImport("combase.dll")] private static extern void RoUninitialize();
}