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

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Runtime.CompilerServices;
using XFEEDToolBox.Core.Models;
using XFEEDToolBox.Core.Navigation;

namespace XFEEDToolBox.Core.Galaxy;

/// <summary>An aligned Galactic cube, reused across pans, rotations and camera distances.</summary>
public readonly record struct GalaxyQueryTile(int Level, long X, long Y, long Z)
{
    public double Size => Math.ScaleB(1d, Level);
    public Coordinates Centre => new((X + .5) * Size, (Y + .5) * Size, (Z + .5) * Size);
    public double Radius => Size * .86602540378443864676;
    public bool Contains(Coordinates p) => p.X >= X * Size && p.X < (X + 1) * Size
        && p.Y >= Y * Size && p.Y < (Y + 1) * Size && p.Z >= Z * Size && p.Z < (Z + 1) * Size;
    public IEnumerable<GalaxyQueryTile> Children()
    {
        for (var x = 0; x < 2; x++) for (var y = 0; y < 2; y++) for (var z = 0; z < 2; z++)
            yield return new(Level - 1, X * 2 + x, Y * 2 + y, Z * 2 + z);
    }
}

public sealed record GalaxyStreamingOptions
{
    public int MaximumCachedTiles { get; init; } = 2048;
    public int MaximumCachedSystems { get; init; } = 200000;
    public int MaximumQueuedTiles { get; init; } = 8192;
    public int MinimumTileLevel { get; init; } = -4;
    public double ViewMargin { get; init; } = 1.12;
    public int UnverifiedCacheMilliseconds { get; init; } = 10000;
}

public sealed record GalaxyStreamUpdate(IReadOnlyList<GalacticSystem> Systems, int QueriedTiles, int CoveredTiles,
    int PendingTiles, int UnverifiedTiles, int RefinedTiles, bool Finished, bool CoverageComplete,
    bool FromCache, Coordinates QueryCentre, double QueryRadius);

/// <summary>Lazy near-to-far frustum coverage. Large nodes split before reaching the native 700 ly sphere API.</summary>
public sealed class GalaxyQueryPlan
{
    private readonly GalaxyViewport viewport;
    private readonly GalaxyStreamingOptions options;
    private readonly PriorityQueue<GalaxyQueryTile, (double Priority, long Sequence)> queue = new();
    private long sequence;
    private GalaxyQueryTile? focusSeed;
    private bool seedTaken;
    public int OmittedTiles { get; private set; }
    public int PendingTiles => queue.Count;

    public GalaxyQueryPlan(GalaxyViewport viewport, GalaxyStreamingOptions? options = null)
    {
        this.viewport = viewport;
        this.options = options ?? new();
        if (this.options.MaximumQueuedTiles < 8 || this.options.MinimumTileLevel is < -4 or > 4
            || !double.IsFinite(this.options.ViewMargin) || this.options.ViewMargin < 1)
            throw new ArgumentException("星图流式查询配置无效。");
        var (min, max) = Bounds(viewport, this.options.ViewMargin);
        var span = Math.Max(max.X - min.X, Math.Max(max.Y - min.Y, max.Z - min.Z));
        if (span <= 0 || !double.IsFinite(span)) return;
        // A small real all-mass sample around the crosshair appears first,
        // while the surrounding frustum progressively fills and refines.
        var seedLevel = Math.Clamp((int)Math.Floor(Math.Log2(Math.Max(.0625, Math.Min(viewport.HalfWidth, viewport.HalfHeight)))),
            this.options.MinimumTileLevel, Math.Max(3,this.options.MinimumTileLevel));
        var seedSize = Math.ScaleB(1d, seedLevel);
        focusSeed = new(seedLevel, (long)Math.Floor(viewport.Focus.X / seedSize), (long)Math.Floor(viewport.Focus.Y / seedSize), (long)Math.Floor(viewport.Focus.Z / seedSize));
        var level = Math.Clamp((int)Math.Ceiling(Math.Log2(span)), this.options.MinimumTileLevel, 19);
        var size = Math.ScaleB(1d, level);
        for (var x = (long)Math.Floor(min.X / size); x <= (long)Math.Floor(max.X / size); x++)
            for (var y = (long)Math.Floor(min.Y / size); y <= (long)Math.Floor(max.Y / size); y++)
                for (var z = (long)Math.Floor(min.Z / size); z <= (long)Math.Floor(max.Z / size); z++) Enqueue(new(level, x, y, z));
    }

