Files
mrgameeng/tests/MrGameEng.Atlases.Tests/ShelfPackerTests.cs
T
Leonid PershinandClaude Fable 5 501d81e19f
CI / build-test (push) Failing after 1m7s
Fix engine-wide code review findings
Collisions: init bucket heads to -1 (QueryAabb hung before the first
rebuild), reset query stamps on truncated QueryAabb (later queries
silently dropped entities), inside-origin raycasts hit at fraction 0 for
circles too, exactly-touching boxes now pair like touching circles.

Graphics: render into the letterbox viewport so the picture matches
ScreenToWorld/WorldToScreen instead of stretching; Y-sort by the
transform pivot rather than the quad center; lock-free snapshot
LayerRegistry (parallel submit read it unsynchronized); validate
InitialCapacity; warn when UseRenderer2D drops options of a later scene.

Core: scenes are explicitly single-use (re-loading threw silently
duplicated systems/entities before — now it throws), Scene.RegisterUnload
for per-scene resources, a switch requested during the reveal phase
covers again instead of hard-swapping, borderless fullscreen
(HardwareModeSwitch off), InputCapture service for input-suppressing
overlays, host disposes the transition renderer and IDisposable services
on shutdown.

Input: game input reads as released while InputCapture is held; mouse
position and wheel freeze so deltas stay zero.

DevConsole: holds InputCapture while open (typing no longer drives the
camera), Revision increments only under the lock, quoted command
arguments, history capped at 256.

UI: scene Desktop skips Myra input processing while the console is open
(clicks no longer fall through), is disposed on scene unload, and Myra
init no longer depends on a process-static flag.

Audio: validate channel count/sample rate before stopping the previous
track, empty looped oggs no longer hang FillBuffers, the instance stops
when a non-looping track drains (IsPlaying was stuck true).

Atlases: metadata v2 stores per-source size+mtime snapshots, so
timestamp-preserving copies and renames invalidate correctly; loader
checks the version and disposes pages on partial load failure; shared
pages never exceed a non-POT MaxPageSize; oversized items pack first
onto exact-size pages instead of splitting an open shared page; the CLI
validates numeric options.

Assets.Generator: file names are escaped in XML docs and string
literals, members no longer collide with the enclosing class (CS0542),
and the Assets root is resolved against build_property.projectdir so
nested "Assets" directories do not shift region paths.

Pathfinding: queries throw when the grid was resized after construction;
generation stamps survive int overflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 21:18:08 +03:00

