Skip to content

Conversation

@Tides
Copy link
Member

@Tides Tides commented Sep 29, 2025

This pr is for trying to get as much memory savings as possible. There were a lot of things with unused data arrays.
Added single value palettes for chunk sections that just consist of 1 block this makes for some more crazy savings. I believe this can be pushed a bit further but as of right now I'll put it on the backburner for when I finish the worldgen-api rework

Summary by CodeRabbit

  • Refactor
    • Improved thread-safety for chunk data access, enhancing stability under load.
    • Simplified chunk section defaults and unified palette initialization for leaner memory use.
    • Deferred storage allocation until needed to reduce overhead.
    • Streamlined heightmap support by focusing on motion-blocking data only.
    • Optimized serialization/deserialization paths for chunks and biomes.
  • Bug Fixes
    • Reduced chances of heightmap and palette inconsistencies during save/load.
  • Chores
    • Cleaned up deprecated heightmap options and constructor variants to align behavior and defaults.

@coderabbitai
Copy link

coderabbitai bot commented Sep 29, 2025

Walkthrough

Thread-safe, palette-driven containers were introduced for chunk data with new constructors, nullable DataArray, and serialization paths. Biome/block containers now delegate to a common base. Heightmap set reduced to MotionBlocking; networking writes only that heightmap. Palettes simplified with fixed global bit counts. Region (de)serialization initializes arrays explicitly.

Changes

Cohort / File(s) Summary of changes
API DataContainer (generic)
Obsidian.API/ChunkData/DataContainer.cs
Added synchronization via lock; new constructors with entryCount and palette/default; DataArray made nullable virtual; BitsPerEntry sources from Palette; IsEmpty virtual; concrete Set/Get with lazy DataArray init; WriteTo added; InitializeDataArray added; EntryCount property exposed.
Palette bases and serialization
Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs
Adjusted WriteTo: special-case BitCount==0 to write single value; namespace updated.
Heightmap enum
Obsidian.API/_Enums/HeightmapType.cs
Commented out OceanFloorWG, OceanFloor, MotionBlockingNoLeaves members.
Chunk containers refactor
Obsidian/ChunkData/BiomeContainer.cs, Obsidian/ChunkData/BlockStateContainer.cs, Obsidian/ChunkData/ChunkSection.cs
Constructors now delegate to base with entry counts and palettes; removed Set/Get/Fill from containers; IsEmpty now palette-based; DataArray nullable and only written if present; default bits set to 0 for blocks/biomes; constant MaxEntryCount=4096.
DataContainer namespace tweak
Obsidian/ChunkData/DataContainer.cs
Removed namespace declaration from file.
Global palettes defaults
Obsidian/ChunkData/GlobalBiomePalette.cs, Obsidian/ChunkData/GlobalBlockStatePalette.cs
Removed bit-count constructors; BitCount now initialized by default (biomes from registry, blocks=15); introduced parameterless constructor for GlobalBlockStatePalette.
Indirect palette hierarchy
Obsidian/ChunkData/IndirectPalette.cs, Obsidian/ChunkData/InternalIndirectPalette.cs, Obsidian/ChunkData/Palette.cs
Classes now inherit only from BaseIndirectPalette; palette factory updated: explicit case 0 for blocks (IndirectPalette), default to global; biome palette simplified with ternary; removed Registries using.
Heightmap file header
Obsidian/ChunkData/Heightmap.cs
Removed using directives and namespace; no functional code shown changed.
Networking heightmap write
Obsidian/Net/Packets/Play/Clientbound/LevelChunkWithLightPacket.cs
Simplified to write only MotionBlocking heightmap via length-prefixed array; streamlined section size write.
Chunk/world generation
Obsidian/WorldData/Chunk.cs, Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs, Obsidian/WorldData/World.cs
Removed constants; reduced initialized heightmaps to WorldSurface/WorldSurfaceWG/MotionBlocking; removed CalculateHeightmap; constructor now uses ChunkSection(yBase only); commented out OceanFloor and MotionBlockingNoLeaves updates; minor method formatting in World.
Region (de)serialization
Obsidian/WorldData/Region.cs
Deserialization casts palette entries; uses InitializeDataArray for blocks/biomes; serialization guards DataArray null; added API palettes using.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant DC as DataContainer<T>
  participant Pal as Palette<T>
  participant DA as DataArray?

  rect rgba(230,240,255,0.5)
  Note over DC: Thread-safe Set
  Caller->>DC: Set(x,y,z, value)
  activate DC
  DC->>DC: lock(storageLock)
  DC->>Pal: GetOrAddId(value)
  alt DataArray is null and BitsPerEntry > 0
    DC->>DC: InitializeDataArray()
    DC->>DA: create storage
  end
  DC->>DA: Set(blockIndex, paletteId)
  deactivate DC
  end
