pygame-community / pygame-ce

🐍🎮 pygame - Community Edition is a FOSS Python library for multimedia applications (like games). Built on top of the excellent SDL library.
https://pyga.me
943 stars 156 forks source link

Segfault when making a Surface out of a Renderer object under some circumstances if logical_size was changed #3245

Open ImNotMeAnymore opened 1 day ago

ImNotMeAnymore commented 1 day ago

Environment:

pygame-ce 2.5.2 (SDL 2.30.8, Python 3.12.2)
Platform:               Linux-6.6.10-76060610-generic-x86_64-with-glibc2.35
System:                 Linux
System Version:         #202401051437~1709764300~22.04~379e7a9 SMP PREEMPT_DYNAMIC Thu M
Processor:              x86_64  SSE2: Yes       AVX2: Yes       NEON: No
Architecture:           Bits: 64bit     Linkage: ELF

Python:                 CPython 3.12.2 (main, Feb 25 2024, 16:35:05) [GCC 11.4.0]
pygame version:         2.5.2
SDL versions:           Linked: 2.30.8  Compiled: 2.30.8
SDL Mixer versions:     Linked: 2.8.0   Compiled: 2.8.0
SDL Font versions:      Linked: 2.22.0  Compiled: 2.22.0
SDL Image versions:     Linked: 2.8.2   Compiled: 2.8.2
Freetype versions:      Linked: 2.13.3  Compiled: 2.13.3

Display Driver:         Display Not Initialized
Mixer Driver:           Mixer Not Initialized

Current behavior: If you make a pygame._sdl2.video.Renderer object and change it's logical_size attribute, then make a surface out of it with Renderer.to_surface(), on some occasions the program will either segfault immediately, or it will run as normal but once it reaches the end it exits with segfault

It's somewhat inconsistent, not every logical_size value will cause the segfault, and some of those who do don't cause it every time, but there are values that appear to segfault every time

Expected behavior:

The program should continue as normal and not segfault under any of these circumstances

Test code All of these return something different every time they're run, so it's better to try them many times, saving or not the Surface in a variable doesn't seem to change the result

Not using it:

from pygame import Window
from pygame._sdl2.video import Renderer

win = Window(size=(100,100))
ren = Renderer(win)
ren.logical_size = 10,10
ren.to_surface()
print("Reaches here like normal")

This will run as normal but once the script is done it segfaults, logical_size needs to be smaller than the window size but not every value causes it to act up, if it's bigger it will always work as intended apparently

Using a different value:

from pygame import Window
from pygame._sdl2.video import Renderer

win = Window(size=(100,100))
ren = Renderer(win)
ren.logical_size = 10,80
ren.to_surface()
print("Never reaches this")

Notice how this is exactly the same as the first one but changing one of the 10's to 80 will make it segfault immediately on ren.to_surface(), having a 80 seems to segfault every time except when the other value is 1, in which case this happens: #3244

There are other specific logical_size values that raise specific errors like (10,1) on a (90,2) sized window

Assigning other value to the Renderer object:

from pygame import Window
from pygame._sdl2.video import Renderer

win = Window(size=(100,100))
ren = Renderer(win)
ren.logical_size = 10,10
ren.to_surface()
ren = "huh?"
print("This is sometimes reached, but most of the time not")

This will either segfault immediately at ren = "huh?" or it will (very rarely) continue but vomit a nasty Fatal Python error at the end of the script and then segfault (same error happens every time if you take the first example and set the window size to 90,2 and logical_size to 10,1)

gresm commented 1 day ago

This will either segfault immediately at ren = "huh?" or it will (very rarely) continue but vomit a nasty Fatal Python error at the end of the script and then segfault

Seems like the crash happens when renderer object is collected by garbage, which might happen at different moments

import gc
from pygame import Window
from pygame._sdl2.video import Renderer

win = Window(size=(100,100))
ren = Renderer(win)
ren.logical_size = 10,10
ren.to_surface()
del win, ren
gc.collect()  # Force garbage collection
print("Shouldn't be reached.")