Skip to content

Commit 2c711c2

Browse files
committed
Freethreading surface early work
1 parent 5728de9 commit 2c711c2

File tree

6 files changed

+162
-9
lines changed

6 files changed

+162
-9
lines changed

examples/multithreaded_surface.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import pygame
2+
import threading
3+
import random
4+
5+
from time import perf_counter
6+
7+
start = perf_counter()
8+
9+
pygame.init()
10+
11+
WIDTH, HEIGHT = 100, 100
12+
NUM_THREADS = 10
13+
surface = pygame.Surface((WIDTH, HEIGHT))
14+
surface.fill("black")
15+
16+
LERP_OFFSET = 0.01
17+
18+
19+
def get_random_color() -> pygame.Color:
20+
r = random.randint(0, 255)
21+
g = random.randint(0, 255)
22+
b = random.randint(0, 255)
23+
a = 255
24+
return pygame.Color(r, g, b, a)
25+
26+
27+
def multithreaded_func(
28+
surf: pygame.Surface, target_pixel: tuple[int, int], target_color: pygame.Color
29+
) -> None:
30+
lerp_distance = 0
31+
32+
original_color = surf.get_at(target_pixel)
33+
34+
while (surf.get_at(target_pixel) != target_color) and lerp_distance < 1:
35+
lerp_distance += LERP_OFFSET
36+
new_color = original_color.lerp(target_color, lerp_distance)
37+
surf.set_at(target_pixel, new_color)
38+
39+
40+
pixels = [(col, row) for col in range(WIDTH) for row in range(HEIGHT)]
41+
42+
colors = [get_random_color() for _ in range(WIDTH * HEIGHT)]
43+
44+
args = [(pixel, colors[i]) for i, pixel in enumerate(pixels)]
45+
batches = {
46+
i: args[i * NUM_THREADS : (i + 1) * NUM_THREADS]
47+
for i in range(WIDTH * HEIGHT // NUM_THREADS)
48+
}
49+
50+
for batch in batches.values():
51+
threads: list[threading.Thread] = []
52+
for pixel, color in batch:
53+
new_thread = threading.Thread(
54+
target=multithreaded_func, args=(surface, pixel, color)
55+
)
56+
new_thread.start()
57+
threads.append(new_thread)
58+
59+
while any([t.is_alive() for t in threads]):
60+
continue
61+
62+
pygame.image.save(pygame.transform.scale_by(surface, 10), "out.png")
63+
64+
end = perf_counter()
65+
66+
print(f"time taken: {end - start}")
67+
68+
for pixel, color in zip(pixels, colors):
69+
surface.set_at(pixel, color)
70+
pygame.image.save(pygame.transform.scale_by(surface, 10), "comparison.png")

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ requires = [
5656
"meson-python<=0.17.1",
5757
"meson<=1.7.0",
5858
"ninja<=1.12.1",
59-
"cython<=3.0.11",
59+
"cython<=3.1.0a1",
6060
"sphinx<=8.1.3",
6161
"sphinx-autoapi<=3.3.2",
6262
]

src_c/color.c

+3-1
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,9 @@ _color_lerp(pgColorObject *self, PyObject *args, PyObject *kw)
808808
return NULL;
809809
}
810810

811-
if (amt < 0 || amt > 1) {
811+
static const double TOLERANCE = 1e-6;
812+
813+
if (amt < -TOLERANCE || amt > (1.0 + TOLERANCE)) {
812814
return RAISE(PyExc_ValueError, "Argument 2 must be in range [0, 1]");
813815
}
814816

src_c/include/_pygame.h

+31
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,28 @@ typedef struct {
320320
PyObject *weakreflist;
321321
PyObject *locklist;
322322
PyObject *dependency;
323+
#if PY_MINOR_VERSION >= 13 && Py_GIL_DISABLED
324+
PyMutex mutex;
325+
#endif
323326
} pgSurfaceObject;
324327
#define pgSurface_AsSurface(x) (((pgSurfaceObject *)x)->surf)
325328

329+
#if PY_MINOR_VERSION >= 13 && Py_GIL_DISABLED
330+
#define LOCK_pgSurfaceObject(pgSurfacePtr) \
331+
PyMutex_Lock(&(((pgSurfaceObject *)pgSurfacePtr) \
332+
->mutex)); // printf("Locking mutex\n");
333+
#else
334+
#define LOCK_pgSurfaceObject(pgSurfacePtr)
335+
#endif
336+
337+
#if PY_MINOR_VERSION >= 13 && Py_GIL_DISABLED
338+
#define UNLOCK_pgSurfaceObject(pgSurfacePtr) \
339+
PyMutex_Unlock(&(((pgSurfaceObject *)pgSurfacePtr) \
340+
->mutex)); // printf("Unlocking mutex\n");
341+
#else
342+
#define UNLOCK_pgSurfaceObject(pgSurfacePtr)
343+
#endif
344+
326345
#ifndef PYGAMEAPI_SURFACE_INTERNAL
327346
#define pgSurface_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(surface, 0))
328347

@@ -617,6 +636,18 @@ PYGAMEAPI_EXTERN_SLOTS(geometry);
617636
} \
618637
}
619638