Loading
sequenceDiagram
  autonumber
  participant Server as LevelChunkWithLightPacket
  participant Chunk as Chunk
  participant HM as Heightmaps
  participant Writer as NetWriter

  Server->>Chunk: Access Heightmaps[MotionBlocking]
  Chunk-->>Server: MotionBlocking data (long[])
  Server->>Writer: Write hash(MotionBlocking)
  Server->>Writer: WriteLengthPrefixedArray(long[])
  Note right of Writer: Only MotionBlocking heightmap serialized
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

performance

Poem

A nibble of bits, a palette reborn,
I hop through chunks at the break of morn.
Arrays awake only when they must,
MotionBlocking rides the packet’s gust.
Heights trimmed neat, with locks that keep—
Carrots of code for restful sleep. 🥕✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Worldgen savings” concisely indicates that the pull request aims to introduce savings in world generation, which aligns with the PR’s objective of reducing resource usage during worldgen, although it remains somewhat broad by not specifying that the savings are in memory usage.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch worldgen-savings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🧪 Early access (Sonnet 4.5): enabled

We are currently testing the Sonnet 4.5 model, which is expected to improve code review quality. However, this model may lead to increased noise levels in the review comments. Please disable the early access features if the noise level causes any inconvenience.

Note:

  • Public repositories are always opted into early access features.
  • You can enable or disable early access features from the CodeRabbit UI or by updating the CodeRabbit configuration file.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the api Relates to Obsidian.API label Sep 29, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
Obsidian/ChunkData/BiomeContainer.cs (1)

17-24: Missing null check for DataArray in serialization.

WriteTo accesses this.DataArray.storage directly (line 23), but DataArray is now nullable per the base class changes. With single-value palettes (when BitsPerEntry == 0), DataArray will be null, causing a NullReferenceException during serialization.

Apply this diff to add a null check:

 public override void WriteTo(INetStreamWriter writer)
 {
     writer.WriteByte(this.BitsPerEntry);
 
     this.Palette.WriteTo(writer);
 
-    writer.WriteLongArray(this.DataArray.storage);
+    if (this.DataArray != null)
+        writer.WriteLongArray(this.DataArray.storage);
 }
Obsidian.API/ChunkData/DataContainer.cs (1)

33-39: Missing null check in GrowDataArray.

GrowDataArray accesses DataArray.BitsPerEntry (line 35) without checking if DataArray is null. Since DataArray is now nullable and can be null when using single-value palettes (BitsPerEntry == 0), this will cause a NullReferenceException if called with a null DataArray.

Apply this diff to add a null check:

 public void GrowDataArray()
 {
-    if (Palette.BitCount <= DataArray.BitsPerEntry)
+    if (DataArray == null || Palette.BitCount <= DataArray.BitsPerEntry)
         return;
 
     DataArray = DataArray.Grow(Palette.BitCount);
 }
Obsidian/ChunkData/BlockStateContainer.cs (1)

31-90: Null reference exception when DataArray is null.

GetNonAirBlocks() accesses DataArray[i] on lines 45, 75, and 85 without checking if DataArray is null. When Palette.BitCount == 0 (single-value palette), DataArray is null, causing a null reference exception.