    public bool TryTake(out GalaxyQueryTile tile)
    {
        if (!seedTaken && focusSeed is { } seed)
        {
            seedTaken = true;
            tile = seed;
            return true;
        }
        while (queue.TryDequeue(out tile, out _))
        {
            if (focusSeed == tile) continue;
            if (tile.Radius <= 700) return true;
            Subdivide(tile);
        }
        tile = default;
        return false;
    }

    public bool Subdivide(GalaxyQueryTile tile)
    {
        if (tile.Level <= options.MinimumTileLevel) return false;
        foreach (var child in tile.Children()) Enqueue(child);
        return true;
    }

    private void Enqueue(GalaxyQueryTile tile)
    {
        // The aligned octree gives each node exactly one parent; no historical
        // visited set is needed as a long-running view progressively refines.
        if (!Intersects(viewport, tile, options.ViewMargin)) return;
        if (queue.Count >= options.MaximumQueuedTiles) { OmittedTiles++; return; }
        queue.Enqueue(tile, (Priority(tile), sequence++));
    }

    private double Priority(GalaxyQueryTile tile)
    {
        var c = tile.Centre;
        if (viewport.Camera is not { } camera) return c.DistanceTo(viewport.Focus) / Math.Max(1, viewport.Radius) + (9 - tile.Level) * .02;
        var delta = new Vector3d(c.X - camera.Eye.X, c.Y - camera.Eye.Y, c.Z - camera.Eye.Z);
        var depth = Vector3d.Dot(delta, camera.Forward);
        var bucket = Math.Floor(Math.Max(0, Math.Abs(depth - camera.Distance) - tile.Radius) / Math.Max(1e-9, camera.Distance * .5));
        var side = Math.Sqrt(Math.Pow(Vector3d.Dot(delta, camera.Right), 2) + Math.Pow(Vector3d.Dot(delta, camera.Up), 2));
        return bucket * 100 + (9 - tile.Level) * .05 + Math.Max(0, side - tile.Radius) / Math.Max(camera.Distance, tile.Radius);
    }

    public static bool Intersects(GalaxyViewport viewport, GalaxyQueryTile tile, double margin = 1)
    {
        var c = tile.Centre; var half = tile.Size / 2;
        static double Support(Vector3d n, double h) => (Math.Abs(n.X) + Math.Abs(n.Y) + Math.Abs(n.Z)) * h;
        if (viewport.Camera is { } camera)
        {
            var delta = new Vector3d(c.X - camera.Eye.X, c.Y - camera.Eye.Y, c.Z - camera.Eye.Z);
            var depth = Vector3d.Dot(delta, camera.Forward); var reach = Support(camera.Forward, half);
            if (depth + reach < camera.Near || depth - reach > camera.QueryFar * margin) return false;
            var tx = camera.Width / (2 * camera.FocalLength) * margin;
            var ty = camera.Height / (2 * camera.FocalLength) * margin;
            foreach (var normal in new[] { camera.Forward * tx + camera.Right, camera.Forward * tx - camera.Right,
                camera.Forward * ty + camera.Up, camera.Forward * ty - camera.Up })
                if (Vector3d.Dot(delta, normal) + Support(normal, half) < 0) return false;
            return true;
        }
        var offset = new Vector3d(c.X - viewport.Focus.X, c.Y - viewport.Focus.Y, c.Z - viewport.Focus.Z);
        return Math.Abs(Vector3d.Dot(offset, viewport.Right)) <= viewport.HalfWidth * margin + Support(viewport.Right, half)
            && Math.Abs(Vector3d.Dot(offset, viewport.Up)) <= viewport.HalfHeight * margin + Support(viewport.Up, half)
            && Math.Abs(Vector3d.Dot(offset, viewport.Depth)) <= viewport.HalfDepth * margin + Support(viewport.Depth, half);
    }

