using System.Collections.Generic; using System.Linq; using AcDream.App.Rendering; using Xunit; namespace AcDream.App.Tests.Rendering; /// /// S3 chunk 3 (§9.3 T3): retail LOD cell (side n, LOD coords /// (X, Y)) covers cx ∈ [X·8/n, (X+1)·8/n), cy ∈ /// [Y·8/n, (Y+1)·8/n) of the 8×8 cell-major mesh index buffer /// (LandblockMesh.Build: cy outer, cx inner, 6 indices /// per cell). is /// pure arithmetic — no baked range table — so this pins it directly, with /// no GPU device needed. /// public sealed class TerrainLandCellIndexRunsTests { [Theory] [InlineData(8)] [InlineData(4)] [InlineData(2)] [InlineData(1)] public void EveryCellOfASide_CoversDisjointRunsTotalingTheWholeLandblock(int side) { var covered = new bool[384]; int expectedTotalPerCell = (8 / side) * (8 / side) * 6; for (int cellIndex = 0; cellIndex < side * side; cellIndex++) { var runs = new List<(int Start, int Count)>(); TerrainModernRenderer.AppendCellIndexRuns(side, cellIndex, runs); Assert.Equal(expectedTotalPerCell, runs.Sum(r => r.Count)); foreach ((int start, int count) in runs) { Assert.InRange(start, 0, 383); Assert.InRange(start + count, 1, 384); for (int i = start; i < start + count; i++) { Assert.False( covered[i], $"index {i} double-covered by side={side} cellIndex={cellIndex}"); covered[i] = true; } } } // Every side's full cell set covers the ENTIRE 384-index landblock // exactly once — disjoint AND exhaustive. Assert.All(covered, c => Assert.True(c)); } [Fact] public void Side1_EmitsOneRunOfTheWholeLandblock() { // R3: the single coarse cell's 8 per-row runs are mutually // contiguous (the mesh's rows sit back-to-back), so they collapse // into ONE run of all 384 indices — not 8 runs of 48. var runs = new List<(int Start, int Count)>(); TerrainModernRenderer.AppendCellIndexRuns(1, 0, runs); Assert.Equal(new[] { (0, 384) }, runs); } [Fact] public void Side4_EmitsTwoRunsOfTwelve() { var runs = new List<(int Start, int Count)>(); TerrainModernRenderer.AppendCellIndexRuns(4, 0, runs); Assert.Equal(2, runs.Count); Assert.All(runs, r => Assert.Equal(12, r.Count)); } [Fact] public void Side2_EmitsFourRunsOfTwentyFour() { var runs = new List<(int Start, int Count)>(); TerrainModernRenderer.AppendCellIndexRuns(2, 0, runs); Assert.Equal(4, runs.Count); Assert.All(runs, r => Assert.Equal(24, r.Count)); } [Fact] public void Side8_EmitsOneSixIndexRunAtTheCellsExactOffset() { // LOD coords (X=3, Y=5) at full resolution -> mesh cell (cx=3, cy=5) // -> vi = (cy*8 + cx)*6 = (5*8 + 3)*6 = 258 (LandblockMesh.Build: // cy outer, cx inner). int cellIndex = 3 * 8 + 5; // cellIndex = X*side + Y var runs = new List<(int Start, int Count)>(); TerrainModernRenderer.AppendCellIndexRuns(8, cellIndex, runs); Assert.Equal(new[] { (258, 6) }, runs); } [Theory] [InlineData(0)] [InlineData(3)] [InlineData(5)] [InlineData(6)] [InlineData(7)] public void InvalidSideCellCount_Throws(int side) { var runs = new List<(int Start, int Count)>(); Assert.Throws( () => TerrainModernRenderer.AppendCellIndexRuns(side, 0, runs)); } [Theory] [InlineData(8, 64)] [InlineData(8, -1)] [InlineData(4, 16)] [InlineData(1, 1)] public void OutOfRangeCellIndex_Throws(int side, int cellIndex) { var runs = new List<(int Start, int Count)>(); Assert.Throws( () => TerrainModernRenderer.AppendCellIndexRuns(side, cellIndex, runs)); } }