feat(pathfinding): .swb format v6 predictive-Z residuals (#2469)

## Summary
Phase #2 of the `.swb` step-cache size-reduction roadmap (after #2465, v5 uniform elision). Stores the 16 base directional Z arrays as masked residuals against each cell's own SourceZ and omits any array that matches its prediction. Lossless, byte-identical reconstruction.

Trammel: 231.9 MB → 124.7 MB (−46%).

## Details
- Predictor: `predict = mask bit ? SourceZ : 0` (matches the baker's 0 on unwalkable directions); residual `Z − predict` via unchecked two's-complement (byte-exact for all inputs); reconstruct `Z = predict + residual`.
- A `u16 ZArrayMask` flags which of the 16 base arrays differ from prediction; matching arrays are omitted and synthesized from mask + SourceZ at read.
- Serializer-layer only: StepChunk, the cache, the algorithm, and the baker are unchanged.
- Format v6; v5 files rejected and re-baked once.

## Tests
21 v6 unit tests; full pathfinding suite green; Release build clean.
This commit is contained in:
Kamron Batman 2026-06-07 00:12:08 -07:00 committed by GitHub
parent 8e5e4f72c8
commit 94537f83f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 280 additions and 130 deletions

View file

@ -5,29 +5,72 @@ using Xunit;
namespace Server.Tests.Pathfinding;
// v5 = uniform-chunk elision on top of the v4 swim-layer format. A uniform chunk (no strata,
// no swim layer, all 19 base arrays constant) serializes to ~28 bytes; Full chunks (incl. swim
// layer + strata) round-trip byte-identically.
// v6 = predictive-Z residuals on top of the v5 uniform-elision format. Each base directional
// Z array is stored as a masked residual against the cell's own SourceZ; arrays that match
// their prediction are omitted entirely (ZArrayMask bit clear) and synthesized at read.
[Collection("Sequential Pathfinding Tests")]
public class StepCacheFileV5Tests
public class StepCacheFileV6Tests
{
private static StepChunk UniformChunk(byte walk = 0xC1, byte wet = 0x00, sbyte z = 10, int multis = 7)
[Theory]
[InlineData((sbyte)0, (sbyte)0)]
[InlineData((sbyte)10, (sbyte)10)]
[InlineData((sbyte)0, (sbyte)10)]
[InlineData((sbyte)10, (sbyte)0)]
[InlineData((sbyte)-20, (sbyte)15)]
[InlineData(sbyte.MinValue, sbyte.MaxValue)]
[InlineData(sbyte.MaxValue, sbyte.MinValue)]
[InlineData(sbyte.MinValue, (sbyte)1)]
[InlineData((sbyte)127, (sbyte)-1)]
public void Residual_RoundTrips_Losslessly_ForAllInputs(sbyte z, sbyte predict)
{
var residual = StepCacheFile.EncodeResidual(z, predict);
Assert.Equal(z, StepCacheFile.DecodeZ(predict, residual));
}
[Theory]
[InlineData((byte)0b0000_0001, 0, (sbyte)42, (sbyte)42)] // bit set -> sourceZ
[InlineData((byte)0b0000_0000, 0, (sbyte)42, (sbyte)0)] // bit clear -> 0
[InlineData((byte)0b1000_0000, 7, (sbyte)-13, (sbyte)-13)]
[InlineData((byte)0b0111_1111, 7, (sbyte)-13, (sbyte)0)]
public void Predict_UsesSourceZWhenBitSet_ZeroOtherwise(byte maskByte, int bit, sbyte sourceZ, sbyte expected)
{
Assert.Equal(expected, StepCacheFile.Predict(maskByte, bit, sourceZ));
}
// ---- builders ----
// A FULL chunk (not uniform: masks/SourceZ vary per cell) whose every directional-Z equals
// its masked prediction => all 16 base Z arrays must elide. Doubles as the coastline case:
// per-cell partial walkability with SourceZ != 0, flat where walkable, 0 where not.
private static StepChunk FlatFullChunk(int multis = 3, sbyte baseZ = 10)
{
var c = new StepChunk { BuiltMultisVersion = multis };
Array.Fill(c.WalkMask, walk);
Array.Fill(c.WetMask, wet);
Array.Fill(c.SourceZ, z);
foreach (var arr in new[]
{
c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW,
c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW
})
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
Array.Fill(arr, z);
c.WalkMask[i] = (byte)(i & 0xFF);
c.WetMask[i] = (byte)((~i) & 0xFF);
c.SourceZ[i] = (sbyte)(baseZ + (i % 7) - 3); // varies, mostly != 0
}
SetFlatDirectional(c);
return c;
}
// Sets every directional-Z to its masked prediction (walkable/wet -> SourceZ, else 0),
// i.e. perfectly flat terrain. Such arrays all elide under v6.
private static void SetFlatDirectional(StepChunk c)
{
var walk = new[] { c.WalkZN, c.WalkZNE, c.WalkZE, c.WalkZSE, c.WalkZS, c.WalkZSW, c.WalkZW, c.WalkZNW };
var swim = new[] { c.SwimZN, c.SwimZNE, c.SwimZE, c.SwimZSE, c.SwimZS, c.SwimZSW, c.SwimZW, c.SwimZNW };
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
for (var b = 0; b < 8; b++)
{
walk[b][i] = (sbyte)((c.WalkMask[i] >> b & 1) != 0 ? c.SourceZ[i] : 0);
swim[b][i] = (sbyte)((c.WetMask[i] >> b & 1) != 0 ? c.SourceZ[i] : 0);
}
}
}
private static StepChunk VariedChunk(int multis = 3)
{
var c = new StepChunk { BuiltMultisVersion = multis };
@ -56,6 +99,35 @@ public class StepCacheFileV5Tests
return c;
}
// ---- round-trip plumbing ----
private static string Write1(StepChunk c, int cx, int cy)
{
var path = Path.Combine(Path.GetTempPath(), $"swbv6_{Guid.NewGuid():N}.swb");
var emitted = false;
StepCacheFile.Write(path, 1u, 1u, (out int ox, out int oy, out StepChunk oc) =>
{
if (emitted) { ox = oy = 0; oc = null!; return false; }
emitted = true; ox = cx; oy = cy; oc = c; return true;
});
return path;
}
private static StepChunk RoundTrip(StepChunk src, int cx, int cy, out long fileLen)
{
var path = Write1(src, cx, cy);
try
{
fileLen = new FileInfo(path).Length;
using var reader = StepCacheFile.OpenForLazy(path);
Assert.NotNull(reader);
var rt = reader!.TryReadChunk(cx, cy);
Assert.NotNull(rt);
return rt!;
}
finally { File.Delete(path); }
}
private static void AssertChunksEqual(StepChunk a, StepChunk b)
{
Assert.Equal(a.BuiltMultisVersion, b.BuiltMultisVersion);
@ -88,61 +160,42 @@ public class StepCacheFileV5Tests
}
}
private static string Write1(StepChunk c, int cx, int cy)
{
var path = Path.Combine(Path.GetTempPath(), $"swbv5_{Guid.NewGuid():N}.swb");
var emitted = false;
StepCacheFile.Write(path, 1u, 1u, (out int ox, out int oy, out StepChunk oc) =>
{
if (emitted) { ox = oy = 0; oc = null!; return false; }
emitted = true; ox = cx; oy = cy; oc = c; return true;
});
return path;
}
private static StepChunk RoundTrip(StepChunk src, int cx, int cy, out long fileLen)
{
var path = Write1(src, cx, cy);
try
{
fileLen = new FileInfo(path).Length;
using var reader = StepCacheFile.OpenForLazy(path);
Assert.NotNull(reader);
var rt = reader!.TryReadChunk(cx, cy);
Assert.NotNull(rt);
return rt!;
}
finally { File.Delete(path); }
}
// ---- transform tests (Task 2) ----
[Fact]
public void IsUniform_TrueForAllIdentical_FalseForVariedStrataOrSwim()
public void FlatFull_AllArraysElide_RoundTripsAndIsCompact()
{
Assert.True(UniformChunk().IsUniform());
var varied = UniformChunk();
varied.WalkZE[42] = 99;
Assert.False(varied.IsUniform());
var strata = UniformChunk();
var offsets = new ushort[StepChunk.CellsPerChunk];
Array.Fill(offsets, StepChunk.NoStrata);
offsets[0] = 0;
strata.SetStrata(offsets, new byte[] { 0 });
Assert.False(strata.IsUniform());
var swim = UniformChunk();
swim.AllocateSwimLayer(); // a uniform-looking base but with a swim layer is NOT uniform
Assert.False(swim.IsUniform());
}
[Fact]
public void Uniform_RoundTrips_Identically_AndIsCompact()
{
var src = UniformChunk(walk: 0xC1, wet: 0x00, z: 12, multis: 9);
var src = FlatFullChunk();
var rt = RoundTrip(src, 5, 6, out var fileLen);
Assert.True(fileLen < 200, $"uniform .swb too large: {fileLen}");
AssertChunksEqual(src, rt);
// Full record with all 16 Z arrays elided: header(48) + ~783-byte record + index(20).
// A v5 full record alone is > 5 KB, so a sub-1100-byte file proves elision fired.
Assert.True(fileLen < 1100, $"expected all base Z arrays to elide; file was {fileLen} bytes");
}
[Fact]
public void SlopedSubset_OnlyVaryingArraysPresent_RoundTrips()
{
var flat = FlatFullChunk();
var flatPath = Write1(flat, 1, 1);
long flatLen;
try { flatLen = new FileInfo(flatPath).Length; } finally { File.Delete(flatPath); }
// Bump WalkZN by +1 on cells walkable to the N (slope in one direction only) -> exactly
// one base Z array (WalkZN, bit 0) becomes present; the other 15 still elide.
var sloped = FlatFullChunk();
for (var i = 0; i < StepChunk.CellsPerChunk; i++)
{
if ((sloped.WalkMask[i] & 1) != 0)
{
sloped.WalkZN[i] = (sbyte)(sloped.WalkZN[i] + 1);
}
}
var rt = RoundTrip(sloped, 2, 3, out var slopedLen);
AssertChunksEqual(sloped, rt);
Assert.True(slopedLen > flatLen, "one present array should grow the record vs all-flat");
Assert.True(slopedLen <= flatLen + StepChunk.CellsPerChunk, "only one 256-byte residual array should be added");
}
[Fact]
@ -152,6 +205,20 @@ public class StepCacheFileV5Tests
AssertChunksEqual(src, RoundTrip(src, 1, 2, out _));
}
// ---- shape coverage (Task 3) ----
[Fact]
public void Coastline_NonzeroSourceZ_PartialWalkability_AllElide()
{
// FlatFullChunk already models a coastline: per-cell partial walk/wet masks, SourceZ != 0,
// flat where walkable and 0 (baker default) where not. A plain SourceZ residual would emit
// -SourceZ on every unwalkable direction; the masked predictor must drive ALL arrays to elide.
var src = FlatFullChunk(multis: 2, baseZ: 25);
var rt = RoundTrip(src, 7, 7, out var fileLen);
AssertChunksEqual(src, rt);
Assert.True(fileLen < 1100, $"masked predictor should elide every array on flat coastline; file was {fileLen} bytes");
}
[Fact]
public void SwimLayer_Full_RoundTrips_Identically()
{
@ -178,24 +245,10 @@ public class StepCacheFileV5Tests
Assert.True(rt.StrataData.SequenceEqual(src.StrataData));
}
[Fact]
public void OlderVersion_IsRejected()
{
var path = Write1(UniformChunk(), 0, 0);
try
{
var bytes = File.ReadAllBytes(path);
bytes[4] = 4; bytes[5] = 0; bytes[6] = 0; bytes[7] = 0; // version 4 < MinSupportedVersion 5
File.WriteAllBytes(path, bytes);
Assert.Null(StepCacheFile.OpenForLazy(path));
}
finally { File.Delete(path); }
}
[Fact]
public void SwimAndStrata_Full_RoundTrips_Identically()
{
// Exercises the combined trailer ordering: swim-layer trailer THEN strata trailer.
// Combined trailer ordering: swim-layer trailer THEN strata trailer, after the residual blocks.
var src = SwimChunk(multis: 11);
var offsets = new ushort[StepChunk.CellsPerChunk];
Array.Fill(offsets, StepChunk.NoStrata);
@ -210,4 +263,18 @@ public class StepCacheFileV5Tests
AssertChunksEqual(src, rt);
Assert.True(rt.StrataData.SequenceEqual(src.StrataData));
}
[Fact]
public void OlderVersion_IsRejected()
{
var path = Write1(FlatFullChunk(), 0, 0);
try
{
var bytes = File.ReadAllBytes(path);
bytes[4] = 5; bytes[5] = 0; bytes[6] = 0; bytes[7] = 0; // version 5 < MinSupportedVersion 6
File.WriteAllBytes(path, bytes);
Assert.Null(StepCacheFile.OpenForLazy(path));
}
finally { File.Delete(path); }
}
}