Skip to content

Free threading draft #3342

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions examples/multithreaded_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import random
import sys
import threading
from time import perf_counter

import pygame

pygame.init()

print(f"{sys._is_gil_enabled() = }")

WIDTH, HEIGHT = 100, 100
NUM_THREADS = 10
surface = pygame.Surface((WIDTH, HEIGHT))
surface.fill("black")

LERP_OFFSET = 0.0001


def get_random_color() -> pygame.Color:
r = random.randint(0, 255)
g = random.randint(0, 255)
b = random.randint(0, 255)
a = 255
return pygame.Color(r, g, b, a)


def multithreaded_func(
surf: pygame.Surface, target_pixel: tuple[int, int], target_color: pygame.Color
) -> None:
lerp_distance = 0

original_color = surf.get_at(target_pixel)

while (surf.get_at(target_pixel) != target_color) and lerp_distance < 1:
lerp_distance += LERP_OFFSET
new_color = original_color.lerp(target_color, lerp_distance)
surf.set_at(target_pixel, new_color)


pixels = [(col, row) for col in range(WIDTH) for row in range(HEIGHT)]

colors = [get_random_color() for _ in range(WIDTH * HEIGHT)]

args = [(pixel, colors[i]) for i, pixel in enumerate(pixels)]
batches = {
i: args[i * NUM_THREADS : (i + 1) * NUM_THREADS]
for i in range(WIDTH * HEIGHT // NUM_THREADS)
}

start = perf_counter()
for batch in batches.values():
threads: list[threading.Thread] = []
for pixel, color in batch:
new_thread = threading.Thread(
target=multithreaded_func, args=(surface, pixel, color)
)
new_thread.start()
threads.append(new_thread)

while any([t.is_alive() for t in threads]):
continue
end = perf_counter()

pygame.image.save(pygame.transform.scale_by(surface, 10), "out.png")

print(f"time taken: {end - start}")

for pixel, color in zip(pixels, colors):
surface.set_at(pixel, color)
pygame.image.save(pygame.transform.scale_by(surface, 10), "comparison.png")

import numpy

threaded_surf = pygame.image.load("out.png")
compare_surf = pygame.image.load("comparison.png")

threaded_arr = pygame.surfarray.array3d(threaded_surf)
compare_arr = pygame.surfarray.array3d(compare_surf)

threaded_alpha = pygame.surfarray.array_alpha(threaded_surf)
compare_alpha = pygame.surfarray.array_alpha(compare_surf)

numpy.testing.assert_array_equal(threaded_arr, compare_arr)
numpy.testing.assert_array_equal(threaded_alpha, compare_alpha)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ requires = [
"meson-python<=0.17.1",
"meson<=1.7.0",
"ninja<=1.12.1",
"cython<=3.0.11",
"cython<=3.1.0a1",
"sphinx<=8.1.3",
"sphinx-autoapi<=3.3.2",
"pyproject-metadata!=0.9.1",
Expand Down
34 changes: 34 additions & 0 deletions src_c/include/_pygame.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,26 @@ typedef struct {
PyObject *weakreflist;
PyObject *locklist;
PyObject *dependency;
#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
PyMutex mutex;
#endif
} pgSurfaceObject;
#define pgSurface_AsSurface(x) (((pgSurfaceObject *)x)->surf)

#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
#define LOCK_pgSurfaceObject(pgSurfacePtr) \
PyMutex_Lock(&(((pgSurfaceObject *)pgSurfacePtr)->mutex));
#else
#define LOCK_pgSurfaceObject(pgSurfacePtr)
#endif

#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
#define UNLOCK_pgSurfaceObject(pgSurfacePtr) \
PyMutex_Unlock(&(((pgSurfaceObject *)pgSurfacePtr)->mutex));
#else
#define UNLOCK_pgSurfaceObject(pgSurfacePtr)
#endif

#ifndef PYGAMEAPI_SURFACE_INTERNAL
#define pgSurface_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(surface, 0))

Expand Down Expand Up @@ -647,6 +664,23 @@ PYGAMEAPI_EXTERN_SLOTS(geometry);
} \
}