    private static (Coordinates Min, Coordinates Max) Bounds(GalaxyViewport viewport, double margin)
    {
        var points = new List<Coordinates>(8);
        if (viewport.Camera is { } camera)
        {
            foreach (var depth in new[] { camera.Near, camera.QueryFar * margin })
                foreach (var x in new[] { -1, 1 }) foreach (var y in new[] { -1, 1 })
                {
                    var offset = camera.Forward * depth + camera.Right * (x * camera.Width / (2 * camera.FocalLength) * depth * margin)
                        + camera.Up * (y * camera.Height / (2 * camera.FocalLength) * depth * margin);
                    points.Add(new(camera.Eye.X + offset.X, camera.Eye.Y + offset.Y, camera.Eye.Z + offset.Z));
                }
        }
        else foreach (var x in new[] { -1, 1 }) foreach (var y in new[] { -1, 1 }) foreach (var z in new[] { -1, 1 })
            {
                var offset = viewport.Right * (x * viewport.HalfWidth * margin) + viewport.Up * (y * viewport.HalfHeight * margin) + viewport.Depth * (z * viewport.HalfDepth * margin);
                points.Add(new(viewport.Focus.X + offset.X, viewport.Focus.Y + offset.Y, viewport.Focus.Z + offset.Z));
            }
        return (new(Math.Max(-200000, points.Min(p => p.X)), Math.Max(-200000, points.Min(p => p.Y)), Math.Max(-200000, points.Min(p => p.Z))),
            new(Math.Min(200000, points.Max(p => p.X)), Math.Min(200000, points.Max(p => p.Y)), Math.Min(200000, points.Max(p => p.Z))));
    }
}

/// <summary>One latest-view worker with reusable exact tiles and progressive partial-to-dense refinement.</summary>
public sealed class GalaxyStreamingSession : IDisposable
{
    private sealed record CacheEntry(IReadOnlyList<GalacticSystem> Systems, bool Complete, bool Refine, long Used,long ReceivedAt);
    private sealed record QueryWork(GalaxyQueryTile Tile, CancellationTokenSource Stop);
    private readonly GalaxyStreamingOptions options;
    private readonly Dictionary<GalaxyQueryTile, CacheEntry> cache = [];
    private readonly SemaphoreSlim gate = new(1, 1);
    private readonly object cacheLock = new();
    private readonly object queryLock = new();
    private CancellationTokenSource? active;
    private QueryWork? currentQuery;
    private long generation, clock, cacheEpoch;
    public int CachedTiles { get { lock (cacheLock) return cache.Count; } }

    public GalaxyStreamingSession(GalaxyStreamingOptions? options = null)
    {
        this.options = options ?? new();
        if (this.options.MaximumCachedTiles < 1 || this.options.MaximumCachedSystems < 1 || this.options.UnverifiedCacheMilliseconds < 0) throw new ArgumentException("星图缓存配置无效。");
    }

