pythonsdlvoid-pointerspysdl2

How to access SDL Surface pixel data in pySDL


I am making a 3d renderer in python, I am using pySDL for the display as pygame didn't seem to have fast enough pixel writes. I need to access pixels as an array and it seems like this shouldn't be too hard in SDL, according to the docs the pixel data can be accessed directly, but here is the problem, its a null pointer and python treats it as an integer, it when printed surface.pixels returns a seemingly random large number, I can write to this value and retrieve it again later but it seems to have no affect on what is displayed. Even stranger, python allows me to index the surface as surface[<some number>].pixels, I know in c you can often index pointers as if they are arrays because of how arrays are handled, and printing surface[0] and surface[1], shows the memory locations to be 32 bits apart, which figures since one pixel is a 32 bit unsigned int. What doesn't make sense is that surface is considered to be a LP_SDL_Surface but surface[0] is a SDL_Surface. I'm sure this would make a lot more sense if I was working in c, but this is python so I am very confused. If its relevant this is a surface created by SDL_GetWindowSurface.

For those who have any idea what is going on, my goal is to be able to access the pixel data of the window/surface as and array of values I can read and write to, a function to modify one pixel at a time is not good enough, unless there is simply no other option.

And before someone tells me about minimum reproducible example

global win, outputSurface
    SDL_Init(SDL_INIT_VIDEO)
    win = SDL_CreateWindow(b"3D-Renderer", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640, 480, 0)
    outputSurface = SDL_GetWindowSurface(win)
print(outputSurface)
print(outputSurface[0])
print(outputSurface[0].pixels)
print(outputSurface[1].pixels)

Solution

  • Take this example

    from sdl2 import *;
    import sys;
    import ctypes;
    
    SDL_Init(SDL_INIT_VIDEO);
    win = SDL_CreateWindow(b"test",
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
            640, 480, 0);
    surf = SDL_GetWindowSurface(win);
    
    # get pointer to pixels as uint32[]
    u32_pixels = ctypes.cast(surf[0].pixels, ctypes.POINTER(ctypes.c_uint32));
    # get surface width
    width = surf[0].w;
    
    # main loop
    while True:
        # process events, exit if window is closed
        # (spinning event loop is a must, even if you don't react to events)
        ev = SDL_Event();
        while(SDL_PollEvent(ev)):
            if(ev.type == SDL_QUIT):
                sys.exit(0);
    
        # clear surface
        SDL_FillRect(surf, None, 0);
    
        # draw diagonal line
        for i in range(0, 480):
            u32_pixels[i*width+i] = 0xffffffff;
    
        # signal SDL that surface is ready to be presented on screen
        SDL_UpdateWindowSurface(win, surf);
    

    What SDL_GetWindowSurface returns is SDL_Surface*. In C, pointers could be dereferenced as arrays (it is dereferencing side's responsibility to make sure it is permitted or makes sense - i.e. size of array needs to be passed separately somehow). ctypes allows two ways of dereferencing a pointer - either as array (surf[0]), or via contents field (e.g. surf.contents.w).

    You may want to check returned surface pixel format (surf[0].format[0].BytesPerPixel and bit masks). Also note that if window is resized surface is invalidated (meaning any access to it may do whatever, usually crash), and you need to get new one via SDL_GetWindowSurface. With that in mind, it may be prefered to manually allocate surface with fixed size and pixel format via SDL_CreateRGBSurface, do whatever drawing you need, and then copy this surface to window surface via SDL_BlitSurface.