#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.IO; using System.Linq; using OpenRA.FileSystem; using OpenRA.Primitives; namespace OpenRA.Graphics { public sealed class SpriteCache : IDisposable { public readonly Dictionary SheetBuilders; readonly ISpriteLoader[] loaders; readonly IReadOnlyFileSystem fileSystem; readonly Dictionary AdjustFrame, bool Premultiplied)> spriteReservations = new(); readonly Dictionary> reservationsByFilename = new(); readonly Dictionary resolvedSprites = new(); readonly Dictionary missingFiles = new(); int nextReservationToken = 1; public SpriteCache(IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1) { SheetBuilders = new Dictionary { { SheetType.Indexed, new SheetBuilder(SheetType.Indexed, indexedSheetSize, indexedSheetMargin) }, { SheetType.BGRA, new SheetBuilder(SheetType.BGRA, bgraSheetSize, bgraSheetMargin) } }; this.fileSystem = fileSystem; this.loaders = loaders; } public int ReserveSprites(string filename, IEnumerable frames, MiniYamlNode.SourceLocation location, Func adjustFrame = null, bool premultiplied = false) { var token = nextReservationToken++; spriteReservations[token] = (frames?.ToArray(), location, adjustFrame, premultiplied); reservationsByFilename.GetOrAdd(filename, _ => new List()).Add(token); return token; } static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders, out TypeDictionary metadata) { metadata = null; if (!fileSystem.TryOpen(filename, out var stream)) return null; using (stream) { foreach (var loader in loaders) if (loader.TryParseSprite(stream, filename, out var frames, out metadata)) return frames; return null; } } public void LoadReservations(ModData modData) { foreach (var sb in SheetBuilders.Values) sb.Current.CreateBuffer(); var pendingResolve = new List<( string Filename, int FrameIndex, bool Premultiplied, Func AdjustFrame, ISpriteFrame Frame, Sprite[] SpritesForToken)>(); foreach (var (filename, tokens) in reservationsByFilename) { modData.LoadScreen?.Display(); var loadedFrames = GetFrames(fileSystem, filename, loaders, out _); foreach (var token in tokens) { if (spriteReservations.TryGetValue(token, out var rs)) { if (loadedFrames != null) { var resolved = new Sprite[loadedFrames.Length]; resolvedSprites[token] = resolved; var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length); foreach (var i in frames) { var frame = loadedFrames[i]; if (rs.AdjustFrame != null) frame = rs.AdjustFrame(frame); pendingResolve.Add((filename, i, rs.Premultiplied, rs.AdjustFrame, frame, resolved)); } } else { resolvedSprites[token] = null; missingFiles[token] = (filename, rs.Location); } } } } spriteReservations.Clear(); spriteReservations.TrimExcess(); reservationsByFilename.Clear(); reservationsByFilename.TrimExcess(); // When the sheet builder is adding sprites, it reserves height for the tallest sprite seen along the row. // We can achieve better sheet packing by keeping sprites with similar heights together. var orderedPendingResolve = pendingResolve.OrderBy(x => x.Frame.Size.Height); var spriteCache = new Dictionary<( string Filename, int FrameIndex, bool Premultiplied, Func AdjustFrame), Sprite>(pendingResolve.Count); foreach (var (filename, frameIndex, premultiplied, adjustFrame, frame, spritesForToken) in orderedPendingResolve) { // Premultiplied and non-premultiplied sprites must be cached separately // to cover the case where the same image is requested in both versions. spritesForToken[frameIndex] = spriteCache.GetOrAdd( (filename, frameIndex, premultiplied, adjustFrame), _ => { var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)]; return sheetBuilder.Add(frame, premultiplied); }); modData.LoadScreen?.Display(); } foreach (var sb in SheetBuilders.Values) sb.Current.ReleaseBuffer(); } public Sprite[] ResolveSprites(int token) { if (!resolvedSprites.Remove(token, out var resolved)) throw new InvalidOperationException($"{nameof(token)} {token} has either already been resolved, or was never reserved via {nameof(ReserveSprites)}"); resolvedSprites.TrimExcess(); if (missingFiles.TryGetValue(token, out var r)) throw new FileNotFoundException($"{r.Location}: {r.Filename} not found", r.Filename); return resolved; } public IEnumerable<(string Filename, MiniYamlNode.SourceLocation Location)> MissingFiles => missingFiles.Values.ToHashSet(); public void Dispose() { foreach (var sb in SheetBuilders.Values) sb.Dispose(); } } }