Skip to content

atlas mode picks the wrong tiles when the tileset image size isn't an exact multiple of the tile size (round vs floor for columns) #641

Description

@Danl2620

Summary

In atlas mode, the vertex shader derives a tileset's column count with round, while the rest of the ecosystem (and this crate's own CPU-side code) uses floor. For any tileset image whose width is not an exact multiple of tile_size.x + spacing.x, the shader computes one column too many and every tile index resolves to the wrong art.

src/render/shaders/tilemap_vertex.wgsl:61:

let columns: u32 = u32(round((tilemap_data.texture_size.x - tilemap_data.spacing.x) / (tilemap_data.tile_size.x + tilemap_data.spacing.x)));

The crate disagrees with itself about this. src/render/extract.rs:93-94 floors:

let tile_count_x = ((texture_size.x) / (tile_size.x + tile_spacing.x)).floor();
let tile_count_y = ((texture_size.y) / (tile_size.y + tile_spacing.y)).floor();

Tiled also floors, so a .tmx authored against a tileset with any trailing slack renders as garbage.

Reproduction

Any tileset image whose width isn't an exact multiple of the tile width. Concretely, from a real Tiled map:

tileset image tile Tiled columns shader round()
oryx_world.png 1024x1024 24 42 43
oryx_16bit_fantasy_world_trans.png 1366x1007 24 56 57
oryx_creatures.png 432x648 24 18 18 ✓
oryx_items.png 352x128 16 22 22 ✓

1024 / 24 = 42.67. Tiled floors to 42 columns and encodes tile ids on that basis; the shader rounds to 43 and unpacks them as (id % 43, id / 43). The row shifts left by one column per row, so the error compounds the deeper into the sheet a tile lives.

The two tilesets that divide exactly are unaffected, which makes this look intermittent — sprites are fine, terrain is scrambled.

Screenshot

Same layer, same .tmx, same PNG, rendered with 42 vs 43 columns:

I have a side-by-side render of the same layer decoded from the .tmx and composited
two ways -- 42 columns vs 43 columns -- against the same source PNG. The 42-column
version matches the Tiled editor exactly; the 43-column version is unrecognisable
(wrong terrain, pots and chests where grass should be). I'll attach it in a comment
below.

Cause

The round was introduced deliberately in #367 ("Fix incorrect tile UVs when using atlas feature on AMD hardware"), which replaced an implicit floor to work around floating-point imprecision:

It seems that there's some sort of floating point imprecision thing going on where var columns ends up as 2 rather than the correct 3 after being cast as u32 / implicitly floored.

That's a real bug and the fix works for it, but round also "corrects" divisions that are legitimately fractional. 42.67 isn't imprecision — it's a tileset with 16px of unused slack on the right edge — and rounding it up to 43 is wrong.

So the current behaviour trades one failure mode for another: before #367, exact divisions could floor short on some hardware; after it, inexact divisions round up on all hardware.

Suggested fix

Tolerate the imprecision without swallowing a genuine fractional remainder — an epsilon-nudged floor rather than a round:

let raw = (tilemap_data.texture_size.x - tilemap_data.spacing.x) / (tilemap_data.tile_size.x + tilemap_data.spacing.x);
let columns: u32 = u32(floor(raw + 1e-3));

An epsilon well under one whole column still absorbs the 2.9999997 case from #367 while leaving 42.67 alone. Alternatively, compute the count once on the CPU (where extract.rs already does it correctly) and pass it through TilemapUniformData, which would remove the duplicated, divergent logic entirely.

Workaround

Crop the tileset image so its width is an exact multiple of the tile width, which makes round and floor agree. This is lossless when the slack contains no tiles (as it did here — tilecount and columns are unchanged after cropping, so existing gids stay valid).

Switching off the atlas feature also avoids it, since the texture-array path floors correctly in both texture_array_cache.rs:81-82 and :286. That isn't always available though: the array path needs one array layer per tile, and a 2296-tile tileset exceeds the 2048 array-layer limit on some hardware.

Environment

  • bevy_ecs_tilemap 0.19.0, atlas feature enabled
  • bevy 0.19.1
  • macOS 13.7.8, Metal, AMD Radeon Pro 560

The offending line is byte-identical on main and as far back as v0.12.0, so this is not specific to a recent release.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions