Description
Issue №2589 opened by penn5 at 2021-05-12 20:01:03
Hi,
I'm using pygame 2.0.1 (SDL 2.0.14, Python 3.9.0) on Windows 10
At random points during my game, mostly when fewer sprites are present/moving (hard to narrow down which, since they are linked), the edges of a sprite are "left behind". For example, if a sprite is moving, it leaves behind a trail. Here is the relevant rendering code:
rects = self.root_group.draw(self.display_area, self.background)
offset = self.display_area.get_offset()
transformed = [rect.move_ip(offset) for rect in rects]
pygame.display.update(transformed)
The issue goes away when using pygame.display.flip instead. The issue also goes away when using the following code:
rects = self.root_group.draw(self.display_area, self.background)
offset = self.display_area.get_offset()
transformed = [pygame.Rect(rect.x + offset[0] - 1, rect.y + offset[1] - 1, rect.w + 2, rect.h + 2) for rect in rects]
pygame.display.update(transformed)
In this snippet, the transformed line is equivalent to this pseudocode:
transformed = rects.move(-1, -1).move_ip(offset).inflate_ip(+2, +2)
In other words, translate by offset and then grow each edge by two pixels, keeping the center the same.
I'm fairly confident that the source doesn't lie in my code, but I'm not 100% on that. I will work on a minimal reproducer for the bug tomorrow if I remember, since it's getting late here.
This bug is certainly low priority since there are two workarounds.
P.S. My rendering code has references to display_area.get_offset(). This just tells the program where to draw at, i.e. the offset of the rendering area within the display. The issue is the same if it is removed, but, interestingly, the artifacts look slightly different.
Comments
# # illume commented at 2021-05-22 20:12:15
Can you see this happening with other examples?
For example with python -m pygame.examples.aliens
?
# # penn5 commented at 2022-01-08 12:50:17
Can you see this happening with other examples?
For example with
python -m pygame.examples.aliens
?
The issue does not occur with aliens. In fact, it does not occur with RenderUpdates (which aliens uses), only LayeredDirty.
# # penn5 commented at 2022-01-08 13:06:40
I've created a minimal reproducer, which consistently causes the bug on my Windows 10 machine.
import pygame
class Moving(pygame.sprite.DirtySprite):
def __init__(self, group, w):
super().__init__(group)
self.dir = 10
self.image = pygame.Surface((20, 20))
self.image.fill(pygame.Color("red"))
self.rect = self.image.get_rect()
self.rect.y = 100
self.w = w
def update(self):
self.dirty = 1
self.rect.x += self.dir
if self.rect.x > self.w - 20 or self.rect.x < 0:
self.dir *= -1
def main():
disp = pygame.display.set_mode((0, 0), pygame.FULLSCREEN)
bgd = disp.copy()
group = pygame.sprite.LayeredDirty()
Moving(group, disp.get_width())
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
group.update()
rects = group.draw(disp, bgd)
pygame.display.update(rects)
clock.tick(20)
if __name__ == "__main__":
main()
# # penn5 commented at 2022-01-08 14:20:23
The bug can't be recorded through Snip&Sketch, SnippingTool or Xbox Game Bar, but OBS captures it well. I can send a screen recording if needed.
# # MyreMylar commented at 2022-01-08 17:04:45
Testing out various scenarios it looks like this broke between SDL 2.0.12 and SDL 2.0.14. It works like I would expect on pygame 2.0.0 (SDL 2.0.12) but breaks in pygame 2.0.1 (2.0.14)
Test case I was using (modified from above):
import pygame
class Moving(pygame.sprite.DirtySprite):
def __init__(self, group, w):
super().__init__(group)
self.dir = 20
self.image = pygame.Surface((20, 20))
self.image.fill(pygame.Color("red"))
self.rect = self.image.get_rect()
self.rect.y = 100
self.w = w
def update(self):
self.dirty = 1
self.rect.x += self.dir
if self.rect.x > self.w - 20 or self.rect.x < 0:
self.dir *= -1
def main():
screen = pygame.display.set_mode((800, 600))
background = pygame.Surface((800, 600))
background.fill((0, 0, 0))
screen.blit(background, (0, 0))
group = pygame.sprite.RenderUpdates()
moving_sprite = Moving(group, screen.get_width())
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
running = False
group.update()
group.clear(screen, background)
rects = group.draw(screen)
pygame.display.update(rects)
clock.tick(60)
if __name__ == "__main__":
main()
However, looking at the 2.0.1 release notes:
There's been a number of bugs fixed we reported in the pygame 2 series. One of which was pygame.display.update() updating the whole screen on windows. So, now 'dirty rect drawing' is faster again on windows.
So it didn't actually work in 2.0.0 either, it just looked better.
Anyway perhaps this patch did not work as hoped?
Or we are both using it wrong.
Rects on the screen/windows are being updated. You can even mess with the rects passed into update and make something that works. See this:
import pygame
class Moving(pygame.sprite.DirtySprite):
def __init__(self, group, w):
super().__init__(group)
self.dir = 5
self.image = pygame.Surface((20, 20))
self.image.fill(pygame.Color("red"))
self.rect = self.image.get_rect()
self.rect.y = 100
self.w = w
def update(self):
self.dirty = 1
self.rect.x += self.dir
if self.rect.x > self.w - 20 or self.rect.x < 0:
self.dir *= -1
def main():
screen = pygame.display.set_mode((800, 600))
background = pygame.Surface((800, 600))
background.fill((0, 0, 0))
screen.blit(background, (0, 0))
group = pygame.sprite.RenderUpdates()
moving_sprite = Moving(group, screen.get_width())
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
running = False
group.update()
group.clear(screen, background)
rects = group.draw(screen)
if rects is not None:
for i in range(0, len(rects)):
rects[i].x += moving_sprite.dir if moving_sprite.dir < 0 else 0
rects[i].w += abs(moving_sprite.dir)
pygame.display.update(rects)
clock.tick(60)
if __name__ == "__main__":
main()
But something feels wrong here.
# # penn5 commented at 2022-01-08 17:53:35
I made some tweaks to your code to make the bug more obvious and easier to understand:
import pygame
class Moving(pygame.sprite.DirtySprite):
def __init__(self, group, w):
super().__init__(group)
self.dir = 40
self.image = pygame.Surface((20, 20))
self.image.fill(pygame.Color("red"))
self.rect = self.image.get_rect()
self.rect.y = 100
self.w = w
def update(self):
self.rect.x += self.dir
if self.rect.x > self.w - 20 or self.rect.x < 0:
self.dir *= -1
pygame.display.flip()
def main():
screen = pygame.display.set_mode((800, 600))
background = pygame.Surface((800, 600))
background.fill((0, 0, 0))
screen.blit(background, (0, 0))
group = pygame.sprite.RenderUpdates()
moving_sprite = Moving(group, screen.get_width())
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
running = False
screen.fill(pygame.Color("red"), pygame.Rect(moving_sprite.rect.x, moving_sprite.rect.y - 6, 20, 5))
group.update()
group.clear(screen, background)
rects = group.draw(screen)
# uncomment me to workaround the bug
# rects = [pygame.Rect(rect.x - 1, rect.y - 1, rect.w + 2, rect.h + 2) for rect in rects]
pygame.display.update(rects)
clock.tick(20)
if __name__ == "__main__":
main()
Note the commented line, which is far simpler than your equivalent. Notably, only one pixel of border is needed.
This code draws a bunch of rectangles above the moving one (they only show after the first lap because I'm lazy). From this, we can see that the "leftover" artifact is always drawn at the "behind" edge of the rect's last position.
# # dr0id commented at 2022-01-22 16:32:35
Either I don't understand the bug or I can't reproduce with:
pygame 2.1.3.dev1 (SDL 2.0.20, Python 3.9.7) on Win10
I don't see any trail. I also tried pygame 2.0.1 but I do not see any difference.
# # penn5 commented at 2022-01-23 11:38:52
I'm using pygame 2.1.2 (SDL 2.0.18, Python 3.9.0) on Win10 21H1. It's the same whether I have one display connected or two, and happens whichever display the window is on. I'm using Intel integrated graphics i5-10210U.
You may be unable to see the bug if you have a very high DPI monitor as it is only one pixel wide. The fact that it doesn't show up in screenshots just exacerbates this problem. To make things even weirder, applying pygame.SCALED makes the bug disappear, even when the scaling is 1x.
I've attached a screenshot of the issue as reference. The thick horizontal bars just above the square are expected behavior, but the vertical one-pixel-wide bars are the bug. The exact code used to create this screenshot is below:
import pygame
class Moving(pygame.sprite.DirtySprite):
def __init__(self, group, w):
super().__init__(group)
self.dir = 40
self.image = pygame.Surface((20, 20))
self.image.fill(pygame.Color("red"))
self.rect = self.image.get_rect()
self.rect.y = 100
self.w = w
def update(self):
self.rect.x += self.dir
if self.rect.x > self.w - 20 or self.rect.x < 0:
self.dir *= -1
pygame.display.flip()
def main():
screen = pygame.display.set_mode((800, 600))
background = pygame.Surface((800, 600))
background.fill((0, 0, 0))
screen.blit(background, (0, 0))
group = pygame.sprite.RenderUpdates()
moving_sprite = Moving(group, screen.get_width())
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
running = False
screen.fill(pygame.Color("red"), pygame.Rect(moving_sprite.rect.x, moving_sprite.rect.y - 6, 20, 5))
group.clear(screen, background)
group.update()
rects = group.draw(screen)
# uncomment me to workaround the bug
# rects = [pygame.Rect(rect.x - 1, rect.y - 1, rect.w + 2, rect.h + 2) for rect in rects]
pygame.display.update(rects)
clock.tick(30)
if __name__ == "__main__":
main()
To create this photo, the order of steps is very important:
- Open OBS Studio (Snip+Sketch/Xbox does NOT work).
- Setup a display capture.
- Go to OBS settings, set base and output resolution to 800x800, downscale bilinear, FPS 10.
- Start recording.
- Launch the reproducer above.
- Wait a few seconds for the recording.
- Return to OBS and stop recording.
- Open the video file in VLC.
- Find a moment where the artifacts are clearly visible.
- Go to video -> take snapshot
# # penn5 commented at 2022-01-23 12:15:20
Reproduced on pygame 2.1.3.dev1 (SDL 2.0.20, Python 3.9.0) (built from latest main)
# # dr0id commented at 2022-01-23 19:46:04
I still can't see it on my setup. But I analyzed the image you posted.
As I can see now you have a trail there (green arrow). If you look closely to the images with blue arrow there is like a 'anti aliased' border around them.
Here I changed the colors a bit to make it more visible:
I wonder why this is happening. Maybe its something new in SDL2. For the small rects the explanation could be that they are drawn using pygame.draw.rect(), but the big one is a blit of a surface. How can this surface 'bleed' pixels? I have no clue.
Do you have some screen scaling enabled? Or does SDL2 do some scaling?
I think 'pygame.SCALED ' is a new option in pygame 2.x but I have no idea what it means.
# # Starbuck5 commented at 2022-01-23 20:01:07
@penn5 I can reproduce this.
What is your display scaling? Settings > System >Display > Scale and layout
I'm at 125%, but when I set it down to 100% I no longer see the problem.
If that's it, this seems like a bug in SDL about rounding.
# # penn5 commented at 2022-01-23 20:21:22
That's it! I had my display at 150% and the issue is gone after changing it to 100%. It seems this might actually go deeper, perhaps it's a bug in Windows. I don't know anything about Windows or SDL but having looked at the SDL source code in the patch linked above, it does seem to be almost a direct passthrough to Windows.