Skip to content

Commit 16a1b7d

Browse files
Cleptomaniapushfoo
andauthored
Abstract the arcade.gl package in order to support different rendering backends (#2666)
* ABC based arcade.gl backend abstraction example * Abstraction of programs and VAOs/geometry. Also some cleanup * Texture and Compute Shader abstraction * Framebuffer abstraction * Abstract samplers * Abstract texture arrays * Abstract queries * Finished context/info abstraction * Types abstraction and more context cleanup * replace pyglet.gl with enums * Work on tests * Kind of hacky fix for tessellation example * Fix stats counting for compute shaders * ruff format * Fix some linting problems with getter/setters * line length linting * Rename gl backend to opengl * Good-enough arcade.gl doc fixes * Explain the design per Clepto's words * Get some mac survival guide suggestions written (no compute shader -> frag + FBO magic) * Good-enough additions of module prefixes for classes * Delete stuff that's broken or just slowing Clepto down (Gotta go fast) * Typing fixes, backends package is excluded from typing for now --------- Co-authored-by: pushfoo <36696816+pushfoo@users.noreply.github.com>
1 parent 580dd21 commit 16a1b7d

55 files changed

Lines changed: 5355 additions & 2969 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

arcade/application.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@
2222
from arcade.clock import GLOBAL_CLOCK, GLOBAL_FIXED_CLOCK, _setup_clock, _setup_fixed_clock
2323
from arcade.color import BLACK
2424
from arcade.context import ArcadeContext
25+
from arcade.gl.provider import get_arcade_context, set_provider
2526
from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255
26-
from arcade.utils import is_raspberry_pi
27+
from arcade.utils import is_pyodide, is_raspberry_pi
2728
from arcade.window_commands import get_display_size, set_window
2829

2930
if TYPE_CHECKING:
@@ -157,7 +158,7 @@ def __init__(
157158
center_window: bool = False,
158159
samples: int = 4,
159160
enable_polling: bool = True,
160-
gl_api: str = "gl",
161+
gl_api: str = "opengl",
161162
draw_rate: float = 1 / 60,
162163
fixed_rate: float = 1.0 / 60.0,
163164
fixed_frame_cap: int | None = None,
@@ -167,10 +168,17 @@ def __init__(
167168
if os.environ.get("REPL_ID"):
168169
antialiasing = False
169170

171+
desired_gl_provider = "opengl"
172+
if is_pyodide():
173+
gl_api = "webgl"
174+
175+
if gl_api == "webgl":
176+
desired_gl_provider = "webgl"
177+
170178
# Detect Raspberry Pi and switch to OpenGL ES 3.1
171179
if is_raspberry_pi():
172180
gl_version = 3, 1
173-
gl_api = "gles"
181+
gl_api = "opengles"
174182

175183
self.closed = False
176184
"""Indicates if the window was closed"""
@@ -184,7 +192,7 @@ def __init__(
184192
config = gl.Config(
185193
major_version=gl_version[0],
186194
minor_version=gl_version[1],
187-
opengl_api=gl_api, # type: ignore # pending: upstream fix
195+
opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix
188196
double_buffer=True,
189197
sample_buffers=1,
190198
samples=samples,
@@ -208,7 +216,7 @@ def __init__(
208216
config = gl.Config(
209217
major_version=gl_version[0],
210218
minor_version=gl_version[1],
211-
opengl_api=gl_api, # type: ignore # pending: upstream fix
219+
opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix
212220
double_buffer=True,
213221
depth_size=24,
214222
stencil_size=8,
@@ -277,7 +285,9 @@ def __init__(
277285

278286
self.push_handlers(on_resize=self._on_resize)
279287

280-
self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api)
288+
set_provider(desired_gl_provider)
289+
self._ctx: ArcadeContext = get_arcade_context(self, gc_mode=gc_mode, gl_api=gl_api)
290+
# self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api)
281291
self._background_color: Color = BLACK
282292

283293
self._current_view: View | None = None

arcade/context.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from arcade.gl.framebuffer import Framebuffer
2222
from arcade.gl.program import Program
2323
from arcade.gl.texture import Texture2D
24-
from arcade.gl.types import PyGLenum
2524
from arcade.gl.vertex_array import Geometry
2625
from arcade.texture_atlas import DefaultTextureAtlas, TextureAtlasBase
2726

@@ -451,9 +450,9 @@ def load_texture(
451450
path: str | Path,
452451
*,
453452
flip: bool = True,
454-
wrap_x: PyGLenum | None = None,
455-
wrap_y: PyGLenum | None = None,
456-
filter: tuple[PyGLenum, PyGLenum] | None = None,
453+
wrap_x=None,
454+
wrap_y=None,
455+
filter=None,
457456
build_mipmaps: bool = False,
458457
internal_format: int | None = None,
459458
immutable: bool = False,

arcade/examples/gl/tessellation.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import arcade
1212
from arcade.gl import BufferDescription
13+
import pyglet.gl
1314

1415
WINDOW_WIDTH = 1280
1516
WINDOW_HEIGHT = 720
@@ -106,7 +107,7 @@ def __init__(self, width, height, title):
106107
def on_draw(self):
107108
self.clear()
108109
self.program["time"] = self.time
109-
self.geometry.render(self.program, mode=self.ctx.PATCHES)
110+
self.geometry.render(self.program, mode=pyglet.gl.GL_PATCHES)
110111

111112

112113
if __name__ == "__main__":

arcade/gl/backends/__init__.py

Whitespace-only changes.

arcade/gl/backends/opengl/__init__.py

Whitespace-only changes.
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
from __future__ import annotations
2+
3+
import weakref
4+
from ctypes import byref, string_at
5+
from typing import TYPE_CHECKING
6+
7+
from pyglet import gl
8+
9+
from arcade.gl.buffer import Buffer
10+
from arcade.types import BufferProtocol
11+
12+
from .utils import data_to_ctypes
13+
14+
if TYPE_CHECKING:
15+
from arcade.gl import Context
16+
17+
_usages = {
18+
"static": gl.GL_STATIC_DRAW,
19+
"dynamic": gl.GL_DYNAMIC_DRAW,
20+
"stream": gl.GL_STREAM_DRAW,
21+
}
22+
23+
24+
class OpenGLBuffer(Buffer):
25+
"""OpenGL buffer object. Buffers store byte data and upload it
26+
to graphics memory so shader programs can process the data.
27+
They are used for storage of vertex data,
28+
element data (vertex indexing), uniform block data etc.
29+
30+
The ``data`` parameter can be anything that implements the
31+
`Buffer Protocol <https://docs.python.org/3/c-api/buffer.html>`_.
32+
33+
This includes ``bytes``, ``bytearray``, ``array.array``, and
34+
more. You may need to use typing workarounds for non-builtin
35+
types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more
36+
information.
37+
38+
.. warning:: Buffer objects should be created using :py:meth:`arcade.gl.Context.buffer`
39+
40+
Args:
41+
ctx:
42+
The context this buffer belongs to
43+
data:
44+
The data this buffer should contain. It can be a ``bytes`` instance or any
45+
object supporting the buffer protocol.
46+
reserve:
47+
Create a buffer of a specific byte size
48+
usage:
49+
A hit of this buffer is ``static`` or ``dynamic`` (can mostly be ignored)
50+
"""
51+
52+
__slots__ = "_glo", "_usage"
53+
54+
def __init__(
55+
self,
56+
ctx: Context,
57+
data: BufferProtocol | None = None,
58+
reserve: int = 0,
59+
usage: str = "static",
60+
):
61+
super().__init__(ctx)
62+
self._usage = _usages[usage]
63+
self._glo = glo = gl.GLuint()
64+
gl.glGenBuffers(1, byref(self._glo))
65+
# print(f"glGenBuffers() -> {self._glo.value}")
66+
if self._glo.value == 0:
67+
raise RuntimeError("Cannot create Buffer object.")
68+
69+
# print(f"glBindBuffer({self._glo.value})")
70+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo)
71+
# print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})")
72+
73+
if data is not None and len(data) > 0: # type: ignore
74+
self._size, data = data_to_ctypes(data)
75+
gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage)
76+
elif reserve > 0:
77+
self._size = reserve
78+
# populate the buffer with zero byte values
79+
data = (gl.GLubyte * self._size)()
80+
gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage)
81+
else:
82+
raise ValueError("Buffer takes byte data or number of reserved bytes")
83+
84+
if self._ctx.gc_mode == "auto":
85+
weakref.finalize(self, OpenGLBuffer.delete_glo, self.ctx, glo)
86+
87+
def __repr__(self):
88+
return f"<Buffer {self._glo.value}>"
89+
90+
def __del__(self):
91+
# Intercept garbage collection if we are using Context.gc()
92+
if self._ctx.gc_mode == "context_gc" and self._glo.value > 0:
93+
self._ctx.objects.append(self)
94+
95+
@property
96+
def glo(self) -> gl.GLuint:
97+
"""The OpenGL resource id."""
98+
return self._glo
99+
100+
def delete(self) -> None:
101+
"""
102+
Destroy the underlying OpenGL resource.
103+
104+
.. warning:: Don't use this unless you know exactly what you are doing.
105+
"""
106+
OpenGLBuffer.delete_glo(self._ctx, self._glo)
107+
self._glo.value = 0
108+
109+
@staticmethod
110+
def delete_glo(ctx: Context, glo: gl.GLuint):
111+
"""
112+
Release/delete open gl buffer.
113+
114+
This is automatically called when the object is garbage collected.
115+
116+
Args:
117+
ctx:
118+
The context the buffer belongs to
119+
glo:
120+
The OpenGL buffer id
121+
"""
122+
# If we have no context, then we are shutting down, so skip this
123+
if gl.current_context is None:
124+
return
125+
126+
if glo.value != 0:
127+
gl.glDeleteBuffers(1, byref(glo))
128+
glo.value = 0
129+
130+
ctx.stats.decr("buffer")
131+
132+
def read(self, size: int = -1, offset: int = 0) -> bytes:
133+
"""Read data from the buffer.
134+
135+
Args:
136+
size:
137+
The bytes to read. -1 means the entire buffer (default)
138+
offset:
139+
Byte read offset
140+
"""
141+
if size == -1:
142+
size = self._size - offset
143+
144+
# Catch this before confusing INVALID_OPERATION is raised
145+
if size < 1:
146+
raise ValueError(
147+
"Attempting to read 0 or less bytes from buffer: "
148+
f"buffer size={self._size} | params: size={size}, offset={offset}"
149+
)
150+
151+
# Manually detect this so it doesn't raise a confusing INVALID_VALUE error
152+
if size + offset > self._size:
153+
raise ValueError(
154+
(
155+
"Attempting to read outside the buffer. "
156+
f"Buffer size: {self._size} "
157+
f"Reading from {offset} to {size + offset}"
158+
)
159+
)
160+
161+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo)
162+
ptr = gl.glMapBufferRange(gl.GL_ARRAY_BUFFER, offset, size, gl.GL_MAP_READ_BIT)
163+
data = string_at(ptr, size=size)
164+
gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER)
165+
return data
166+
167+
def write(self, data: BufferProtocol, offset: int = 0):
168+
"""Write byte data to the buffer from a buffer protocol object.
169+
170+
The ``data`` value can be anything that implements the
171+
`Buffer Protocol <https://docs.python.org/3/c-api/buffer.html>`_.
172+
173+
This includes ``bytes``, ``bytearray``, ``array.array``, and
174+
more. You may need to use typing workarounds for non-builtin
175+
types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more
176+
information.
177+
178+
If the supplied data is larger than the buffer, it will be
179+
truncated to fit. If the supplied data is smaller than the
180+
buffer, the remaining bytes will be left unchanged.
181+
182+
Args:
183+
data:
184+
The byte data to write. This can be bytes or any object
185+
supporting the buffer protocol.
186+
offset:
187+
The byte offset
188+
"""
189+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo)
190+
size, data = data_to_ctypes(data)
191+
# Ensure we don't write outside the buffer
192+
size = min(size, self._size - offset)
193+
if size < 0:
194+
raise ValueError("Attempting to write negative number bytes to buffer")
195+
gl.glBufferSubData(gl.GL_ARRAY_BUFFER, gl.GLintptr(offset), size, data)
196+
197+
def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0):
198+
"""Copy data into this buffer from another buffer.
199+
200+
Args:
201+
source:
202+
The buffer to copy from
203+
size:
204+
The amount of bytes to copy
205+
offset:
206+
The byte offset to write the data in this buffer
207+
source_offset:
208+
The byte offset to read from the source buffer
209+
"""
210+
# Read the entire source buffer into this buffer
211+
if size == -1:
212+
size = source.size
213+
214+
# TODO: Check buffer bounds
215+
if size + source_offset > source.size:
216+
raise ValueError("Attempting to read outside the source buffer")
217+
218+
if size + offset > self._size:
219+
raise ValueError("Attempting to write outside the buffer")
220+
221+
gl.glBindBuffer(gl.GL_COPY_READ_BUFFER, source.glo)
222+
gl.glBindBuffer(gl.GL_COPY_WRITE_BUFFER, self._glo)
223+
gl.glCopyBufferSubData(
224+
gl.GL_COPY_READ_BUFFER,
225+
gl.GL_COPY_WRITE_BUFFER,
226+
gl.GLintptr(source_offset), # readOffset
227+
gl.GLintptr(offset), # writeOffset
228+
size, # size (number of bytes to copy)
229+
)
230+
231+
def orphan(self, size: int = -1, double: bool = False):
232+
"""
233+
Re-allocate the entire buffer memory. This can be used to resize
234+
a buffer or for re-specification (orphan the buffer to avoid blocking).
235+
236+
If the current buffer is busy in rendering operations
237+
it will be deallocated by OpenGL when completed.
238+
239+
Args:
240+
size:
241+
New size of buffer. -1 will retain the current size.
242+
Takes precedence over ``double`` parameter if specified.
243+
double:
244+
Is passed in with `True` the buffer size will be doubled
245+
from its current size.
246+
"""
247+
if size > 0:
248+
self._size = size
249+
elif double is True:
250+
self._size *= 2
251+
252+
gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo)
253+
gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, None, self._usage)
254+
255+
def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1):
256+
"""Bind this buffer to a uniform block location.
257+
In most cases it will be sufficient to only provide a binding location.
258+
259+
Args:
260+
binding:
261+
The binding location
262+
offset:
263+
Byte offset
264+
size:
265+
Size of the buffer to bind.
266+
"""
267+
if size < 0:
268+
size = self.size
269+
270+
gl.glBindBufferRange(gl.GL_UNIFORM_BUFFER, binding, self._glo, offset, size)
271+
272+
def bind_to_storage_buffer(self, *, binding=0, offset=0, size=-1):
273+
"""
274+
Bind this buffer as a shader storage buffer.
275+
276+
Args:
277+
binding:
278+
The binding location
279+
offset:
280+
Byte offset in the buffer
281+
size:
282+
The size in bytes. The entire buffer will be mapped by default.
283+
"""
284+
if size < 0:
285+
size = self.size
286+
287+
gl.glBindBufferRange(gl.GL_SHADER_STORAGE_BUFFER, binding, self._glo, offset, size)

0 commit comments

Comments
 (0)