149 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using MrGameEng.Atlases;
using Xunit;
namespace MrGameEng.Atlases.Tests;
public class ShelfPackerTests
{
private static List<PackItem> Squares(int count, int size) =>
Enumerable.Range(0, count).Select(i => new PackItem($"item{i:D3}", size, size)).ToList();
[Fact]
public void Pack_PlacesEveryItem_WithinPageBounds()
{
var result = ShelfPacker.Pack(Squares(50, 60), maxPageSize: 256, padding: 2);
Assert.Equal(50, result.Placements.Count);
foreach (var p in result.Placements)
{
var (pageWidth, pageHeight) = result.PageSizes[p.Page];
Assert.True(p.X >= 2 && p.Y >= 2);
Assert.True(p.X + p.Width <= pageWidth);
Assert.True(p.Y + p.Height <= pageHeight);
}
}
[Fact]
public void Pack_NoTwoPlacements_Overlap()
{
var items = new List<PackItem>();
var random = new Random(42);
for (var i = 0; i < 200; i++)
{
items.Add(new PackItem($"r{i:D3}", random.Next(4, 90), random.Next(4, 90)));
}
var result = ShelfPacker.Pack(items, maxPageSize: 512, padding: 2);
var byPage = result.Placements.GroupBy(p => p.Page);
foreach (var page in byPage)
{
var list = page.ToList();
for (var i = 0; i < list.Count; i++)
{
for (var j = i + 1; j < list.Count; j++)
{
var a = list[i];
var b = list[j];
var separated =
a.X + a.Width + 2 <= b.X || b.X + b.Width + 2 <= a.X ||
a.Y + a.Height + 2 <= b.Y || b.Y + b.Height + 2 <= a.Y;
Assert.True(separated, $"{a.Key} overlaps {b.Key} (padding included)");
}
}
}
}
[Fact]
public void Pack_Overflows_ToMultiplePages()
{
// 9 квадратов 100² на страницу 256² помещаются максимум по 4.
var result = ShelfPacker.Pack(Squares(9, 100), maxPageSize: 256, padding: 2);
Assert.True(result.PageSizes.Count >= 3);
Assert.Equal(Enumerable.Range(0, result.PageSizes.Count), result.Placements.Select(p => p.Page).Distinct().Order());
}
[Fact]
public void Pack_OversizedItem_GetsDedicatedPage()
{
var items = Squares(2, 30);
items.Add(new PackItem("huge", 500, 40));
var result = ShelfPacker.Pack(items, maxPageSize: 256, padding: 2);
var huge = result.Placements.Single(p => p.Key == "huge");
Assert.Single(result.Placements, p => p.Page == huge.Page);
Assert.True(result.PageSizes[huge.Page].Width >= 504);
}
[Fact]
public void Pack_IsDeterministic_RegardlessOfInputOrder()
{
var items = Squares(30, 20).Concat(Squares(10, 50).Select(i => i with { Key = "b" + i.Key })).ToList();
var shuffled = items.AsEnumerable().Reverse().ToList();
var a = ShelfPacker.Pack(items, 128, 2);
var b = ShelfPacker.Pack(shuffled, 128, 2);
Assert.Equal(
a.Placements.OrderBy(p => p.Key, StringComparer.Ordinal),
b.Placements.OrderBy(p => p.Key, StringComparer.Ordinal));
}
[Fact]
public void Pack_TrimsPages_ToPowerOfTwo()
{
var result = ShelfPacker.Pack(Squares(1, 50), maxPageSize: 2048, padding: 2);
Assert.Equal((64, 64), result.PageSizes.Single());
}
[Theory]
[InlineData(1, 1)]
[InlineData(64, 64)]
[InlineData(65, 128)]
[InlineData(2048, 2048)]
public void NextPowerOfTwo_RoundsUp(int value, int expected)
{
Assert.Equal(expected, ShelfPacker.NextPowerOfTwo(value));
}
[Fact]
public void Pack_NonPowerOfTwoLimit_SharedPagesNeverExceedIt()
{
// 40² с padding на лимит 100: POT-округление дало бы 128 > лимита.
var result = ShelfPacker.Pack(Squares(10, 40), maxPageSize: 100, padding: 2);
Assert.All(result.PageSizes, size =>
{
Assert.True(size.Width <= 100, $"page width {size.Width} > 100");
Assert.True(size.Height <= 100, $"page height {size.Height} > 100");
});
}
[Fact]
public void Pack_OversizedItem_PageHasExactPaddedSize()
{
var result = ShelfPacker.Pack([new PackItem("big", 300, 50)], maxPageSize: 128, padding: 2);
Assert.Equal((304, 54), result.PageSizes.Single());
}
[Fact]
public void Pack_OversizedAmongSmall_DoesNotSplitSharedPage()
{
// Широкий и низкий негабарит сортируется в конец по высоте; раньше он закрывал
// наполовину заполненную общую страницу, и остаток уезжал на новую.
var items = new List<PackItem> { new("big", 300, 10) };
for (var i = 0; i < 4; i++)
{
items.Add(new PackItem($"s{i}", 30, 30));
}
var result = ShelfPacker.Pack(items, maxPageSize: 128, padding: 2);
Assert.Equal(2, result.PageSizes.Count); // отдельная страница big + одна общая
}
}