Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
## Unreleased

### Fixes
- Fixed `arcade.math.rand_vec_spread_deg()` returning vectors in random directions. It passed the angle in degrees to a function that expects radians, so for example the spread emitters in the `particle_systems` example sprayed everywhere ([#2648](https://github.com/pythonarcade/arcade/issues/2648)).
- Fixed reading 2 GB or more from an OpenGL texture, texture array, framebuffer or buffer on Windows, which failed because `ctypes.string_at` takes its size as a 32-bit C long there ([#2836](https://github.com/pythonarcade/arcade/issues/2836)).
- Fixed a player in `PhysicsEnginePlatformer` sliding along when standing across two moving platforms. It moved by both platforms' speeds added together, so it slid until it stood on only one. It now moves with the platform it stands on the most, and isn't moved by a platform it only touches the edge of while standing mostly on something still ([#2782](https://github.com/pythonarcade/arcade/issues/2782)).
- Fixed the PyInstaller hook bundling Arcade's version file at `arcade/VERSION/VERSION`. Frozen apps printed an error on startup and reported `arcade.VERSION` as `0.0.0`, and builds using `collect_all("arcade")` failed because the hook and `collect_all` put a folder and a file at the same path. The version file is now `arcade/_VERSION`, so it can't be confused with the `arcade.version` module ([#2837](https://github.com/pythonarcade/arcade/issues/2837)).
- Fixed drawing into a texture atlas losing new drawing when the atlas grows during `render_into()`. Nested rendering and exception exits now restore the previous camera and render region.
Expand Down
6 changes: 4 additions & 2 deletions arcade/gl/backends/opengl/buffer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import weakref
from ctypes import byref, string_at
from ctypes import POINTER, byref, c_ubyte, cast
from typing import TYPE_CHECKING

from pyglet.graphics import core
Expand Down Expand Up @@ -158,7 +158,9 @@ def read(self, size: int = -1, offset: int = 0) -> bytes:

gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo)
ptr = gl.glMapBufferRange(gl.GL_ARRAY_BUFFER, offset, size, gl.GL_MAP_READ_BIT)
data = string_at(ptr, size=size)
# Copy through a ctypes array rather than ctypes.string_at, which can't
# read 2 GB or more on Windows: its size is a C long
data = bytes(cast(ptr, POINTER(c_ubyte * size)).contents)
gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER)
return data

Expand Down
6 changes: 4 additions & 2 deletions arcade/gl/backends/opengl/framebuffer.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import weakref
from ctypes import Array, c_int, c_uint, string_at
from ctypes import Array, c_int, c_uint
from typing import TYPE_CHECKING

from pyglet.graphics import core
Expand Down Expand Up @@ -296,7 +296,9 @@ def read(self, *, viewport=None, components=3, attachment=0, dtype="f1") -> byte
if not self.is_default:
gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default

return string_at(data, len(data))
# bytes() rather than ctypes.string_at, which can't read 2 GB or more on
# Windows: its size is a C long
return bytes(data)

def delete(self):
"""
Expand Down
6 changes: 4 additions & 2 deletions arcade/gl/backends/opengl/texture.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import weakref
from ctypes import byref, string_at
from ctypes import byref
from typing import TYPE_CHECKING

from pyglet.graphics import core
Expand Down Expand Up @@ -517,7 +517,9 @@ def read(self, level: int = 0, alignment: int = 1) -> bytes:
gl.GLubyte * (self.width * self.height * self._component_size * self._components)
)()
gl.glGetTexImage(gl.GL_TEXTURE_2D, level, self._format, self._type, buffer)
return string_at(buffer, len(buffer))
# bytes() rather than ctypes.string_at, which can't read 2 GB or more on
# Windows: its size is a C long
return bytes(buffer)
elif self._ctx.gl_api == "opengles":
fbo = self._ctx.framebuffer(color_attachments=[self])
return fbo.read(components=self._components, dtype=self._dtype)
Expand Down
6 changes: 4 additions & 2 deletions arcade/gl/backends/opengl/texture_array.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

import weakref
from ctypes import byref, string_at
from ctypes import byref
from typing import TYPE_CHECKING

from pyglet.graphics import core
Expand Down Expand Up @@ -468,7 +468,9 @@ def read(self, level: int = 0, alignment: int = 1) -> bytes:
* (self.width * self.height * self.layers * self._component_size * self._components)
)()
gl.glGetTexImage(self._target, level, self._format, self._type, buffer)
return string_at(buffer, len(buffer))
# bytes() rather than ctypes.string_at, which can't read 2 GB or more on
# Windows: its size is a C long
return bytes(buffer)
elif self._ctx.gl_api == "opengles":
# FIXME: Check if we can attach a layer to the framebuffer. See Texture2D.read()
raise ValueError("Reading texture array data not supported in GLES yet")
Expand Down
6 changes: 3 additions & 3 deletions arcade/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,12 +285,12 @@ def rand_vec_spread_deg(angle: float, half_angle_spread: float, length: float) -
Returns a random vector, within a spread of the given angle.

Args:
angle (float): The angle to spread from
half_angle_spread (float): The half angle spread
angle (float): The angle to spread from, in degrees
half_angle_spread (float): The half angle spread, in degrees
length (float): The length of the vector
"""
a = rand_angle_spread_deg(angle, half_angle_spread)
vel = Vec2.from_polar(a, length)
vel = Vec2.from_polar(math.radians(a), length)
return vel.x, vel.y


Expand Down
25 changes: 6 additions & 19 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,30 +433,17 @@ def get_image(self) -> PIL.Image.Image:
region = LBWH(0, 0, 1280, 720)
return self.read_region_image(region)

def read_pixel(self, x, y, components=3) -> tuple[int, int, int, int] | tuple[int, int, int]:
"""Read a single RGBA pixel from the offscreen buffer"""
data = self.fbo.read(components=4, viewport=(x, y, 1, 1))
return (
int.from_bytes(data[0:4], "little"),
int.from_bytes(data[4:8], "little"),
int.from_bytes(data[8:12], "little"),
int.from_bytes(data[12:16], "little"),
)
def read_pixel(self, x, y, components=3) -> tuple[int, ...]:
"""Read a single RGB or RGBA pixel from the offscreen buffer"""
# One byte per channel
return tuple(self.fbo.read(components=components, viewport=(x, y, 1, 1)))

def read_region(self, rect: Rect) -> list[tuple[int, int, int, int]]:
"""Read a region of RGBA pixels from the offscreen buffer"""
"""Read a region of RGBA pixels from the offscreen buffer, row by row from the bottom"""
data = self.fbo.read(
components=4, viewport=(rect.left, rect.bottom, rect.width, rect.height)
)
return [
(
int.from_bytes(data[i : i + 4], "little"),
int.from_bytes(data[i + 4 : i + 8], "little"),
int.from_bytes(data[i + 8 : i + 12], "little"),
int.from_bytes(data[i + 12 : i + 16], "little"),
)
for i in range(0, len(data), 16)
]
return [(data[i], data[i + 1], data[i + 2], data[i + 3]) for i in range(0, len(data), 4)]

def read_region_bytes(self, rect: Rect, components=3) -> bytes:
"""Read a region of RGBA pixels from the offscreen buffer as bytes"""
Expand Down
12 changes: 9 additions & 3 deletions tests/unit/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import math

import pytest
from pytest import approx

from arcade.math import (
Expand Down Expand Up @@ -94,9 +95,14 @@ def test_rand_angle_spread_deg():
rand_angle_spread_deg(45.0, 5.0)


def test_rand_vec_spread_deg():
"""Smoke test"""
rand_vec_spread_deg(-45.0, 5.0, 3.3)
@pytest.mark.parametrize("angle", [-45.0, 0.0, 90.0, 200.0])
def test_rand_vec_spread_deg(angle):
# The vector points within the spread of the angle, which is in degrees
for _ in range(200):
x, y = rand_vec_spread_deg(angle, 5.0, 3.3)
assert math.hypot(x, y) == approx(3.3)
difference = (math.degrees(math.atan2(y, x)) - angle + 180) % 360 - 180
assert abs(difference) <= 5.0 + 1e-9


def test_rand_vec_magnitude():
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_offscreen_fixture.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""The offscreen fixture's pixel readers return one value per channel."""

from arcade import LBWH


def test_read_pixel(offscreen):
offscreen.fbo.clear(color=(10, 20, 30, 40))
assert offscreen.read_pixel(5, 6) == (10, 20, 30)
assert offscreen.read_pixel(5, 6, components=4) == (10, 20, 30, 40)


def test_read_region(offscreen):
offscreen.fbo.clear(color=(10, 20, 30, 40))
offscreen.fbo.clear(color=(200, 100, 50, 255), viewport=(1, 0, 1, 1))
# Bottom row first, left to right
assert offscreen.read_region(LBWH(0, 0, 2, 2)) == [
(10, 20, 30, 40),
(200, 100, 50, 255),
(10, 20, 30, 40),
(10, 20, 30, 40),
]
Loading