Skip to content
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

Free threading draft #3342

Draft
wants to merge 5 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
80 changes: 80 additions & 0 deletions examples/multithreaded_surface.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import pygame
import threading
import random
import sys

from time import perf_counter

print(f"GIL Enabled: {sys._is_gil_enabled()}")

pygame.init()

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

LERP_OFFSET = 0.01


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 get_random_format() -> int:
return random.choice([8, 12, 15, 16, 24, 32])


def multithreaded_func(
surf: pygame.Surface,
target_pixel: tuple[int, int],
target_color: pygame.Color,
depth: int,
) -> 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)
surf.convert(depth)


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

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

depths = [get_random_format() for _ in range(WIDTH * HEIGHT)]

args = [(pixel, colors[i], depths[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 arg in batch:
new_thread = threading.Thread(target=multithreaded_func, args=(surface, *arg))
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")
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",
]
Expand Down
1 change: 1 addition & 0 deletions src_c/_pygame.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ PG_GetSurfaceFormat(SDL_Surface *surf)
#define PG_CreateSurfaceFrom(width, height, format, pixels, pitch) \
SDL_CreateRGBSurfaceWithFormatFrom(pixels, width, height, 0, pitch, format)
#define PG_ConvertSurface(src, fmt) SDL_ConvertSurface(src, fmt, 0)

#define PG_ConvertSurfaceFormat(src, pixel_format) \
SDL_ConvertSurfaceFormat(src, pixel_format, 0)

Expand Down
64 changes: 64 additions & 0 deletions src_c/include/_pygame.h
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,61 @@ typedef struct {
PyObject *weakreflist;
PyObject *locklist;
PyObject *dependency;
#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
PyMutex mutex;
unsigned int num_locks;
uint64_t locking_thread_id;
#endif
} pgSurfaceObject;
#define pgSurface_AsSurface(x) (((pgSurfaceObject *)x)->surf)

#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
#define LOCK_pgSurfaceObject(pgSurfacePtr) \
{ \
pgSurfaceObject *surfObj = (pgSurfaceObject *)pgSurfacePtr; \
uint64_t thread_id = PyThreadState_Get()->id; \
if (surfObj->num_locks) { \
if (thread_id == surfObj->locking_thread_id) { \
++(surfObj->num_locks); \
} \
else { \
PyMutex_Lock(&surfObj->mutex); \
surfObj->num_locks = 1U; \
surfObj->locking_thread_id = thread_id; \
} \
} \
else { \
PyMutex_Lock(&surfObj->mutex); \
surfObj->num_locks = 1U; \
surfObj->locking_thread_id = thread_id; \
} \
}
#else
#define LOCK_pgSurfaceObject(pgSurfacePtr)
#endif

#if PY_VERSION_HEX >= 0x030D0000 && Py_GIL_DISABLED
#define UNLOCK_pgSurfaceObject(pgSurfacePtr) \
{ \
pgSurfaceObject *surfObj = (pgSurfaceObject *)pgSurfacePtr; \
uint64_t thread_id = PyThreadState_Get()->id; \
if (surfObj->num_locks) { \
if (thread_id == surfObj->locking_thread_id) { \
if (surfObj->num_locks > 1U) { \
--(surfObj->num_locks); \
} \
else { \
surfObj->locking_thread_id = 0U; \
surfObj->num_locks = 0U; \
PyMutex_Unlock(&surfObj->mutex); \
} \
} \
} \
}
#else
#define UNLOCK_pgSurfaceObject(pgSurfacePtr)
#endif

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

Expand Down Expand Up @@ -617,6 +669,18 @@ 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\n", name);
#else
#define DISABLE_GIL_SINGLE_INITIALIZATION(module, name) \
printf("%s was compiled with GIL enabled\n", name);
#endif

static PG_INLINE PyObject *
pg_tuple_couple_from_values_int(int val1, int val2)
{
Expand Down
64 changes: 53 additions & 11 deletions src_c/surface.c
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,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 @@ -725,8 +728,9 @@ surf_get_at(PyObject *self, PyObject *position)
"position must be a sequence of two numbers");
}

if (x < 0 || x >= surf->w || y < 0 || y >= surf->h)
if (x < 0 || x >= surf->w || y < 0 || y >= surf->h) {
return RAISE(PyExc_IndexError, "pixel index out of range");
}

PG_PixelFormat *format;
SDL_Palette *palette;
Expand All @@ -735,11 +739,18 @@ surf_get_at(PyObject *self, PyObject *position)
}
int bpp = PG_FORMAT_BytesPerPixel(format);