#if Py_GIL_DISABLED
#define DISABLE_GIL_SINGLE_INITIALIZATION(module, name) \
if (PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0) { \
Py_DECREF(module); \
return NULL; \
} \
printf("%s was compiled with GIL disabled (single)\n", name);
#define DISABLE_GIL_MULTIPHASE_INITIALIZATION(name) \
{Py_mod_gil, Py_MOD_GIL_NOT_USED},

#else // PY_GIL_DISABLED is not defined
#define DISABLE_GIL_SINGLE_INITIALIZATION(module, name) \
printf("%s was compiled with GIL disabled (single)\n", name);
#define DISABLE_GIL_MULTIPHASE_INITIALIZATION(name) \
{Py_mod_gil, Py_MOD_GIL_USED},
#endif

static PG_INLINE PyObject *
pg_tuple_couple_from_values_int(int val1, int val2)
{
Expand Down
127 changes: 82 additions & 45 deletions src_c/surface.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ typedef enum {
} SurfViewKind;

/* To avoid problems with non-const Py_buffer format field */
static char FormatUint8[] = "B";
static char FormatUint16[] = "=H";
static char FormatUint24[] = "3x";
static char FormatUint32[] = "=I";
#define FormatUint8 "B"
#define FormatUint16 "=H"
#define FormatUint24 "3x"
#define FormatUint32 "=I"

typedef struct pg_bufferinternal_s {
PyObject *consumer_ref; /* A weak reference to a bufferproxy object */
Expand Down Expand Up @@ -393,6 +393,9 @@ surface_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
self->weakreflist = NULL;
self->dependency = NULL;
self->locklist = NULL;
#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
memset(&(self->mutex), 0, sizeof(PyMutex));
#endif
}
return (PyObject *)self;
}
Expand Down Expand Up @@ -911,6 +914,11 @@ surf_get_at(PyObject *self, PyObject *position)
}

if (!pgSurface_Lock((pgSurfaceObject *)self)) {
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyErr_SetString(pgExc_SDLError, "Failed to lock surface");
return NULL;
}

Expand Down Expand Up @@ -943,6 +951,11 @@ surf_get_at(PyObject *self, PyObject *position)
break;
}
if (!pgSurface_Unlock((pgSurfaceObject *)self)) {
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyErr_SetString(pgExc_SDLError, "Failed to unlock surface in get_at");
return NULL;
}

Expand Down Expand Up @@ -991,6 +1004,7 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
if (x < clip_rect.x || x >= clip_rect.x + clip_rect.w || y < clip_rect.y ||
y >= clip_rect.y + clip_rect.h) {
/* out of clip area */

Py_RETURN_NONE;
}

Expand All @@ -999,6 +1013,12 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
}

if (!pgSurface_Lock((pgSurfaceObject *)self)) {
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyErr_SetString(pgExc_SDLError, "Failed to lock surface");

return NULL;
}
pixels = (Uint8 *)surf->pixels;
Expand Down Expand Up @@ -1036,6 +1056,12 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
}

if (!pgSurface_Unlock((pgSurfaceObject *)self)) {
if (PyErr_Occurred()) {
PyErr_Print();
PyErr_Clear();
}
PyErr_SetString(pgExc_SDLError, "Failed to unlock surface in set_at");

return NULL;
}

