Sheets can be buffered - where a copy of their data is kept in RAM. This allows for modifications to the sheet to be batched in the RAM buffer, before later being committed to VRAM in a single operation. The SheetBuilder class allows for sprites to be allocated onto one or more sheets. Each time a sheet is filled, it will allocate a new sheet. Sheets allocated by the sheet builder will typically use the buffering mechanism. Previously each time the builder allocated a new sheet, the buffer would get thrown away, and the next sheet would allocate a fresh buffer. These buffers can be large and may accumulate before the GC cleans them up. So although only one buffer will be live at a time, they can cause a spike in memory used by the process during loading. We can avoid this issue by allowing the buffer from the previous sheet to be reused by the next sheet. This is possible because the sheet builder only has one live sheet for modifications at a time, and they are all the same type and size. By allocating only one buffer per builder instead of one per sheet, we reduce the peak memory required during loading.
166 lines
4.9 KiB
C#
166 lines
4.9 KiB
C#
#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 OpenRA.FileFormats;
|
|
using OpenRA.Primitives;
|
|
|
|
namespace OpenRA.Graphics
|
|
{
|
|
[Serializable]
|
|
public class SheetOverflowException : Exception
|
|
{
|
|
public SheetOverflowException(string message)
|
|
: base(message) { }
|
|
}
|
|
|
|
// The enum values indicate the number of channels used by the type
|
|
// They are not arbitrary IDs!
|
|
public enum SheetType
|
|
{
|
|
Indexed = 1,
|
|
BGRA = 4,
|
|
}
|
|
|
|
public sealed class SheetBuilder : IDisposable
|
|
{
|
|
public readonly SheetType Type;
|
|
readonly List<Sheet> sheets = new();
|
|
readonly Func<Sheet> allocateSheet;
|
|
readonly int margin;
|
|
int rowHeight = 0;
|
|
int2 p;
|
|
|
|
public Sheet Current { get; private set; }
|
|
public TextureChannel CurrentChannel { get; private set; }
|
|
public IEnumerable<Sheet> AllSheets => sheets;
|
|
|
|
public static Sheet AllocateSheet(SheetType type, int sheetSize)
|
|
{
|
|
return new Sheet(type, new Size(sheetSize, sheetSize));
|
|
}
|
|
|
|
public static SheetType FrameTypeToSheetType(SpriteFrameType t)
|
|
{
|
|
switch (t)
|
|
{
|
|
case SpriteFrameType.Indexed8:
|
|
return SheetType.Indexed;
|
|
|
|
// Util.FastCopyIntoChannel will automatically convert these to BGRA
|
|
case SpriteFrameType.Bgra32:
|
|
case SpriteFrameType.Bgr24:
|
|
case SpriteFrameType.Rgba32:
|
|
case SpriteFrameType.Rgb24:
|
|
return SheetType.BGRA;
|
|
default: throw new NotImplementedException($"Unknown SpriteFrameType {t}");
|
|
}
|
|
}
|
|
|
|
public SheetBuilder(SheetType t)
|
|
: this(t, Game.Settings.Graphics.SheetSize) { }
|
|
|
|
public SheetBuilder(SheetType t, int sheetSize, int margin = 1)
|
|
: this(t, () => AllocateSheet(t, sheetSize), margin) { }
|
|
|
|
public SheetBuilder(SheetType t, Func<Sheet> allocateSheet, int margin = 1)
|
|
{
|
|
CurrentChannel = t == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
|
Type = t;
|
|
Current = allocateSheet();
|
|
sheets.Add(Current);
|
|
this.allocateSheet = allocateSheet;
|
|
this.margin = margin;
|
|
}
|
|
|
|
public Sprite Add(ISpriteFrame frame, bool premultiplied = false) { return Add(frame.Data, frame.Type, frame.Size, 0, frame.Offset, premultiplied); }
|
|
public Sprite Add(byte[] src, SpriteFrameType type, Size size, bool premultiplied = false) { return Add(src, type, size, 0, float3.Zero, premultiplied); }
|
|
public Sprite Add(byte[] src, SpriteFrameType type, Size size, float zRamp, in float3 spriteOffset, bool premultiplied = false)
|
|
{
|
|
// Don't bother allocating empty sprites
|
|
if (size.Width == 0 || size.Height == 0)
|
|
return new Sprite(Current, Rectangle.Empty, 0, spriteOffset, CurrentChannel, BlendMode.Alpha);
|
|
|
|
var rect = Allocate(size, zRamp, spriteOffset);
|
|
Util.FastCopyIntoChannel(rect, src, type, premultiplied);
|
|
Current.CommitBufferedData();
|
|
return rect;
|
|
}
|
|
|
|
public Sprite Add(Png src, float scale = 1f)
|
|
{
|
|
var rect = Allocate(new Size(src.Width, src.Height), scale);
|
|
Util.FastCopyIntoSprite(rect, src);
|
|
Current.CommitBufferedData();
|
|
return rect;
|
|
}
|
|
|
|
TextureChannel? NextChannel(TextureChannel t)
|
|
{
|
|
var nextChannel = (int)t + (int)Type;
|
|
if (nextChannel > (int)TextureChannel.Alpha)
|
|
return null;
|
|
|
|
return (TextureChannel)nextChannel;
|
|
}
|
|
|
|
public Sprite Allocate(Size imageSize, float scale = 1f) { return Allocate(imageSize, 0, float3.Zero, scale); }
|
|
public Sprite Allocate(Size imageSize, float zRamp, in float3 spriteOffset, float scale = 1f)
|
|
{
|
|
if (imageSize.Width + p.X + margin > Current.Size.Width)
|
|
{
|
|
p = new int2(0, p.Y + rowHeight + margin);
|
|
rowHeight = imageSize.Height;
|
|
}
|
|
|
|
if (imageSize.Height > rowHeight)
|
|
rowHeight = imageSize.Height;
|
|
|
|
if (p.Y + imageSize.Height + margin > Current.Size.Height)
|
|
{
|
|
var next = NextChannel(CurrentChannel);
|
|
if (next == null)
|
|
{
|
|
var previous = Current;
|
|
Current = allocateSheet();
|
|
|
|
// Reuse the backing buffer between sheets where possible.
|
|
// This avoids allocating additional buffers which the GC must clean up.
|
|
previous.ReleaseBufferAndTryTransferTo(Current);
|
|
|
|
sheets.Add(Current);
|
|
CurrentChannel = Type == SheetType.Indexed ? TextureChannel.Red : TextureChannel.RGBA;
|
|
}
|
|
else
|
|
CurrentChannel = next.Value;
|
|
|
|
rowHeight = imageSize.Height;
|
|
p = int2.Zero;
|
|
}
|
|
|
|
var rect = new Sprite(
|
|
Current, new Rectangle(p.X + margin, p.Y + margin, imageSize.Width, imageSize.Height),
|
|
zRamp, spriteOffset, CurrentChannel, BlendMode.Alpha, scale);
|
|
p += new int2(imageSize.Width + margin, 0);
|
|
|
|
return rect;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var sheet in sheets)
|
|
sheet.Dispose();
|
|
sheets.Clear();
|
|
}
|
|
}
|
|
}
|