6

I have been fooling around with some simple graphics things in Python, but to improve performance, I'd like to do some stuff in C.

I know how to transfer arrays and stuff back and forth. That's no biggie. But I thought it might be beneficial if I could just create a windows with a canvas, pass a pointer to the video memory (of course not the physical video memory) and then let Python take care of just putting that memory to the screen, while the rest is done in C. Possibly asynchronously, but I don̈́'t know if that matters.

Is this possible, and does it make sense? Or am I on the completely wrong track?

My efforts so far:

# graphics.py

import ctypes
import pygame

screen = pygame.display.set_mode((300,200))

f = ctypes.CDLL('/path/to/engine.so')

f.loop(screen._pixels_address)

And

// engine.c

#include <stdint.h>

void loop(void *mem) {
    while(1) {
        uint8_t *p = (uint8_t*) mem;
      
        // Was hoping this would make some pixels change
        for(int i=0; i<20000; i++)
            *p=127;
    }
}

This didn't work and ended up with a crash. I'm not surprised, but well, it's what I've got so far.

It's not strictly necessary to use Python. But I do want to use C. And I'm also aware that in most cases, my approach is not the best one, but I do enjoy coding in C and doing stuff old school.

klutt
  • 30,332
  • 17
  • 55
  • 95
  • Can you upload some code to show where you would like speed improvements? In my experience, python with openCV and numpy will be quite fast. – Deepak Garud Sep 01 '22 at 09:40
  • @DeepakGarud I don't have any code yet. It's just that I'm fairly comfortable manipulating pointers in C and I enjoy doing things old school. – klutt Sep 01 '22 at 09:43
  • I was trying to understand what you are trying to do. Do you need python just to put an image on screen? If you are looping over image data, what are you trying to process? – Deepak Garud Sep 01 '22 at 09:46
  • @DeepakGarud I'm not strictly in need of python, no. But that's a language I'm fairly comfortable with. What I want to do is to have a chunk of memory that acts as video memory which I can directly manipulate in C, and then have this displayed in a window on Linux. – klutt Sep 01 '22 at 10:35
  • What about using some GUI library for C? – the busybee Sep 01 '22 at 12:44
  • @thebusybee Those are usually very platform dependent, so I'd like to avoid it if possible. But it is an option. – klutt Sep 01 '22 at 13:43
  • In contrary, the purpose of common libraries is to abstract from the underlying platform. See SDL, fltk, tkinter, ... I'm not talking about Win32 or X Window System. – the busybee Sep 01 '22 at 14:20
  • SFML might be an option... https://stackoverflow.com/a/60787990/2836621 – Mark Setchell Sep 02 '22 at 07:25
  • @MarkSetchell That actually seems to work – klutt Sep 02 '22 at 09:06

2 Answers2

3

Most GUI toolkits and game libraries will provide one way or another of getting access to the pixel buffer.

An easy way to get started would be with pygame. The get_buffer().raw array of pygame's Surface can be passed into a C program.

import pygame

background_colour = (255,255,255) # For the background color of your window
(width, height) = (300, 200) # Dimension of the window

screen = pygame.display.set_mode((width, height))

f = CDLL('engine.so')
f.loop(screen.get_buffer().raw)
mnistic
  • 10,866
  • 2
  • 19
  • 33
2

Essentially, you want to share a buffer with the Python application. There is a buffer protocol which exists for this exact purpose. Essentially, you share a Py_buffer object which has a buf and len field. (I didn't read through the whole thing, but you can for more info.)

It turns out, that Surface.get_buffer() already returns such a buffer object. In other words, you can directly pass it to your external function:

import pygame

background_colour = (255,255,255) # For the background color of your window
(width, height) = (300, 200) # Dimension of the window

screen = pygame.display.set_mode((width, height))

import engine # engine.so

b_running = True
while b_running:
    # Quit when 'X' button is pressed.
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            b_running = False

    # Add some amount of delay.
    pygame.time.wait(50)

    # Give the buffer object to C.
    engine.paint_gray(screen.get_buffer())
    pygame.display.flip()

The key here is:

engine.paint_gray(screen.get_buffer())

This isn't exactly what you were doing, but I think you should keep the loop in the Python application to be able to handle events. Therefore, this function simply paints on the buffer. This is how this is implemented in C:

// Related: https://docs.python.org/3/extending/extending.html

#define PY_SSIZE_T_CLEAN
#include <Python.h>

#include <stdint.h>
#include <assert.h>

static PyObject* engine_paint_gray(PyObject *self, PyObject *args)
{
    Py_buffer buffer;

    if (!PyArg_ParseTuple(args, "s*", &buffer))
        assert(0);

    uint8_t *raw_buffer = buffer.buf;
    for (int index=0; index<buffer.len; ++index)
        raw_buffer[index] = 127;

    return Py_None;
}

static PyMethodDef EngineMethods[] = {
    {"paint_gray", engine_paint_gray, METH_VARARGS, NULL},
    {NULL, NULL, 0, NULL} /* sentinel */
};

static struct PyModuleDef enginemodule = {
    PyModuleDef_HEAD_INIT,
    "engine",
    NULL,
    -1,
    EngineMethods
};

PyMODINIT_FUNC
PyInit_engine(void)
{
    return PyModule_Create(&enginemodule);
}

Note that I am also calling pygame.display.flip(). This is necessary, because pygame keeps two buffers one front buffer that is drawn on the screen and one back buffer that can be modified.

asynts
  • 2,213
  • 2
  • 21
  • 35
  • Close enough I suppose – klutt Sep 06 '22 at 15:23
  • @klutt By the way: You can also do the loop in C if you want. While testing it I just noticed that the close button didn't work because the event had to be handled in Python, that's why I changed it. – asynts Sep 06 '22 at 15:26