Apply this diff to handle the null DataArray case:

 private short GetNonAirBlocks()
 {
+    // Single-value palette: all blocks are the same.
+    if (this.DataArray is null)
+    {
+        if (Palette.TryGetId(BlocksRegistry.Air, out _) ||
+            Palette.TryGetId(BlocksRegistry.CaveAir, out _) ||
+            Palette.TryGetId(BlocksRegistry.VoidAir, out _))
+        {
+            return 0; // All blocks are air variants
+        }
+        return MaxEntryCount; // All blocks are non-air
+    }
+
     int validBlocksCount = 0;

     if (!Palette.TryGetId(BlocksRegistry.Air, out var indexOne))
🧹 Nitpick comments (1)
Obsidian/ChunkData/BiomeContainer.cs (1)

11-15: Redundant property assignments in constructor.

The private constructor delegates to base(64, palette) which already sets the Palette property in the base class, but then reassigns Palette = palette in the body. This is redundant.

Apply this diff to remove the redundant assignment:

-private BiomeContainer(IPalette<Biome> palette, DataArray dataArray) : base(64, palette)
-{
-    Palette = palette;
-    DataArray = dataArray;
-}
+private BiomeContainer(IPalette<Biome> palette, DataArray dataArray) : base(64, palette)
+{
+    DataArray = dataArray;
+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 796f2ca and cd8649c.

📒 Files selected for processing (18)
  • Obsidian.API/ChunkData/DataContainer.cs (2 hunks)
  • Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (3 hunks)
  • Obsidian.API/_Enums/HeightmapType.cs (1 hunks)
  • Obsidian/ChunkData/BiomeContainer.cs (1 hunks)
  • Obsidian/ChunkData/BlockStateContainer.cs (4 hunks)
  • Obsidian/ChunkData/ChunkSection.cs (1 hunks)
  • Obsidian/ChunkData/DataContainer.cs (0 hunks)
  • Obsidian/ChunkData/GlobalBiomePalette.cs (1 hunks)
  • Obsidian/ChunkData/GlobalBlockStatePalette.cs (1 hunks)
  • Obsidian/ChunkData/Heightmap.cs (0 hunks)
  • Obsidian/ChunkData/IndirectPalette.cs (1 hunks)
  • Obsidian/ChunkData/InternalIndirectPalette.cs (1 hunks)
  • Obsidian/ChunkData/Palette.cs (1 hunks)
  • Obsidian/Net/Packets/Play/Clientbound/LevelChunkWithLightPacket.cs (2 hunks)
  • Obsidian/WorldData/Chunk.cs (1 hunks)
  • Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs (2 hunks)
  • Obsidian/WorldData/Region.cs (5 hunks)
  • Obsidian/WorldData/World.cs (1 hunks)
💤 Files with no reviewable changes (2)
  • Obsidian/ChunkData/Heightmap.cs
  • Obsidian/ChunkData/DataContainer.cs
🧰 Additional context used
🧬 Code graph analysis (12)
Obsidian/WorldData/World.cs (2)
Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs (1)
  • Heightmaps (278-337)
Obsidian/Utilities/NumericsHelper.cs (1)
  • NumericsHelper (5-19)
Obsidian/WorldData/Region.cs (6)
Obsidian.Nbt/NbtCompound.cs (7)
  • NbtCompound (8-153)
  • NbtCompound (22-28)
  • NbtCompound (30-34)
  • NbtCompound (36-40)
  • GetInt (75-75)
  • TryGetTag (46-46)
  • TryGetTag (47-57)
Obsidian/ChunkData/GlobalBiomePalette.cs (1)
  • GetOrAddId (16-16)
Obsidian/Registries/BlocksRegistry.cs (2)
  • BlocksRegistry (6-165)
  • BlocksRegistry (11-30)
Obsidian/ChunkData/BlockStateContainer.cs (4)
  • BlockStateContainer (3-95)
  • BlockStateContainer (10-10)
  • BlockStateContainer (12-16)
  • BlockStateContainer (92-92)
Obsidian.API/ChunkData/DataContainer.cs (1)
  • InitializeDataArray (41-47)
Obsidian/ChunkData/BiomeContainer.cs (4)
  • BiomeContainer (3-29)
  • BiomeContainer (9-9)
  • BiomeContainer (11-15)
  • BiomeContainer (26-26)
Obsidian/ChunkData/IndirectPalette.cs (2)
Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (3)
  • BaseIndirectPalette (5-103)
  • BaseIndirectPalette (11-15)
  • BaseIndirectPalette (17-22)
Obsidian/ChunkData/GlobalBlockStatePalette.cs (1)
  • IBlock (19-19)
Obsidian/Net/Packets/Play/Clientbound/LevelChunkWithLightPacket.cs (3)
Obsidian/WorldData/Chunk.cs (3)
  • Chunk (6-251)
  • Chunk (22-43)
  • Chunk (45-52)
Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs (1)
  • Heightmaps (278-337)
Obsidian.API/_Interfaces/INetStreamWriter.cs (1)
  • WriteLong (19-19)
Obsidian/ChunkData/GlobalBiomePalette.cs (1)
Obsidian.API/Registries/CodecRegistry.cs (1)
  • CodecRegistry (8-18)
Obsidian/ChunkData/GlobalBlockStatePalette.cs (2)
Obsidian/ChunkData/IndirectPalette.cs (2)
  • IPalette (24-29)
  • IBlock (16-22)
Obsidian/ChunkData/Palette.cs (2)
  • IPalette (5-14)
  • IPalette (16-21)
Obsidian.API/ChunkData/DataContainer.cs (5)
Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (4)
  • T (24-24)
  • IPalette (77-80)
  • GetOrAddId (51-75)
  • WriteTo (82-96)
Obsidian/ChunkData/InternalIndirectPalette.cs (2)
  • T (16-25)
  • IPalette (27-32)
Obsidian/ChunkData/BlockStateContainer.cs (2)
  • GetIndex (94-94)
  • WriteTo (18-29)
Obsidian/ChunkData/BiomeContainer.cs (2)
  • GetIndex (28-28)
  • WriteTo (17-24)
Obsidian.API/_Interfaces/INetStreamWriter.cs (1)
  • WriteLongArray (28-28)
Obsidian/ChunkData/InternalIndirectPalette.cs (2)
Obsidian.API/ChunkData/DataContainer.cs (1)
  • T (68-80)
Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (4)
  • T (24-24)
  • BaseIndirectPalette (5-103)
  • BaseIndirectPalette (11-15)
  • BaseIndirectPalette (17-22)
Obsidian/ChunkData/BiomeContainer.cs (4)
Obsidian/WorldData/Chunk.cs (1)
  • Biome (65-74)
Obsidian/ChunkData/ChunkSection.cs (2)
  • Biome (44-44)
  • Biome (95-95)
Obsidian/ChunkData/GlobalBiomePalette.cs (2)
  • Biome (18-18)
  • IPalette (20-20)
Obsidian/ChunkData/Palette.cs (2)
  • IPalette (5-14)
  • IPalette (16-21)
Obsidian/ChunkData/Palette.cs (4)
Obsidian/ChunkData/GlobalBiomePalette.cs (3)
  • IPalette (20-20)
  • Biome (18-18)
  • GlobalBiomePalette (3-25)
Obsidian/ChunkData/GlobalBlockStatePalette.cs (3)
  • IPalette (21-21)
  • IBlock (19-19)
  • GlobalBlockStatePalette (3-24)
Obsidian/ChunkData/IndirectPalette.cs (5)
  • IPalette (24-29)
  • IBlock (16-22)
  • IndirectPalette (6-30)
  • IndirectPalette (8-10)
  • IndirectPalette (12-14)
Obsidian/ChunkData/InternalIndirectPalette.cs (4)
  • IPalette (27-32)
  • InternalIndirectPalette (6-33)
  • InternalIndirectPalette (8-10)
  • InternalIndirectPalette (12-14)
Obsidian/WorldData/Chunk.cs (1)
Obsidian/ChunkData/ChunkSection.cs (3)
  • ChunkSection (5-102)
  • ChunkSection (24-33)
  • ChunkSection (35-40)
Obsidian/ChunkData/BlockStateContainer.cs (5)
Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (2)
  • IPalette (77-80)
  • WriteTo (82-96)
Obsidian/ChunkData/GlobalBlockStatePalette.cs (3)
  • IPalette (21-21)
  • IBlock (19-19)
  • WriteTo (23-23)
Obsidian/ChunkData/IndirectPalette.cs (2)
  • IPalette (24-29)
  • IBlock (16-22)
Obsidian/ChunkData/Palette.cs (3)
  • IPalette (5-14)
  • IPalette (16-21)
  • Palette (3-22)
Obsidian.API/ChunkData/DataContainer.cs (1)
  • WriteTo (82-90)
🔇 Additional comments (61)
Obsidian/Net/Packets/Play/Clientbound/LevelChunkWithLightPacket.cs (3)

14-20: Good memory optimization by reducing heightmap data.

The change to send only the MotionBlocking heightmap aligns well with the PR's memory savings objectives. The comment clearly documents this intentional reduction.


33-33: Confirmed Size returns int; cast removal is safe
The Size property in NetworkBuffer is defined as public int Size => size;, so passing it directly to WriteVarInt is correct.


16-17: No changes required for GetDataArray() return type or dictionary access
GetDataArray() returns long[], and the Heightmaps dictionary is always seeded with the MotionBlocking key, so using the indexer here is safe.

Obsidian/ChunkData/GlobalBlockStatePalette.cs (4)

3-3: LGTM: Parameterless constructor simplifies instantiation.

The addition of a parameterless constructor aligns with the PR's goal to simplify palette initialization and reduce configurability overhead.


6-6: LGTM: Fixed BitCount is correct for global palette.

Initializing BitCount to 15 is appropriate for a global block state palette, as it supports up to 32,768 unique block states, which covers the full range of Minecraft block states. This change eliminates runtime configurability and aligns with the memory savings objective.


3-3: LGTM: Parameterless constructor added.

The primary constructor syntax is appropriate here and aligns with the simplified palette initialization pattern across the PR.


6-6: GlobalBlockStatePalette.BitCount remains hard-coded to 15 but is never referenced elsewhere in the codebase; please confirm against the Minecraft 1.21.x specification that 15 bits (32 768 entries) is the correct fixed size for the global block state palette.

Obsidian/WorldData/World.cs (2)

181-181: LGTM: Simplified fluent call improves readability.

The refactor to a single chained call is cleaner and maintains the same behavior. The use of HeightmapType.WorldSurface aligns with the reduced heightmap set introduced in this PR.


181-181: LGTM: Refactored to fluent call.

The collapsed expression is more concise and maintains the same logic. The null-conditional operator correctly handles the case where the chunk is not available.

Obsidian.API/ChunkData/Palettes/BaseIndirectPalette.cs (2)

86-90: LGTM: Single-value palette optimization is a key memory savings.

This special case for BitCount == 0 is central to the PR's memory savings goal. When a chunk section consists of a single block type, serialization now writes only that single value instead of a count and a loop, reducing both memory and network overhead. The logic is correct: for a single-value palette, values[0] holds the sole entry.


92-95: LGTM: Standard palette serialization path retained.

The existing serialization path for multi-value palettes remains unchanged and correctly writes the count followed by all palette entries.

Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs (2)

287-287: LGTM: OceanFloor heightmap removed.

Commenting out the OceanFloor heightmap assignment aligns with the PR's goal to reduce memory usage by limiting the active heightmap set. This change is consistent with the removal of OceanFloor from the HeightmapType enum.


326-327: LGTM: MotionBlockingNoLeaves heightmap removed.

Commenting out the MotionBlockingNoLeaves heightmap operation reduces memory overhead. The boolean flag motionBlockingLeavesSet is still updated, preserving the early-exit optimization in the loop. This aligns with the reduced heightmap set in the PR.

Obsidian/ChunkData/ChunkSection.cs (1)

24-24: LGTM: Default to single-value palettes is a major memory optimization.

Changing the default bitsPerBlock and bitsPerBiome from 4 and 2 to 0 is a key memory savings in this PR. New chunk sections will now start with single-value palettes, minimizing memory usage for uniform or empty sections. As blocks/biomes are added, the palettes will expand dynamically as needed. This aligns perfectly with the PR's goal to reduce memory usage in world generation.

Obsidian/ChunkData/GlobalBiomePalette.cs (2)

6-6: LGTM: Direct initialization simplifies palette construction.

The removal of the constructor parameter and direct initialization of BitCount from CodecRegistry.Biomes.GlobalBitsPerEntry eliminates runtime configurability in favor of a consistent, compile-time-known value. This aligns with the PR's memory optimization goals.


6-6: LGTM: BitCount initialized from registry.

Using CodecRegistry.Biomes.GlobalBitsPerEntry is a good approach as it centralizes the configuration and makes it easier to maintain compared to a hardcoded value. The removal of the constructor parameter aligns with the PR's goal to simplify palette initialization.

Obsidian/ChunkData/InternalIndirectPalette.cs (3)

1-1: LGTM: Correct namespace import for base palette.

The using Obsidian.API.ChunkData.Palettes directive is necessary for the BaseIndirectPalette<T> type reference.


6-6: LGTM: Redundant interface declaration removed.

Removing explicit IPalette<T> from the inheritance list is correct since BaseIndirectPalette<T> already implements this interface. This simplifies the declaration without changing behavior, mirroring the same refactoring applied to IndirectPalette.


1-6: LGTM!

The inheritance change correctly delegates IPalette<T> implementation to BaseIndirectPalette<T>, simplifying the class signature while maintaining functionality.

Obsidian.API/_Enums/HeightmapType.cs (3)

11-11: Confirmed no active references to MotionBlockingNoLeaves
All occurrences are in commented-out code; removal is safe.


7-8: OceanFloor enum removal — only commented references remain.
No active usages; commented occurrences at Obsidian/WorldData/Region.cs:338, Obsidian/WorldData/Chunk.cs:30, Obsidian/WorldData/Generators/Overworld/ChunkBuilder.cs:287. Safe to remove from the public API. Remove or update those commented lines to avoid confusion.


7-11: No active references to the removed heightmap types remain. All found occurrences are commented-out, so there are no dangling enum references to fix.

Obsidian/WorldData/Chunk.cs (4)

36-40: LGTM! Single-value palette optimization applied.

The constructor now defaults to bitsPerBlock=0 and bitsPerBiome=0, enabling single-value palettes for chunk sections. This is a key memory optimization mentioned in the PR objectives, as sections with uniform blocks will use minimal storage.


27-34: No active dependencies on OceanFloor or MotionBlockingNoLeaves. Instantiation in Chunk.cs and generation logic in ChunkBuilder.cs are both commented out; no other code references these types.


27-34: LGTM! Heightmap reduction aligns with enum changes.

The commented-out heightmap entries (OceanFloor, MotionBlockingNoLeaves) align with the updated HeightmapType enum, reducing memory overhead by maintaining only the active heightmap types.


39-39: LGTM! Default zero-bit initialization supports memory savings.

The updated constructor call new ChunkSection(yBase: i - 4) defaults to 0 bits per block/biome, enabling single-value palettes for uniform sections. This aligns with the PR's memory-saving objectives.

Obsidian/WorldData/Region.cs (10)

2-2: LGTM!

The using directive is necessary for palette type references in the serialization/deserialization logic.


184-192: LGTM!

The explicit cast to NbtCompound properly handles the NBT list enumeration. The null-forgiving operator is safe given the TryGetTag check on line 184.


194-200: LGTM! Nullable DataArray pattern correctly implemented.

The code properly initializes the DataArray only when needed. For single-value palettes (BitCount==0), InitializeDataArray() returns early, leaving DataArray null. The subsequent check on line 194 ensures the data array is only assigned when present in NBT.


213-219: LGTM!

Consistent nullable DataArray pattern for biome containers.


289-291: LGTM! Critical null check for single-value palettes.

The null check is essential for single-value palettes where DataArray remains null to save memory. This prevents serialization errors when writing chunk sections with uniform block states.


2-2: LGTM!

Adding the Obsidian.API.ChunkData.Palettes namespace aligns with the palette refactor across the codebase.


187-191: LGTM! Explicit cast clarifies NbtList element type.

The explicit cast to NbtCompound ensures type safety when iterating over the palette entries, making the deserialization logic more robust.


289-291: LGTM! Null check correctly guards DataArray write.

The null check ensures that DataArray is only written when present, supporting the single-value palette optimization where DataArray is null for uniform sections.


310-311: LGTM! Null check correctly guards biome DataArray write.

The null check ensures that biome DataArray is only written when present, aligning with the single-value palette optimization.


194-200: No guard needed: data tag always matches non-zero BitCount
The NBT writer only emits the data tag when DataArray != null (i.e. BitsPerEntry > 0), and InitializeDataArray() will allocate DataArray in that case—so a data tag can never coincide with BitCount == 0.

Obsidian/ChunkData/BlockStateContainer.cs (12)

5-5: LGTM!

The named constant improves readability and maintainability.


10-16: LGTM! Constructors support palette optimization.

The constructors properly support the single-value palette pattern:

  • Default constructor uses DetermineBlockPalette() to select appropriate palette type
  • Private constructor enables efficient cloning with shared palette semantics

27-29: LGTM! Required for single-value palette serialization.

The null check prevents errors when serializing sections with single-value palettes (where DataArray is null to save memory).


43-49: LGTM!

Loop bounds correctly use MaxEntryCount constant for all three air-type checking paths.


73-79: LGTM!

Consistent use of MaxEntryCount in the single-index path.


83-89: LGTM!

Consistent use of MaxEntryCount in the two-index path.


5-5: LGTM!

Defining MaxEntryCount as a constant improves code maintainability and clarity.


8-8: LGTM! IsEmpty logic aligns with palette-driven design.

Checking Palette.Count == 0 correctly identifies empty containers, as an empty palette indicates no blocks have been added beyond the default Air block.


10-16: LGTM! Constructor refactor supports single-value palette optimization.

The updated constructors align with the palette-driven container design, enabling memory savings through single-value palettes and nullable DataArray.


27-28: LGTM! Null check correctly supports single-value palette optimization.

The null check ensures DataArray is only written when present, saving memory for uniform sections with BitCount == 0.


43-43: LGTM! Loop bounds correctly use MaxEntryCount constant.

The loop bounds correctly iterate over all 4096 entries in the chunk section.

Also applies to: 73-73, 83-83


92-94: LGTM! Clone method correctly uses private constructor.

The clone implementation properly copies the palette and data array using the private constructor.

Obsidian.API/ChunkData/DataContainer.cs (7)

8-8: Good use of Lock for thread-safe palette operations.

The introduction of storageLock ensures thread-safe access to palette and data array operations in Set and Get methods. The use of the modern Lock type is appropriate here.


21-31: Constructors correctly implement lazy DataArray allocation.

The new constructors properly support single-value palette optimization by only allocating a DataArray when BitsPerEntry != 0 (line 25). This aligns well with the PR's memory-saving objectives. The defaultValue constructor variant correctly adds the default value to the palette.


41-47: InitializeDataArray correctly handles single-value palettes.

The new InitializeDataArray method appropriately checks if BitCount == 0 before allocating a DataArray, which is essential for the single-value palette optimization.


49-66: Set implementation correctly handles lazy initialization and single-value palettes.

The Set method properly:

  • Uses locking for thread safety
  • Returns early when BitCount == 0 (line 57-58), avoiding unnecessary array operations for single-value palettes
  • Lazily initializes DataArray when needed (line 60)
  • Grows the array when the palette expands (line 62)

68-80: Get implementation correctly handles single-value palettes.

The Get method properly:

  • Uses locking for thread safety
  • Returns the single palette value when BitCount == 0 (lines 72-73), which is correct for single-value palettes
  • Safely accesses the DataArray only after confirming it exists

82-90: WriteTo correctly handles nullable DataArray.

The WriteTo method appropriately checks if DataArray != null before serializing it (line 88-89), which is essential for single-value palettes where DataArray can be null.


19-19: GetIndex is never called when BitsPerEntry == 0 Verified that DataContainer.Get/Set early-return for zero-entry palettes and all overrides use non-zero shifts; no additional guard required.

Obsidian/ChunkData/BiomeContainer.cs (1)

3-29: Inherited Get/Set cover all callers; no changes required
DataContainer defines public virtual Get(int,int,int) and Set(int,int,int,T) that BiomeContainer inherits, and BiomeContainer’s override of GetIndex ensures correct indexing for ChunkSection.GetBiome/SetBiome.

Obsidian/ChunkData/Palette.cs (3)

9-9: LGTM: Single-value palette optimization added.

The new case for bitsPerEntry == 0 returning IndirectPalette(0) is a key memory optimization for chunk sections consisting of a single block type (as mentioned in the PR objectives). This avoids allocating a full data array for uniform sections.


12-12: LGTM: Consistent with GlobalBlockStatePalette changes.

Using the parameterless constructor aligns with the removal of the bitCount parameter in GlobalBlockStatePalette, centralizing the BitCount value within the palette class itself.


18-20: LGTM: Simplified biome palette selection.

The ternary expression is more concise and moves the responsibility for BitCount configuration into the palette classes themselves, improving encapsulation.

Obsidian/ChunkData/IndirectPalette.cs (2)

1-1: LGTM: Added required using directive.

The using directive is necessary for the BaseIndirectPalette<IBlock> base class reference.


6-6: LGTM: Removed redundant interface declaration.

Since BaseIndirectPalette<IBlock> already implements IPalette<IBlock>, explicitly declaring it again on IndirectPalette is unnecessary. This simplifies the class declaration without changing behavior.

public override IPalette<IBlock> Palette { get; internal set; }

public bool IsEmpty => DataArray.storage.Length == 0;
public override bool IsEmpty => this.Palette.Count == 0;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Verify IsEmpty logic for single-value air palettes.

The IsEmpty check now uses Palette.Count == 0, but a section filled with only air would have Palette.Count == 1 (containing just the air block entry). This means an empty section would incorrectly return IsEmpty = false.

Verify whether:

  1. The palette can ever have Count == 0 (since air is added by default in ChunkSection constructor line 27-28 of relevant snippet)
  2. The intended behavior for sections containing only air blocks
  3. Whether IsEmpty should check if all entries are air rather than palette size

Consider this logic instead:

-public override bool IsEmpty => this.Palette.Count == 0;
+public override bool IsEmpty => this.Palette.Count == 1 && this.Palette.TryGetId(BlocksRegistry.Air, out _);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public override bool IsEmpty => this.Palette.Count == 0;
public override bool IsEmpty => this.Palette.Count == 1
&& this.Palette.TryGetId(BlocksRegistry.Air, out _);
🤖 Prompt for AI Agents
In Obsidian/ChunkData/BlockStateContainer.cs around line 8, the IsEmpty getter
uses Palette.Count == 0 which misses the common case where the palette contains
a single air entry; change IsEmpty to return true when the palette is empty OR
when it contains exactly one entry that represents air (e.g. Palette.Count == 1
&& Palette[0].IsAir / Palette[0].Equals(BlockState.Air) or equivalent in your
codebase). Also confirm the constructor behavior that ensures air is added by
default and adjust the check accordingly so sections filled only with air return
IsEmpty = true.

@Tides Tides marked this pull request as draft September 29, 2025 23:10
@Tides Tides closed this Oct 20, 2025
@Tides Tides deleted the worldgen-savings branch October 20, 2025 23:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Relates to Obsidian.API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants