Skip to content

[Audit][High] Off-by-one in buildSubchunk Y loop causes extra slice at subchunk boundary #705

@MichaelFisher1997

Description

@MichaelFisher1997

🔍 Module Scanned

modules/world-meshing/src/ (automated audit scan)

📝 Summary

The buildSubchunk function in chunk_mesh.zig has an off-by-one error in its Y-axis loop for horizontal (top/bottom) face meshing. The loop condition while (sy <= y1) iterates 17 times instead of 16, causing an extra slice to be generated at the boundary between subchunks. For the topmost subchunk (si=15, y=240-256), this results in querying getLightSafe at y=256, which triggers the known light leak bug (issue #702).

📍 Location

  • File: modules/world-meshing/src/chunk_mesh.zig:142
  • Function/Scope: buildSubchunk

🔴 Severity: High

  • High: Memory leaks, race conditions, incorrect rendering, broken features

💥 Impact

When meshing subchunk 15 (the topmost subchunk, Y range 240-255), the loop iterates with sy = 240, 241, ..., 255, 256. The final iteration queries getLightSafe at y=256, which returns MAX_LIGHT (15) due to the bug in getLightSafe at modules/world-core/src/chunk.zig:152. This causes:

  • Sky light leakage: Incorrectly bright faces rendered at chunk tops
  • Duplicate mesh slices: 17 slices instead of 16 for top faces, wasting GPU bandwidth
  • Incorrect face generation: The boundary slice at y=256 queries blocks at y=255 and y=256, but y=256 is outside the world

🔎 Evidence

// Line 137-144 in chunk_mesh.zig
const y0: i32 = @intCast(si * SUBCHUNK_SIZE);
const y1: i32 = y0 + SUBCHUNK_SIZE;

// Mesh horizontal slices (top/bottom faces)
var sy: i32 = y0;
while (sy <= y1) : (sy += 1) {  // BUG: should be sy < y1
    try greedy_mesher.meshSlice(self.allocator, chunk, neighbors, .top, sy, si, &solid_verts, &cutout_verts, &fluid_verts, atlas);
}

For subchunk 0 (si=0, y0=0, y1=16): loop runs 17 times (sy=0..16) instead of 16.
For subchunk 15 (si=15, y0=240, y1=256): loop runs 17 times (sy=240..256), with sy=256 triggering the getLightSafe bug.

Compare with the correct pattern in flat_quad_mesher.zig:38:

while (y < y1) : (y += 1) {  // Correct: only 16 iterations

And the x/z loops in the same function which are intentionally different (chunk boundaries, not subchunk boundaries):

var sx: i32 = 0;
while (sx <= CHUNK_SIZE_X) : (sx += 1) {  // Correct: 17 iterations for 16-wide chunk

🛠️ Proposed Fix

Change line 142 in chunk_mesh.zig from:

while (sy <= y1) : (sy += 1) {

to:

while (sy < y1) : (sy += 1) {

This matches the pattern used in all other meshers (flat_quad_mesher.zig, tall_cross_mesher.zig, wall_attached_mesher.zig, cross_mesher.zig) which correctly use < y1.

✅ Acceptance Criteria

  • All unit tests in zig build test pass
  • Loop iterates exactly 16 times per subchunk (sy goes from y0 to y0+15)
  • No queries to getLightSafe with y >= 256 from the meshing system
  • The fix has been verified with nix develop --command zig build test

📚 References

Metadata

Metadata

Assignees

No one assigned

    Labels

    automated-auditIssues found by automated opencode audit scansbugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions