bevyengine / bevyengine/bevy

Nearest-neighbor sprite scaling produces 1-pixel artifacts on diagonal when window has odd dimensions

Open
#22,875 0 comments 0 reactions 0 assignees View on GitHub
A-Rendering C-Bug S-Needs-Investigation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version and features

0.18.0 with the wayland feature enabled

## \[Optional\] Relevant system information

I am running hyprland.
```ignore
`AdapterInfo { name: "AMD Radeon RX 6800 XT (RADV NAVI21)", vendor: 4098, device: 29631, device_type: DiscreteGpu, driver: "radv", driver_info: "Mesa 25.2.6", backend: Vulkan }`
```

## What you did
```rust
use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_systems(Startup, setup)
.run();
}

fn setup(mut commands: Commands, asset_server: Res) {
commands.spawn((
Camera2d,
Msaa::Off,
Projection::Orthographic(OrthographicProjection {
scale: 1.0 / 6.0,
..OrthographicProjection::default_2d()
}),
));
commands.spawn((
Sprite::from_image(asset_server.load("at_sign.png")),
Transform::from_xyz(0.0, 0.0, 0.0),
));
}
```

## What went wrong

An at sign with some weird pixel artifacts
Here is a closeup view of how the sprite renders with odd window widths.
An at sign
And here is the sprite itself.

Image
Here is the window without the artifact on the right, where hyprland assigns it an even width.

Image
And here it is on the left with the artifact, where hyprland has assigned it an odd width.

I didn't even restart the game between these screenshots. I just moved the window.

## Additional information
I tried scaling the sprite instead of the camera, and re-exporting the sprite, but neither had any effect. I ended up adding this system to my setup which was effective at eliminating the bug:
```rust
fn force_even_viewport(windows: Query<&Window>, mut cameras: Query<&mut Camera>) {
let Ok(window) = windows.single() else {
return;
};
let Ok(mut camera) = cameras.single_mut() else {
return;
};

let w = window.physical_width();
let h = window.physical_height();
let even_w = w & !1;
let even_h = h & !1;

if w == even_w && h == even_h {
camera.viewport = None;
} else {
camera.viewport = Some(Viewport {
physical_position: UVec2::ZERO,
physical_size: UVec2::new(even_w, even_h),
..default()
});
}
```

Contributor guide

Open the contributing guide

Research direction

Reproduce the provided setup with Bevy 0.18.0, ImagePlugin::default_nearest(), Camera2d, and an odd physical window width under Wayland. Compare the sprite with even and odd dimensions, then trace the camera viewport and nearest-neighbor rendering behavior; done means the diagonal 1-pixel artifact no longer appears without requiring the force_even_viewport workaround.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.