639+
#if Py_GIL_DISABLED
640+
#define DISABLE_GIL_SINGLE_INITIALIZATION(module, name) \
641+
if (PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED) < 0) { \
642+
Py_DECREF(module); \
643+
return NULL; \
644+
} \
645+
printf("%s was compiled wtih GIL disabled\n", name);
646+
#else
647+
#define DISABLE_GIL_SINGLE_INITIALIZATION(module, name) \
648+
printf("%s was compiled with GIL enabled\n", name);
649+
#endif
650+
620651
static PG_INLINE PyObject *
621652
pg_tuple_couple_from_values_int(int val1, int val2)
622653
{

src_c/surface.c

+43-7
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,9 @@ surface_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
392392
self->weakreflist = NULL;
393393
self->dependency = NULL;
394394
self->locklist = NULL;
395+
#if PY_VERSION_MINOR >= 13 && Py_GIL_DISABLED
396+
self->mutex[0] = 0;
397+
#endif
395398
}
396399
return (PyObject *)self;
397400
}
@@ -725,8 +728,9 @@ surf_get_at(PyObject *self, PyObject *position)
725728
"position must be a sequence of two numbers");
726729
}
727730

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

731735
PG_PixelFormat *format;
732736
SDL_Palette *palette;
@@ -735,11 +739,18 @@ surf_get_at(PyObject *self, PyObject *position)
735739
}
736740
int bpp = PG_FORMAT_BytesPerPixel(format);
737741

738-
if (bpp < 1 || bpp > 4)
742+
if (bpp < 1 || bpp > 4) {
739743
return RAISE(PyExc_RuntimeError, "invalid color depth for surface");
744+
}
740745

741-
if (!pgSurface_Lock((pgSurfaceObject *)self))
746+
if (!pgSurface_Lock((pgSurfaceObject *)self)) {
747+
if (PyErr_Occurred()) {
748+
PyErr_Print();
749+
PyErr_Clear();
750+
}
751+
PyErr_SetString(pgExc_SDLError, "Failed to lock surface");
742752
return NULL;
753+
}
743754

744755
pixels = (Uint8 *)surf->pixels;
745756

@@ -769,8 +780,14 @@ surf_get_at(PyObject *self, PyObject *position)
769780
rgba + 3);
770781
break;
771782
}
772-
if (!pgSurface_Unlock((pgSurfaceObject *)self))
783+
if (!pgSurface_Unlock((pgSurfaceObject *)self)) {
784+
if (PyErr_Occurred()) {
785+
PyErr_Print();
786+
PyErr_Clear();
787+
}
788+
PyErr_SetString(pgExc_SDLError, "Failed to unlock surface in get_at");
773789
return NULL;
790+
}
774791

775792
return pgColor_New(rgba);
776793
}
@@ -805,8 +822,9 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
805822
}
806823
int bpp = PG_FORMAT_BytesPerPixel(format);
807824

808-
if (bpp < 1 || bpp > 4)
825+
if (bpp < 1 || bpp > 4) {
809826
return RAISE(PyExc_RuntimeError, "invalid color depth for surface");
827+
}
810828

