Skip to content

[Audit][High] ClassificationCache recenter alignment causes boundary position mismatch #475

@MichaelFisher1997

Description

@MichaelFisher1997

🔍 Module Scanned

src/world/worldgen/ (automated audit scan)

📝 Summary

The ClassificationCache.recenter function does not enforce alignment of the center position to CELL_SIZE. When a misaligned center is provided, the contains() check can return true for world positions that map to out-of-bounds cell indices in getCellIndex(), causing cache corruption or missed lookups at chunk boundaries.

📍 Location

  • File: src/world/worldgen/gen_region.zig:418-447
  • Function/Scope: ClassificationCache.recenter and ClassificationCache.getCellIndex

🔴 Severity: High

  • High: Memory corruption, cache misses, incorrect terrain at chunk boundaries

💥 Impact

When recenter(center_x, center_z) is called with coordinates that are not multiples of CELL_SIZE (8 blocks), the cache's contains() check returns true for positions near the grid boundary that actually map to invalid cell indices. This causes:

  1. get() to return garbage data from out-of-bounds cells
  2. put() to write to incorrect cells
  3. has() to return incorrect results

In practice, this affects LOD terrain generation at chunk boundaries when the player moves to non-aligned positions.

🔎 Evidence

// gen_region.zig:418-427
pub fn recenter(self: *ClassificationCache, center_x: i32, center_z: i32) void {
    const half_size: i32 = @intCast((GRID_SIZE * CELL_SIZE) / 2);
    self.origin_x = center_x - half_size;  // NOT aligned to CELL_SIZE!
    self.origin_z = center_z - half_size;
    ...
}

// gen_region.zig:429-436
pub fn contains(self: *const ClassificationCache, world_x: i32, world_z: i32) bool {
    const grid_extent: i32 = @intCast(GRID_SIZE * CELL_SIZE);
    return world_x >= self.origin_x and
        world_x < self.origin_x + grid_extent;  // Half-open interval [origin, origin+extent)
}

// gen_region.zig:439-447
fn getCellIndex(self: *const ClassificationCache, world_x: i32, world_z: i32) ?usize {
    if (!self.contains(world_x, world_z)) return null;
    const local_x: u32 = @intCast(world_x - self.origin_x);
    const cell_x = local_x / CELL_SIZE;  // Can exceed GRID_SIZE when origin is misaligned!
    ...
}

Example of bug:

  • center_x = 1 (not aligned to 8)
  • origin_x = 1 - 1024 = -1023
  • contains(1024)1024 >= -1023 and 1024 < -1023 + 2048 = 1025TRUE
  • getCellIndex(1024)local_x = 2047, cell_x = 2047 / 8 = 256OUT OF BOUNDS (valid: 0-255)

When center_x = 0 (aligned), origin_x = -1024, and contains(1023) returns TRUE with cell_x = 255 (correct, the last valid cell).

🛠️ Proposed Fix

Modify recenter to align center_x and center_z to CELL_SIZE before computing origin:

pub fn recenter(self: *ClassificationCache, center_x: i32, center_z: i32) void {
    const half_size: i32 = @intCast((GRID_SIZE * CELL_SIZE) / 2);
    // Align center to CELL_SIZE before computing origin
    const aligned_center_x = @divFloor(center_x, @as(i32, @intCast(CELL_SIZE))) * @as(i32, @intCast(CELL_SIZE));
    const aligned_center_z = @divFloor(center_z, @as(i32, @intCast(CELL_SIZE))) * @as(i32, @intCast(CELL_SIZE));
    self.origin_x = aligned_center_x - half_size;
    self.origin_z = aligned_center_z - half_size;
    ...
}

Alternatively, use @divTrunc which is more idiomatic in Zig for this alignment pattern.

✅ Acceptance Criteria

  • recenter() properly aligns the cache origin to CELL_SIZE boundaries
  • All positions that pass contains() return valid cell indices from getCellIndex()
  • Existing tests pass with nix develop --command zig build test
  • New unit test verifies alignment behavior: when center is misaligned, cache still operates correctly

📚 References

  • Related module: ClassificationCache in gen_region.zig
  • CELL_SIZE = 8 defined in world_class.zig:22
  • Issue affects LOD terrain generation via OverworldGenerator.generateHeightmapOnly()

Metadata

Metadata

Assignees

No one assigned

    Labels

    automated-auditIssues found by automated opencode audit scansbugSomething isn't workinghotfixquestionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions