diff --git a/src/AcDream.App/Streaming/LandblockStreamJob.cs b/src/AcDream.App/Streaming/LandblockStreamJob.cs
new file mode 100644
index 0000000..7542c68
--- /dev/null
+++ b/src/AcDream.App/Streaming/LandblockStreamJob.cs
@@ -0,0 +1,28 @@
+using AcDream.Core.World;
+
+namespace AcDream.App.Streaming;
+
+///
+/// A job posted to 's inbox. Either a load
+/// (fetch this landblock from the dats and build its CPU-side mesh data)
+/// or an unload (release any state tied to this landblock on the render
+/// thread's next Tick drain).
+///
+public abstract record LandblockStreamJob(uint LandblockId)
+{
+ public sealed record Load(uint LandblockId) : LandblockStreamJob(LandblockId);
+ public sealed record Unload(uint LandblockId) : LandblockStreamJob(LandblockId);
+}
+
+///
+/// Outbox record the render thread drains. Either a successful load, a
+/// failed load (logged and ignored until region recenters off/back), or
+/// an unload notification (tells the render thread to release GPU state
+/// for this landblock id).
+///
+public abstract record LandblockStreamResult(uint LandblockId)
+{
+ public sealed record Loaded(uint LandblockId, LoadedLandblock Landblock) : LandblockStreamResult(LandblockId);
+ public sealed record Failed(uint LandblockId, string Error) : LandblockStreamResult(LandblockId);
+ public sealed record Unloaded(uint LandblockId) : LandblockStreamResult(LandblockId);
+}
diff --git a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs
index 767dd6d..d65b480 100644
--- a/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs
+++ b/tests/AcDream.Core.Tests/Streaming/StreamingRegionTests.cs
@@ -45,17 +45,22 @@ public class StreamingRegionTests
[Fact]
public void RecenterTo_SingleStepEast_LoadsColumn_NoUnloadsDueToHysteresis()
{
- // Radius 2 → unload threshold is radius+1 = 3.
+ // Radius 2 → unload threshold is radius+2 = 4.
// Starting center (50,50) covers X in [48..52]. Step to (51,50):
// new coverage X in [49..53]. New column is x=53 (5 entries).
- // Departing column would be x=48, but |48-51| = 3 which equals the
- // threshold, so it stays loaded (hysteresis keeps radius+1).
+ // Departing column x=48 is at distance |48-51| = 3, which is within
+ // the radius+2 threshold, so it stays loaded (hysteresis keeps radius+2).
var region = new StreamingRegion(cx: 50, cy: 50, radius: 2);
var diff = region.RecenterTo(51, 50);
Assert.Equal(5, diff.ToLoad.Count);
Assert.Empty(diff.ToUnload);
+ // Visible is strictly the 5×5 window around (51, 50).
+ Assert.Equal(25, region.Visible.Count);
+ // Resident includes the hysteresis-retained x=48 column (5 entries)
+ // plus the full new window, for 30 total.
+ Assert.Equal(30, region.Resident.Count);
}
[Fact]
@@ -63,7 +68,7 @@ public class StreamingRegionTests
{
// Starting (50,50) covers X in [48..52]. Step to (53,50):
// new coverage X in [51..55]. New columns: x=53,54,55 (15 entries).
- // x=48 is now 5 away → unload. x=49,50 still within radius+1 → keep.
+ // x=48 is now 5 away, > radius+2 = 4 → unload. x=49 is 4 away, not > 4 → keep. x=50 is 3 away, not > 4 → keep.
var region = new StreamingRegion(cx: 50, cy: 50, radius: 2);
var diff = region.RecenterTo(53, 50);