811829
SDL_Rect clip_rect;
812830
if (!PG_GetSurfaceClipRect(surf, &clip_rect)) {
@@ -816,15 +834,23 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
816834
if (x < clip_rect.x || x >= clip_rect.x + clip_rect.w || y < clip_rect.y ||
817835
y >= clip_rect.y + clip_rect.h) {
818836
/* out of clip area */
837+
819838
Py_RETURN_NONE;
820839
}
821840

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

826-
if (!pgSurface_Lock((pgSurfaceObject *)self))
845+
if (!pgSurface_Lock((pgSurfaceObject *)self)) {
846+
if (PyErr_Occurred()) {
847+
PyErr_Print();
848+
PyErr_Clear();
849+
}
850+
PyErr_SetString(pgExc_SDLError, "Failed to lock surface");
851+
827852
return NULL;
853+
}
828854
pixels = (Uint8 *)surf->pixels;
829855

830856
switch (bpp) {
@@ -859,8 +885,15 @@ surf_set_at(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
859885
break;
860886
}
861887

862-
if (!pgSurface_Unlock((pgSurfaceObject *)self))
888+
if (!pgSurface_Unlock((pgSurfaceObject *)self)) {
889+
if (PyErr_Occurred()) {
890+
PyErr_Print();
891+
PyErr_Clear();
892+
}
893+
PyErr_SetString(pgExc_SDLError, "Failed to unlock surface in set_at");
894+
863895
return NULL;
896+
}
864897

865898
Py_RETURN_NONE;
866899
}
@@ -4223,6 +4256,9 @@ MODINIT_DEFINE(surface)
42234256
if (module == NULL) {
42244257
return NULL;
42254258
}
4259+
4260+
DISABLE_GIL_SINGLE_INITIALIZATION(module, _module.m_name)
4261+
42264262
if (pg_warn_simd_at_runtime_but_uncompiled() < 0) {
42274263
Py_DECREF(module);
42284264
return NULL;

src_c/surflock.c

+14
Original file line numberDiff line numberDiff line change
@@ -76,22 +76,30 @@ pgSurface_LockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
7676
PyObject *ref;
7777
pgSurfaceObject *surf = (pgSurfaceObject *)surfobj;
7878

79+
// PyThreadState* ts = PyGILState_GetThisThreadState();
80+
// printf("Locking surface in thread %llu\n", PyThreadState_GetID(ts));
81+
LOCK_pgSurfaceObject(surfobj);
82+
7983
if (surf->locklist == NULL) {
8084
surf->locklist = PyList_New(0);
8185
if (surf->locklist == NULL) {
86+
UNLOCK_pgSurfaceObject(surfobj);
8287
return 0;
8388
}
8489
}
8590
ref = PyWeakref_NewRef(lockobj, NULL);
8691
if (ref == NULL) {
92+
UNLOCK_pgSurfaceObject(surfobj);
8793
return 0;
8894
}
8995
if (ref == Py_None) {
9096
Py_DECREF(ref);
97+
UNLOCK_pgSurfaceObject(surfobj);
9198
return 0;
9299
}
93100
if (0 != PyList_Append(surf->locklist, ref)) {
94101
Py_DECREF(ref);
102+
UNLOCK_pgSurfaceObject(surfobj);
95103
return 0; /* Exception already set. */
96104
}
97105
Py_DECREF(ref);
@@ -106,8 +114,10 @@ pgSurface_LockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
106114
#endif
107115
{
108116
PyErr_SetString(PyExc_RuntimeError, "error locking surface");
117+
UNLOCK_pgSurfaceObject(surfobj);
109118
return 0;
110119
}
120+
111121
return 1;
112122
}
113123

@@ -167,6 +177,7 @@ pgSurface_UnlockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
167177
}
168178

169179
if (!found) {
180+
UNLOCK_pgSurfaceObject(surfobj);
170181
return noerror;
171182
}
172183

@@ -181,6 +192,9 @@ pgSurface_UnlockBy(pgSurfaceObject *surfobj, PyObject *lockobj)
181192
found--;
182193
}
183194

195+
// PyThreadState* ts = PyGILState_GetThisThreadState();
196+
// printf("Unlocking surface in thread %llu\n", PyThreadState_GetID(ts));
197+
UNLOCK_pgSurfaceObject(surfobj);
184198
return noerror;
185199
}
186200

0 commit comments

Comments
 (0)