Skip to content

Commit

Permalink
Improve sheet packing.
Browse files Browse the repository at this point in the history
When sheet builders are adding sprites to a sheet, they work left to right along each row. They reserve height for the highest sprite seen along that row, resetting the height reservation when the row runs out of space and it moves down to the next row.

As the SpriteCache adds the sprites in a giant batch, it can optimise this operation by ordering the sprites by their height. This reduces wastage where shorter sprites don't use the the full height reserved within the row. The reduced wastage can help the sheet builder allocate fewer sheets, improving load times and improving GPU memory usage as less texture memory is required.
  • Loading branch information
RoosterDragon authored and PunkPun committed Mar 11, 2024
1 parent 519db10 commit a3d0a50
Showing 1 changed file with 22 additions and 11 deletions.
33 changes: 22 additions & 11 deletions OpenRA.Game/Graphics/SpriteCache.cs
Expand Up @@ -84,7 +84,7 @@ public void LoadReservations(ModData modData)
foreach (var sb in SheetBuilders.Values)
sb.Current.CreateBuffer();

var spriteCache = new Dictionary<int, Sprite>();
var pendingResolve = new List<(string Filename, int FrameIndex, bool Premultiplied, ISpriteFrame Frame, Sprite[] SpritesForToken)>();
foreach (var (filename, tokens) in reservationsByFilename)
{
modData.LoadScreen?.Display();
Expand Down Expand Up @@ -117,18 +117,11 @@ public void LoadReservations(ModData modData)
if (loadedFrames != null)
{
var resolved = new Sprite[loadedFrames.Length];
resolvedSprites[token] = resolved;
var frames = rs.Frames ?? Enumerable.Range(0, loadedFrames.Length);

// Premultiplied and non-premultiplied sprites must be cached separately
// to cover the case where the same image is requested in both versions.
// The premultiplied sprites are stored with an index offset for efficiency
// rather than allocating a second dictionary.
var di = rs.Premultiplied ? loadedFrames.Length : 0;
foreach (var i in frames)
resolved[i] = spriteCache.GetOrAdd(i + di,
f => SheetBuilders[SheetBuilder.FrameTypeToSheetType(loadedFrames[f - di].Type)].Add(loadedFrames[f - di], rs.Premultiplied));

resolvedSprites[token] = resolved;
pendingResolve.Add((filename, i, rs.Premultiplied, loadedFrames[i], resolved));
}
else
{
Expand All @@ -137,8 +130,26 @@ public void LoadReservations(ModData modData)
}
}
}
}

// 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);

spriteCache.Clear();
var spriteCache = new Dictionary<(string Filename, int FrameIndex, bool Premultiplied), Sprite>(pendingResolve.Count);
foreach (var (filename, frameIndex, premultiplied, 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),
_ =>
{
var sheetBuilder = SheetBuilders[SheetBuilder.FrameTypeToSheetType(frame.Type)];
return sheetBuilder.Add(frame, premultiplied);
});

modData.LoadScreen?.Display();
}

spriteReservations.Clear();
Expand Down

0 comments on commit a3d0a50

Please sign in to comment.