Initial commit: OpenRA game engine
Fork from OpenRA/OpenRA with one-click launch script (start-ra.cmd) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
183
OpenRA.Game/Graphics/SpriteCache.cs
Normal file
183
OpenRA.Game/Graphics/SpriteCache.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
#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.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using OpenRA.FileSystem;
|
||||
|
||||
namespace OpenRA.Graphics
|
||||
{
|
||||
public delegate ISpriteFrame AdjustFrame(ISpriteFrame input, int index, int total);
|
||||
|
||||
public sealed class SpriteCache : IDisposable
|
||||
{
|
||||
public readonly Dictionary<SheetType, SheetBuilder> SheetBuilders;
|
||||
readonly ISpriteLoader[] loaders;
|
||||
readonly IReadOnlyFileSystem fileSystem;
|
||||
|
||||
readonly Dictionary<
|
||||
int,
|
||||
(ImmutableArray<int> Frames, MiniYamlNode.SourceLocation Location, AdjustFrame AdjustFrame, bool Premultiplied)> spriteReservations = [];
|
||||
readonly Dictionary<string, List<int>> reservationsByFilename = [];
|
||||
|
||||
readonly Dictionary<int, Sprite[]> resolvedSprites = [];
|
||||
|
||||
readonly Dictionary<int, (string Filename, MiniYamlNode.SourceLocation Location)> missingFiles = [];
|
||||
|
||||
int nextReservationToken = 1;
|
||||
|
||||
public SpriteCache(
|
||||
IReadOnlyFileSystem fileSystem, ISpriteLoader[] loaders, int bgraSheetSize, int indexedSheetSize, int bgraSheetMargin = 1, int indexedSheetMargin = 1)
|
||||
{
|
||||
SheetBuilders = new Dictionary<SheetType, SheetBuilder>
|
||||
{
|
||||
{ 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, ImmutableArray<int> frames, MiniYamlNode.SourceLocation location,
|
||||
AdjustFrame adjustFrame = null, bool premultiplied = false)
|
||||
{
|
||||
var token = nextReservationToken++;
|
||||
spriteReservations[token] = (frames, location, adjustFrame, premultiplied);
|
||||
reservationsByFilename.GetOrAdd(filename, _ => []).Add(token);
|
||||
return token;
|
||||
}
|
||||
|
||||
static ISpriteFrame[] GetFrames(IReadOnlyFileSystem fileSystem, string filename, ISpriteLoader[] loaders)
|
||||
{
|
||||
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 _))
|
||||
return frames;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ISpriteFrame[] LoadFramesUncached(string filename)
|
||||
{
|
||||
return GetFrames(fileSystem, filename, loaders);
|
||||
}
|
||||
|
||||
public void LoadReservations(ModData modData)
|
||||
{
|
||||
var pendingResolve = new List<(
|
||||
string Filename,
|
||||
int FrameIndex,
|
||||
bool Premultiplied,
|
||||
AdjustFrame AdjustFrame,
|
||||
ISpriteFrame Frame,
|
||||
Sprite[] SpritesForToken)>();
|
||||
foreach (var (filename, tokens) in reservationsByFilename)
|
||||
{
|
||||
modData.LoadScreen?.Display();
|
||||
var loadedFrames = GetFrames(fileSystem, filename, loaders);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
if (spriteReservations.TryGetValue(token, out var rs))
|
||||
{
|
||||
if (loadedFrames != null)
|
||||
{
|
||||
var resolved = new Sprite[loadedFrames.Length];
|
||||
resolvedSprites[token] = resolved;
|
||||
if (rs.Frames != null && rs.Frames.Any(i => i >= loadedFrames.Length))
|
||||
throw new InvalidOperationException($"{rs.Location}: {filename} does not contain frames: " +
|
||||
string.Join(',', rs.Frames.Where(f => f >= loadedFrames.Length)));
|
||||
|
||||
var frames = rs.Frames != null ? rs.Frames : Enumerable.Range(0, loadedFrames.Length);
|
||||
var total = rs.Frames != null ? rs.Frames.Length : loadedFrames.Length;
|
||||
|
||||
var j = 0;
|
||||
foreach (var i in frames)
|
||||
{
|
||||
var frame = loadedFrames[i];
|
||||
if (rs.AdjustFrame != null)
|
||||
frame = rs.AdjustFrame(frame, j++, total);
|
||||
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,
|
||||
AdjustFrame 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user