Target a couple of hot paths:
- In Actor, cache the renderables enumerable by tracking the last used WorldRenderer. It's slightly uncouth but in practise the WorldRenderer will be the same for the lifetime of the Actor and outlive it anyway.
- In RenderSprites, cache the renderables enumerable. If we refresh the palettes directly, we don't need the WorldRenderer for anything else so can return a cached enumerable. This could result in iterating the animation list twice, so use a flag to only do it when required, which is rare.
- In AutoTarget, the compiler has an unfortunate behaviour where it allocates the helper class for a capturing lambda at the top of the loop, but it's on a rare path and so this allocation is usually unused. We can discourage the compiler from allocating until it's actually needed by wrapping the call in a local function.
Using OrderBy provides a stable sort, but internally allocates a buffer every time for storing the sort keys. We'd like to avoid this allocation, but we also can't directly use Array.Sort/Span.Sort as these are unstable sorts.
By calculating sort keys with the item index embedded, we can ensure a stable sorting result whilst being able to reuse the buffer for the sort keys across future calls.
Since these will live for the lifetime of the process, it is worth freezing them on creation to improve lookup performance. If the dictionaries were iterated, change to an immutable array instead to preserve the order.
As config is often meant to be loaded and then never modified, it is helpful to support collections that are read-only after creation. This allows various classes that load config from YAML and elsewhere to deserialize straight into read-only collections and enforce invariants around data mutation.
- Improve the AudReader and WavReader by performing fewer, larger stream reads to consume bytes more efficiently.
- Improve ImaAdpcmReader.LoadImaAdpcmSound by accepting an output buffer rather than allocating a new array.
- Improve StreamExts.ReadAllBytes by using MemoryStream.CopyTo. This internally rents a much larger buffer from the array pool, allowing for fewer, larger stream reads.
Painting a MultiBrush previously required it to be non-empty. When
using Replaceability.Any, this is unlikely to be the desire behavior,
as some utilities (such as the upcoming LatTiler and RampTiler) may
reasonably generate an empty MultiBrush as output.
This changes painting MultiBrushes using Replaceability.Any to be
effectively no-ops. The .Tile and .Actor cases still use checks, as
these still plausibly represent mistakes.