using System; using System.Collections.Generic; using System.Linq; using Age.Engine.Model; internal readonly record struct MovieSurfaceBinding(long PlaybackId, long ResourceId, int SurfaceSlot); internal sealed record MovieSurfaceFrame(RgbaImage Image, string Name, int AssetId); public sealed record MovieSurfaceDiagnostic(int SurfaceSlot, long PlaybackId, long ResourceId, bool Completed, bool HasFrame, string? Name); internal enum MovieSurfaceReleaseKind { NotBound, Active, Released } internal readonly record struct MovieSurfaceRelease(MovieSurfaceReleaseKind Kind, MovieSurfaceBinding Binding); /// Instance-keyed movie/surface ownership. A resource may be played more than once concurrently; /// completion and frames therefore belong to a playback, never to the shared asset id. internal sealed class MovieSurfaceRegistry { private readonly object _lock = new(); private readonly Dictionary _bySurface = new(); private readonly Dictionary _byPlayback = new(); private readonly Dictionary _frames = new(); private readonly HashSet _completed = new(); private readonly HashSet _knownMovieResources = new(); private long _nextPlaybackId; public MovieSurfaceBinding Begin(int surfaceSlot, long resourceId, out MovieSurfaceBinding? replaced) { lock (_lock) { replaced = _bySurface.TryGetValue(surfaceSlot, out var prior) ? prior : null; if (replaced.HasValue) RemoveLocked(prior); var binding = new MovieSurfaceBinding(++_nextPlaybackId, resourceId, surfaceSlot); _knownMovieResources.Add(resourceId); _bySurface[surfaceSlot] = binding; _byPlayback[binding.PlaybackId] = binding; return binding; } } public bool PublishFrame(long playbackId, RgbaImage image, string name, int assetId) { lock (_lock) { if (!_byPlayback.ContainsKey(playbackId)) return false; _frames[playbackId] = new MovieSurfaceFrame(image, name, assetId); return true; } } public bool Complete(long playbackId) { lock (_lock) return _byPlayback.ContainsKey(playbackId) && _completed.Add(playbackId); } public bool IsActive(int surfaceSlot) { lock (_lock) return _bySurface.TryGetValue(surfaceSlot, out var binding) && !_completed.Contains(binding.PlaybackId); } public bool IsBound(int surfaceSlot) { lock (_lock) return _bySurface.ContainsKey(surfaceSlot); } public bool HasActivePlayback { get { lock (_lock) return _byPlayback.Keys.Any(playbackId => !_completed.Contains(playbackId)); } } public bool TryResolveSurface(int surfaceSlot, out MovieSurfaceFrame? frame) { lock (_lock) { if (_bySurface.TryGetValue(surfaceSlot, out var binding) && _frames.TryGetValue(binding.PlaybackId, out var found)) { frame = found; return true; } frame = null; return false; } } public bool TryResolveResource(long resourceId, out MovieSurfaceFrame? frame) { lock (_lock) { long newestPlaybackId = long.MinValue; MovieSurfaceFrame? newestFrame = null; foreach (var binding in _byPlayback.Values) { if (binding.ResourceId != resourceId || binding.PlaybackId <= newestPlaybackId || !_frames.TryGetValue(binding.PlaybackId, out var found)) continue; newestPlaybackId = binding.PlaybackId; newestFrame = found; } frame = newestFrame; return newestFrame != null; } } /// Catalog ids are immutable within a mounted resource set. Remembering that an id entered /// the typed movie path prevents a cleanup-frame render snapshot from treating its .AGF-named MPEG /// payload as a still image after the last live surface binding has been detached. public bool IsKnownMovieResource(long resourceId) { lock (_lock) return _knownMovieResources.Contains(resourceId); } public MovieSurfaceRelease ReleaseIfCompleted(int surfaceSlot) { lock (_lock) { if (!_bySurface.TryGetValue(surfaceSlot, out var binding)) return new MovieSurfaceRelease(MovieSurfaceReleaseKind.NotBound, default); if (!_completed.Contains(binding.PlaybackId)) return new MovieSurfaceRelease(MovieSurfaceReleaseKind.Active, binding); RemoveLocked(binding); return new MovieSurfaceRelease(MovieSurfaceReleaseKind.Released, binding); } } public bool Abandon(long playbackId, out MovieSurfaceBinding binding) { lock (_lock) { if (!_byPlayback.TryGetValue(playbackId, out binding)) return false; RemoveLocked(binding); return true; } } public IReadOnlyList ReleaseRange(int firstSlot, int count) { int end = checked(firstSlot + count); lock (_lock) { var released = _bySurface.Values .Where(binding => binding.SurfaceSlot >= firstSlot && binding.SurfaceSlot < end) .OrderBy(binding => binding.SurfaceSlot) .ToArray(); foreach (var binding in released) RemoveLocked(binding); return released; } } public IReadOnlyList Snapshot() { lock (_lock) return _bySurface.Values .OrderBy(binding => binding.SurfaceSlot) .Select(binding => new MovieSurfaceDiagnostic( binding.SurfaceSlot, binding.PlaybackId, binding.ResourceId, _completed.Contains(binding.PlaybackId), _frames.ContainsKey(binding.PlaybackId), _frames.TryGetValue(binding.PlaybackId, out var frame) ? frame.Name : null)) .ToArray(); } public IReadOnlyList CompletedPlaybackIds() { lock (_lock) return _completed.OrderBy(id => id).ToArray(); } private void RemoveLocked(MovieSurfaceBinding binding) { _bySurface.Remove(binding.SurfaceSlot); _byPlayback.Remove(binding.PlaybackId); _frames.Remove(binding.PlaybackId); _completed.Remove(binding.PlaybackId); } }