diff --git a/buildconfig/stubs/pygame/__init__.pyi b/buildconfig/stubs/pygame/__init__.pyi index 8611d810da..ba617180f4 100644 --- a/buildconfig/stubs/pygame/__init__.pyi +++ b/buildconfig/stubs/pygame/__init__.pyi @@ -109,11 +109,26 @@ from .constants import ( AUDIO_U16SYS as AUDIO_U16SYS, AUDIO_U8 as AUDIO_U8, BIG_ENDIAN as BIG_ENDIAN, + BLENDFACTOR_DST_ALPHA as BLENDFACTOR_DST_ALPHA, + BLENDFACTOR_DST_COLOR as BLENDFACTOR_DST_COLOR, + BLENDFACTOR_ONE as BLENDFACTOR_ONE, + BLENDFACTOR_ONE_MINUS_DST_ALPHA as BLENDFACTOR_ONE_MINUS_DST_ALPHA, + BLENDFACTOR_ONE_MINUS_DST_COLOR as BLENDFACTOR_ONE_MINUS_DST_COLOR, + BLENDFACTOR_ONE_MINUS_SRC_ALPHA as BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + BLENDFACTOR_ONE_MINUS_SRC_COLOR as BLENDFACTOR_ONE_MINUS_SRC_COLOR, + BLENDFACTOR_SRC_ALPHA as BLENDFACTOR_SRC_ALPHA, + BLENDFACTOR_SRC_COLOR as BLENDFACTOR_SRC_COLOR, + BLENDFACTOR_ZERO as BLENDFACTOR_ZERO, BLENDMODE_ADD as BLENDMODE_ADD, BLENDMODE_BLEND as BLENDMODE_BLEND, BLENDMODE_MOD as BLENDMODE_MOD, BLENDMODE_MUL as BLENDMODE_MUL, BLENDMODE_NONE as BLENDMODE_NONE, + BLENDOPERATION_ADD as BLENDOPERATION_ADD, + BLENDOPERATION_MAXIMUM as BLENDOPERATION_MAXIMUM, + BLENDOPERATION_MINIMUM as BLENDOPERATION_MINIMUM, + BLENDOPERATION_REV_SUBTRACT as BLENDOPERATION_REV_SUBTRACT, + BLENDOPERATION_SUBTRACT as BLENDOPERATION_SUBTRACT, BLEND_ADD as BLEND_ADD, BLEND_ALPHA_SDL2 as BLEND_ALPHA_SDL2, BLEND_MAX as BLEND_MAX, diff --git a/buildconfig/stubs/pygame/_render.pyi b/buildconfig/stubs/pygame/_render.pyi new file mode 100644 index 0000000000..2a76653dec --- /dev/null +++ b/buildconfig/stubs/pygame/_render.pyi @@ -0,0 +1,81 @@ +from typing import Optional, Protocol, Union, final + +from pygame.color import Color +from pygame.rect import Rect +from pygame.surface import Surface +from pygame.typing import ColorLike, IntPoint, Point, RectLike, SequenceLike +from pygame.window import Window +from typing_extensions import deprecated # added in 3.13 + +class _DrawableClass(Protocol): + # Object that has the draw method that accepts area and dest arguments + def draw( + self, area: Optional[RectLike] = None, dest: Optional[RectLike] = None + ): ... + +@final +class Renderer: + def __init__( + self, + window: Window, + index: int = -1, + accelerated: int = -1, + vsync: bool = False, + target_texture: bool = False, + ) -> None: ... + def blit( + self, + source: Union["Texture", "Image", _DrawableClass], + dest: Optional[RectLike] = None, + area: Optional[RectLike] = None, + special_flags: int = 0, + ) -> Rect: ... + def clear(self) -> None: ... + def draw_line(self, p1: Point, p2: Point) -> None: ... + def draw_point(self, point: Point) -> None: ... + def draw_quad(self, p1: Point, p2: Point, p3: Point, p4: Point) -> None: ... + def draw_rect(self, rect: RectLike) -> None: ... + def draw_triangle(self, p1: Point, p2: Point, p3: Point) -> None: ... + def fill_quad(self, p1: Point, p2: Point, p3: Point, p4: Point) -> None: ... + def fill_rect(self, rect: RectLike) -> None: ... + def fill_triangle(self, p1: Point, p2: Point, p3: Point) -> None: ... + def get_viewport(self) -> Rect: ... + def present(self) -> None: ... + def set_viewport(self, area: Optional[RectLike]) -> None: ... + def to_surface( + self, surface: Optional[Surface] = None, area: Optional[RectLike] = None + ) -> Surface: ... + @property + def draw_blend_mode(self) -> int: ... + @draw_blend_mode.setter + def draw_blend_mode(self, value: int) -> None: ... + @property + def draw_color(self) -> Color: ... + @draw_color.setter + def draw_color(self, value: ColorLike) -> None: ... + @property + def logical_size(self) -> tuple[int, int]: ... + @logical_size.setter + def logical_size(self, value: IntPoint) -> None: ... + @property + def scale(self) -> tuple[float, float]: ... + @scale.setter + def scale(self, value: Point) -> None: ... + @property + def target(self) -> "Texture": ... + @target.setter + def target(self, value: "Texture") -> None: ... + @classmethod + def compose_custom_blend_mode( + cls, color_mode: SequenceLike[int], alpha_mode: SequenceLike[int] + ) -> int: ... + @classmethod + def from_window(cls, window: Window) -> Renderer: ... + +@final +class Texture: + pass + +@final +class Image: + pass diff --git a/buildconfig/stubs/pygame/constants.pyi b/buildconfig/stubs/pygame/constants.pyi index 2c4f5030f9..a824f88bb5 100644 --- a/buildconfig/stubs/pygame/constants.pyi +++ b/buildconfig/stubs/pygame/constants.pyi @@ -31,11 +31,26 @@ AUDIO_U16MSB: int AUDIO_U16SYS: int AUDIO_U8: int BIG_ENDIAN: int +BLENDFACTOR_DST_ALPHA: int +BLENDFACTOR_DST_COLOR: int +BLENDFACTOR_ONE: int +BLENDFACTOR_ONE_MINUS_DST_ALPHA: int +BLENDFACTOR_ONE_MINUS_DST_COLOR: int +BLENDFACTOR_ONE_MINUS_SRC_ALPHA: int +BLENDFACTOR_ONE_MINUS_SRC_COLOR: int +BLENDFACTOR_SRC_ALPHA: int +BLENDFACTOR_SRC_COLOR: int +BLENDFACTOR_ZERO: int BLENDMODE_ADD: int BLENDMODE_BLEND: int BLENDMODE_MOD: int BLENDMODE_MUL: int BLENDMODE_NONE: int +BLENDOPERATION_ADD: int +BLENDOPERATION_MAXIMUM: int +BLENDOPERATION_MINIMUM: int +BLENDOPERATION_REV_SUBTRACT: int +BLENDOPERATION_SUBTRACT: int BLEND_ADD: int BLEND_ALPHA_SDL2: int BLEND_MAX: int diff --git a/buildconfig/stubs/pygame/locals.pyi b/buildconfig/stubs/pygame/locals.pyi index b1d3a99093..329bda5c2b 100644 --- a/buildconfig/stubs/pygame/locals.pyi +++ b/buildconfig/stubs/pygame/locals.pyi @@ -31,11 +31,26 @@ AUDIO_U16MSB: int AUDIO_U16SYS: int AUDIO_U8: int BIG_ENDIAN: int +BLENDFACTOR_DST_ALPHA: int +BLENDFACTOR_DST_COLOR: int +BLENDFACTOR_ONE: int +BLENDFACTOR_ONE_MINUS_DST_ALPHA: int +BLENDFACTOR_ONE_MINUS_DST_COLOR: int +BLENDFACTOR_ONE_MINUS_SRC_ALPHA: int +BLENDFACTOR_ONE_MINUS_SRC_COLOR: int +BLENDFACTOR_SRC_ALPHA: int +BLENDFACTOR_SRC_COLOR: int +BLENDFACTOR_ZERO: int BLENDMODE_ADD: int BLENDMODE_BLEND: int BLENDMODE_MOD: int BLENDMODE_MUL: int BLENDMODE_NONE: int +BLENDOPERATION_ADD: int +BLENDOPERATION_MAXIMUM: int +BLENDOPERATION_MINIMUM: int +BLENDOPERATION_REV_SUBTRACT: int +BLENDOPERATION_SUBTRACT: int BLEND_ADD: int BLEND_ALPHA_SDL2: int BLEND_MAX: int diff --git a/src_c/constants.c b/src_c/constants.c index 04b8e0771d..07ae11b90a 100644 --- a/src_c/constants.c +++ b/src_c/constants.c @@ -161,6 +161,22 @@ MODINIT_DEFINE(constants) DEC_CONST(BLENDMODE_ADD); DEC_CONST(BLENDMODE_MOD); DEC_CONST(BLENDMODE_MUL); + DEC_CONST(BLENDFACTOR_ZERO); + DEC_CONST(BLENDFACTOR_ONE); + DEC_CONST(BLENDFACTOR_SRC_COLOR); + DEC_CONST(BLENDFACTOR_ONE_MINUS_SRC_COLOR); + DEC_CONST(BLENDFACTOR_SRC_ALPHA); + DEC_CONST(BLENDFACTOR_ONE_MINUS_SRC_ALPHA); + DEC_CONST(BLENDFACTOR_DST_COLOR); + DEC_CONST(BLENDFACTOR_ONE_MINUS_DST_COLOR); + DEC_CONST(BLENDFACTOR_DST_ALPHA); + DEC_CONST(BLENDFACTOR_ONE_MINUS_DST_ALPHA); + DEC_CONST(BLENDOPERATION_ADD); + DEC_CONST(BLENDOPERATION_SUBTRACT); + DEC_CONST(BLENDOPERATION_REV_SUBTRACT); + DEC_CONST(BLENDOPERATION_MINIMUM); + DEC_CONST(BLENDOPERATION_MAXIMUM); + DEC_CONST(GL_STEREO); DEC_CONST(GL_MULTISAMPLEBUFFERS); DEC_CONST(GL_MULTISAMPLESAMPLES); diff --git a/src_c/render.c b/src_c/render.c index 9f873d615c..7dbdf29ccc 100644 --- a/src_c/render.c +++ b/src_c/render.c @@ -12,9 +12,603 @@ static PyTypeObject pgTexture_Type; static PyTypeObject pgImage_Type; -static PyMethodDef renderer_methods[] = {{NULL, NULL, 0, NULL}}; +#define pgRenderer_Check(x) \ + (PyObject_IsInstance((x), (PyObject *)&pgRenderer_Type)) -static PyGetSetDef renderer_getset[] = {{NULL, 0, NULL, NULL, NULL}}; +#define pgTexture_Check(x) \ + (PyObject_IsInstance((x), (PyObject *)&pgTexture_Type)) + +#define pgImage_Check(x) (PyObject_IsInstance((x), (PyObject *)&pgImage_Type)) + +#define RENDERER_ERROR_CHECK(x) \ + if (x < 0) { \ + return RAISE(pgExc_SDLError, SDL_GetError()); \ + } + +#define RENDERER_PROPERTY_ERROR_CHECK(x) \ + if (x < 0) { \ + RAISERETURN(pgExc_SDLError, SDL_GetError(), -1); \ + } + +#define PARSE_POINT(obj, x, y, name) \ + if (!pg_TwoFloatsFromObj(obj, &x, &y)) { \ + return RAISE(PyExc_TypeError, "invalid " #name " argument"); \ + } + +static void +texture_renderer_draw(pgTextureObject *self, PyObject *area, PyObject *dest); + +static void +image_renderer_draw(pgImageObject *self, PyObject *area, PyObject *dest); + +/* Renderer implementation */ +static PyObject * +renderer_from_window(PyTypeObject *cls, PyObject *args, PyObject *kwargs) +{ + PyObject *window; + static char *keywords[] = {"window", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!", keywords, + &pgWindow_Type, &window)) { + return NULL; + } + pgRendererObject *self = + (pgRendererObject *)(cls->tp_new(cls, NULL, NULL)); + self->window = (pgWindowObject *)window; + if (self->window->_is_borrowed) { + self->_is_borrowed = SDL_TRUE; + } + else { + return RAISE(pgExc_SDLError, + "Window is not created from display module"); + } + self->renderer = SDL_GetRenderer(self->window->_win); + if (!self->renderer) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + self->target = NULL; + Py_INCREF(self); + return (PyObject *)self; +} + +static PyObject * +renderer_draw_point(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *point; + SDL_FPoint pos; + static char *keywords[] = {"point", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &point)) { + return NULL; + } + if (!pg_TwoFloatsFromObj(point, &pos.x, &pos.y)) { + return RAISE(PyExc_TypeError, "invalid argument"); + } + RENDERER_ERROR_CHECK(SDL_RenderDrawPointF(self->renderer, pos.x, pos.y)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_draw_line(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *start, *end; + SDL_FPoint start_pos, end_pos; + static char *keywords[] = {"p1", "p2", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OO", keywords, &start, + &end)) { + return NULL; + } + PARSE_POINT(start, start_pos.x, start_pos.y, "p1") + PARSE_POINT(end, end_pos.x, end_pos.y, "p2") + RENDERER_ERROR_CHECK(SDL_RenderDrawLineF( + self->renderer, start_pos.x, start_pos.y, end_pos.x, end_pos.y)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_draw_rect(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *rectobj; + SDL_FRect *rect = NULL, temp; + static char *keywords[] = {"rect", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &rectobj)) { + return NULL; + } + if (!(rect = pgFRect_FromObject(rectobj, &temp))) { + return RAISE(PyExc_TypeError, "rect argument is invalid"); + } + RENDERER_ERROR_CHECK(SDL_RenderDrawRectF(self->renderer, rect)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_fill_rect(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *rectobj; + SDL_FRect *rect = NULL, temp; + static char *keywords[] = {"rect", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &rectobj)) { + return NULL; + } + if (!(rect = pgFRect_FromObject(rectobj, &temp))) { + return RAISE(PyExc_TypeError, "rect argument is invalid"); + } + RENDERER_ERROR_CHECK(SDL_RenderFillRectF(self->renderer, rect)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_draw_triangle(pgRendererObject *self, PyObject *args, + PyObject *kwargs) +{ + PyObject *p1, *p2, *p3; + SDL_FPoint points[4]; + static char *keywords[] = {"p1", "p2", "p3", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO", keywords, &p1, &p2, + &p3)) { + return NULL; + } + PARSE_POINT(p1, points[0].x, points[0].y, "p1") + PARSE_POINT(p2, points[1].x, points[1].y, "p2") + PARSE_POINT(p3, points[2].x, points[2].y, "p3") + points[3] = points[0]; + RENDERER_ERROR_CHECK(SDL_RenderDrawLinesF(self->renderer, points, 4)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_fill_triangle(pgRendererObject *self, PyObject *args, + PyObject *kwargs) +{ +#if SDL_VERSION_ATLEAST(2, 0, 18) + PyObject *p1, *p2, *p3; + SDL_Vertex vertices[3]; + static char *keywords[] = {"p1", "p2", "p3", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOO", keywords, &p1, &p2, + &p3)) { + return NULL; + } + PARSE_POINT(p1, vertices[0].position.x, vertices[0].position.y, "p1") + PARSE_POINT(p2, vertices[1].position.x, vertices[1].position.y, "p2") + PARSE_POINT(p3, vertices[2].position.x, vertices[2].position.y, "p3") + RENDERER_ERROR_CHECK(SDL_GetRenderDrawColor( + self->renderer, &vertices[0].color.r, &vertices[0].color.g, + &vertices[0].color.b, &vertices[0].color.a)) + vertices[1].color = vertices[0].color; + vertices[2].color = vertices[0].color; + RENDERER_ERROR_CHECK( + SDL_RenderGeometry(self->renderer, NULL, vertices, 3, NULL, 0)) + Py_RETURN_NONE; +#else + RAISE(PyExc_TypeError, "fill_triangle() requires SDL 2.0.18 or newer"); + Py_RETURN_NONE; +#endif +} + +static PyObject * +renderer_draw_quad(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *p1, *p2, *p3, *p4; + SDL_FPoint points[5]; + static char *keywords[] = {"p1", "p2", "p3", "p4", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOOO", keywords, &p1, &p2, + &p3, &p4)) { + return NULL; + } + PARSE_POINT(p1, points[0].x, points[0].y, "p1") + PARSE_POINT(p2, points[1].x, points[1].y, "p2") + PARSE_POINT(p3, points[2].x, points[2].y, "p3") + PARSE_POINT(p4, points[3].x, points[3].y, "p4") + points[4] = points[0]; + RENDERER_ERROR_CHECK(SDL_RenderDrawLinesF(self->renderer, points, 5)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_fill_quad(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ +#if SDL_VERSION_ATLEAST(2, 0, 18) + PyObject *p1, *p2, *p3, *p4; + SDL_Vertex vertices[4]; + static char *keywords[] = {"p1", "p2", "p3", "p4", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "OOOO", keywords, &p1, &p2, + &p3, &p4)) { + return NULL; + } + PARSE_POINT(p1, vertices[0].position.x, vertices[0].position.y, "p1") + PARSE_POINT(p2, vertices[1].position.x, vertices[1].position.y, "p2") + PARSE_POINT(p3, vertices[2].position.x, vertices[2].position.y, "p3") + PARSE_POINT(p4, vertices[3].position.x, vertices[3].position.y, "p4") + RENDERER_ERROR_CHECK(SDL_GetRenderDrawColor( + self->renderer, &vertices[0].color.r, &vertices[0].color.g, + &vertices[0].color.b, &vertices[0].color.a)) + for (int i = 1; i < 4; i++) { + vertices[i].color = vertices[0].color; + } + const int indices[] = {0, 1, 2, 2, 3, 0}; + RENDERER_ERROR_CHECK( + SDL_RenderGeometry(self->renderer, NULL, vertices, 4, indices, 6)) + Py_RETURN_NONE; +#else + RAISE(PyExc_TypeError, "fill_quad() requires SDL 2.0.18 or newer"); + Py_RETURN_NONE; +#endif +} + +static PyObject * +renderer_present(pgRendererObject *self, PyObject *_null) +{ + SDL_RenderPresent(self->renderer); + Py_RETURN_NONE; +} + +static PyObject * +renderer_clear(pgRendererObject *self, PyObject *_null) +{ + RENDERER_ERROR_CHECK(SDL_RenderClear(self->renderer)) + Py_RETURN_NONE; +} + +static PyObject * +renderer_get_viewport(pgRendererObject *self, PyObject *_null) +{ + SDL_Rect rect; + SDL_RenderGetViewport(self->renderer, &rect); + return pgRect_New(&rect); +} + +static PyObject * +renderer_set_viewport(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *rectobj; + SDL_Rect *rect = NULL, temp; + static char *keywords[] = {"area", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", keywords, &rectobj)) { + return NULL; + } + if (rectobj == Py_None) { + RENDERER_ERROR_CHECK(SDL_RenderSetViewport(self->renderer, NULL)) + } + else { + if (!(rect = pgRect_FromObject(rectobj, &temp))) { + return RAISE(PyExc_TypeError, "area must be rectangle or None"); + } + RENDERER_ERROR_CHECK(SDL_RenderSetViewport(self->renderer, rect)) + } + Py_RETURN_NONE; +} + +static PyObject * +renderer_compose_custom_blend_mode(PyObject *self, PyObject *args, + PyObject *kwargs) +{ + int mode[6]; + SDL_BlendMode blend_mode; + static char *keywords[] = {"color_mode", "alpha_mode", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "(iii)(iii)", keywords, + &mode[0], &mode[1], &mode[2], &mode[3], + &mode[4], &mode[5])) { + return NULL; + } + blend_mode = SDL_ComposeCustomBlendMode(mode[0], mode[1], mode[2], mode[3], + mode[4], mode[5]); + return PyLong_FromLong((long)blend_mode); +} + +static PyObject * +renderer_to_surface(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *surfobj = Py_None, *rectobj = Py_None; + SDL_Surface *surf; + pgSurfaceObject *surface; + SDL_Rect viewport, *areaparam, temp, *rect = &temp; + Uint32 format; + static char *keywords[] = {"surface", "area", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OO", keywords, &surfobj, + &rectobj)) { + return NULL; + } + if (!Py_IsNone(rectobj)) { + if (!(rect = pgRect_FromObject(rectobj, &temp))) { + return RAISE(PyExc_TypeError, "area must be None or a rect"); + } + SDL_RenderGetViewport(self->renderer, &viewport); + SDL_IntersectRect(rect, &viewport, rect); + areaparam = rect; + } + else { + SDL_RenderGetViewport(self->renderer, rect); + areaparam = NULL; + } + if (!Py_IsNone(surfobj)) { + if (!(pgSurface_Check(surfobj))) { + return RAISE(PyExc_TypeError, "surface must be None or a Surface"); + } + surface = (pgSurfaceObject *)surfobj; + Py_INCREF(surface); + surf = surface->surf; + if (surf->w < rect->w || surf->h < rect->h) { + return RAISE(PyExc_ValueError, "the surface is too small"); + } + format = surf->format->format; + } + else { + format = SDL_GetWindowPixelFormat(self->window->_win); + if (format == SDL_PIXELFORMAT_UNKNOWN) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + surf = SDL_CreateRGBSurfaceWithFormat( + 0, rect->w, rect->h, SDL_BITSPERPIXEL(format), format); + if (surf == NULL) { + return RAISE(pgExc_SDLError, SDL_GetError()); + } + surface = pgSurface_New(surf); + } + RENDERER_ERROR_CHECK(SDL_RenderReadPixels( + self->renderer, areaparam, format, surf->pixels, surf->pitch)); + return (PyObject *)surface; +} + +static PyObject * +renderer_blit(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + PyObject *sourceobj, *destobj = Py_None, *areaobj = Py_None; + int special_flags = 0; + static char *keywords[] = {"source", "dest", "area", "special_flags", + NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OOi", keywords, + &sourceobj, &destobj, &areaobj, + &special_flags)) { + return NULL; + } + + if (pgTexture_Check(sourceobj)) { + texture_renderer_draw((pgTextureObject *)sourceobj, areaobj, destobj); + } + else if (pgImage_Check(sourceobj)) { + image_renderer_draw((pgImageObject *)sourceobj, areaobj, destobj); + } + else { + if (!PyObject_CallFunctionObjArgs( + PyObject_GetAttrString(sourceobj, "draw"), areaobj, destobj, + NULL)) { + return NULL; + } + } + + if (Py_IsNone(destobj)) { + return renderer_get_viewport(self, NULL); + } + Py_INCREF(destobj); + return destobj; +} + +static PyObject * +renderer_get_draw_color(pgRendererObject *self, void *closure) +{ + Uint8 rgba[4]; + RENDERER_ERROR_CHECK(SDL_GetRenderDrawColor(self->renderer, &rgba[0], + &rgba[1], &rgba[2], &rgba[3])) + return pgColor_NewLength(rgba, 4); +} + +static int +renderer_set_draw_color(pgRendererObject *self, PyObject *arg, void *closure) +{ + Uint8 color[4]; + if (!pg_RGBAFromObjEx(arg, color, PG_COLOR_HANDLE_ALL)) { + return -1; + } + RENDERER_PROPERTY_ERROR_CHECK(SDL_SetRenderDrawColor( + self->renderer, color[0], color[1], color[2], color[3])) + return 0; +} + +static PyObject * +renderer_get_draw_blend_mode(pgRendererObject *self, void *closure) +{ + SDL_BlendMode blend_mode; + RENDERER_ERROR_CHECK( + SDL_GetRenderDrawBlendMode(self->renderer, &blend_mode)) + return PyLong_FromLong((long)blend_mode); +} + +static int +renderer_set_draw_blend_mode(pgRendererObject *self, PyObject *arg, + void *closure) +{ + if (!PyLong_Check(arg)) { + RAISERETURN(PyExc_TypeError, "Draw blend mode must be int", -1); + } + RENDERER_PROPERTY_ERROR_CHECK( + SDL_SetRenderDrawBlendMode(self->renderer, (int)PyLong_AsLong(arg))) + return 0; +} + +static PyObject * +renderer_get_logical_size(pgRendererObject *self, void *closure) +{ + int w, h; + SDL_RenderGetLogicalSize(self->renderer, &w, &h); + return pg_tuple_couple_from_values_int(w, h); +} + +static int +renderer_set_logical_size(pgRendererObject *self, PyObject *arg, void *closure) +{ + int w, h; + if (!pg_TwoIntsFromObj(arg, &w, &h)) { + RAISERETURN(PyExc_TypeError, "invalid logical size", -1); + } + RENDERER_PROPERTY_ERROR_CHECK( + SDL_RenderSetLogicalSize(self->renderer, w, h)) + return 0; +} + +static PyObject * +renderer_get_scale(pgRendererObject *self, void *closure) +{ + float x, y; + SDL_RenderGetScale(self->renderer, &x, &y); + return pg_tuple_couple_from_values_double(x, y); +} + +static int +renderer_set_scale(pgRendererObject *self, PyObject *arg, void *closure) +{ + float x, y; + if (!pg_TwoFloatsFromObj(arg, &x, &y)) { + RAISERETURN(PyExc_TypeError, "invalid scale", -1); + } + RENDERER_PROPERTY_ERROR_CHECK(SDL_RenderSetScale(self->renderer, x, y)) + return 0; +} + +static PyObject * +renderer_get_target(pgRendererObject *self, void *closure) +{ + if (self->target == NULL) { + Py_RETURN_NONE; + } + Py_INCREF(self->target); + return (PyObject *)self->target; +} + +static int +renderer_set_target(pgRendererObject *self, PyObject *arg, void *closure) +{ + if (Py_IsNone(arg)) { + self->target = NULL; + RENDERER_PROPERTY_ERROR_CHECK( + SDL_SetRenderTarget(self->renderer, NULL)) + return 0; + } + else if (pgTexture_Check(arg)) { + self->target = (pgTextureObject *)arg; + RENDERER_PROPERTY_ERROR_CHECK( + SDL_SetRenderTarget(self->renderer, self->target->texture)) + return 0; + } + else { + RAISERETURN(PyExc_TypeError, "target must be Texture object or None", + -1); + } +} + +static int +renderer_init(pgRendererObject *self, PyObject *args, PyObject *kwargs) +{ + SDL_Renderer *renderer = NULL; + pgWindowObject *window; + int index = -1; + int accelerated = -1; + int vsync = 0; + int target_texture = 0; + Uint32 flags = 0; + + char *keywords[] = {"window", "index", "accelerated", + "vsync", "target_texture", NULL}; + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!|iipp", keywords, + &pgWindow_Type, &window, &index, + &accelerated, &vsync, &target_texture)) { + return -1; + } + if (accelerated >= 0) { + flags |= + accelerated ? SDL_RENDERER_ACCELERATED : SDL_RENDERER_SOFTWARE; + } + if (vsync) { + flags |= SDL_RENDERER_PRESENTVSYNC; + } + if (target_texture) { + flags |= SDL_RENDERER_TARGETTEXTURE; + } + renderer = SDL_CreateRenderer(window->_win, index, flags); + if (!renderer) { + PyErr_SetString(pgExc_SDLError, SDL_GetError()); + return -1; + } + self->renderer = renderer; + self->window = window; + self->target = NULL; + self->_is_borrowed = SDL_FALSE; + return 0; +} + +static void +renderer_dealloc(pgRendererObject *self, PyObject *_null) +{ + if (!self->_is_borrowed && self->renderer) { + SDL_DestroyRenderer(self->renderer); + } + Py_TYPE(self)->tp_free(self); +} + +/* Texture implementation */ +static void +texture_renderer_draw(pgTextureObject *self, PyObject *area, PyObject *dest) +{ + return; +} + +/* Image implementation */ +static void +image_renderer_draw(pgImageObject *self, PyObject *area, PyObject *dest) +{ + return; +} + +/* Module definition */ +static PyMethodDef renderer_methods[] = { + {"draw_point", (PyCFunction)renderer_draw_point, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_DRAWPOINT}, + {"draw_line", (PyCFunction)renderer_draw_line, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_DRAWLINE}, + {"draw_rect", (PyCFunction)renderer_draw_rect, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_DRAWRECT}, + {"draw_triangle", (PyCFunction)renderer_draw_triangle, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_DRAWTRIANGLE}, + {"draw_quad", (PyCFunction)renderer_draw_quad, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_DRAWQUAD}, + {"fill_rect", (PyCFunction)renderer_fill_rect, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_FILLRECT}, + {"fill_triangle", (PyCFunction)renderer_fill_triangle, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_FILLTRIANGLE}, + {"fill_quad", (PyCFunction)renderer_fill_quad, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_FILLQUAD}, + {"present", (PyCFunction)renderer_present, METH_NOARGS, + DOC_SDL2_VIDEO_RENDERER_PRESENT}, + {"clear", (PyCFunction)renderer_clear, METH_NOARGS, + DOC_SDL2_VIDEO_RENDERER_CLEAR}, + {"set_viewport", (PyCFunction)renderer_set_viewport, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_SETVIEWPORT}, + {"get_viewport", (PyCFunction)renderer_get_viewport, METH_NOARGS, + DOC_SDL2_VIDEO_RENDERER_GETVIEWPORT}, + {"compose_custom_blend_mode", + (PyCFunction)renderer_compose_custom_blend_mode, + METH_VARARGS | METH_KEYWORDS | METH_CLASS, + DOC_SDL2_VIDEO_RENDERER_COMPOSECUSTOMBLENDMODE}, + {"from_window", (PyCFunction)renderer_from_window, + METH_CLASS | METH_VARARGS | METH_KEYWORDS, + DOC_SDL2_VIDEO_GETGRABBEDWINDOW}, + {"to_surface", (PyCFunction)renderer_to_surface, + METH_VARARGS | METH_KEYWORDS, DOC_SDL2_VIDEO_RENDERER_TOSURFACE}, + {"blit", (PyCFunction)renderer_blit, METH_VARARGS | METH_KEYWORDS, + DOC_SDL2_VIDEO_RENDERER_SETVIEWPORT}, + {NULL, NULL, 0, NULL}}; + +static PyGetSetDef renderer_getset[] = { + {"draw_color", (getter)renderer_get_draw_color, + (setter)renderer_set_draw_color, DOC_SDL2_VIDEO_RENDERER_DRAWCOLOR, NULL}, + {"draw_blend_mode", (getter)renderer_get_draw_blend_mode, + (setter)renderer_set_draw_blend_mode, DOC_SDL2_VIDEO_RENDERER_DRAWCOLOR, + NULL}, + {"logical_size", (getter)renderer_get_logical_size, + (setter)renderer_set_logical_size, DOC_SDL2_VIDEO_RENDERER_LOGICALSIZE, + NULL}, + {"scale", (getter)renderer_get_scale, (setter)renderer_set_scale, + DOC_SDL2_VIDEO_RENDERER_SCALE, NULL}, + {"target", (getter)renderer_get_target, (setter)renderer_set_target, + DOC_SDL2_VIDEO_RENDERER_TARGET, NULL}, + {NULL, 0, NULL, NULL, NULL}}; static PyMethodDef texture_methods[] = {{NULL, NULL, 0, NULL}}; @@ -27,10 +621,12 @@ static PyGetSetDef image_getset[] = {{NULL, 0, NULL, NULL, NULL}}; static PyTypeObject pgRenderer_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame._render.Renderer", .tp_basicsize = sizeof(pgRendererObject), - //.tp_dealloc = (destructor)renderer_dealloc, - .tp_doc = DOC_SDL2_VIDEO_RENDERER, .tp_methods = renderer_methods, - //.tp_init = (initproc)renderer_init, - .tp_new = PyType_GenericNew, .tp_getset = renderer_getset}; + .tp_dealloc = (destructor)renderer_dealloc, + .tp_doc = DOC_SDL2_VIDEO_RENDERER, + .tp_methods = renderer_methods, + .tp_init = (initproc)renderer_init, + .tp_new = PyType_GenericNew, + .tp_getset = renderer_getset}; static PyTypeObject pgTexture_Type = { PyVarObject_HEAD_INIT(NULL, 0).tp_name = "pygame._render.Texture", @@ -73,6 +669,26 @@ MODINIT_DEFINE(_render) return NULL; } + import_pygame_surface(); + if (PyErr_Occurred()) { + return NULL; + } + + import_pygame_rect(); + if (PyErr_Occurred()) { + return NULL; + } + + import_pygame_color(); + if (PyErr_Occurred()) { + return NULL; + } + + import_pygame_window(); + if (PyErr_Occurred()) { + return NULL; + } + if (PyType_Ready(&pgRenderer_Type) < 0) { return NULL; } diff --git a/src_c/static.c b/src_c/static.c index 2a2a6fe1aa..87c0264d14 100644 --- a/src_c/static.c +++ b/src_c/static.c @@ -370,7 +370,6 @@ PyInit_pygame_static() #include "simd_blitters_sse2.c" #include "window.c" -#include "render.c" #undef pgVidInfo_Type #undef pgVidInfo_New @@ -389,6 +388,7 @@ PyInit_pygame_static() #include "rwobject.c" #define pgSurface_New(surface) (pgSurfaceObject *)pgSurface_New2((surface), 1) +#include "render.c" #include "image.c" #include "imageext.c" diff --git a/test/meson.build b/test/meson.build index d789ad01e0..1e2cadfa7d 100644 --- a/test/meson.build +++ b/test/meson.build @@ -35,6 +35,7 @@ test_files = files( 'pixelarray_test.py', 'pixelcopy_test.py', 'rect_test.py', + 'render_test.py', 'rwobject_test.py', 'scrap_tags.py', 'scrap_test.py', diff --git a/test/render_test.py b/test/render_test.py new file mode 100644 index 0000000000..b04a635ddd --- /dev/null +++ b/test/render_test.py @@ -0,0 +1,214 @@ +import unittest + +import pygame +import pygame._render as _render + + +class DrawableObject: + def __init__(self): + self.drawn = False + self.area = None + self.dest = None + + def draw(self, area, dest): + self.drawn = True + self.area = area + self.dest = dest + + +class RendererTest(unittest.TestCase): + def setUp(self): + self.window = pygame.Window(size=(100, 100)) + self.renderer = _render.Renderer(self.window) + + def test_to_surface(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_point((10, 10)) # assumes Renderer.draw_point works + surf1 = self.renderer.to_surface() + self.assertEqual(surf1.get_at((10, 10)), pygame.Color(255, 255, 0, 255)) + self.assertEqual(surf1.size, (100, 100)) + + surf2 = pygame.Surface((150, 150)) + self.renderer.to_surface(surf2) + self.assertEqual(surf2.get_at((10, 10)), pygame.Color(255, 255, 0, 255)) + + surf3 = self.renderer.to_surface(area=pygame.Rect(5, 5, 20, 20)) + self.assertEqual(surf3.get_at((5, 5)), pygame.Color(255, 255, 0, 255)) + self.assertEqual(surf3.size, (20, 20)) + + surf4 = pygame.Surface((150, 150)) + self.renderer.to_surface(surf4, pygame.Rect(7, 7, 40, 40)) + self.assertEqual(surf4.get_at((3, 3)), pygame.Color(255, 255, 0, 255)) + + small_surf = pygame.Surface((50, 50)) + with self.assertRaises(ValueError): + self.renderer.to_surface(small_surf) + + def test_blit(self): + texture = _render.Texture(self.renderer, (20, 20)) + image = _render.Image(texture) + drawable_object = DrawableObject() + dest = pygame.Rect(10, 10, 20, 20) + area = pygame.Rect(0, 0, 15, 15) + + self.renderer.blit( + texture, dest, area + ) # TODO Assert after Texture implementation + + self.renderer.blit(image, dest, area) # TODO Assert after Image implementation + + self.renderer.blit(drawable_object, dest, area) + self.assertEqual(drawable_object.drawn, True) + self.assertEqual(drawable_object.area, area) + self.assertEqual(drawable_object.dest, dest) + + def test_clear(self): + self.renderer.draw_color = "YELLOW" + self.renderer.clear() + surf = self.renderer.to_surface() + for x in range(surf.width): + for y in range(surf.height): + self.assertEqual(surf.get_at((x, y)), pygame.Color(255, 255, 0, 255)) + + def test_draw_point(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_point((10, 10)) + surf = self.renderer.to_surface() + for x in range(-1, 2): + for y in range(-1, 2): + if x or y: + self.assertEqual( + surf.get_at((10 + x, 10 + y)), pygame.Color(0, 0, 0, 255) + ) + else: + self.assertEqual( + surf.get_at((10, 10)), pygame.Color(255, 255, 0, 255) + ) + + def test_draw_line(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_line((10, 10), (40, 40)) + surf = self.renderer.to_surface() + test_points = ((10, 10), (20, 20), (30, 30), (40, 40)) + for point in test_points: + self.assertEqual(surf.get_at(point), pygame.Color(255, 255, 0, 255)) + + def test_draw_triangle(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_triangle((10, 10), (10, 40), (40, 10)) + surf = self.renderer.to_surface() + test_points = ( + (10, 10), + (20, 10), + (30, 10), + (40, 10), + (10, 20), + (30, 20), + (10, 30), + (20, 30), + (10, 40), + ) + for point in test_points: + self.assertEqual(surf.get_at(point), pygame.Color(255, 255, 0, 255)) + + def test_draw_rect(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_rect(pygame.Rect(10, 10, 11, 11)) + surf = self.renderer.to_surface() + test_points = ((10, 15), (15, 10), (20, 15), (15, 20)) + for point in test_points: + self.assertEqual(surf.get_at(point), pygame.Color(255, 255, 0, 255)) + + def test_draw_quad(self): + self.renderer.draw_color = "YELLOW" + self.renderer.draw_quad((10, 40), (40, 10), (70, 40), (40, 70)) + surf = self.renderer.to_surface() + test_points = ((30, 20), (50, 20), (30, 60), (50, 60)) + for point in test_points: + self.assertEqual(surf.get_at(point), pygame.Color(255, 255, 0, 255)) + + def test_fill_triangle(self): + self.renderer.draw_color = "YELLOW" + self.renderer.fill_triangle((10, 10), (10, 40), (40, 10)) + surf = self.renderer.to_surface() + for x in range(10, 29): + self.assertEqual(surf.get_at((x, 20)), pygame.Color(255, 255, 0, 255)) + for x in range(10, 19): + self.assertEqual(surf.get_at((x, 30)), pygame.Color(255, 255, 0, 255)) + + def test_fill_rect(self): + self.renderer.draw_color = "YELLOW" + self.renderer.fill_rect(pygame.Rect(10, 10, 11, 11)) + surf = self.renderer.to_surface() + for x in range(10, 21): + for y in range(10, 21): + self.assertEqual(surf.get_at((x, y)), pygame.Color(255, 255, 0, 255)) + + def test_fill_quad(self): + self.renderer.draw_color = "GREEN" + self.renderer.draw_quad((10, 40), (40, 10), (70, 40), (40, 70)) + self.renderer.draw_color = "YELLOW" + self.renderer.fill_quad((10, 40), (40, 10), (70, 40), (40, 70)) + surf = self.renderer.to_surface() + for x in range(30, 50): + self.assertEqual(surf.get_at((x, 20)), pygame.Color(255, 255, 0, 255)) + self.assertEqual(surf.get_at((x, 59)), pygame.Color(255, 255, 0, 255)) + + def test_viewport(self): + self.assertEqual(self.renderer.get_viewport(), pygame.Rect(0, 0, 100, 100)) + self.renderer.set_viewport(pygame.Rect(20, 20, 60, 60)) + self.assertEqual(self.renderer.get_viewport(), pygame.Rect(20, 20, 60, 60)) + self.renderer.set_viewport(None) + self.assertEqual(self.renderer.get_viewport(), pygame.Rect(0, 0, 100, 100)) + + def test_logical_size(self): + self.assertEqual(self.renderer.logical_size, (0, 0)) + self.renderer.logical_size = (10, 10) + self.assertEqual(self.renderer.logical_size, (10, 10)) + + def test_scale(self): + self.assertEqual(self.renderer.scale, (1.0, 1.0)) + self.renderer.scale = (0.5, 2) + self.assertEqual(self.renderer.scale, (0.5, 2.0)) + + def test_target(self): + self.assertEqual(self.renderer.target, None) + texture = _render.Texture(self.renderer, (10, 10), target=True) + self.renderer.target = texture + self.assertEqual(id(self.renderer.target), id(texture)) + self.renderer.target = None + self.assertEqual(self.renderer.target, None) + + @unittest.skip("Unable to create that blend_mode on all devices") + def test_compose_custom_blend_mode(self): + color_mode, alpha_mode = ( + ( + pygame.BLENDFACTOR_SRC_COLOR, + pygame.BLENDFACTOR_ONE_MINUS_DST_COLOR, + pygame.BLENDOPERATION_MAXIMUM, + ), + ( + pygame.BLENDFACTOR_ONE_MINUS_SRC_ALPHA, + pygame.BLENDFACTOR_DST_ALPHA, + pygame.BLENDOPERATION_MINIMUM, + ), + ) + custom_blend_mode = self.renderer.compose_custom_blend_mode( + color_mode, alpha_mode + ) + self.assertEqual(custom_blend_mode, 157550645) + self.renderer.draw_blend_mode = custom_blend_mode + self.assertEqual(self.renderer.draw_blend_mode, custom_blend_mode) + + def test_draw_blend_mode(self): + self.assertEqual(self.renderer.draw_blend_mode, pygame.BLENDMODE_NONE) + self.renderer.draw_blend_mode = pygame.BLENDMODE_MUL + self.assertEqual(self.renderer.draw_blend_mode, pygame.BLENDMODE_MUL) + unsupported_blend_mode = pygame.BLENDMODE_MUL + 1 + with self.assertRaises(Exception): + self.renderer.draw_blend_mode = unsupported_blend_mode + + def test_draw_color(self): + self.assertEqual(self.renderer.draw_color, pygame.Color(0, 0, 0, 0)) + self.renderer.draw_color = "YELLOW" + self.assertEqual(self.renderer.draw_color, pygame.Color(255, 255, 0, 255))