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.
This rule recommends use of primary constructors. Apply the suggestions on simple POCOs, adjusting some to readonly/structs/records at the same time. For more complex classes, the use of primary constructors is more distracting than helpful, so silence the rule. IDEs can still offer fixes for using primary constructors, but it will not show up as a build issue.
If a unit is above the terrain, the shadow shouldn't display directly underneath the unit, it should take the height into account and display further down. This fix uses the same adjustment as applied by the WithShadow trait.
Provide an explicit size of 512, smaller than the default 2048. The cursors for all mods still pack onto this single sheet, so we avoid wasting memory on larger sheets.
Sorting the cursors also helps pack them onto the sheets more efficiently.
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.
- In CursorManager, we can release the buffer on the final sheet after loading cursors. We don't need to release the buffer on every sheet in the builder, as the builder will handle that.
- In SpriteCache, we don't need to call CreateBuffer explicitly, as the builder will do that for us.
- In RadarWidget, we don't need to call CreateBuffer explicitly, as GetData will do that for us.
As the SpriteCache is used as a one-shot operation in practise, holding on to the capacity of backing collections is not required. Memory usage can be reduced by allowing the capacity to be reset after the SpriteCache has resolved items.
- Once LoadReservations is called, reset the reservation dictionaries so their backing collections can be reclaimed.
- When ResolveSprites is called, shrink the resolved dictionary as resolutions take place.
In a3d0a50f4d, SpriteCache is updated to sort sprites by height before adding them onto the sheet. This improves packing by reducing wasted space as the sprites are packed onto the sheet. D2kSpriteSequence does not fully benefit from this change, as it creates additional sprites afterwards in the ResolveSprites method. These are not sorted, so they often waste space due to height changes between adjacent sprites and cause an inefficient packing. Sorting them in place is insufficient, as each sequence performs the operation independently. So sets of sprites across different sequences end up with poor packing overall. We need all the sprites to be collected together and sorted in one place for best effect.
We restructure SpriteCache to allow a frame mutation function to be provided when reserving sprites. This removes the need for the ReserveFrames and ResolveFrames methods in SpriteCache. D2kSpriteSequence can use this new function to pass in the required modification, and no longer has to add frames to the sheet builder itself. Now the SpriteCache can apply the desired frame mutations, it can batch together these mutated frames with the other frames and sort them all as a single batch. With all frames sorted together the maximum benefit of this packing approach is realised.
This reduces the number of BGRA sheets required for the d2k mod from 3 to 2.
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.
The assets for the Tiberian Dawn HD mod are much larger than assets for the default mods, causing a lot of load time to be spent in Util.FastCopyIntoChannel.
We can provide a special case for the SpriteFrameType.Bgra32 format, which is the same format as the destination buffer. In this scenario we can just perform memory copies between the source and destination. Additionally, whilst the default mods require all their assets to get their alpha premultiplied, many of the Tiberian Dawn assets are already premultiplied. Being able to skip this step for these assets saves us having to interpret the bytes into colors and back again.
For the default mods, there isn't a noticeable timing difference. For Tiberian Dawn HD or other mods with modern assets sizes, a large speedup is achieved.