if (bpp < 1 || bpp > 4)
if (bpp < 1 || bpp > 4) {
return RAISE(PyExc_RuntimeError, "invalid color depth for surface");
}

if (!pgSurface_Lock((pgSurfaceObject *)self))
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 @@ -769,8 +780,14 @@ surf_get_at(PyObject *self, PyObject *position)
rgba + 3);
break;
}
if (!pgSurface_Unlock((pgSurfaceObject *)self))
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;
}

return pgColor_New(rgba);
}
Expand Down Expand Up @@ -805,8 +822,9 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
}
int bpp = PG_FORMAT_BytesPerPixel(format);

if (bpp < 1 || bpp > 4)
if (bpp < 1 || bpp > 4) {
return RAISE(PyExc_RuntimeError, "invalid color depth for surface");
}

SDL_Rect clip_rect;
if (!PG_GetSurfaceClipRect(surf, &clip_rect)) {
Expand All @@ -816,15 +834,23 @@ 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;
}

if (!pg_MappedColorFromObj(rgba_obj, surf, &color, PG_COLOR_HANDLE_ALL)) {
return NULL;
}

if (!pgSurface_Lock((pgSurfaceObject *)self))
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;

switch (bpp) {
Expand Down Expand Up @@ -859,8 +885,15 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
break;
}

if (!pgSurface_Unlock((pgSurfaceObject *)self))
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;
}

Py_RETURN_NONE;
}
Expand Down Expand Up @@ -1438,7 +1471,7 @@ surf_convert(pgSurfaceObject *self, PyObject *args)

SURF_INIT_CHECK(surf)

pgSurface_Prep(self);
pgSurface_Lock(self);

if ((has_colorkey = SDL_HasColorKey(surf))) {
SDL_GetColorKey(surf, &colorkey);
Expand Down Expand Up @@ -1480,6 +1513,7 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
Amask = 0xFF << 24;
break;
default:
pgSurface_Unlock(self);
return RAISE(PyExc_ValueError,
"no standard masks exist for given "
"bitdepth with alpha");
Expand Down Expand Up @@ -1515,6 +1549,7 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
Bmask = 0xFF;
break;
default:
pgSurface_Unlock(self);
return RAISE(PyExc_ValueError,
"nonstandard bit depth given");
}
Expand All @@ -1532,7 +1567,7 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
!pg_UintFromObjIndex(argobject, 1, &format.Gmask) ||
!pg_UintFromObjIndex(argobject, 2, &format.Bmask) ||
!pg_UintFromObjIndex(argobject, 3, &format.Amask)) {
pgSurface_Unprep(self);
pgSurface_Unlock(self);
return RAISE(PyExc_ValueError,
"invalid color masks given");
}
Expand All @@ -1543,7 +1578,7 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
break;
}
else {
pgSurface_Unprep(self);
pgSurface_Unlock(self);
return RAISE(
PyExc_ValueError,
"invalid argument specifying new format to convert to");
Expand Down Expand Up @@ -1578,7 +1613,9 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
SDL_SetPixelFormatPalette(&format, palette);
}
}
// pgSurface_Lock(self);
newsurf = PG_ConvertSurface(surf, &format);
// pgSurface_Unlock(self);
SDL_SetSurfaceBlendMode(newsurf, SDL_BLENDMODE_NONE);
SDL_FreePalette(palette);
}
Expand All @@ -1590,19 +1627,21 @@ surf_convert(pgSurfaceObject *self, PyObject *args)
}

if (newsurf == NULL) {
pgSurface_Unlock(self);
return RAISE(pgExc_SDLError, SDL_GetError());
}

if (has_colorkey) {
colorkey = SDL_MapRGBA(newsurf->format, key_r, key_g, key_b, key_a);
if (SDL_SetColorKey(newsurf, SDL_TRUE, colorkey) != 0) {
PyErr_SetString(pgExc_SDLError, SDL_GetError());
pgSurface_Unlock(self);
SDL_FreeSurface(newsurf);
return NULL;
}
}

pgSurface_Unprep(self);
pgSurface_Unlock(self);

final = surf_subtype_new(Py_TYPE(self), newsurf, 1);
if (!final)
Expand Down Expand Up @@ -4223,6 +4262,9 @@ MODINIT_DEFINE(surface)
if (module == NULL) {
return NULL;
}

DISABLE_GIL_SINGLE_INITIALIZATION(module, _module.m_name)

if (pg_warn_simd_at_runtime_but_uncompiled() < 0) {
Py_DECREF(module);
return NULL;
Expand Down
Loading
Loading