Expand Down Expand Up @@ -4597,17 +4623,64 @@ pgSurface_Blit(pgSurfaceObject *dstobj, pgSurfaceObject *srcobj,

static PyMethodDef _surface_methods[] = {{NULL, NULL, 0, NULL}};

MODINIT_DEFINE(surface)
int
exec_surface(PyObject *module)
{
PyObject *module, *apiobj;
PyObject *apiobj;
static void *c_api[PYGAMEAPI_SURFACE_NUMSLOTS];

if (pg_warn_simd_at_runtime_but_uncompiled() < 0) {
return -1;
}

if (PyModule_AddObjectRef(module, "SurfaceType",
(PyObject *)&pgSurface_Type)) {
return -1;
}

if (PyModule_AddObjectRef(module, "Surface",
(PyObject *)&pgSurface_Type)) {
return -1;
}

/* export the c api */
c_api[0] = &pgSurface_Type;
c_api[1] = pgSurface_New2;
c_api[2] = pgSurface_Blit;
c_api[3] = pgSurface_SetSurface;
apiobj = encapsulate_api(c_api, "surface");
if (PyModule_AddObjectRef(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) {
Py_XDECREF(apiobj);
return -1;
}

if (PyModule_AddObjectRef(module, "_dict", pgSurface_Type.tp_dict)) {
return -1;
}

return 0;
}

MODINIT_DEFINE(surface)
{
static PyModuleDef_Slot surf_slots[] = {
{Py_mod_exec, &exec_surface},
#if PY_VERSION_HEX >= 0x030c0000
{Py_mod_multiple_interpreters,
Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED}, // TODO: see if this can
// be supported later
#endif
#if PY_VERSION_HEX >= 0x030d0000
DISABLE_GIL_MULTIPHASE_INITIALIZATION("surface")
#endif
{0, NULL}};

static struct PyModuleDef _module = {PyModuleDef_HEAD_INIT,
"surface",
DOC_SURFACE,
-1,
0,
_surface_methods,
NULL,
surf_slots,
NULL,
NULL,
NULL};
Expand Down Expand Up @@ -4641,41 +4714,5 @@ MODINIT_DEFINE(surface)
return NULL;
}

/* create the module */
module = PyModule_Create(&_module);
if (module == NULL) {
return NULL;
}
if (pg_warn_simd_at_runtime_but_uncompiled() < 0) {
Py_DECREF(module);
return NULL;
}
if (PyModule_AddObjectRef(module, "SurfaceType",
(PyObject *)&pgSurface_Type)) {
Py_DECREF(module);
return NULL;
}

if (PyModule_AddObjectRef(module, "Surface",
(PyObject *)&pgSurface_Type)) {
Py_DECREF(module);
return NULL;
}

/* export the c api */
c_api[0] = &pgSurface_Type;
c_api[1] = pgSurface_New2;
c_api[2] = pgSurface_Blit;
c_api[3] = pgSurface_SetSurface;
apiobj = encapsulate_api(c_api, "surface");
if (PyModule_AddObject(module, PYGAMEAPI_LOCAL_ENTRY, apiobj)) {
Py_XDECREF(apiobj);
Py_DECREF(module);
return NULL;
}
if (PyModule_AddObjectRef(module, "_dict", pgSurface_Type.tp_dict)) {
Py_DECREF(module);
return NULL;
}
return module;
return PyModuleDef_Init(&_module);
}
10 changes: 10 additions & 0 deletions src_c/surflock.c
Original file line number Diff line number Diff line change
Expand Up @@ -76,22 +76,28 @@ pgSurface_LockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
PyObject *ref;
pgSurfaceObject *surf = (pgSurfaceObject *)surfobj;

LOCK_pgSurfaceObject(surfobj);

if (surf->locklist == NULL) {
surf->locklist = PyList_New(0);
if (surf->locklist == NULL) {
UNLOCK_pgSurfaceObject(surfobj);
return 0;
}
}
ref = PyWeakref_NewRef(lockobj, NULL);
if (ref == NULL) {
UNLOCK_pgSurfaceObject(surfobj);
return 0;
}
if (ref == Py_None) {
Py_DECREF(ref);
UNLOCK_pgSurfaceObject(surfobj);
return 0;
}
if (0 != PyList_Append(surf->locklist, ref)) {
Py_DECREF(ref);
UNLOCK_pgSurfaceObject(surfobj);
return 0; /* Exception already set. */
}
Py_DECREF(ref);
Expand All @@ -106,8 +112,10 @@ pgSurface_LockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
#endif
{
PyErr_SetString(PyExc_RuntimeError, "error locking surface");
UNLOCK_pgSurfaceObject(surfobj);
return 0;
}

return 1;
}

Expand Down Expand Up @@ -183,6 +191,7 @@ pgSurface_UnlockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
}

if (!found) {
UNLOCK_pgSurfaceObject(surfobj);
return noerror;
}

Expand All @@ -197,6 +206,7 @@ pgSurface_UnlockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
found--;
}

UNLOCK_pgSurfaceObject(surfobj);
return noerror;
}

Expand Down
Loading