    public async IAsyncEnumerable<GalaxyStreamUpdate> StreamAsync(GalaxyViewport viewport,
        Func<Coordinates, double, CancellationToken, Task<GalaxyRegionResult>> query,
        [EnumeratorCancellation] CancellationToken token = default)
    {
        var version = Interlocked.Increment(ref generation);
        using var stop = CancellationTokenSource.CreateLinkedTokenSource(token);
        lock (queryLock)
            if (currentQuery is { } running && !GalaxyQueryPlan.Intersects(viewport, running.Tile, options.ViewMargin)) Cancel(running.Stop);
        Cancel(Interlocked.Exchange(ref active, stop));
        await gate.WaitAsync(stop.Token);
        try
        {
            var plan = new GalaxyQueryPlan(viewport, options);
            var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            var queried = 0; var covered = 0; var unverified = 0; var refined = 0;
            while (plan.TryTake(out var tile))
            {
                stop.Token.ThrowIfCancellationRequested();
                if (version != Volatile.Read(ref generation)) throw new OperationCanceledException(stop.Token);
                CacheEntry? entry;
                lock (cacheLock)
                {
                    cache.TryGetValue(tile, out entry);
                    if(entry is {Complete:false,Refine:true}&&tile.Level<=options.MinimumTileLevel)
                    {
                        // A bounded terminal failure is not durable coverage.
                        // A subsequent view/retry must be able to ask again.
                        cache.Remove(tile);entry=null;
                    }
                    // A legacy reply may have observed an unloaded cell. Do not
                    // freeze unverified emptiness for the entire page session.
                    if(entry is {Complete:false,Refine:false}&&Environment.TickCount64-entry.ReceivedAt>=options.UnverifiedCacheMilliseconds)
                    {cache.Remove(tile);entry=null;}
                    if (entry is not null) cache[tile] = entry = entry with { Used = ++clock };
                }
                var cached = entry is not null;
                if (entry is null)
                {
                    var epoch = Volatile.Read(ref cacheEpoch);
                    using var queryStop = new CancellationTokenSource();
                    var work = new QueryWork(tile, queryStop);
                    lock (queryLock) currentQuery = work;
                    // A new overlapping view can use the same accepted tile.
                    // Page cancellation still retires it; superseded camera
                    // enumerations cannot publish it into an obsolete view.
                    using var cancellation = stop.Token.Register(() =>
                    { if (version == Volatile.Read(ref generation)) Cancel(queryStop); });
                    GalaxyRegionResult result;
                    try { result = await query(tile.Centre, tile.Radius, queryStop.Token); }
                    finally { lock (queryLock) if (ReferenceEquals(currentQuery, work)) currentQuery = null; }
                    queryStop.Token.ThrowIfCancellationRequested();
                    queried++;
                    var systems = result.Systems.Where(s => s.Position.IsValid && tile.Contains(s.Position)).ToArray();
                    // Legacy replies cannot prove failed-cell coverage. Small, uncapped
                    // all-mass replies may be displayed/reused but remain unverified.
                    var legacyTerminal = !result.Complete && !result.NeedsSubdivision && result.FailedCells == 0 && tile.Radius <= 20;
                    entry = new(systems, result.IsFullyCovered, !result.IsFullyCovered && !legacyTerminal, ++clock,Environment.TickCount64);
                    if (epoch == Volatile.Read(ref cacheEpoch)) Remember(tile, entry);
                    stop.Token.ThrowIfCancellationRequested();
                    if (version != Volatile.Read(ref generation)) throw new OperationCanceledException(stop.Token);
                }
                if (entry.Complete) covered++;
                else if (entry.Refine && plan.Subdivide(tile)) refined++;
                else unverified++;
                var visible = entry.Systems.Where(s => viewport.Contains(s.Position, options.ViewMargin))
                    .Where(s => seen.Add(s.SystemAddress is > 0 ? "#" + s.SystemAddress : s.Name)).ToArray();
                yield return new(visible, queried, covered, plan.PendingTiles + plan.OmittedTiles, unverified, refined,
                    false, false, cached, tile.Centre, tile.Radius);
                if (cached) await Task.Yield();
            }
            yield return new([], queried, covered, plan.OmittedTiles, unverified, refined, true,
                plan.OmittedTiles == 0 && unverified == 0, false, viewport.Focus, 0);
        }
        finally
        {
            Interlocked.CompareExchange(ref active, null, stop);
            gate.Release();
        }
    }

    private void Remember(GalaxyQueryTile tile, CacheEntry entry)
    {
        lock (cacheLock)
        {
            cache[tile] = entry;
            var rows = cache.Values.Sum(e => (long)e.Systems.Count);
            while (cache.Count > options.MaximumCachedTiles || rows > options.MaximumCachedSystems)
            {
                var oldest = cache.MinBy(e => e.Value.Used);
                rows -= oldest.Value.Systems.Count;
                cache.Remove(oldest.Key);
            }
        }
    }

    private static void Cancel(CancellationTokenSource? source) { try { source?.Cancel(); } catch (ObjectDisposedException) { } }
    public void Clear()
    {
        Interlocked.Increment(ref generation);
        Interlocked.Increment(ref cacheEpoch);
        Cancel(Interlocked.Exchange(ref active, null));
        lock (queryLock) if (currentQuery is { } running) Cancel(running.Stop);
        lock (cacheLock) cache.Clear();
    }
    public void Dispose() => Clear();
}