From 0a6b9d75980e2b80afc864d3fc1fa57e4db38cbe Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Thu, 3 Apr 2025 16:24:54 -0400 Subject: [PATCH 01/16] ABC based arcade.gl backend abstraction example --- arcade/application.py | 4 +- arcade/gl/backends/__init__.py | 0 arcade/gl/backends/gl/__init__.py | 0 arcade/gl/backends/gl/buffer.py | 289 ++++++++++++++++++++++++++++++ arcade/gl/backends/gl/context.py | 22 +++ arcade/gl/backends/gl/provider.py | 10 ++ arcade/gl/backends/gl/utils.py | 35 ++++ arcade/gl/buffer.py | 164 ++--------------- arcade/gl/context.py | 7 +- arcade/gl/provider.py | 43 +++++ 10 files changed, 426 insertions(+), 148 deletions(-) create mode 100644 arcade/gl/backends/__init__.py create mode 100644 arcade/gl/backends/gl/__init__.py create mode 100644 arcade/gl/backends/gl/buffer.py create mode 100644 arcade/gl/backends/gl/context.py create mode 100644 arcade/gl/backends/gl/provider.py create mode 100644 arcade/gl/backends/gl/utils.py create mode 100644 arcade/gl/provider.py diff --git a/arcade/application.py b/arcade/application.py index 7b7e17297..5155e1770 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -24,6 +24,7 @@ from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255 from arcade.utils import is_raspberry_pi from arcade.window_commands import get_display_size, set_window +from arcade.gl.provider import get_arcade_context if TYPE_CHECKING: from arcade.camera import Projector @@ -274,7 +275,8 @@ def __init__( self.push_handlers(on_resize=self._on_resize) - self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) + self._ctx: ArcadeContext = get_arcade_context(self, gc_mode=gc_mode, gl_api=gl_api) + #self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) self._background_color: Color = BLACK self._current_view: View | None = None diff --git a/arcade/gl/backends/__init__.py b/arcade/gl/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/arcade/gl/backends/gl/__init__.py b/arcade/gl/backends/gl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/arcade/gl/backends/gl/buffer.py b/arcade/gl/backends/gl/buffer.py new file mode 100644 index 000000000..2f1250de6 --- /dev/null +++ b/arcade/gl/backends/gl/buffer.py @@ -0,0 +1,289 @@ +from __future__ import annotations + +import weakref +from ctypes import byref, string_at +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.types import BufferProtocol + +from .utils import data_to_ctypes + +from arcade.gl.buffer import Buffer + +if TYPE_CHECKING: + from arcade.gl import Context + +_usages = { + "static": gl.GL_STATIC_DRAW, + "dynamic": gl.GL_DYNAMIC_DRAW, + "stream": gl.GL_STREAM_DRAW, +} + + +class GLBuffer(Buffer): + """OpenGL buffer object. Buffers store byte data and upload it + to graphics memory so shader programs can process the data. + They are used for storage of vertex data, + element data (vertex indexing), uniform block data etc. + + The ``data`` parameter can be anything that implements the + `Buffer Protocol `_. + + This includes ``bytes``, ``bytearray``, ``array.array``, and + more. You may need to use typing workarounds for non-builtin + types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more + information. + + .. warning:: Buffer objects should be created using :py:meth:`arcade.gl.Context.buffer` + + Args: + ctx: + The context this buffer belongs to + data: + The data this buffer should contain. It can be a ``bytes`` instance or any + object supporting the buffer protocol. + reserve: + Create a buffer of a specific byte size + usage: + A hit of this buffer is ``static`` or ``dynamic`` (can mostly be ignored) + """ + + __slots__ = "_glo", "_usage" + + + def __init__( + self, + ctx: Context, + data: BufferProtocol | None = None, + reserve: int = 0, + usage: str = "static", + ): + super().__init__(ctx, data, reserve, usage) + self._usage = _usages[usage] + self._glo = glo = gl.GLuint() + gl.glGenBuffers(1, byref(self._glo)) + # print(f"glGenBuffers() -> {self._glo.value}") + if self._glo.value == 0: + raise RuntimeError("Cannot create Buffer object.") + + # print(f"glBindBuffer({self._glo.value})") + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) + # print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})") + + if data is not None and len(data) > 0: # type: ignore + self._size, data = data_to_ctypes(data) + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) + elif reserve > 0: + self._size = reserve + # populate the buffer with zero byte values + data = (gl.GLubyte * self._size)() + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) + else: + raise ValueError("Buffer takes byte data or number of reserved bytes") + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLBuffer.delete_glo, self.ctx, glo) + + def __repr__(self): + return f"" + + def __del__(self): + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: + self._ctx.objects.append(self) + + @property + def glo(self) -> gl.GLuint: + """The OpenGL resource id.""" + return self._glo + + def delete(self) -> None: + """ + Destroy the underlying OpenGL resource. + + .. warning:: Don't use this unless you know exactly what you are doing. + """ + GLBuffer.delete_glo(self._ctx, self._glo) + self._glo.value = 0 + + @staticmethod + def delete_glo(ctx: Context, glo: gl.GLuint): + """ + Release/delete open gl buffer. + + This is automatically called when the object is garbage collected. + + Args: + ctx: + The context the buffer belongs to + glo: + The OpenGL buffer id + """ + # If we have no context, then we are shutting down, so skip this + if gl.current_context is None: + return + + if glo.value != 0: + gl.glDeleteBuffers(1, byref(glo)) + glo.value = 0 + + ctx.stats.decr("buffer") + + def read(self, size: int = -1, offset: int = 0) -> bytes: + """Read data from the buffer. + + Args: + size: + The bytes to read. -1 means the entire buffer (default) + offset: + Byte read offset + """ + if size == -1: + size = self._size - offset + + # Catch this before confusing INVALID_OPERATION is raised + if size < 1: + raise ValueError( + "Attempting to read 0 or less bytes from buffer: " + f"buffer size={self._size} | params: size={size}, offset={offset}" + ) + + # Manually detect this so it doesn't raise a confusing INVALID_VALUE error + if size + offset > self._size: + raise ValueError( + ( + "Attempting to read outside the buffer. " + f"Buffer size: {self._size} " + f"Reading from {offset} to {size + offset}" + ) + ) + + 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) + gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER) + return data + + def write(self, data: BufferProtocol, offset: int = 0): + """Write byte data to the buffer from a buffer protocol object. + + The ``data`` value can be anything that implements the + `Buffer Protocol `_. + + This includes ``bytes``, ``bytearray``, ``array.array``, and + more. You may need to use typing workarounds for non-builtin + types. See :ref:`prog-guide-gl-buffer-protocol-typing` for more + information. + + If the supplied data is larger than the buffer, it will be + truncated to fit. If the supplied data is smaller than the + buffer, the remaining bytes will be left unchanged. + + Args: + data: + The byte data to write. This can be bytes or any object + supporting the buffer protocol. + offset: + The byte offset + """ + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) + size, data = data_to_ctypes(data) + # Ensure we don't write outside the buffer + size = min(size, self._size - offset) + if size < 0: + raise ValueError("Attempting to write negative number bytes to buffer") + gl.glBufferSubData(gl.GL_ARRAY_BUFFER, gl.GLintptr(offset), size, data) + + def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0): + """Copy data into this buffer from another buffer. + + Args: + source: + The buffer to copy from + size: + The amount of bytes to copy + offset: + The byte offset to write the data in this buffer + source_offset: + The byte offset to read from the source buffer + """ + # Read the entire source buffer into this buffer + if size == -1: + size = source.size + + # TODO: Check buffer bounds + if size + source_offset > source.size: + raise ValueError("Attempting to read outside the source buffer") + + if size + offset > self._size: + raise ValueError("Attempting to write outside the buffer") + + gl.glBindBuffer(gl.GL_COPY_READ_BUFFER, source.glo) + gl.glBindBuffer(gl.GL_COPY_WRITE_BUFFER, self._glo) + gl.glCopyBufferSubData( + gl.GL_COPY_READ_BUFFER, + gl.GL_COPY_WRITE_BUFFER, + gl.GLintptr(source_offset), # readOffset + gl.GLintptr(offset), # writeOffset + size, # size (number of bytes to copy) + ) + + def orphan(self, size: int = -1, double: bool = False): + """ + Re-allocate the entire buffer memory. This can be used to resize + a buffer or for re-specification (orphan the buffer to avoid blocking). + + If the current buffer is busy in rendering operations + it will be deallocated by OpenGL when completed. + + Args: + size: + New size of buffer. -1 will retain the current size. + Takes precedence over ``double`` parameter if specified. + double: + Is passed in with `True` the buffer size will be doubled + from its current size. + """ + if size > 0: + self._size = size + elif double is True: + self._size *= 2 + + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) + gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, None, self._usage) + + def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1): + """Bind this buffer to a uniform block location. + In most cases it will be sufficient to only provide a binding location. + + Args: + binding: + The binding location + offset: + Byte offset + size: + Size of the buffer to bind. + """ + if size < 0: + size = self.size + + gl.glBindBufferRange(gl.GL_UNIFORM_BUFFER, binding, self._glo, offset, size) + + def bind_to_storage_buffer(self, *, binding=0, offset=0, size=-1): + """ + Bind this buffer as a shader storage buffer. + + Args: + binding: + The binding location + offset: + Byte offset in the buffer + size: + The size in bytes. The entire buffer will be mapped by default. + """ + if size < 0: + size = self.size + + gl.glBindBufferRange(gl.GL_SHADER_STORAGE_BUFFER, binding, self._glo, offset, size) diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py new file mode 100644 index 000000000..3873ccebf --- /dev/null +++ b/arcade/gl/backends/gl/context.py @@ -0,0 +1,22 @@ +from arcade.gl.context import Context +from arcade.context import ArcadeContext + + +import pyglet + +from arcade.types import BufferProtocol + +from .buffer import GLBuffer + +class GLContext(Context): + def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): + super().__init__(window, gc_mode, gl_api) + + def buffer(self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> GLBuffer: + return GLBuffer(self, data, reserve=reserve, usage=usage) + + +class GLArcadeContext(ArcadeContext, GLContext): + def __init__(self, *args, **kwargs): + GLContext.__init__(self, *args, **kwargs) + ArcadeContext.__init__(self, *args, **kwargs) \ No newline at end of file diff --git a/arcade/gl/backends/gl/provider.py b/arcade/gl/backends/gl/provider.py new file mode 100644 index 000000000..1af139fa7 --- /dev/null +++ b/arcade/gl/backends/gl/provider.py @@ -0,0 +1,10 @@ +from arcade.gl.provider import BaseProvider + +from .context import GLContext, GLArcadeContext + +class Provider(BaseProvider): + def create_context(self, *args, **kwargs): + return GLContext(*args, **kwargs) + + def create_arcade_context(self, *args, **kwargs): + return GLArcadeContext(*args, **kwargs) \ No newline at end of file diff --git a/arcade/gl/backends/gl/utils.py b/arcade/gl/backends/gl/utils.py new file mode 100644 index 000000000..cf3249cb3 --- /dev/null +++ b/arcade/gl/backends/gl/utils.py @@ -0,0 +1,35 @@ +""" +Various utility functions for the gl module. +""" + +from array import array +from ctypes import c_byte +from typing import Any + + +def data_to_ctypes(data: Any) -> tuple[int, Any]: + """ + Attempts to convert the data to ctypes if needed by using the buffer protocol. + + - bytes will be returned as is + - Tuples will be converted to array + - Other types will be converted to ctypes by using the buffer protocol + by creating a memoryview and then a ctypes array of bytes. + + Args: + data: The data to convert to ctypes. + Returns: + A tuple containing the size of the data in bytes + and the data object optionally converted to ctypes. + """ + if isinstance(data, bytes): + return len(data), data + else: + if isinstance(data, tuple): + data = array("f", data) + try: + m_view = memoryview(data) + c_bytes = c_byte * m_view.nbytes + return m_view.nbytes, c_bytes.from_buffer(m_view) + except Exception as ex: + raise TypeError(f"Failed to convert data to ctypes: {ex}") diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index 7504bc46b..a0a514c80 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -1,20 +1,16 @@ from __future__ import annotations -import weakref -from ctypes import byref, string_at -from typing import TYPE_CHECKING +from abc import ABC, abstractmethod -from pyglet import gl +from typing import TYPE_CHECKING from arcade.types import BufferProtocol -from .utils import data_to_ctypes - if TYPE_CHECKING: from arcade.gl import Context -class Buffer: +class Buffer(ABC): """OpenGL buffer object. Buffers store byte data and upload it to graphics memory so shader programs can process the data. They are used for storage of vertex data, @@ -42,12 +38,7 @@ class Buffer: A hit of this buffer is ``static`` or ``dynamic`` (can mostly be ignored) """ - __slots__ = "_ctx", "_glo", "_size", "_usage", "__weakref__" - _usages = { - "static": gl.GL_STATIC_DRAW, - "dynamic": gl.GL_DYNAMIC_DRAW, - "stream": gl.GL_STREAM_DRAW, - } + __slots__ = "_ctx", "_size", "__weakref__" def __init__( self, @@ -57,43 +48,9 @@ def __init__( usage: str = "static", ): self._ctx = ctx - self._glo = glo = gl.GLuint() self._size = -1 - self._usage = Buffer._usages[usage] - - gl.glGenBuffers(1, byref(self._glo)) - # print(f"glGenBuffers() -> {self._glo.value}") - if self._glo.value == 0: - raise RuntimeError("Cannot create Buffer object.") - - # print(f"glBindBuffer({self._glo.value})") - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) - # print(f"glBufferData(gl.GL_ARRAY_BUFFER, {self._size}, data, {self._usage})") - - if data is not None and len(data) > 0: # type: ignore - self._size, data = data_to_ctypes(data) - gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) - elif reserve > 0: - self._size = reserve - # populate the buffer with zero byte values - data = (gl.GLubyte * self._size)() - gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, data, self._usage) - else: - raise ValueError("Buffer takes byte data or number of reserved bytes") - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, Buffer.delete_glo, self.ctx, glo) - self._ctx.stats.incr("buffer") - def __repr__(self): - return f"" - - def __del__(self): - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: - self._ctx.objects.append(self) - @property def size(self) -> int: """The byte size of the buffer.""" @@ -104,43 +61,16 @@ def ctx(self) -> "Context": """The context this resource belongs to.""" return self._ctx - @property - def glo(self) -> gl.GLuint: - """The OpenGL resource id.""" - return self._glo - + @abstractmethod def delete(self) -> None: """ - Destroy the underlying OpenGL resource. + Destroy the underlying native buffer resource. .. warning:: Don't use this unless you know exactly what you are doing. """ - Buffer.delete_glo(self._ctx, self._glo) - self._glo.value = 0 - - @staticmethod - def delete_glo(ctx: Context, glo: gl.GLuint): - """ - Release/delete open gl buffer. - - This is automatically called when the object is garbage collected. - - Args: - ctx: - The context the buffer belongs to - glo: - The OpenGL buffer id - """ - # If we have no context, then we are shutting down, so skip this - if gl.current_context is None: - return - - if glo.value != 0: - gl.glDeleteBuffers(1, byref(glo)) - glo.value = 0 - - ctx.stats.decr("buffer") + pass + @abstractmethod def read(self, size: int = -1, offset: int = 0) -> bytes: """Read data from the buffer. @@ -150,32 +80,9 @@ def read(self, size: int = -1, offset: int = 0) -> bytes: offset: Byte read offset """ - if size == -1: - size = self._size - offset - - # Catch this before confusing INVALID_OPERATION is raised - if size < 1: - raise ValueError( - "Attempting to read 0 or less bytes from buffer: " - f"buffer size={self._size} | params: size={size}, offset={offset}" - ) - - # Manually detect this so it doesn't raise a confusing INVALID_VALUE error - if size + offset > self._size: - raise ValueError( - ( - "Attempting to read outside the buffer. " - f"Buffer size: {self._size} " - f"Reading from {offset} to {size + offset}" - ) - ) - - 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) - gl.glUnmapBuffer(gl.GL_ARRAY_BUFFER) - return data + pass + @abstractmethod def write(self, data: BufferProtocol, offset: int = 0): """Write byte data to the buffer from a buffer protocol object. @@ -198,14 +105,9 @@ def write(self, data: BufferProtocol, offset: int = 0): offset: The byte offset """ - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) - size, data = data_to_ctypes(data) - # Ensure we don't write outside the buffer - size = min(size, self._size - offset) - if size < 0: - raise ValueError("Attempting to write negative number bytes to buffer") - gl.glBufferSubData(gl.GL_ARRAY_BUFFER, gl.GLintptr(offset), size, data) + pass + @abstractmethod def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0): """Copy data into this buffer from another buffer. @@ -219,27 +121,9 @@ def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0): source_offset: The byte offset to read from the source buffer """ - # Read the entire source buffer into this buffer - if size == -1: - size = source.size - - # TODO: Check buffer bounds - if size + source_offset > source.size: - raise ValueError("Attempting to read outside the source buffer") - - if size + offset > self._size: - raise ValueError("Attempting to write outside the buffer") - - gl.glBindBuffer(gl.GL_COPY_READ_BUFFER, source.glo) - gl.glBindBuffer(gl.GL_COPY_WRITE_BUFFER, self._glo) - gl.glCopyBufferSubData( - gl.GL_COPY_READ_BUFFER, - gl.GL_COPY_WRITE_BUFFER, - gl.GLintptr(source_offset), # readOffset - gl.GLintptr(offset), # writeOffset - size, # size (number of bytes to copy) - ) + pass + @abstractmethod def orphan(self, size: int = -1, double: bool = False): """ Re-allocate the entire buffer memory. This can be used to resize @@ -256,14 +140,9 @@ def orphan(self, size: int = -1, double: bool = False): Is passed in with `True` the buffer size will be doubled from its current size. """ - if size > 0: - self._size = size - elif double is True: - self._size *= 2 - - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, self._glo) - gl.glBufferData(gl.GL_ARRAY_BUFFER, self._size, None, self._usage) + pass + @abstractmethod def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1): """Bind this buffer to a uniform block location. In most cases it will be sufficient to only provide a binding location. @@ -276,11 +155,9 @@ def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = - size: Size of the buffer to bind. """ - if size < 0: - size = self.size - - gl.glBindBufferRange(gl.GL_UNIFORM_BUFFER, binding, self._glo, offset, size) + pass + @abstractmethod def bind_to_storage_buffer(self, *, binding=0, offset=0, size=-1): """ Bind this buffer as a shader storage buffer. @@ -293,7 +170,4 @@ def bind_to_storage_buffer(self, *, binding=0, offset=0, size=-1): size: The size in bytes. The entire buffer will be mapped by default. """ - if size < 0: - size = self.size - - gl.glBindBufferRange(gl.GL_SHADER_STORAGE_BUFFER, binding, self._glo, offset, size) + pass diff --git a/arcade/gl/context.py b/arcade/gl/context.py index d1a86c08b..e00a7555f 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -1,5 +1,7 @@ from __future__ import annotations +from abc import ABC, abstractmethod + import logging import weakref from collections import deque @@ -39,7 +41,7 @@ LOG = logging.getLogger(__name__) -class Context: +class Context(ABC): """ Represents an OpenGL context. This context belongs to a pyglet window. normally accessed through ``window.ctx``. @@ -898,6 +900,7 @@ def copy_framebuffer( # --- Resource methods --- + @abstractmethod def buffer( self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static" ) -> Buffer: @@ -947,7 +950,7 @@ def buffer( usage: Buffer usage. 'static', 'dynamic' or 'stream' """ - return Buffer(self, data, reserve=reserve, usage=usage) + pass def framebuffer( self, diff --git a/arcade/gl/provider.py b/arcade/gl/provider.py new file mode 100644 index 000000000..e47cd44d6 --- /dev/null +++ b/arcade/gl/provider.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod + +import importlib + +from arcade.context import ArcadeContext + +from .context import Context + +_current_provider: BaseProvider = None + +def set_provider(provider_name: str): + global _current_provider + + try: + module = importlib.import_module(f"arcade.gl.backends.{provider_name}.provider") + _current_provider = module.Provider() + except ImportError: + raise ImportError(f"GL Backend Provider '{provider_name}' not found") + + +def get_context(*args, **kwargs) -> Context: + if _current_provider is None: + set_provider("gl") + + return _current_provider.create_context(*args, **kwargs) + +def get_arcade_context(*args, **kwargs) -> ArcadeContext: + if _current_provider is None: + set_provider("gl") + + return _current_provider.create_arcade_context(*args, **kwargs) + +class BaseProvider(ABC): + + @abstractmethod + def create_context(self, *args, **kwargs) -> Context: + pass + + @abstractmethod + def create_arcade_context(self, *args, **kwargs) -> ArcadeContext: + pass \ No newline at end of file From 1d917d55efd83f6b8740367d0052d12d7acf319b Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 7 Apr 2025 21:25:03 -0400 Subject: [PATCH 02/16] Abstraction of programs and VAOs/geometry. Also some cleanup --- arcade/gl/backends/gl/buffer.py | 2 +- arcade/gl/backends/gl/context.py | 77 +++- arcade/gl/backends/gl/glsl.py | 169 ++++++++ arcade/gl/backends/gl/program.py | 548 ++++++++++++++++++++++++++ arcade/gl/backends/gl/types.py | 536 +++++++++++++++++++++++++ arcade/gl/backends/gl/uniform.py | 422 ++++++++++++++++++++ arcade/gl/backends/gl/vertex_array.py | 492 +++++++++++++++++++++++ arcade/gl/buffer.py | 3 - arcade/gl/context.py | 55 +-- arcade/gl/program.py | 414 ++----------------- arcade/gl/provider.py | 5 +- arcade/gl/vertex_array.py | 335 ++-------------- 12 files changed, 2313 insertions(+), 745 deletions(-) create mode 100644 arcade/gl/backends/gl/glsl.py create mode 100644 arcade/gl/backends/gl/program.py create mode 100644 arcade/gl/backends/gl/types.py create mode 100644 arcade/gl/backends/gl/uniform.py create mode 100644 arcade/gl/backends/gl/vertex_array.py diff --git a/arcade/gl/backends/gl/buffer.py b/arcade/gl/backends/gl/buffer.py index 2f1250de6..1324d7a69 100644 --- a/arcade/gl/backends/gl/buffer.py +++ b/arcade/gl/backends/gl/buffer.py @@ -60,7 +60,7 @@ def __init__( reserve: int = 0, usage: str = "static", ): - super().__init__(ctx, data, reserve, usage) + super().__init__(ctx) self._usage = _usages[usage] self._glo = glo = gl.GLuint() gl.glGenBuffers(1, byref(self._glo)) diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 3873ccebf..9dffd0f30 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -1,12 +1,17 @@ +from typing import List, Dict, Sequence + from arcade.gl.context import Context from arcade.context import ArcadeContext - import pyglet from arcade.types import BufferProtocol from .buffer import GLBuffer +from .glsl import ShaderSource +from .types import BufferDescription +from .program import GLProgram +from .vertex_array import GLGeometry class GLContext(Context): def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): @@ -15,6 +20,76 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl def buffer(self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> GLBuffer: return GLBuffer(self, data, reserve=reserve, usage=usage) + def program( + self, + *, + vertex_shader: str, + fragment_shader: str | None = None, + geometry_shader: str | None = None, + tess_control_shader: str | None = None, + tess_evaluation_shader: str | None = None, + common: List[str] | None = None, + defines: Dict[str, str] | None = None, + varyings: Sequence[str] | None = None, + varyings_capture_mode: str = "interleaved", + ) -> GLProgram: + source_vs = ShaderSource(self, vertex_shader, common, pyglet.gl.GL_VERTEX_SHADER) + source_fs = ( + ShaderSource(self, fragment_shader, common, pyglet.gl.GL_FRAGMENT_SHADER) + if fragment_shader + else None + ) + source_geo = ( + ShaderSource(self, geometry_shader, common, pyglet.gl.GL_GEOMETRY_SHADER) + if geometry_shader + else None + ) + source_tc = ( + ShaderSource(self, tess_control_shader, common, pyglet.gl.GL_TESS_CONTROL_SHADER) + if tess_control_shader + else None + ) + source_te = ( + ShaderSource(self, tess_evaluation_shader, common, pyglet.gl.GL_TESS_EVALUATION_SHADER) + if tess_evaluation_shader + else None + ) + + # If we don't have a fragment shader we are doing transform feedback. + # When a geometry shader is present the out attributes will be located there + out_attributes = list(varyings) if varyings is not None else [] # type: List[str] + if not source_fs and not out_attributes: + if source_geo: + out_attributes = source_geo.out_attributes + else: + out_attributes = source_vs.out_attributes + + return GLProgram( + self, + vertex_shader=source_vs.get_source(defines=defines), + fragment_shader=source_fs.get_source(defines=defines) if source_fs else None, + geometry_shader=source_geo.get_source(defines=defines) if source_geo else None, + tess_control_shader=source_tc.get_source(defines=defines) if source_tc else None, + tess_evaluation_shader=source_te.get_source(defines=defines) if source_te else None, + varyings=out_attributes, + varyings_capture_mode=varyings_capture_mode, + ) + + def geometry( + self, + content: Sequence[BufferDescription] | None = None, + index_buffer: GLBuffer | None = None, + mode: int | None = None, + index_element_size: int = 4, + ): + return GLGeometry( + self, + content, + index_buffer=index_buffer, + mode=mode, + index_element_size=index_element_size, + ) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): diff --git a/arcade/gl/backends/gl/glsl.py b/arcade/gl/backends/gl/glsl.py new file mode 100644 index 000000000..f88fd44bb --- /dev/null +++ b/arcade/gl/backends/gl/glsl.py @@ -0,0 +1,169 @@ +import re +from typing import TYPE_CHECKING, Iterable + +from pyglet import gl + +if TYPE_CHECKING: + from .context import Context as ArcadeGlContext + +from arcade.gl.exceptions import ShaderException +from .types import SHADER_TYPE_NAMES, PyGLenum + + +class ShaderSource: + """ + GLSL source container for making source parsing simpler. + + We support locating out attributes, applying ``#defines`` values + and injecting common source. + + .. note::: + We do assume the source is neat enough to be parsed + this way and don't contain several statements on one line. + + Args: + ctx: + The context this framebuffer belongs to + source: + The GLSL source code + common: + Common source code to inject + source_type: + The shader type + """ + + def __init__( + self, + ctx: "ArcadeGlContext", + source: str, + common: Iterable[str] | None, + source_type: PyGLenum, + ): + self._source = source.strip() + self._type = source_type + self._lines = self._source.split("\n") if source else [] + self._out_attributes: list[str] = [] + + if not self._lines: + raise ValueError("Shader source is empty") + + self._version = self._find_glsl_version() + + # GLES specific modifications + if ctx.gl_api == "gles": + # TODO: Use the version from the context + self._lines[0] = "#version 310 es" + self._lines.insert(1, "precision mediump float;") + + if self._type == gl.GL_GEOMETRY_SHADER: + self._lines.insert(1, "#extension GL_EXT_geometry_shader : require") + + if self._type == gl.GL_COMPUTE_SHADER: + self._lines.insert(1, "precision mediump image2D;") + + self._version = self._find_glsl_version() + + # Inject common source + self.inject_common_sources(common) + + if self._type in [gl.GL_VERTEX_SHADER, gl.GL_GEOMETRY_SHADER]: + self._parse_out_attributes() + + @property + def version(self) -> int: + """The glsl version""" + return self._version + + @property + def out_attributes(self) -> list[str]: + """The out attributes for this program""" + return self._out_attributes + + def inject_common_sources(self, common: Iterable[str] | None) -> None: + """ + Inject common source code into the shader source. + + Args: + common: + A list of common source code strings to inject + """ + if not common: + return + + # Find the main function + for line_number, line in enumerate(self._lines): + if "main()" in line: + break + else: + raise ShaderException("No main() function found when injecting common source") + + # Insert all common sources + for source in common: + lines = source.split("\n") + self._lines = self._lines[:line_number] + lines + self._lines[line_number:] + + def get_source(self, *, defines: dict[str, str] | None = None) -> str: + """Return the shader source + + Args: + defines: Defines to replace in the source. + """ + if not defines: + return "\n".join(self._lines) + + lines = ShaderSource.apply_defines(self._lines, defines) + return "\n".join(lines) + + def _find_glsl_version(self) -> int: + if self._lines[0].strip().startswith("#version"): + try: + return int(self._lines[0].split()[1]) + except Exception: + pass + + source = "\n".join(f"{str(i + 1).zfill(3)}: {line} " for i, line in enumerate(self._lines)) + + raise ShaderException( + ( + "Cannot find #version in shader source. " + "Please provide at least a #version 330 statement in the beginning of the shader.\n" + f"---- [{SHADER_TYPE_NAMES[self._type]}] ---\n" + f"{source}" + ) + ) + + @staticmethod + def apply_defines(lines: list[str], defines: dict[str, str]) -> list[str]: + """Locate and apply #define values + + Args: + lines: + List of source lines + defines: + dict with ``name: value`` pairs. + """ + for nr, line in enumerate(lines): + line = line.strip() + if line.startswith("#define"): + try: + name = line.split()[1] + value = defines.get(name, None) + if value is None: + continue + + lines[nr] = "#define {} {}".format(name, str(value)) + except IndexError: + pass + + return lines + + def _parse_out_attributes(self): + """ + Locates out attributes so we don't have to manually supply them. + + Note that this currently doesn't work for structs. + """ + for line in self._lines: + res = re.match(r"(layout(.+)\))?(\s+)?(out)(\s+)(\w+)(\s+)(\w+)", line.strip()) + if res: + self._out_attributes.append(res.groups()[-1]) diff --git a/arcade/gl/backends/gl/program.py b/arcade/gl/backends/gl/program.py new file mode 100644 index 000000000..815f196bd --- /dev/null +++ b/arcade/gl/backends/gl/program.py @@ -0,0 +1,548 @@ +from __future__ import annotations + +import typing +import weakref +from ctypes import ( + POINTER, + byref, + c_buffer, + c_char, + c_char_p, + c_int, + cast, + create_string_buffer, + pointer, +) +from typing import TYPE_CHECKING, Any, Iterable + +from pyglet import gl + +from arcade.gl.exceptions import ShaderException +from arcade.gl.program import Program + +from .types import SHADER_TYPE_NAMES, AttribFormat, GLTypes, PyGLenum +from .uniform import Uniform, UniformBlock + +if TYPE_CHECKING: + from arcade.gl import Context + + +class GLProgram(Program): + """ + Compiled and linked shader program. + + Currently supports + + - vertex shader + - fragment shader + - geometry shader + - tessellation control shader + - tessellation evaluation shader + + Transform feedback also supported when output attributes + names are passed in the varyings parameter. + + The best way to create a program instance is through :py:meth:`arcade.gl.Context.program` + + Args: + ctx: + The context this program belongs to + vertex_shader: + Vertex shader source + fragment_shader: + Fragment shader source + geometry_shader: + Geometry shader source + tess_control_shader: + Tessellation control shader source + tess_evaluation_shader: + Tessellation evaluation shader source + varyings: + List of out attributes used in transform feedback. + varyings_capture_mode: + The capture mode for transforms. + ``"interleaved"`` means all out attribute will be written to a single buffer. + ``"separate"`` means each out attribute will be written separate buffers. + Based on these settings the `transform()` method will accept a single + buffer or a list of buffer. + """ + + __slots__ = ( + "_glo", + "_uniforms", + "_varyings", + "_geometry_info", + "_attributes", + ) + + _valid_capture_modes = ("interleaved", "separate") + + def __init__( + self, + ctx: Context, + *, + vertex_shader: str, + fragment_shader: str | None = None, + geometry_shader: str | None = None, + tess_control_shader: str | None = None, + tess_evaluation_shader: str | None = None, + varyings: list[str] | None = None, + varyings_capture_mode: str = "interleaved", + ): + super().__init__(ctx) + self._glo = glo = gl.glCreateProgram() + self._varyings = varyings or [] + self._varyings_capture_mode = varyings_capture_mode.strip().lower() + self._geometry_info = (0, 0, 0) + self._attributes = [] # type: list[AttribFormat] + #: Internal cache key used with vertex arrays + self._uniforms: dict[str, Uniform | UniformBlock] = {} + + if self._varyings_capture_mode not in self._valid_capture_modes: + raise ValueError( + f"Invalid capture mode '{self._varyings_capture_mode}'. " + f"Valid modes are: {self._valid_capture_modes}." + ) + + shaders: list[tuple[str, int]] = [(vertex_shader, gl.GL_VERTEX_SHADER)] + if fragment_shader: + shaders.append((fragment_shader, gl.GL_FRAGMENT_SHADER)) + if geometry_shader: + shaders.append((geometry_shader, gl.GL_GEOMETRY_SHADER)) + if tess_control_shader: + shaders.append((tess_control_shader, gl.GL_TESS_CONTROL_SHADER)) + if tess_evaluation_shader: + shaders.append((tess_evaluation_shader, gl.GL_TESS_EVALUATION_SHADER)) + + # Inject a dummy fragment shader on gles when doing transforms + if self._ctx.gl_api == "gles" and not fragment_shader: + dummy_frag_src = """ + #version 310 es + precision mediump float; + out vec4 fragColor; + void main() { fragColor = vec4(1.0); } + """ + shaders.append((dummy_frag_src, gl.GL_FRAGMENT_SHADER)) + + shaders_id = [] + for shader_code, shader_type in shaders: + shader = GLProgram.compile_shader(shader_code, shader_type) + gl.glAttachShader(self._glo, shader) + shaders_id.append(shader) + + # For now we assume varyings can be set up if no fragment shader + if not fragment_shader: + self._configure_varyings() + + GLProgram.link(self._glo) + + if geometry_shader: + geometry_in = gl.GLint() + geometry_out = gl.GLint() + geometry_vertices = gl.GLint() + gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_INPUT_TYPE, geometry_in) + gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_OUTPUT_TYPE, geometry_out) + gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_VERTICES_OUT, geometry_vertices) + self._geometry_info = ( + geometry_in.value, + geometry_out.value, + geometry_vertices.value, + ) + + # Delete shaders (not needed after linking) + for shader in shaders_id: + gl.glDeleteShader(shader) + gl.glDetachShader(self._glo, shader) + + # Handle uniforms + self._introspect_attributes() + self._introspect_uniforms() + self._introspect_uniform_blocks() + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLProgram.delete_glo, self._ctx, glo) + + def __del__(self): + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and self._glo > 0: + self._ctx.objects.append(self) + + @property + def ctx(self) -> "Context": + """The context this program belongs to.""" + return self._ctx + + @property + def glo(self) -> int: + """The OpenGL resource id for this program.""" + return self._glo + + @property + def attributes(self) -> Iterable[AttribFormat]: + """List of attribute information.""" + return self._attributes + + @property + def varyings(self) -> list[str]: + """Out attributes names used in transform feedback.""" + return self._varyings + + @property + def out_attributes(self) -> list[str]: + """ + Out attributes names used in transform feedback. + + Alias for `varyings`. + """ + return self._varyings + + @property + def varyings_capture_mode(self) -> str: + """ + Get the capture more for transform feedback (single, multiple). + + This is a read only property since capture mode + can only be set before the program is linked. + """ + return self._varyings_capture_mode + + @property + def geometry_input(self) -> int: + """ + The geometry shader's input primitive type. + + This an be compared with ``GL_TRIANGLES``, ``GL_POINTS`` etc. + and is queried when the program is created. + """ + return self._geometry_info[0] + + @property + def geometry_output(self) -> int: + """The geometry shader's output primitive type. + + This an be compared with ``GL_TRIANGLES``, ``GL_POINTS`` etc. + and is queried when the program is created. + """ + return self._geometry_info[1] + + @property + def geometry_vertices(self) -> int: + """ + The maximum number of vertices that can be emitted. + This is queried when the program is created. + """ + return self._geometry_info[2] + + def delete(self): + """ + Destroy the underlying OpenGL resource. + + Don't use this unless you know exactly what you are doing. + """ + GLProgram.delete_glo(self._ctx, self._glo) + self._glo = 0 + + @staticmethod + def delete_glo(ctx, prog_id): + """ + Deletes a program. This is normally called automatically when the + program is garbage collected. + + Args: + ctx: + The context this program belongs to + prog_id: + The OpenGL resource id + """ + # Check to see if the context was already cleaned up from program + # shut down. If so, we don't need to delete the shaders. + if gl.current_context is None: + return + + gl.glDeleteProgram(prog_id) + ctx.stats.decr("program") + + def __getitem__(self, item) -> Uniform | UniformBlock: + """Get a uniform or uniform block""" + try: + uniform = self._uniforms[item] + except KeyError: + raise KeyError(f"Uniform with the name `{item}` was not found.") + + return uniform.getter() + + def __setitem__(self, key, value): + """ + Set a uniform value. + + Example:: + + program['color'] = 1.0, 1.0, 1.0, 1.0 + program['mvp'] = projection @ view @ model + + Args: + key: + The uniform name + value: + The uniform value + """ + try: + uniform = self._uniforms[key] + except KeyError: + raise KeyError(f"Uniform with the name `{key}` was not found.") + + uniform.setter(value) + + def set_uniform_safe(self, name: str, value: Any): + """ + Safely set a uniform catching KeyError. + + Args: + name: + The uniform name + value: + The uniform value + """ + try: + self[name] = value + except KeyError: + pass + + def set_uniform_array_safe(self, name: str, value: list[Any]): + """ + Safely set a uniform array. + + Arrays can be shortened by the glsl compiler not all elements are determined + to be in use. This function checks the length of the actual array and sets a + subset of the values if needed. If the uniform don't exist no action will be + done. + + Args: + name: + Name of uniform + value: + List of values + """ + if name not in self._uniforms: + return + + uniform = typing.cast(Uniform, self._uniforms[name]) + _len = uniform._array_length * uniform._components + if _len == 1: + self.set_uniform_safe(name, value[0]) + else: + self.set_uniform_safe(name, value[:_len]) + + def use(self): + """ + Activates the shader. + + This is normally done for you automatically. + """ + # IMPORTANT: This is the only place glUseProgram should be called + # so we can track active program. + # if self._ctx.active_program != self: + gl.glUseProgram(self._glo) + # self._ctx.active_program = self + + def _configure_varyings(self): + """Set up transform feedback varyings""" + if not self._varyings: + return + + # Covert names to char** + c_array = (c_char_p * len(self._varyings))() + for i, name in enumerate(self._varyings): + c_array[i] = name.encode() + + ptr = cast(c_array, POINTER(POINTER(c_char))) + + # Are we capturing in interlaved or separate buffers? + mode = ( + gl.GL_INTERLEAVED_ATTRIBS + if self._varyings_capture_mode == "interleaved" + else gl.GL_SEPARATE_ATTRIBS + ) + + gl.glTransformFeedbackVaryings( + self._glo, # program + len(self._varyings), # number of varying variables used for transform feedback + ptr, # zero-terminated strings specifying the names of the varying variables + mode, + ) + + def _introspect_attributes(self): + """Introspect and store detailed info about an attribute""" + # TODO: Ensure gl_* attributes are ignored + num_attrs = gl.GLint() + gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_ATTRIBUTES, num_attrs) + num_varyings = gl.GLint() + gl.glGetProgramiv(self._glo, gl.GL_TRANSFORM_FEEDBACK_VARYINGS, num_varyings) + # print(f"attrs {num_attrs.value} varyings={num_varyings.value}") + + for i in range(num_attrs.value): + c_name = create_string_buffer(256) + c_size = gl.GLint() + c_type = gl.GLenum() + gl.glGetActiveAttrib( + self._glo, # program to query + i, # index (not the same as location) + 256, # max attr name size + None, # c_length, # length of name + c_size, # size of attribute (array or not) + c_type, # attribute type (enum) + c_name, # name buffer + ) + + # Get the actual location. Do not trust the original order + location = gl.glGetAttribLocation(self._glo, c_name) + + # print(c_name.value, c_size, c_type) + type_info = GLTypes.get(c_type.value) + # print(type_info) + self._attributes.append( + AttribFormat( + c_name.value.decode(), + type_info.gl_type, + type_info.components, + type_info.gl_size, + location=location, + ) + ) + + # The attribute key is used to cache VertexArrays + self.attribute_key = ":".join( + f"{attr.name}[{attr.gl_type}/{attr.components}]" for attr in self._attributes + ) + + def _introspect_uniforms(self): + """Figure out what uniforms are available and build an internal map""" + # Number of active uniforms in the program + active_uniforms = gl.GLint(0) + gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORMS, byref(active_uniforms)) + + # Loop all the active uniforms + for index in range(active_uniforms.value): + # Query uniform information like name, type, size etc. + u_name, u_type, u_size = self._query_uniform(index) + u_location = gl.glGetUniformLocation(self._glo, u_name.encode()) + + # Skip uniforms that may be in Uniform Blocks + # TODO: We should handle all uniforms + if u_location == -1: + # print(f"Uniform {u_location} {u_name} {u_size} {u_type} skipped") + continue + + u_name = u_name.replace("[0]", "") # Remove array suffix + self._uniforms[u_name] = Uniform( + self._ctx, self._glo, u_location, u_name, u_type, u_size + ) + + def _introspect_uniform_blocks(self): + active_uniform_blocks = gl.GLint(0) + gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORM_BLOCKS, byref(active_uniform_blocks)) + # print('GL_ACTIVE_UNIFORM_BLOCKS', active_uniform_blocks) + + for loc in range(active_uniform_blocks.value): + index, size, name = self._query_uniform_block(loc) + block = UniformBlock(self._glo, index, size, name) + self._uniforms[name] = block + + def _query_uniform(self, location: int) -> tuple[str, int, int]: + """Retrieve Uniform information at given location. + + Returns the name, the type as a GLenum (GL_FLOAT, ...) and the size. Size is + greater than 1 only for Uniform arrays, like an array of floats or an array + of Matrices. + """ + u_size = gl.GLint() + u_type = gl.GLenum() + buf_size = 192 # max uniform character length + u_name = create_string_buffer(buf_size) + gl.glGetActiveUniform( + self._glo, # program to query + location, # location to query + buf_size, # size of the character/name buffer + None, # the number of characters actually written by OpenGL in the string + u_size, # size of the uniform variable + u_type, # data type of the uniform variable + u_name, # string buffer for storing the name + ) + return u_name.value.decode(), u_type.value, u_size.value + + def _query_uniform_block(self, location: int) -> tuple[int, int, str]: + """Query active uniform block by retrieving the name and index and size""" + # Query name + u_size = gl.GLint() + buf_size = 192 # max uniform character length + u_name = create_string_buffer(buf_size) + gl.glGetActiveUniformBlockName( + self._glo, # program to query + location, # location to query + 256, # max size if the name + u_size, # length + u_name, + ) + # Query index + index = gl.glGetUniformBlockIndex(self._glo, u_name) + # Query size + b_size = gl.GLint() + gl.glGetActiveUniformBlockiv(self._glo, index, gl.GL_UNIFORM_BLOCK_DATA_SIZE, b_size) + return index, b_size.value, u_name.value.decode() + + @staticmethod + def compile_shader(source: str, shader_type: PyGLenum) -> gl.GLuint: + """ + Compile the shader code of the given type. + + Args: + source: + The shader source code + shader_type: + The type of shader to compile. + ``GL_VERTEX_SHADER``, ``GL_FRAGMENT_SHADER`` etc. + + Returns: + The created shader id + """ + shader = gl.glCreateShader(shader_type) + source_bytes = source.encode("utf-8") + # Turn the source code string into an array of c_char_p arrays. + strings = byref(cast(c_char_p(source_bytes), POINTER(c_char))) + # Make an array with the strings lengths + lengths = pointer(c_int(len(source_bytes))) + gl.glShaderSource(shader, 1, strings, lengths) + gl.glCompileShader(shader) + result = c_int() + gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS, byref(result)) + if result.value == gl.GL_FALSE: + msg = create_string_buffer(512) + length = c_int() + gl.glGetShaderInfoLog(shader, 512, byref(length), msg) + raise ShaderException( + ( + f"Error compiling {SHADER_TYPE_NAMES[shader_type]} " + f"({result.value}): {msg.value.decode('utf-8')}\n" + f"---- [{SHADER_TYPE_NAMES[shader_type]}] ---\n" + ) + + "\n".join( + f"{str(i + 1).zfill(3)}: {line} " for i, line in enumerate(source.split("\n")) + ) + ) + return shader + + @staticmethod + def link(glo): + """Link a shader program""" + gl.glLinkProgram(glo) + status = c_int() + gl.glGetProgramiv(glo, gl.GL_LINK_STATUS, status) + if not status.value: + length = c_int() + gl.glGetProgramiv(glo, gl.GL_INFO_LOG_LENGTH, length) + log = c_buffer(length.value) + gl.glGetProgramInfoLog(glo, len(log), None, log) + raise ShaderException("Program link error: {}".format(log.value.decode())) + + def __repr__(self): + return "".format(self._glo) diff --git a/arcade/gl/backends/gl/types.py b/arcade/gl/backends/gl/types.py new file mode 100644 index 000000000..2af7fcbd6 --- /dev/null +++ b/arcade/gl/backends/gl/types.py @@ -0,0 +1,536 @@ +import re +from typing import Iterable, Sequence, Union + +from pyglet import gl +from typing_extensions import TypeAlias + +from arcade.types import BufferProtocol + +from .buffer import Buffer + +BufferOrBufferProtocol = Union[BufferProtocol, Buffer] + +GLenumLike = Union[gl.GLenum, int] +PyGLenum = int +GLuintLike = Union[gl.GLuint, int] +PyGLuint = int + + +OpenGlFilter: TypeAlias = tuple[PyGLenum, PyGLenum] +BlendFunction: TypeAlias = Union[ + tuple[PyGLenum, PyGLenum], tuple[PyGLenum, PyGLenum, PyGLenum, PyGLenum] +] + +#: Depth compare functions +compare_funcs: dict[str | None, int] = { + None: gl.GL_NONE, + "<=": gl.GL_LEQUAL, + "<": gl.GL_LESS, + ">=": gl.GL_GEQUAL, + ">": gl.GL_GREATER, + "==": gl.GL_EQUAL, + "!=": gl.GL_NOTEQUAL, + "0": gl.GL_NEVER, + "1": gl.GL_ALWAYS, +} + +#: Swizzle conversion lookup +swizzle_enum_to_str: dict[int, str] = { + gl.GL_RED: "R", + gl.GL_GREEN: "G", + gl.GL_BLUE: "B", + gl.GL_ALPHA: "A", + gl.GL_ZERO: "0", + gl.GL_ONE: "1", +} + +#: Swizzle conversion lookup +swizzle_str_to_enum: dict[str, int] = { + "R": gl.GL_RED, + "G": gl.GL_GREEN, + "B": gl.GL_BLUE, + "A": gl.GL_ALPHA, + "0": gl.GL_ZERO, + "1": gl.GL_ONE, +} + +_float_base_format = (0, gl.GL_RED, gl.GL_RG, gl.GL_RGB, gl.GL_RGBA) +_int_base_format = ( + 0, + gl.GL_RED_INTEGER, + gl.GL_RG_INTEGER, + gl.GL_RGB_INTEGER, + gl.GL_RGBA_INTEGER, +) +#: Pixel format lookup (base_format, internal_format, type, size) +pixel_formats = { + # float formats + "f1": ( + _float_base_format, + (0, gl.GL_R8, gl.GL_RG8, gl.GL_RGB8, gl.GL_RGBA8), + gl.GL_UNSIGNED_BYTE, + 1, + ), + "f2": ( + _float_base_format, + (0, gl.GL_R16F, gl.GL_RG16F, gl.GL_RGB16F, gl.GL_RGBA16F), + gl.GL_HALF_FLOAT, + 2, + ), + "f4": ( + _float_base_format, + (0, gl.GL_R32F, gl.GL_RG32F, gl.GL_RGB32F, gl.GL_RGBA32F), + gl.GL_FLOAT, + 4, + ), + # int formats + "i1": ( + _int_base_format, + (0, gl.GL_R8I, gl.GL_RG8I, gl.GL_RGB8I, gl.GL_RGBA8I), + gl.GL_BYTE, + 1, + ), + "i2": ( + _int_base_format, + (0, gl.GL_R16I, gl.GL_RG16I, gl.GL_RGB16I, gl.GL_RGBA16I), + gl.GL_SHORT, + 2, + ), + "i4": ( + _int_base_format, + (0, gl.GL_R32I, gl.GL_RG32I, gl.GL_RGB32I, gl.GL_RGBA32I), + gl.GL_INT, + 4, + ), + # uint formats + "u1": ( + _int_base_format, + (0, gl.GL_R8UI, gl.GL_RG8UI, gl.GL_RGB8UI, gl.GL_RGBA8UI), + gl.GL_UNSIGNED_BYTE, + 1, + ), + "u2": ( + _int_base_format, + (0, gl.GL_R16UI, gl.GL_RG16UI, gl.GL_RGB16UI, gl.GL_RGBA16UI), + gl.GL_UNSIGNED_SHORT, + 2, + ), + "u4": ( + _int_base_format, + (0, gl.GL_R32UI, gl.GL_RG32UI, gl.GL_RGB32UI, gl.GL_RGBA32UI), + gl.GL_UNSIGNED_INT, + 4, + ), +} + + +#: String representation of a shader types +SHADER_TYPE_NAMES = { + gl.GL_VERTEX_SHADER: "vertex shader", + gl.GL_FRAGMENT_SHADER: "fragment shader", + gl.GL_GEOMETRY_SHADER: "geometry shader", + gl.GL_TESS_CONTROL_SHADER: "tessellation control shader", + gl.GL_TESS_EVALUATION_SHADER: "tessellation evaluation shader", +} + +#: Lookup table for OpenGL type names +GL_NAMES = { + gl.GL_HALF_FLOAT: "GL_HALF_FLOAT", + gl.GL_FLOAT: "GL_FLOAT", + gl.GL_DOUBLE: "GL_DOUBLE", + gl.GL_INT: "GL_INT", + gl.GL_UNSIGNED_INT: "GL_UNSIGNED_INT", + gl.GL_SHORT: "GL_SHORT", + gl.GL_UNSIGNED_SHORT: "GL_UNSIGNED_SHORT", + gl.GL_BYTE: "GL_BYTE", + gl.GL_UNSIGNED_BYTE: "GL_UNSIGNED_BYTE", +} + + +def gl_name(gl_type: PyGLenum | None) -> str | PyGLenum | None: + """Return the name of a gl type""" + if gl_type is None: + return None + return GL_NAMES.get(gl_type, gl_type) + + +class AttribFormat: + """ + Represents a vertex attribute in a BufferDescription / Program. + This is attribute metadata used when attempting to map vertex + shader inputs. + + Args: + name: + Name of the attribute + gl_type: + The OpenGL type such as GL_FLOAT, GL_HALF_FLOAT etc. + bytes_per_component: + Number of bytes for a single component + offset: + Offset for BufferDescription + location: + Location for program attribute + """ + + __slots__ = ( + "name", + "gl_type", + "components", + "bytes_per_component", + "offset", + "location", + ) + + def __init__( + self, + name: str | None, + gl_type: PyGLenum | None, + components: int, + bytes_per_component: int, + offset=0, + location=0, + ): + self.name = name + """The name of the attribute in the program""" + self.gl_type = gl_type + """The OpenGL type of the attribute""" + self.components = components + """Number of components for this attribute (1, 2, 3 or 4)""" + self.bytes_per_component = bytes_per_component + """How many bytes for a single component""" + self.offset = offset + """Offset of the attribute in the buffer""" + self.location = location + """Location of the attribute in the program""" + + @property + def bytes_total(self) -> int: + """Total number of bytes for this attribute""" + return self.components * self.bytes_per_component + + def __repr__(self): + return ( + f"" + ) + + +class BufferDescription: + """Buffer Object description used with :py:class:`arcade.gl.Geometry`. + + This class provides a Buffer object with a description of its content, allowing the + a :py:class:`~arcade.gl.Geometry` object to correctly map shader attributes + to a program/shader. + + The formats is a string providing the number and type of each attribute. Currently + we only support f (float), i (integer) and B (unsigned byte). + + ``normalized`` enumerates the attributes which must have their values normalized. + This is useful for instance for colors attributes given as unsigned byte and + normalized to floats with values between 0.0 and 1.0. + + ``instanced`` allows this buffer to be used as instanced buffer. Each value will + be used once for the whole geometry. The geometry will be repeated a number of + times equal to the number of items in the Buffer. + + .. code-block:: python + + # Describe my_buffer + # It contains two floating point numbers being a 2d position + # and two floating point numbers being texture coordinates. + # We expect the shader using this buffer to have an in_pos and in_uv attribute (exact name) + BufferDescription( + my_buffer, + '2f 2f', + ['in_pos', 'in_uv'], + ) + + Args: + buffer: The buffer to describe + formats: The format of each attribute + attributes: List of attributes names (strings) + normalized: list of attribute names that should be normalized + instanced: ``True`` if this is per instance data + """ + + # Describe all variants of a format string to simplify parsing (single component) + # format: gl_type, byte_size + _formats: dict[str, tuple[PyGLenum | None, int]] = { + # (gl enum, byte size) + # Floats + "f": (gl.GL_FLOAT, 4), + "f1": (gl.GL_UNSIGNED_BYTE, 1), + "f2": (gl.GL_HALF_FLOAT, 2), + "f4": (gl.GL_FLOAT, 4), + "f8": (gl.GL_DOUBLE, 8), + # Unsigned integers + "u": (gl.GL_UNSIGNED_INT, 4), + "u1": (gl.GL_UNSIGNED_BYTE, 1), + "u2": (gl.GL_UNSIGNED_SHORT, 2), + "u4": (gl.GL_UNSIGNED_INT, 4), + # Signed integers + "i": (gl.GL_INT, 4), + "i1": (gl.GL_BYTE, 1), + "i2": (gl.GL_SHORT, 2), + "i4": (gl.GL_INT, 4), + # Padding (1, 2, 4, 8 bytes) + "x": (None, 1), + "x1": (None, 1), + "x2": (None, 2), + "x4": (None, 4), + "x8": (None, 8), + } + + __slots__ = ( + "buffer", + "attributes", + "normalized", + "instanced", + "formats", + "stride", + "num_vertices", + ) + + def __init__( + self, + buffer: Buffer, + formats: str, + attributes: Sequence[str], + normalized: Iterable[str] | None = None, + instanced: bool = False, + ): + #: The :py:class:`~arcade.gl.Buffer` this description object describes + self.buffer = buffer # type: Buffer + #: List of string attributes + self.attributes = attributes + #: List of normalized attributes + self.normalized: set[str] = set() if normalized is None else set(normalized) + #: Instanced flag (bool) + self.instanced: bool = instanced + #: Formats of each attribute + self.formats: list[AttribFormat] = [] + #: The byte stride of the buffer + self.stride: int = -1 + #: Number of vertices in the buffer + self.num_vertices: int = -1 + + if not isinstance(buffer, Buffer): + raise ValueError("buffer parameter must be an arcade.gl.Buffer") + + if not isinstance(self.attributes, (list, tuple)): + raise ValueError("Attributes must be a list or tuple") + + if self.normalized > set(self.attributes): + raise ValueError("Normalized attribute not found in attributes.") + + formats_list = formats.split(" ") + non_padded_formats = [f for f in formats_list if "x" not in f] + + if len(non_padded_formats) != len(self.attributes): + raise ValueError( + f"Different lengths of formats ({len(non_padded_formats)}) and " + f"attributes ({len(self.attributes)})" + ) + + def zip_attrs(formats: list[str], attributes: Sequence[str]): + """Join together formats and attribute names taking padding into account""" + attr_index = 0 + for f in formats: + if "x" in f: + yield f, None + else: + yield f, attributes[attr_index] + attr_index += 1 + + self.stride = 0 + for attr_fmt, attr_name in zip_attrs(formats_list, self.attributes): + # Automatically make f1 attributes normalized + if attr_name is not None and "f1" in attr_fmt: + self.normalized.add(attr_name) + try: + components_str, data_type_str, data_size_str = re.split(r"([fiux])", attr_fmt) + data_type = f"{data_type_str}{data_size_str}" if data_size_str else data_type_str + components = int(components_str) if components_str else 1 # 1 component is default + data_size = ( + int(data_size_str) if data_size_str else 4 + ) # 4 byte float and integer types are default + # Limit components to 4 for non-padded formats + if components > 4 and data_size is not None: + raise ValueError("Number of components must be 1, 2, 3 or 4") + except Exception as ex: + raise ValueError(f"Could not parse attribute format: '{attr_fmt} : {ex}'") + + gl_type, byte_size = self._formats[data_type] + self.formats.append( + AttribFormat(attr_name, gl_type, components, byte_size, offset=self.stride) + ) + + self.stride += byte_size * components + + if self.buffer.size % self.stride != 0: + raise ValueError( + f"Buffer size must align by {self.stride} bytes. " + f"{self.buffer} size={self.buffer.size}" + ) + + # Estimate number of vertices for this buffer + self.num_vertices = self.buffer.size // self.stride + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other) -> bool: + if not isinstance(other, BufferDescription): + raise ValueError( + f"The only logical comparison to a BufferDescription" + f"is a BufferDescription not {type(other)}" + ) + + # Equal if we share the same attribute + return len(set(self.attributes) & set(other.attributes)) > 0 + + +class TypeInfo: + """ + Describes an opengl type. + + Args: + name: + The string representation of this type + enum: + The enum of this type + gl_type: + The base enum of this type + gl_size: + byte size if the gl_type + components: + Number of components for this enum + """ + + __slots__ = "name", "enum", "gl_type", "gl_size", "components" + + def __init__( + self, name: str, enum: GLenumLike, gl_type: PyGLenum, gl_size: int, components: int + ): + self.name = name + """The string representation of this type""" + self.enum = enum + """The OpenEL enum of this type""" + self.gl_type = gl_type + """The base OpenGL data type""" + self.gl_size = gl_size + """The size of the base OpenGL data type""" + self.components = components + """The number of components (1, 2, 3 or 4)""" + + @property + def size(self) -> int: + """The total size of this type in bytes""" + return self.gl_size * self.components + + def __repr__(self) -> str: + return ( + f"" + ) + + +class GLTypes: + """ + Detailed Information about all attribute type. + + During introspection we often just get integers telling us what type is used. + This can for example be ``35664`` telling us it's a ``GL_FLOAT_VEC2``. + + During introspection we need to know the exact datatype of the attribute. + It's not enough to know it's a float, we need to know if it's a vec2, vec3, vec4 + or any other type that OpenGL supports. + + Examples of types are:: + + GL_FLOAT_VEC2 + GL_DOUBLE_VEC4 + GL_INT_VEC3 + GL_UNSIGNED_INT_VEC2 + GL_UNSIGNED_BYTE + GL_FLOAT + GL_DOUBLE + GL_INT + GL_UNSIGNED_INT + ... + """ + + types = { + # Floats + gl.GL_FLOAT: TypeInfo("GL_FLOAT", gl.GL_FLOAT, gl.GL_FLOAT, 4, 1), + gl.GL_FLOAT_VEC2: TypeInfo("GL_FLOAT_VEC2", gl.GL_FLOAT_VEC2, gl.GL_FLOAT, 4, 2), + gl.GL_FLOAT_VEC3: TypeInfo("GL_FLOAT_VEC3", gl.GL_FLOAT_VEC3, gl.GL_FLOAT, 4, 3), + gl.GL_FLOAT_VEC4: TypeInfo("GL_FLOAT_VEC4", gl.GL_FLOAT_VEC4, gl.GL_FLOAT, 4, 4), + # Doubles + gl.GL_DOUBLE: TypeInfo("GL_DOUBLE", gl.GL_DOUBLE, gl.GL_DOUBLE, 8, 1), + gl.GL_DOUBLE_VEC2: TypeInfo("GL_DOUBLE_VEC2", gl.GL_DOUBLE_VEC2, gl.GL_DOUBLE, 8, 2), + gl.GL_DOUBLE_VEC3: TypeInfo("GL_DOUBLE_VEC3", gl.GL_DOUBLE_VEC3, gl.GL_DOUBLE, 8, 3), + gl.GL_DOUBLE_VEC4: TypeInfo("GL_DOUBLE_VEC4", gl.GL_DOUBLE_VEC4, gl.GL_DOUBLE, 8, 4), + # Booleans (ubyte) + gl.GL_BOOL: TypeInfo("GL_BOOL", gl.GL_BOOL, gl.GL_BOOL, 1, 1), + gl.GL_BOOL_VEC2: TypeInfo("GL_BOOL_VEC2", gl.GL_BOOL_VEC2, gl.GL_BOOL, 1, 2), + gl.GL_BOOL_VEC3: TypeInfo("GL_BOOL_VEC3", gl.GL_BOOL_VEC3, gl.GL_BOOL, 1, 3), + gl.GL_BOOL_VEC4: TypeInfo("GL_BOOL_VEC4", gl.GL_BOOL_VEC4, gl.GL_BOOL, 1, 4), + # Integers + gl.GL_INT: TypeInfo("GL_INT", gl.GL_INT, gl.GL_INT, 4, 1), + gl.GL_INT_VEC2: TypeInfo("GL_INT_VEC2", gl.GL_INT_VEC2, gl.GL_INT, 4, 2), + gl.GL_INT_VEC3: TypeInfo("GL_INT_VEC3", gl.GL_INT_VEC3, gl.GL_INT, 4, 3), + gl.GL_INT_VEC4: TypeInfo("GL_INT_VEC4", gl.GL_INT_VEC4, gl.GL_INT, 4, 4), + # Unsigned Integers + gl.GL_UNSIGNED_INT: TypeInfo( + "GL_UNSIGNED_INT", gl.GL_UNSIGNED_INT, gl.GL_UNSIGNED_INT, 4, 1 + ), + gl.GL_UNSIGNED_INT_VEC2: TypeInfo( + "GL_UNSIGNED_INT_VEC2", gl.GL_UNSIGNED_INT_VEC2, gl.GL_UNSIGNED_INT, 4, 2 + ), + gl.GL_UNSIGNED_INT_VEC3: TypeInfo( + "GL_UNSIGNED_INT_VEC3", gl.GL_UNSIGNED_INT_VEC3, gl.GL_UNSIGNED_INT, 4, 3 + ), + gl.GL_UNSIGNED_INT_VEC4: TypeInfo( + "GL_UNSIGNED_INT_VEC4", gl.GL_UNSIGNED_INT_VEC4, gl.GL_UNSIGNED_INT, 4, 4 + ), + # Unsigned Short (mostly used for short index buffers) + gl.GL_UNSIGNED_SHORT: TypeInfo( + "GL.GL_UNSIGNED_SHORT", gl.GL_UNSIGNED_SHORT, gl.GL_UNSIGNED_SHORT, 2, 2 + ), + # Byte + gl.GL_BYTE: TypeInfo("GL_BYTE", gl.GL_BYTE, gl.GL_BYTE, 1, 1), + gl.GL_UNSIGNED_BYTE: TypeInfo( + "GL_UNSIGNED_BYTE", gl.GL_UNSIGNED_BYTE, gl.GL_UNSIGNED_BYTE, 1, 1 + ), + # Matrices + gl.GL_FLOAT_MAT2: TypeInfo("GL_FLOAT_MAT2", gl.GL_FLOAT_MAT2, gl.GL_FLOAT, 4, 4), + gl.GL_FLOAT_MAT3: TypeInfo("GL_FLOAT_MAT3", gl.GL_FLOAT_MAT3, gl.GL_FLOAT, 4, 9), + gl.GL_FLOAT_MAT4: TypeInfo("GL_FLOAT_MAT4", gl.GL_FLOAT_MAT4, gl.GL_FLOAT, 4, 16), + gl.GL_FLOAT_MAT2x3: TypeInfo("GL_FLOAT_MAT2x3", gl.GL_FLOAT_MAT2x3, gl.GL_FLOAT, 4, 6), + gl.GL_FLOAT_MAT2x4: TypeInfo("GL_FLOAT_MAT2x4", gl.GL_FLOAT_MAT2x4, gl.GL_FLOAT, 4, 8), + gl.GL_FLOAT_MAT3x2: TypeInfo("GL_FLOAT_MAT3x2", gl.GL_FLOAT_MAT3x2, gl.GL_FLOAT, 4, 6), + gl.GL_FLOAT_MAT3x4: TypeInfo("GL_FLOAT_MAT3x4", gl.GL_FLOAT_MAT3x4, gl.GL_FLOAT, 4, 12), + gl.GL_FLOAT_MAT4x2: TypeInfo("GL_FLOAT_MAT4x2", gl.GL_FLOAT_MAT4x2, gl.GL_FLOAT, 4, 8), + gl.GL_FLOAT_MAT4x3: TypeInfo("GL_FLOAT_MAT4x3", gl.GL_FLOAT_MAT4x3, gl.GL_FLOAT, 4, 12), + # Double matrices + gl.GL_DOUBLE_MAT2: TypeInfo("GL_DOUBLE_MAT2", gl.GL_DOUBLE_MAT2, gl.GL_DOUBLE, 8, 4), + gl.GL_DOUBLE_MAT3: TypeInfo("GL_DOUBLE_MAT3", gl.GL_DOUBLE_MAT3, gl.GL_DOUBLE, 8, 9), + gl.GL_DOUBLE_MAT4: TypeInfo("GL_DOUBLE_MAT4", gl.GL_DOUBLE_MAT4, gl.GL_DOUBLE, 8, 16), + gl.GL_DOUBLE_MAT2x3: TypeInfo("GL_DOUBLE_MAT2x3", gl.GL_DOUBLE_MAT2x3, gl.GL_DOUBLE, 8, 6), + gl.GL_DOUBLE_MAT2x4: TypeInfo("GL_DOUBLE_MAT2x4", gl.GL_DOUBLE_MAT2x4, gl.GL_DOUBLE, 8, 8), + gl.GL_DOUBLE_MAT3x2: TypeInfo("GL_DOUBLE_MAT3x2", gl.GL_DOUBLE_MAT3x2, gl.GL_DOUBLE, 8, 6), + gl.GL_DOUBLE_MAT3x4: TypeInfo("GL_DOUBLE_MAT3x4", gl.GL_DOUBLE_MAT3x4, gl.GL_DOUBLE, 8, 12), + gl.GL_DOUBLE_MAT4x2: TypeInfo("GL_DOUBLE_MAT4x2", gl.GL_DOUBLE_MAT4x2, gl.GL_DOUBLE, 8, 8), + gl.GL_DOUBLE_MAT4x3: TypeInfo("GL_DOUBLE_MAT4x3", gl.GL_DOUBLE_MAT4x3, gl.GL_DOUBLE, 8, 12), + # TODO: Add sampler types if needed. Only needed for better uniform introspection. + } + + @classmethod + def get(cls, enum: int) -> TypeInfo: + """Get the TypeInfo for a given""" + try: + return cls.types[enum] + except KeyError: + raise ValueError(f"Unknown gl type {enum}. Someone needs to add it") diff --git a/arcade/gl/backends/gl/uniform.py b/arcade/gl/backends/gl/uniform.py new file mode 100644 index 000000000..f664bc320 --- /dev/null +++ b/arcade/gl/backends/gl/uniform.py @@ -0,0 +1,422 @@ +import struct +from ctypes import POINTER, c_double, c_float, c_int, c_uint, cast +from typing import Callable + +from pyglet import gl + +from arcade.gl.exceptions import ShaderException + + +class Uniform: + """ + A Program uniform + + Args: + ctx: + The context + program_id: + The program id to which this uniform belongs + location: + The uniform location + name: + The uniform name + data_type: + The data type of the uniform + array_length: + The array length of the uniform + """ + + _type_to_struct = { + c_float: "f", + c_int: "i", + c_uint: "I", + c_double: "d", + } + + _uniform_getters = { + gl.GLint: gl.glGetUniformiv, + gl.GLuint: gl.glGetUniformuiv, + gl.GLfloat: gl.glGetUniformfv, + } + + _uniform_setters = { + # uniform type: (gl_type, setter, length, count) + # Integers 32 bit + gl.GL_INT: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_INT_VEC2: (gl.GLint, gl.glProgramUniform2iv, gl.glUniform2iv, 2, 1), + gl.GL_INT_VEC3: (gl.GLint, gl.glProgramUniform3iv, gl.glUniform3iv, 3, 1), + gl.GL_INT_VEC4: (gl.GLint, gl.glProgramUniform4iv, gl.glUniform4iv, 4, 1), + # Unsigned integers 32 bit + gl.GL_UNSIGNED_INT: (gl.GLuint, gl.glProgramUniform1uiv, gl.glUniform1uiv, 1, 1), + gl.GL_UNSIGNED_INT_VEC2: (gl.GLuint, gl.glProgramUniform2uiv, gl.glUniform2uiv, 2, 1), + gl.GL_UNSIGNED_INT_VEC3: (gl.GLuint, gl.glProgramUniform3uiv, gl.glUniform3uiv, 3, 1), + gl.GL_UNSIGNED_INT_VEC4: (gl.GLuint, gl.glProgramUniform4uiv, gl.glUniform4uiv, 4, 1), + # Integers 64 bit unsigned + gl.GL_INT64_ARB: (gl.GLint64, gl.glProgramUniform1i64vARB, gl.glUniform1i64vARB, 1, 1), + gl.GL_INT64_VEC2_ARB: (gl.GLint64, gl.glProgramUniform2i64vARB, gl.glUniform2i64vARB, 2, 1), + gl.GL_INT64_VEC3_ARB: (gl.GLint64, gl.glProgramUniform3i64vARB, gl.glUniform3i64vARB, 3, 1), + gl.GL_INT64_VEC4_ARB: (gl.GLint64, gl.glProgramUniform4i64vARB, gl.glUniform4i64vARB, 4, 1), + # Unsigned integers 64 bit + gl.GL_UNSIGNED_INT64_ARB: ( + gl.GLuint64, + gl.glProgramUniform1ui64vARB, + gl.glUniform1ui64vARB, + 1, + 1, + ), + gl.GL_UNSIGNED_INT64_VEC2_ARB: ( + gl.GLuint64, + gl.glProgramUniform2ui64vARB, + gl.glUniform2ui64vARB, + 2, + 1, + ), + gl.GL_UNSIGNED_INT64_VEC3_ARB: ( + gl.GLuint64, + gl.glProgramUniform3ui64vARB, + gl.glUniform3ui64vARB, + 3, + 1, + ), + gl.GL_UNSIGNED_INT64_VEC4_ARB: ( + gl.GLuint64, + gl.glProgramUniform4ui64vARB, + gl.glUniform4ui64vARB, + 4, + 1, + ), + # Bools + gl.GL_BOOL: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_BOOL_VEC2: (gl.GLint, gl.glProgramUniform2iv, gl.glUniform2iv, 2, 1), + gl.GL_BOOL_VEC3: (gl.GLint, gl.glProgramUniform3iv, gl.glUniform3iv, 3, 1), + gl.GL_BOOL_VEC4: (gl.GLint, gl.glProgramUniform4iv, gl.glUniform4iv, 4, 1), + # Floats 32 bit + gl.GL_FLOAT: (gl.GLfloat, gl.glProgramUniform1fv, gl.glUniform1fv, 1, 1), + gl.GL_FLOAT_VEC2: (gl.GLfloat, gl.glProgramUniform2fv, gl.glUniform2fv, 2, 1), + gl.GL_FLOAT_VEC3: (gl.GLfloat, gl.glProgramUniform3fv, gl.glUniform3fv, 3, 1), + gl.GL_FLOAT_VEC4: (gl.GLfloat, gl.glProgramUniform4fv, gl.glUniform4fv, 4, 1), + # Floats 64 bit + gl.GL_DOUBLE: (gl.GLdouble, gl.glProgramUniform1dv, gl.glUniform1dv, 1, 1), + gl.GL_DOUBLE_VEC2: (gl.GLdouble, gl.glProgramUniform2dv, gl.glUniform2dv, 2, 1), + gl.GL_DOUBLE_VEC3: (gl.GLdouble, gl.glProgramUniform3dv, gl.glUniform3dv, 3, 1), + gl.GL_DOUBLE_VEC4: (gl.GLdouble, gl.glProgramUniform4dv, gl.glUniform4dv, 4, 1), + # 1D Samplers + gl.GL_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_INT_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_UNSIGNED_INT_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_TEXTURE_1D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + # 2D samplers + gl.GL_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_SAMPLER_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_INT_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_UNSIGNED_INT_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_TEXTURE_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + # Array + gl.GL_SAMPLER_2D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY: ( + gl.GLint, + gl.glProgramUniform1iv, + gl.glUniform1iv, + 1, + 1, + ), + # 3D + gl.GL_SAMPLER_3D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + # Cube + gl.GL_SAMPLER_CUBE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_TEXTURE_CUBE_MAP_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + # Matrices + gl.GL_FLOAT_MAT2: (gl.GLfloat, gl.glProgramUniformMatrix2fv, gl.glUniformMatrix2fv, 4, 1), + gl.GL_FLOAT_MAT3: (gl.GLfloat, gl.glProgramUniformMatrix3fv, gl.glUniformMatrix3fv, 9, 1), + gl.GL_FLOAT_MAT4: (gl.GLfloat, gl.glProgramUniformMatrix4fv, gl.glUniformMatrix4fv, 16, 1), + # Image (compute shader) + gl.GL_IMAGE_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_2D_RECT: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_3D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_CUBE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_1D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_2D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_CUBE_MAP_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_2D_MULTISAMPLE_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + gl.GL_IMAGE_BUFFER: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), + # TODO: test/implement these: + # gl.GL_FLOAT_MAT2x3: glUniformMatrix2x3fv, + # gl.GL_FLOAT_MAT2x4: glUniformMatrix2x4fv, + # + # gl.GL_FLOAT_MAT3x2: glUniformMatrix3x2fv, + # gl.GL_FLOAT_MAT3x4: glUniformMatrix3x4fv, + # + # gl.GL_FLOAT_MAT4x2: glUniformMatrix4x2fv, + # gl.GL_FLOAT_MAT4x3: glUniformMatrix4x3fv, + } + + __slots__ = ( + "_program_id", + "_location", + "_name", + "_data_type", + "_array_length", + "_components", + "getter", + "setter", + "_ctx", + ) + + def __init__(self, ctx, program_id, location, name, data_type, array_length): + self._ctx = ctx + self._program_id = program_id + self._location = location + self._name = name + self._data_type = data_type + # Array length of the uniform (1 if no array) + self._array_length = array_length + # Number of components (including per array entry) + self._components = 0 + self.getter: Callable + """The getter function configured for this uniform""" + self.setter: Callable + """The setter function configured for this uniform""" + self._setup_getters_and_setters() + + @property + def location(self) -> int: + """The location of the uniform in the program""" + return self._location + + @property + def name(self) -> str: + """Name of the uniform""" + return self._name + + @property + def array_length(self) -> int: + """Length of the uniform array. If not an array 1 will be returned""" + return self._array_length + + @property + def components(self) -> int: + """ + How many components for the uniform. + + A vec4 will for example have 4 components. + """ + return self._components + + def _setup_getters_and_setters(self): + """Maps the right getter and setter functions for this uniform""" + try: + gl_type, gl_program_setter, gl_setter, length, count = self._uniform_setters[ + self._data_type + ] + self._components = length + except KeyError: + raise ShaderException(f"Unsupported Uniform type: {self._data_type}") + + gl_getter = self._uniform_getters[gl_type] + is_matrix = self._data_type in ( + gl.GL_FLOAT_MAT2, + gl.GL_FLOAT_MAT3, + gl.GL_FLOAT_MAT4, + ) + + # Create persistent mini c_array for getters and setters: + length = length * self._array_length # Increase buffer size to include arrays + c_array = (gl_type * length)() + ptr = cast(c_array, POINTER(gl_type)) + + # Create custom dedicated getters and setters for each uniform: + self.getter = Uniform._create_getter_func( + self._program_id, + self._location, + gl_getter, + c_array, + length, + ) + + self.setter = Uniform._create_setter_func( + self._ctx, + self._program_id, + self._location, + gl_program_setter, + gl_setter, + c_array, + gl_type, + length, + self._array_length, + count, + ptr, + is_matrix, + ) + + @staticmethod + def _create_getter_func(program_id, location, gl_getter, c_array, length): + """Create a function for getting/setting OpenGL data.""" + + def getter_func1(): + """Get single-element OpenGL uniform data.""" + gl_getter(program_id, location, c_array) + return c_array[0] + + def getter_func2(): + """Get list of OpenGL uniform data.""" + gl_getter(program_id, location, c_array) + return tuple(c_array) + + if length == 1: + return getter_func1 + else: + return getter_func2 + + @classmethod + def _create_setter_func( + cls, + ctx, + program_id, + location, + gl_program_setter, + gl_setter, + c_array, + gl_type, + length, + array_length, + count, + ptr, + is_matrix, + ): + """Create setters for OpenGL data.""" + # Matrix uniforms + if is_matrix: + if ctx._ext_separate_shader_objects_enabled: + + def setter_func(value): # type: ignore #conditional function variants must have identical signature + """Set OpenGL matrix uniform data.""" + try: + # FIXME: Configure the struct format on the uniform to support + # other types than float + fmt = cls._type_to_struct[gl_type] + c_array[:] = struct.unpack(f"{length}{fmt}", value) + except Exception: + c_array[:] = value + gl_program_setter(program_id, location, array_length, gl.GL_FALSE, ptr) + + else: + + def setter_func(value): # type: ignore #conditional function variants must have identical signature + """Set OpenGL matrix uniform data.""" + c_array[:] = value + gl.glUseProgram(program_id) + gl_setter(location, array_length, gl.GL_FALSE, ptr) + + # Single value uniforms + elif length == 1 and count == 1: + if ctx._ext_separate_shader_objects_enabled: + + def setter_func(value): # type: ignore #conditional function variants must have identical signature + """Set OpenGL uniform data value.""" + c_array[0] = value + gl_program_setter(program_id, location, array_length, ptr) + + else: + + def setter_func(value): # type: ignore #conditional function variants must have identical signature + """Set OpenGL uniform data value.""" + c_array[0] = value + gl.glUseProgram(program_id) + gl_setter(location, array_length, ptr) + + # Uniforms types with multiple components + elif length > 1 and count == 1: + if ctx._ext_separate_shader_objects_enabled: + + def setter_func(values): # type: ignore #conditional function variants must have identical signature + """Set list of OpenGL uniform data.""" + # Support buffer protocol + try: + # FIXME: Configure the struct format on the uniform to support + # other types than float + fmt = cls._type_to_struct[gl_type] + c_array[:] = struct.unpack(f"{length}{fmt}", values) + except Exception: + c_array[:] = values + + gl_program_setter(program_id, location, array_length, ptr) + + else: + + def setter_func(values): # type: ignore #conditional function variants must have identical signature + """Set list of OpenGL uniform data.""" + c_array[:] = values + gl.glUseProgram(program_id) + gl_setter(location, array_length, ptr) + + else: + raise NotImplementedError("Uniform type not yet supported.") + + return setter_func + + def __repr__(self) -> str: + return f"" + + +class UniformBlock: + """ + Wrapper for a uniform block in shaders. + + Args: + glo: + The OpenGL object handle + index: + The index of the uniform block + size: + The size of the uniform block + name: + The name of the uniform + """ + + __slots__ = ("glo", "index", "size", "name") + + def __init__(self, glo: int, index: int, size: int, name: str): + self.glo = glo + """The OpenGL object handle""" + + self.index = index + """The index of the uniform block""" + + self.size = size + """The size of the uniform block""" + + self.name = name + """The name of the uniform block""" + + @property + def binding(self) -> int: + """Get or set the binding index for this uniform block""" + binding = gl.GLint() + gl.glGetActiveUniformBlockiv(self.glo, self.index, gl.GL_UNIFORM_BLOCK_BINDING, binding) + return binding.value + + @binding.setter + def binding(self, binding: int): + gl.glUniformBlockBinding(self.glo, self.index, binding) + + def getter(self): + """ + The getter function for this uniform block. + + Returns self. + """ + return self + + def setter(self, value: int): + """ + The setter function for this uniform block. + + Args: + value: The binding index to set. + """ + self.binding = value + + def __str__(self) -> str: + return f"" diff --git a/arcade/gl/backends/gl/vertex_array.py b/arcade/gl/backends/gl/vertex_array.py new file mode 100644 index 000000000..5c1fed58d --- /dev/null +++ b/arcade/gl/backends/gl/vertex_array.py @@ -0,0 +1,492 @@ +from __future__ import annotations + +import weakref +from ctypes import byref, c_void_p +from typing import TYPE_CHECKING, Sequence + +from pyglet import gl + +from arcade.gl.vertex_array import VertexArray, Geometry + +from .buffer import Buffer +from .program import Program +from .types import BufferDescription, GLenumLike, GLuintLike, gl_name + +if TYPE_CHECKING: + from arcade.gl import Context + +# Index buffer types based on index element size +index_types = [ + None, # 0 (not supported) + gl.GL_UNSIGNED_BYTE, # 1 ubyte8 + gl.GL_UNSIGNED_SHORT, # 2 ubyte16 + None, # 3 (not supported) + gl.GL_UNSIGNED_INT, # 4 ubyte32 +] + + +class GLVertexArray(VertexArray): + """ + Wrapper for Vertex Array Objects (VAOs). + + This objects should not be instantiated from user code. + Use :py:class:`arcade.gl.Geometry` instead. It will create VAO instances for you + automatically. There is a lot of complex interaction between programs + and vertex arrays that will be done for you automatically. + + Args: + ctx: + The context this object belongs to + program: + The program to use + content: + List of BufferDescriptions + index_buffer: + Index/element buffer + index_element_size: + Byte size of the index buffer datatype. + """ + + __slots__ = ( + "glo", + "_index_element_type", + ) + + def __init__( + self, + ctx: Context, + program: Program, + content: Sequence[BufferDescription], + index_buffer: Buffer | None = None, + index_element_size: int = 4, + ) -> None: + super().__init__(ctx, program, content, index_buffer, index_element_size) + + self.glo = glo = gl.GLuint() + """The OpenGL resource ID""" + + self._index_element_type = index_types[index_element_size] + + self._build(program, content, index_buffer) + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLVertexArray.delete_glo, self.ctx, glo) + + def __repr__(self) -> str: + return f"" + + def __del__(self) -> None: + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and self.glo.value > 0: + self._ctx.objects.append(self) + + def delete(self) -> None: + """ + Destroy the underlying OpenGL resource. + + Don't use this unless you know exactly what you are doing. + """ + GLVertexArray.delete_glo(self._ctx, self.glo) + self.glo.value = 0 + + @staticmethod + def delete_glo(ctx: Context, glo: gl.GLuint) -> None: + """ + Delete the OpenGL resource. + + This is automatically called when this object is garbage collected. + """ + # If we have no context, then we are shutting down, so skip this + if gl.current_context is None: + return + + if glo.value != 0: + gl.glDeleteVertexArrays(1, byref(glo)) + glo.value = 0 + + ctx.stats.decr("vertex_array") + + def _build( + self, program: Program, content: Sequence[BufferDescription], index_buffer: Buffer | None + ) -> None: + """ + Build a vertex array compatible with the program passed in. + + This method will bind the vertex array and set up all the vertex attributes + according to the program's attribute specifications. + + Args: + program: + The program to use + content: + List of BufferDescriptions + index_buffer: + Index/element buffer + """ + gl.glGenVertexArrays(1, byref(self.glo)) + gl.glBindVertexArray(self.glo) + + if index_buffer is not None: + gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, index_buffer.glo) + + # Lookup dict for BufferDescription attrib names + descr_attribs = {attr.name: (descr, attr) for descr in content for attr in descr.formats} + + # Build the vao according to the shader's attribute specifications + for _, prog_attr in enumerate(program.attributes): + # Do we actually have an attribute with this name in buffer descriptions? + if prog_attr.name is not None and prog_attr.name.startswith("gl_"): + continue + try: + buff_descr, attr_descr = descr_attribs[prog_attr.name] + except KeyError: + raise ValueError( + ( + f"Program needs attribute '{prog_attr.name}', but is not present in buffer " + f"description. Buffer descriptions: {content}" + ) + ) + + # Make sure components described in BufferDescription and in the shader match + if prog_attr.components != attr_descr.components: + raise ValueError( + ( + f"Program attribute '{prog_attr.name}' has {prog_attr.components} " + f"components while the buffer description has {attr_descr.components} " + " components. " + ) + ) + + gl.glEnableVertexAttribArray(prog_attr.location) + gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buff_descr.buffer.glo) + + # TODO: Detect normalization + normalized = gl.GL_TRUE if attr_descr.name in buff_descr.normalized else gl.GL_FALSE + + # Map attributes groups + float_types = (gl.GL_FLOAT, gl.GL_HALF_FLOAT) + double_types = (gl.GL_DOUBLE,) + int_types = ( + gl.GL_INT, + gl.GL_UNSIGNED_INT, + gl.GL_SHORT, + gl.GL_UNSIGNED_SHORT, + gl.GL_BYTE, + gl.GL_UNSIGNED_BYTE, + ) + attrib_type = attr_descr.gl_type + # Normalized integers must be mapped as floats + if attrib_type in int_types and buff_descr.normalized: + attrib_type = prog_attr.gl_type + + # Sanity check attribute types between shader and buffer description + if attrib_type != prog_attr.gl_type: + raise ValueError( + ( + f"Program attribute '{prog_attr.name}' has type " + f"{gl_name(prog_attr.gl_type)} " + f"while the buffer description has type {gl_name(attr_descr.gl_type)}. " + ) + ) + + if attrib_type in float_types: + gl.glVertexAttribPointer( + prog_attr.location, # attrib location + attr_descr.components, # 1, 2, 3 or 4 + attr_descr.gl_type, # GL_FLOAT etc + normalized, # normalize + buff_descr.stride, + c_void_p(attr_descr.offset), + ) + elif attrib_type in double_types: + gl.glVertexAttribLPointer( + prog_attr.location, # attrib location + attr_descr.components, # 1, 2, 3 or 4 + attr_descr.gl_type, # GL_DOUBLE etc + buff_descr.stride, + c_void_p(attr_descr.offset), + ) + elif attrib_type in int_types: + gl.glVertexAttribIPointer( + prog_attr.location, # attrib location + attr_descr.components, # 1, 2, 3 or 4 + attr_descr.gl_type, # GL_FLOAT etc + buff_descr.stride, + c_void_p(attr_descr.offset), + ) + else: + raise ValueError(f"Unsupported attribute type: {attr_descr.gl_type}") + + # print(( + # f"gl.glVertexAttribXPointer(\n" + # f" {prog_attr.location}, # attrib location\n" + # f" {attr_descr.components}, # 1, 2, 3 or 4\n" + # f" {attr_descr.gl_type}, # GL_FLOAT etc\n" + # f" {normalized}, # normalize\n" + # f" {buff_descr.stride},\n" + # f" c_void_p({attr_descr.offset}),\n" + # )) + # TODO: Sanity check this + if buff_descr.instanced: + gl.glVertexAttribDivisor(prog_attr.location, 1) + + def render( + self, mode: GLenumLike, first: int = 0, vertices: int = 0, instances: int = 1 + ) -> None: + """ + Render the VertexArray to the currently active framebuffer. + + Args: + mode: + Primitive type to render. TRIANGLES, LINES etc. + first: + The first vertex to render from + vertices: + Number of vertices to render + instances: + OpenGL instance, used in using vertices over and over + """ + gl.glBindVertexArray(self.glo) + if self._ibo is not None: + # HACK: re-bind index buffer just in case. + # pyglet rendering was somehow replacing the index buffer. + gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._ibo.glo) + gl.glDrawElementsInstanced( + mode, + vertices, + self._index_element_type, + first * self._index_element_size, + instances, + ) + else: + gl.glDrawArraysInstanced(mode, first, vertices, instances) + + def render_indirect(self, buffer: Buffer, mode: GLuintLike, count, first, stride) -> None: + """ + Render the VertexArray to the framebuffer using indirect rendering. + + .. Warning:: This requires OpenGL 4.3 + + Args: + buffer: + The buffer containing one or multiple draw parameters + mode: + Primitive type to render. TRIANGLES, LINES etc. + count: + The number if indirect draw calls to run + first: + The first indirect draw call to start on + stride: + The byte stride of the draw command buffer. + Keep the default (0) if the buffer is tightly packed. + """ + # The default buffer stride for array and indexed + _stride = 20 if self._ibo is not None else 16 + stride = stride or _stride + if stride % 4 != 0 or stride < 0: + raise ValueError(f"stride must be positive integer in multiples of 4, not {stride}.") + + # The maximum number of draw calls in the buffer + max_commands = buffer.size // stride + if count < 0: + count = max_commands + elif (first + count) > max_commands: + raise ValueError( + "Attempt to issue rendering commands outside the buffer. " + f"first = {first}, count = {count} is reaching past " + f"the buffer end. The buffer have room for {max_commands} " + f"draw commands. byte size {buffer.size}, stride {stride}." + ) + + gl.glBindVertexArray(self.glo) + gl.glBindBuffer(gl.GL_DRAW_INDIRECT_BUFFER, buffer._glo) + if self._ibo: + gl.glMultiDrawElementsIndirect( + mode, self._index_element_type, first * stride, count, stride + ) + else: + gl.glMultiDrawArraysIndirect(mode, first * stride, count, stride) + + def transform_interleaved( + self, + buffer: Buffer, + mode: GLenumLike, + output_mode: GLenumLike, + first: int = 0, + vertices: int = 0, + instances: int = 1, + buffer_offset=0, + ) -> None: + """ + Run a transform feedback. + + Args: + buffer: + The buffer to write the output + mode: + The input primitive mode + output_mode: + The output primitive mode + first: + Offset start vertex + vertices: + Number of vertices to render + instances: + Number of instances to render + buffer_offset: + Byte offset for the buffer (target) + """ + if vertices < 0: + raise ValueError(f"Cannot determine the number of vertices: {vertices}") + + if buffer_offset >= buffer.size: + raise ValueError("buffer_offset at end or past the buffer size") + + gl.glBindVertexArray(self.glo) + gl.glEnable(gl.GL_RASTERIZER_DISCARD) + + if buffer_offset > 0: + gl.glBindBufferRange( + gl.GL_TRANSFORM_FEEDBACK_BUFFER, + 0, + buffer.glo, + buffer_offset, + buffer.size - buffer_offset, + ) + else: + gl.glBindBufferBase(gl.GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer.glo) + + gl.glBeginTransformFeedback(output_mode) + + if self._ibo is not None: + count = self._ibo.size // 4 + # TODO: Support first argument by offsetting pointer (second last arg) + gl.glDrawElementsInstanced(mode, vertices or count, gl.GL_UNSIGNED_INT, None, instances) + else: + # print(f"glDrawArraysInstanced({mode}, {first}, {vertices}, {instances})") + gl.glDrawArraysInstanced(mode, first, vertices, instances) + + gl.glEndTransformFeedback() + gl.glDisable(gl.GL_RASTERIZER_DISCARD) + + def transform_separate( + self, + buffers: list[Buffer], + mode: GLenumLike, + output_mode: GLenumLike, + first: int = 0, + vertices: int = 0, + instances: int = 1, + buffer_offset=0, + ) -> None: + """ + Run a transform feedback writing to separate buffers. + + Args: + buffers: + The buffers to write the output + mode: + The input primitive mode + output_mode: + The output primitive mode + first: + Offset start vertex + vertices: + Number of vertices to render + instances: + Number of instances to render + buffer_offset: + Byte offset for the buffer (target) + """ + if vertices < 0: + raise ValueError(f"Cannot determine the number of vertices: {vertices}") + + # Get size from the smallest buffer + size = min(buf.size for buf in buffers) + if buffer_offset >= size: + raise ValueError("buffer_offset at end or past the buffer size") + + gl.glBindVertexArray(self.glo) + gl.glEnable(gl.GL_RASTERIZER_DISCARD) + + if buffer_offset > 0: + for index, buffer in enumerate(buffers): + gl.glBindBufferRange( + gl.GL_TRANSFORM_FEEDBACK_BUFFER, + index, + buffer.glo, + buffer_offset, + buffer.size - buffer_offset, + ) + else: + for index, buffer in enumerate(buffers): + gl.glBindBufferBase(gl.GL_TRANSFORM_FEEDBACK_BUFFER, index, buffer.glo) + + gl.glBeginTransformFeedback(output_mode) + + if self._ibo is not None: + count = self._ibo.size // 4 + # TODO: Support first argument by offsetting pointer (second last arg) + gl.glDrawElementsInstanced(mode, vertices or count, gl.GL_UNSIGNED_INT, None, instances) + else: + # print(f"glDrawArraysInstanced({mode}, {first}, {vertices}, {instances})") + gl.glDrawArraysInstanced(mode, first, vertices, instances) + + gl.glEndTransformFeedback() + gl.glDisable(gl.GL_RASTERIZER_DISCARD) + + +class GLGeometry(Geometry): + """A higher level abstraction of the VertexArray. + + It generates VertexArray instances on the fly internally matching the incoming + program. This means we can render the same geometry with different programs + as long as the :py:class:`~arcade.gl.Program` and :py:class:`~arcade.gl.BufferDescription` + have compatible attributes. This is an extremely powerful concept that allows + for very flexible rendering pipelines and saves the user from a lot of manual + bookkeeping. + + Geometry objects should be created through :py:meth:`arcade.gl.Context.geometry` + + Args: + ctx: + The context this object belongs to + content: + List of BufferDescriptions + index_buffer: + Index/element buffer + mode: + The default draw mode + index_element_size: + Byte size of the index buffer datatype. + Can be 1, 2 or 4 (8, 16 or 32bit integer) + """ + def __init__( + self, + ctx: "Context", + content: Sequence[BufferDescription] | None, + index_buffer: Buffer | None = None, + mode: int | None = None, + index_element_size: int = 4, + ) -> None: + super().__init__(ctx, content, index_buffer, mode, index_element_size) + + + def _generate_vao(self, program: Program) -> VertexArray: + """ + Create a new VertexArray for the given program. + + Args: + program: The program to use + """ + # print(f"Generating vao for key {program.attribute_key}") + + vao = GLVertexArray( + self._ctx, + program, + self._content, + index_buffer=self._index_buffer, + index_element_size=self._index_element_size, + ) + self._vao_cache[program.attribute_key] = vao + return vao diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index a0a514c80..214f9bf7c 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -43,9 +43,6 @@ class Buffer(ABC): def __init__( self, ctx: Context, - data: BufferProtocol | None = None, - reserve: int = 0, - usage: str = "static", ): self._ctx = ctx self._size = -1 diff --git a/arcade/gl/context.py b/arcade/gl/context.py index e00a7555f..4766b4e24 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -950,7 +950,7 @@ def buffer( usage: Buffer usage. 'static', 'dynamic' or 'stream' """ - pass + raise NotImplementedError("The enabled graphics backend does not support this method.") def framebuffer( self, @@ -1133,13 +1133,14 @@ def sampler(self, texture: Texture2D) -> Sampler: """ return Sampler(self, texture) + @abstractmethod def geometry( self, content: Sequence[BufferDescription] | None = None, index_buffer: Buffer | None = None, mode: int | None = None, index_element_size: int = 4, - ): + ) -> Geometry: """ Create a Geometry instance. This is Arcade's version of a vertex array adding a lot of convenience for the user. Geometry objects are fairly light. They are @@ -1216,13 +1217,7 @@ def geometry( In other words, the index buffer can be 1, 2 or 4 byte integers. Can be 1, 2 or 4 (8, 16 or 32 bit unsigned integer) """ - return Geometry( - self, - content, - index_buffer=index_buffer, - mode=mode, - index_element_size=index_element_size, - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") def program( self, @@ -1269,47 +1264,7 @@ def program( Based on these settings the ``transform()`` method will accept a single buffer or a list of buffer. """ - source_vs = ShaderSource(self, vertex_shader, common, gl.GL_VERTEX_SHADER) - source_fs = ( - ShaderSource(self, fragment_shader, common, gl.GL_FRAGMENT_SHADER) - if fragment_shader - else None - ) - source_geo = ( - ShaderSource(self, geometry_shader, common, gl.GL_GEOMETRY_SHADER) - if geometry_shader - else None - ) - source_tc = ( - ShaderSource(self, tess_control_shader, common, gl.GL_TESS_CONTROL_SHADER) - if tess_control_shader - else None - ) - source_te = ( - ShaderSource(self, tess_evaluation_shader, common, gl.GL_TESS_EVALUATION_SHADER) - if tess_evaluation_shader - else None - ) - - # If we don't have a fragment shader we are doing transform feedback. - # When a geometry shader is present the out attributes will be located there - out_attributes = list(varyings) if varyings is not None else [] # type: List[str] - if not source_fs and not out_attributes: - if source_geo: - out_attributes = source_geo.out_attributes - else: - out_attributes = source_vs.out_attributes - - return Program( - self, - vertex_shader=source_vs.get_source(defines=defines), - fragment_shader=source_fs.get_source(defines=defines) if source_fs else None, - geometry_shader=source_geo.get_source(defines=defines) if source_geo else None, - tess_control_shader=source_tc.get_source(defines=defines) if source_tc else None, - tess_evaluation_shader=source_te.get_source(defines=defines) if source_te else None, - varyings=out_attributes, - varyings_capture_mode=varyings_capture_mode, - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") def query(self, *, samples=True, time=True, primitives=True) -> Query: """ diff --git a/arcade/gl/program.py b/arcade/gl/program.py index b75a91a79..2976af8d5 100644 --- a/arcade/gl/program.py +++ b/arcade/gl/program.py @@ -1,29 +1,14 @@ -import typing -import weakref -from ctypes import ( - POINTER, - byref, - c_buffer, - c_char, - c_char_p, - c_int, - cast, - create_string_buffer, - pointer, -) -from typing import TYPE_CHECKING, Any, Iterable +from __future__ import annotations -from pyglet import gl +from typing import TYPE_CHECKING, Any, Iterable -from .exceptions import ShaderException -from .types import SHADER_TYPE_NAMES, AttribFormat, GLTypes, PyGLenum -from .uniform import Uniform, UniformBlock +from abc import ABC, abstractmethod if TYPE_CHECKING: from arcade.gl import Context -class Program: +class Program(ABC): """ Compiled and linked shader program. @@ -65,110 +50,21 @@ class Program: __slots__ = ( "_ctx", - "_glo", - "_uniforms", - "_varyings", "_varyings_capture_mode", - "_geometry_info", - "_attributes", "attribute_key", "__weakref__", ) - _valid_capture_modes = ("interleaved", "separate") - def __init__( self, - ctx: "Context", + ctx: Context, *, - vertex_shader: str, - fragment_shader: str | None = None, - geometry_shader: str | None = None, - tess_control_shader: str | None = None, - tess_evaluation_shader: str | None = None, - varyings: list[str] | None = None, varyings_capture_mode: str = "interleaved", ): self._ctx = ctx - self._glo = glo = gl.glCreateProgram() - self._varyings = varyings or [] - self._varyings_capture_mode = varyings_capture_mode.strip().lower() - self._geometry_info = (0, 0, 0) - self._attributes = [] # type: list[AttribFormat] - #: Internal cache key used with vertex arrays self.attribute_key = "INVALID" # type: str - self._uniforms: dict[str, Uniform | UniformBlock] = {} - - if self._varyings_capture_mode not in self._valid_capture_modes: - raise ValueError( - f"Invalid capture mode '{self._varyings_capture_mode}'. " - f"Valid modes are: {self._valid_capture_modes}." - ) - - shaders: list[tuple[str, int]] = [(vertex_shader, gl.GL_VERTEX_SHADER)] - if fragment_shader: - shaders.append((fragment_shader, gl.GL_FRAGMENT_SHADER)) - if geometry_shader: - shaders.append((geometry_shader, gl.GL_GEOMETRY_SHADER)) - if tess_control_shader: - shaders.append((tess_control_shader, gl.GL_TESS_CONTROL_SHADER)) - if tess_evaluation_shader: - shaders.append((tess_evaluation_shader, gl.GL_TESS_EVALUATION_SHADER)) - - # Inject a dummy fragment shader on gles when doing transforms - if self._ctx.gl_api == "gles" and not fragment_shader: - dummy_frag_src = """ - #version 310 es - precision mediump float; - out vec4 fragColor; - void main() { fragColor = vec4(1.0); } - """ - shaders.append((dummy_frag_src, gl.GL_FRAGMENT_SHADER)) - - shaders_id = [] - for shader_code, shader_type in shaders: - shader = Program.compile_shader(shader_code, shader_type) - gl.glAttachShader(self._glo, shader) - shaders_id.append(shader) - - # For now we assume varyings can be set up if no fragment shader - if not fragment_shader: - self._configure_varyings() - - Program.link(self._glo) - - if geometry_shader: - geometry_in = gl.GLint() - geometry_out = gl.GLint() - geometry_vertices = gl.GLint() - gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_INPUT_TYPE, geometry_in) - gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_OUTPUT_TYPE, geometry_out) - gl.glGetProgramiv(self._glo, gl.GL_GEOMETRY_VERTICES_OUT, geometry_vertices) - self._geometry_info = ( - geometry_in.value, - geometry_out.value, - geometry_vertices.value, - ) - - # Delete shaders (not needed after linking) - for shader in shaders_id: - gl.glDeleteShader(shader) - gl.glDetachShader(self._glo, shader) - - # Handle uniforms - self._introspect_attributes() - self._introspect_uniforms() - self._introspect_uniform_blocks() - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, Program.delete_glo, self._ctx, glo) - - self.ctx.stats.incr("program") - - def __del__(self): - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and self._glo > 0: - self._ctx.objects.append(self) + self._varyings_capture_mode = varyings_capture_mode.strip().lower() + self._ctx.stats.incr("program") @property def ctx(self) -> "Context": @@ -176,30 +72,29 @@ def ctx(self) -> "Context": return self._ctx @property - def glo(self) -> int: - """The OpenGL resource id for this program.""" - return self._glo - - @property - def attributes(self) -> Iterable[AttribFormat]: + @abstractmethod + def attributes(self) -> Iterable: # TODO: Typing on this Iterable, need generic type for AttribFormat? """List of attribute information.""" - return self._attributes + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def varyings(self) -> list[str]: """Out attributes names used in transform feedback.""" - return self._varyings + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def out_attributes(self) -> list[str]: """ Out attributes names used in transform feedback. Alias for `varyings`. """ - return self._varyings + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def varyings_capture_mode(self) -> str: """ Get the capture more for transform feedback (single, multiple). @@ -207,9 +102,10 @@ def varyings_capture_mode(self) -> str: This is a read only property since capture mode can only be set before the program is linked. """ - return self._varyings_capture_mode + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def geometry_input(self) -> int: """ The geometry shader's input primitive type. @@ -217,63 +113,42 @@ def geometry_input(self) -> int: This an be compared with ``GL_TRIANGLES``, ``GL_POINTS`` etc. and is queried when the program is created. """ - return self._geometry_info[0] + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def geometry_output(self) -> int: """The geometry shader's output primitive type. This an be compared with ``GL_TRIANGLES``, ``GL_POINTS`` etc. and is queried when the program is created. """ - return self._geometry_info[1] + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def geometry_vertices(self) -> int: """ The maximum number of vertices that can be emitted. This is queried when the program is created. """ - return self._geometry_info[2] + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def delete(self): """ Destroy the underlying OpenGL resource. Don't use this unless you know exactly what you are doing. """ - Program.delete_glo(self._ctx, self._glo) - self._glo = 0 - - @staticmethod - def delete_glo(ctx, prog_id): - """ - Deletes a program. This is normally called automatically when the - program is garbage collected. + raise NotImplementedError("The enabled graphics backend does not support this method.") - Args: - ctx: - The context this program belongs to - prog_id: - The OpenGL resource id - """ - # Check to see if the context was already cleaned up from program - # shut down. If so, we don't need to delete the shaders. - if gl.current_context is None: - return - - gl.glDeleteProgram(prog_id) - ctx.stats.decr("program") - - def __getitem__(self, item) -> Uniform | UniformBlock: + @abstractmethod + def __getitem__(self, item): # TODO: typing, this should return Uniform | UniformBlock, but need generic options for those: """Get a uniform or uniform block""" - try: - uniform = self._uniforms[item] - except KeyError: - raise KeyError(f"Uniform with the name `{item}` was not found.") - - return uniform.getter() + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def __setitem__(self, key, value): """ Set a uniform value. @@ -289,13 +164,9 @@ def __setitem__(self, key, value): value: The uniform value """ - try: - uniform = self._uniforms[key] - except KeyError: - raise KeyError(f"Uniform with the name `{key}` was not found.") - - uniform.setter(value) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def set_uniform_safe(self, name: str, value: Any): """ Safely set a uniform catching KeyError. @@ -306,11 +177,9 @@ def set_uniform_safe(self, name: str, value: Any): value: The uniform value """ - try: - self[name] = value - except KeyError: - pass + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def set_uniform_array_safe(self, name: str, value: list[Any]): """ Safely set a uniform array. @@ -326,226 +195,13 @@ def set_uniform_array_safe(self, name: str, value: list[Any]): value: List of values """ - if name not in self._uniforms: - return - - uniform = typing.cast(Uniform, self._uniforms[name]) - _len = uniform._array_length * uniform._components - if _len == 1: - self.set_uniform_safe(name, value[0]) - else: - self.set_uniform_safe(name, value[:_len]) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def use(self): """ Activates the shader. This is normally done for you automatically. """ - # IMPORTANT: This is the only place glUseProgram should be called - # so we can track active program. - # if self._ctx.active_program != self: - gl.glUseProgram(self._glo) - # self._ctx.active_program = self - - def _configure_varyings(self): - """Set up transform feedback varyings""" - if not self._varyings: - return - - # Covert names to char** - c_array = (c_char_p * len(self._varyings))() - for i, name in enumerate(self._varyings): - c_array[i] = name.encode() - - ptr = cast(c_array, POINTER(POINTER(c_char))) - - # Are we capturing in interlaved or separate buffers? - mode = ( - gl.GL_INTERLEAVED_ATTRIBS - if self._varyings_capture_mode == "interleaved" - else gl.GL_SEPARATE_ATTRIBS - ) - - gl.glTransformFeedbackVaryings( - self._glo, # program - len(self._varyings), # number of varying variables used for transform feedback - ptr, # zero-terminated strings specifying the names of the varying variables - mode, - ) - - def _introspect_attributes(self): - """Introspect and store detailed info about an attribute""" - # TODO: Ensure gl_* attributes are ignored - num_attrs = gl.GLint() - gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_ATTRIBUTES, num_attrs) - num_varyings = gl.GLint() - gl.glGetProgramiv(self._glo, gl.GL_TRANSFORM_FEEDBACK_VARYINGS, num_varyings) - # print(f"attrs {num_attrs.value} varyings={num_varyings.value}") - - for i in range(num_attrs.value): - c_name = create_string_buffer(256) - c_size = gl.GLint() - c_type = gl.GLenum() - gl.glGetActiveAttrib( - self._glo, # program to query - i, # index (not the same as location) - 256, # max attr name size - None, # c_length, # length of name - c_size, # size of attribute (array or not) - c_type, # attribute type (enum) - c_name, # name buffer - ) - - # Get the actual location. Do not trust the original order - location = gl.glGetAttribLocation(self._glo, c_name) - - # print(c_name.value, c_size, c_type) - type_info = GLTypes.get(c_type.value) - # print(type_info) - self._attributes.append( - AttribFormat( - c_name.value.decode(), - type_info.gl_type, - type_info.components, - type_info.gl_size, - location=location, - ) - ) - - # The attribute key is used to cache VertexArrays - self.attribute_key = ":".join( - f"{attr.name}[{attr.gl_type}/{attr.components}]" for attr in self._attributes - ) - - def _introspect_uniforms(self): - """Figure out what uniforms are available and build an internal map""" - # Number of active uniforms in the program - active_uniforms = gl.GLint(0) - gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORMS, byref(active_uniforms)) - - # Loop all the active uniforms - for index in range(active_uniforms.value): - # Query uniform information like name, type, size etc. - u_name, u_type, u_size = self._query_uniform(index) - u_location = gl.glGetUniformLocation(self._glo, u_name.encode()) - - # Skip uniforms that may be in Uniform Blocks - # TODO: We should handle all uniforms - if u_location == -1: - # print(f"Uniform {u_location} {u_name} {u_size} {u_type} skipped") - continue - - u_name = u_name.replace("[0]", "") # Remove array suffix - self._uniforms[u_name] = Uniform( - self._ctx, self._glo, u_location, u_name, u_type, u_size - ) - - def _introspect_uniform_blocks(self): - active_uniform_blocks = gl.GLint(0) - gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORM_BLOCKS, byref(active_uniform_blocks)) - # print('GL_ACTIVE_UNIFORM_BLOCKS', active_uniform_blocks) - - for loc in range(active_uniform_blocks.value): - index, size, name = self._query_uniform_block(loc) - block = UniformBlock(self._glo, index, size, name) - self._uniforms[name] = block - - def _query_uniform(self, location: int) -> tuple[str, int, int]: - """Retrieve Uniform information at given location. - - Returns the name, the type as a GLenum (GL_FLOAT, ...) and the size. Size is - greater than 1 only for Uniform arrays, like an array of floats or an array - of Matrices. - """ - u_size = gl.GLint() - u_type = gl.GLenum() - buf_size = 192 # max uniform character length - u_name = create_string_buffer(buf_size) - gl.glGetActiveUniform( - self._glo, # program to query - location, # location to query - buf_size, # size of the character/name buffer - None, # the number of characters actually written by OpenGL in the string - u_size, # size of the uniform variable - u_type, # data type of the uniform variable - u_name, # string buffer for storing the name - ) - return u_name.value.decode(), u_type.value, u_size.value - - def _query_uniform_block(self, location: int) -> tuple[int, int, str]: - """Query active uniform block by retrieving the name and index and size""" - # Query name - u_size = gl.GLint() - buf_size = 192 # max uniform character length - u_name = create_string_buffer(buf_size) - gl.glGetActiveUniformBlockName( - self._glo, # program to query - location, # location to query - 256, # max size if the name - u_size, # length - u_name, - ) - # Query index - index = gl.glGetUniformBlockIndex(self._glo, u_name) - # Query size - b_size = gl.GLint() - gl.glGetActiveUniformBlockiv(self._glo, index, gl.GL_UNIFORM_BLOCK_DATA_SIZE, b_size) - return index, b_size.value, u_name.value.decode() - - @staticmethod - def compile_shader(source: str, shader_type: PyGLenum) -> gl.GLuint: - """ - Compile the shader code of the given type. - - Args: - source: - The shader source code - shader_type: - The type of shader to compile. - ``GL_VERTEX_SHADER``, ``GL_FRAGMENT_SHADER`` etc. - - Returns: - The created shader id - """ - shader = gl.glCreateShader(shader_type) - source_bytes = source.encode("utf-8") - # Turn the source code string into an array of c_char_p arrays. - strings = byref(cast(c_char_p(source_bytes), POINTER(c_char))) - # Make an array with the strings lengths - lengths = pointer(c_int(len(source_bytes))) - gl.glShaderSource(shader, 1, strings, lengths) - gl.glCompileShader(shader) - result = c_int() - gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS, byref(result)) - if result.value == gl.GL_FALSE: - msg = create_string_buffer(512) - length = c_int() - gl.glGetShaderInfoLog(shader, 512, byref(length), msg) - raise ShaderException( - ( - f"Error compiling {SHADER_TYPE_NAMES[shader_type]} " - f"({result.value}): {msg.value.decode('utf-8')}\n" - f"---- [{SHADER_TYPE_NAMES[shader_type]}] ---\n" - ) - + "\n".join( - f"{str(i + 1).zfill(3)}: {line} " for i, line in enumerate(source.split("\n")) - ) - ) - return shader - - @staticmethod - def link(glo): - """Link a shader program""" - gl.glLinkProgram(glo) - status = c_int() - gl.glGetProgramiv(glo, gl.GL_LINK_STATUS, status) - if not status.value: - length = c_int() - gl.glGetProgramiv(glo, gl.GL_INFO_LOG_LENGTH, length) - log = c_buffer(length.value) - gl.glGetProgramInfoLog(glo, len(log), None, log) - raise ShaderException("Program link error: {}".format(log.value.decode())) - - def __repr__(self): - return "".format(self._glo) + raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/provider.py b/arcade/gl/provider.py index e47cd44d6..4143c43ef 100644 --- a/arcade/gl/provider.py +++ b/arcade/gl/provider.py @@ -16,8 +16,9 @@ def set_provider(provider_name: str): try: module = importlib.import_module(f"arcade.gl.backends.{provider_name}.provider") _current_provider = module.Provider() - except ImportError: - raise ImportError(f"GL Backend Provider '{provider_name}' not found") + except ImportError as e: + print(e) + raise ImportError(f"arcade.gl Backend Provider '{provider_name}' not found") def get_context(*args, **kwargs) -> Context: diff --git a/arcade/gl/vertex_array.py b/arcade/gl/vertex_array.py index 80ed9eda9..e3190e3fc 100644 --- a/arcade/gl/vertex_array.py +++ b/arcade/gl/vertex_array.py @@ -1,29 +1,17 @@ from __future__ import annotations import weakref -from ctypes import byref, c_void_p from typing import TYPE_CHECKING, Sequence -from pyglet import gl +from abc import ABC, abstractmethod from .buffer import Buffer from .program import Program -from .types import BufferDescription, GLenumLike, GLuintLike, gl_name if TYPE_CHECKING: from arcade.gl import Context -# Index buffer types based on index element size -index_types = [ - None, # 0 (not supported) - gl.GL_UNSIGNED_BYTE, # 1 ubyte8 - gl.GL_UNSIGNED_SHORT, # 2 ubyte16 - None, # 3 (not supported) - gl.GL_UNSIGNED_INT, # 4 ubyte32 -] - - -class VertexArray: +class VertexArray(ABC): """ Wrapper for Vertex Array Objects (VAOs). @@ -47,12 +35,10 @@ class VertexArray: __slots__ = ( "_ctx", - "glo", "_program", "_content", "_ibo", "_index_element_size", - "_index_element_type", "_num_vertices", "__weakref__", ) @@ -61,7 +47,7 @@ def __init__( self, ctx: Context, program: Program, - content: Sequence[BufferDescription], + content: Sequence, # TODO: typing, this should be Sequence[BufferDescription] need generic BufferDescription though index_buffer: Buffer | None = None, index_element_size: int = 4, ) -> None: @@ -69,29 +55,12 @@ def __init__( self._program = program self._content = content - self.glo = glo = gl.GLuint() - """The OpenGL resource ID""" - self._num_vertices = -1 self._ibo = index_buffer self._index_element_size = index_element_size - self._index_element_type = index_types[index_element_size] - - self._build(program, content, index_buffer) - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, VertexArray.delete_glo, self.ctx, glo) self.ctx.stats.incr("vertex_array") - def __repr__(self) -> str: - return f"" - - def __del__(self) -> None: - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and self.glo.value > 0: - self._ctx.objects.append(self) - @property def ctx(self) -> Context: """The Context this object belongs to.""" @@ -112,158 +81,18 @@ def num_vertices(self) -> int: """The number of vertices.""" return self._num_vertices + @abstractmethod def delete(self) -> None: """ Destroy the underlying OpenGL resource. Don't use this unless you know exactly what you are doing. """ - VertexArray.delete_glo(self._ctx, self.glo) - self.glo.value = 0 - - @staticmethod - def delete_glo(ctx: Context, glo: gl.GLuint) -> None: - """ - Delete the OpenGL resource. - - This is automatically called when this object is garbage collected. - """ - # If we have no context, then we are shutting down, so skip this - if gl.current_context is None: - return - - if glo.value != 0: - gl.glDeleteVertexArrays(1, byref(glo)) - glo.value = 0 - - ctx.stats.decr("vertex_array") - - def _build( - self, program: Program, content: Sequence[BufferDescription], index_buffer: Buffer | None - ) -> None: - """ - Build a vertex array compatible with the program passed in. - - This method will bind the vertex array and set up all the vertex attributes - according to the program's attribute specifications. - - Args: - program: - The program to use - content: - List of BufferDescriptions - index_buffer: - Index/element buffer - """ - gl.glGenVertexArrays(1, byref(self.glo)) - gl.glBindVertexArray(self.glo) - - if index_buffer is not None: - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, index_buffer.glo) - - # Lookup dict for BufferDescription attrib names - descr_attribs = {attr.name: (descr, attr) for descr in content for attr in descr.formats} - - # Build the vao according to the shader's attribute specifications - for _, prog_attr in enumerate(program.attributes): - # Do we actually have an attribute with this name in buffer descriptions? - if prog_attr.name is not None and prog_attr.name.startswith("gl_"): - continue - try: - buff_descr, attr_descr = descr_attribs[prog_attr.name] - except KeyError: - raise ValueError( - ( - f"Program needs attribute '{prog_attr.name}', but is not present in buffer " - f"description. Buffer descriptions: {content}" - ) - ) - - # Make sure components described in BufferDescription and in the shader match - if prog_attr.components != attr_descr.components: - raise ValueError( - ( - f"Program attribute '{prog_attr.name}' has {prog_attr.components} " - f"components while the buffer description has {attr_descr.components} " - " components. " - ) - ) - - gl.glEnableVertexAttribArray(prog_attr.location) - gl.glBindBuffer(gl.GL_ARRAY_BUFFER, buff_descr.buffer.glo) - - # TODO: Detect normalization - normalized = gl.GL_TRUE if attr_descr.name in buff_descr.normalized else gl.GL_FALSE - - # Map attributes groups - float_types = (gl.GL_FLOAT, gl.GL_HALF_FLOAT) - double_types = (gl.GL_DOUBLE,) - int_types = ( - gl.GL_INT, - gl.GL_UNSIGNED_INT, - gl.GL_SHORT, - gl.GL_UNSIGNED_SHORT, - gl.GL_BYTE, - gl.GL_UNSIGNED_BYTE, - ) - attrib_type = attr_descr.gl_type - # Normalized integers must be mapped as floats - if attrib_type in int_types and buff_descr.normalized: - attrib_type = prog_attr.gl_type - - # Sanity check attribute types between shader and buffer description - if attrib_type != prog_attr.gl_type: - raise ValueError( - ( - f"Program attribute '{prog_attr.name}' has type " - f"{gl_name(prog_attr.gl_type)} " - f"while the buffer description has type {gl_name(attr_descr.gl_type)}. " - ) - ) - - if attrib_type in float_types: - gl.glVertexAttribPointer( - prog_attr.location, # attrib location - attr_descr.components, # 1, 2, 3 or 4 - attr_descr.gl_type, # GL_FLOAT etc - normalized, # normalize - buff_descr.stride, - c_void_p(attr_descr.offset), - ) - elif attrib_type in double_types: - gl.glVertexAttribLPointer( - prog_attr.location, # attrib location - attr_descr.components, # 1, 2, 3 or 4 - attr_descr.gl_type, # GL_DOUBLE etc - buff_descr.stride, - c_void_p(attr_descr.offset), - ) - elif attrib_type in int_types: - gl.glVertexAttribIPointer( - prog_attr.location, # attrib location - attr_descr.components, # 1, 2, 3 or 4 - attr_descr.gl_type, # GL_FLOAT etc - buff_descr.stride, - c_void_p(attr_descr.offset), - ) - else: - raise ValueError(f"Unsupported attribute type: {attr_descr.gl_type}") - - # print(( - # f"gl.glVertexAttribXPointer(\n" - # f" {prog_attr.location}, # attrib location\n" - # f" {attr_descr.components}, # 1, 2, 3 or 4\n" - # f" {attr_descr.gl_type}, # GL_FLOAT etc\n" - # f" {normalized}, # normalize\n" - # f" {buff_descr.stride},\n" - # f" c_void_p({attr_descr.offset}),\n" - # )) - # TODO: Sanity check this - if buff_descr.instanced: - gl.glVertexAttribDivisor(prog_attr.location, 1) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def render( - self, mode: GLenumLike, first: int = 0, vertices: int = 0, instances: int = 1 + self, mode: int, first: int = 0, vertices: int = 0, instances: int = 1 # TODO: typing, technically mode can also be a ctypes uint in GL backend ) -> None: """ Render the VertexArray to the currently active framebuffer. @@ -278,22 +107,10 @@ def render( instances: OpenGL instance, used in using vertices over and over """ - gl.glBindVertexArray(self.glo) - if self._ibo is not None: - # HACK: re-bind index buffer just in case. - # pyglet rendering was somehow replacing the index buffer. - gl.glBindBuffer(gl.GL_ELEMENT_ARRAY_BUFFER, self._ibo.glo) - gl.glDrawElementsInstanced( - mode, - vertices, - self._index_element_type, - first * self._index_element_size, - instances, - ) - else: - gl.glDrawArraysInstanced(mode, first, vertices, instances) + raise NotImplementedError("The enabled graphics backend does not support this method.") - def render_indirect(self, buffer: Buffer, mode: GLuintLike, count, first, stride) -> None: + @abstractmethod + def render_indirect(self, buffer: Buffer, mode: int, count, first, stride) -> None: # TODO: typing, technically mode can also be a ctypes uint in GL backend """ Render the VertexArray to the framebuffer using indirect rendering. @@ -312,38 +129,14 @@ def render_indirect(self, buffer: Buffer, mode: GLuintLike, count, first, stride The byte stride of the draw command buffer. Keep the default (0) if the buffer is tightly packed. """ - # The default buffer stride for array and indexed - _stride = 20 if self._ibo is not None else 16 - stride = stride or _stride - if stride % 4 != 0 or stride < 0: - raise ValueError(f"stride must be positive integer in multiples of 4, not {stride}.") - - # The maximum number of draw calls in the buffer - max_commands = buffer.size // stride - if count < 0: - count = max_commands - elif (first + count) > max_commands: - raise ValueError( - "Attempt to issue rendering commands outside the buffer. " - f"first = {first}, count = {count} is reaching past " - f"the buffer end. The buffer have room for {max_commands} " - f"draw commands. byte size {buffer.size}, stride {stride}." - ) - - gl.glBindVertexArray(self.glo) - gl.glBindBuffer(gl.GL_DRAW_INDIRECT_BUFFER, buffer._glo) - if self._ibo: - gl.glMultiDrawElementsIndirect( - mode, self._index_element_type, first * stride, count, stride - ) - else: - gl.glMultiDrawArraysIndirect(mode, first * stride, count, stride) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def transform_interleaved( self, buffer: Buffer, - mode: GLenumLike, - output_mode: GLenumLike, + mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet first: int = 0, vertices: int = 0, instances: int = 1, @@ -368,44 +161,13 @@ def transform_interleaved( buffer_offset: Byte offset for the buffer (target) """ - if vertices < 0: - raise ValueError(f"Cannot determine the number of vertices: {vertices}") - - if buffer_offset >= buffer.size: - raise ValueError("buffer_offset at end or past the buffer size") - - gl.glBindVertexArray(self.glo) - gl.glEnable(gl.GL_RASTERIZER_DISCARD) - - if buffer_offset > 0: - gl.glBindBufferRange( - gl.GL_TRANSFORM_FEEDBACK_BUFFER, - 0, - buffer.glo, - buffer_offset, - buffer.size - buffer_offset, - ) - else: - gl.glBindBufferBase(gl.GL_TRANSFORM_FEEDBACK_BUFFER, 0, buffer.glo) - - gl.glBeginTransformFeedback(output_mode) - - if self._ibo is not None: - count = self._ibo.size // 4 - # TODO: Support first argument by offsetting pointer (second last arg) - gl.glDrawElementsInstanced(mode, vertices or count, gl.GL_UNSIGNED_INT, None, instances) - else: - # print(f"glDrawArraysInstanced({mode}, {first}, {vertices}, {instances})") - gl.glDrawArraysInstanced(mode, first, vertices, instances) - - gl.glEndTransformFeedback() - gl.glDisable(gl.GL_RASTERIZER_DISCARD) + raise NotImplementedError("The enabled graphics backend does not support this method.") def transform_separate( self, buffers: list[Buffer], - mode: GLenumLike, - output_mode: GLenumLike, + mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet first: int = 0, vertices: int = 0, instances: int = 1, @@ -430,45 +192,9 @@ def transform_separate( buffer_offset: Byte offset for the buffer (target) """ - if vertices < 0: - raise ValueError(f"Cannot determine the number of vertices: {vertices}") - - # Get size from the smallest buffer - size = min(buf.size for buf in buffers) - if buffer_offset >= size: - raise ValueError("buffer_offset at end or past the buffer size") - - gl.glBindVertexArray(self.glo) - gl.glEnable(gl.GL_RASTERIZER_DISCARD) - - if buffer_offset > 0: - for index, buffer in enumerate(buffers): - gl.glBindBufferRange( - gl.GL_TRANSFORM_FEEDBACK_BUFFER, - index, - buffer.glo, - buffer_offset, - buffer.size - buffer_offset, - ) - else: - for index, buffer in enumerate(buffers): - gl.glBindBufferBase(gl.GL_TRANSFORM_FEEDBACK_BUFFER, index, buffer.glo) - - gl.glBeginTransformFeedback(output_mode) - - if self._ibo is not None: - count = self._ibo.size // 4 - # TODO: Support first argument by offsetting pointer (second last arg) - gl.glDrawElementsInstanced(mode, vertices or count, gl.GL_UNSIGNED_INT, None, instances) - else: - # print(f"glDrawArraysInstanced({mode}, {first}, {vertices}, {instances})") - gl.glDrawArraysInstanced(mode, first, vertices, instances) + raise NotImplementedError("The enabled graphics backend does not support this method.") - gl.glEndTransformFeedback() - gl.glDisable(gl.GL_RASTERIZER_DISCARD) - - -class Geometry: +class Geometry(ABC): """A higher level abstraction of the VertexArray. It generates VertexArray instances on the fly internally matching the incoming @@ -507,8 +233,8 @@ class Geometry: def __init__( self, - ctx: "Context", - content: Sequence[BufferDescription] | None, + ctx: Context, + content: Sequence | None, # TODO: typing, this should be Sequence[BufferDescription] index_buffer: Buffer | None = None, mode: int | None = None, index_element_size: int = 4, @@ -564,7 +290,7 @@ def num_vertices(self) -> int: def num_vertices(self, value: int): self._num_vertices = value - def append_buffer_description(self, descr: BufferDescription): + def append_buffer_description(self, descr): # TODO: typing, descr should be BufferDescription """ Append a new BufferDescription to the existing Geometry. @@ -592,7 +318,7 @@ def render( self, program: Program, *, - mode: GLenumLike | None = None, + mode = None, # TODO: typing, mode should be GLenumLike | None first: int = 0, vertices: int | None = None, instances: int = 1, @@ -670,7 +396,7 @@ def render_indirect( program: Program, buffer: Buffer, *, - mode: GLuintLike | None = None, + mode = None, # TODO: typing, mode should be GLuintLike | None count: int = -1, first: int = 0, stride: int = 0, @@ -808,6 +534,7 @@ def flush(self) -> None: """ self._vao_cache = {} + @abstractmethod def _generate_vao(self, program: Program) -> VertexArray: """ Create a new VertexArray for the given program. @@ -815,17 +542,7 @@ def _generate_vao(self, program: Program) -> VertexArray: Args: program: The program to use """ - # print(f"Generating vao for key {program.attribute_key}") - - vao = VertexArray( - self._ctx, - program, - self._content, - index_buffer=self._index_buffer, - index_element_size=self._index_element_size, - ) - self._vao_cache[program.attribute_key] = vao - return vao + raise NotImplementedError("The enabled graphics backend does not support this method.") @staticmethod def _release(ctx) -> None: From 59fa6fbb88bd63219b11c35ea89b6b2cceb85abe Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 8 Apr 2025 18:56:15 -0400 Subject: [PATCH 03/16] Texture and Compute Shader abstraction --- arcade/gl/backends/gl/compute_shader.py | 274 +++++++++ arcade/gl/backends/gl/context.py | 48 +- arcade/gl/backends/gl/texture.py | 718 ++++++++++++++++++++++++ arcade/gl/compute_shader.py | 221 +------- arcade/gl/context.py | 28 +- arcade/gl/glsl.py | 169 ------ arcade/gl/texture.py | 422 ++------------ 7 files changed, 1090 insertions(+), 790 deletions(-) create mode 100644 arcade/gl/backends/gl/compute_shader.py create mode 100644 arcade/gl/backends/gl/texture.py delete mode 100644 arcade/gl/glsl.py diff --git a/arcade/gl/backends/gl/compute_shader.py b/arcade/gl/backends/gl/compute_shader.py new file mode 100644 index 000000000..f47fc5ade --- /dev/null +++ b/arcade/gl/backends/gl/compute_shader.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +import weakref +from ctypes import ( + POINTER, + byref, + c_buffer, + c_char, + c_char_p, + c_int, + cast, + create_string_buffer, + pointer, +) +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.gl.compute_shader import ComputeShader + +from .uniform import Uniform, UniformBlock + +if TYPE_CHECKING: + from arcade.gl import Context + + +class GLComputeShader(ComputeShader): + """ + A higher level wrapper for an OpenGL compute shader. + + Args: + ctx: + The context this shader belongs to. + glsl_source: + The GLSL source code for the compute shader. + """ + + def __init__(self, ctx: Context, glsl_source: str) -> None: + super().__init__(ctx, glsl_source) + self._uniforms: dict[str, UniformBlock | Uniform] = dict() + + from arcade.gl import ShaderException + + # Create the program + self._glo = glo = gl.glCreateProgram() + if not self._glo: + raise ShaderException("Failed to create program object") + + self._shader_obj = gl.glCreateShader(gl.GL_COMPUTE_SHADER) + if not self._shader_obj: + raise ShaderException("Failed to create compute shader object") + + # Set source + source_bytes = self._source.encode("utf-8") + strings = byref(cast(c_char_p(source_bytes), POINTER(c_char))) + lengths = pointer(c_int(len(source_bytes))) + gl.glShaderSource(self._shader_obj, 1, strings, lengths) + + # Compile and check result + gl.glCompileShader(self._shader_obj) + result = c_int() + gl.glGetShaderiv(self._shader_obj, gl.GL_COMPILE_STATUS, byref(result)) + if result.value == gl.GL_FALSE: + msg = create_string_buffer(512) + length = c_int() + gl.glGetShaderInfoLog(self._shader_obj, 512, byref(length), msg) + raise ShaderException( + ( + f"Error compiling compute shader " + f"({result.value}): {msg.value.decode('utf-8')}\n" + f"---- [compute shader] ---\n" + ) + + "\n".join( + f"{str(i + 1).zfill(3)}: {line} " + for i, line in enumerate(self._source.split("\n")) + ) + ) + + # Attach and link shader + gl.glAttachShader(self._glo, self._shader_obj) + gl.glLinkProgram(self._glo) + gl.glDeleteShader(self._shader_obj) + status = c_int() + gl.glGetProgramiv(self._glo, gl.GL_LINK_STATUS, status) + if not status.value: + length = c_int() + gl.glGetProgramiv(self._glo, gl.GL_INFO_LOG_LENGTH, length) + log = c_buffer(length.value) + gl.glGetProgramInfoLog(self._glo, len(log), None, log) + raise ShaderException("Program link error: {}".format(log.value.decode())) + + self._introspect_uniforms() + self._introspect_uniform_blocks() + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLComputeShader.delete_glo, self._ctx, glo) + + ctx.stats.incr("compute_shader") + + @property + def glo(self) -> int: + """The name/id of the OpenGL resource""" + return self._glo + + def _use(self) -> None: + """ + Use/activate the compute shader. + + .. Note:: + + This is not necessary to call in normal use cases + since ``run()`` already does this for you. + """ + gl.glUseProgram(self._glo) + self._ctx.active_program = self + + def run(self, group_x=1, group_y=1, group_z=1) -> None: + """ + Run the compute shader. + + When running a compute shader we specify how many work groups should + be executed on the ``x``, ``y`` and ``z`` dimension. The size of the work group + is defined in the compute shader. + + .. code:: glsl + + // Work group with one dimension. 16 work groups executed. + layout(local_size_x=16) in; + // Work group with two dimensions. 256 work groups executed. + layout(local_size_x=16, local_size_y=16) in; + // Work group with three dimensions. 4096 work groups executed. + layout(local_size_x=16, local_size_y=16, local_size_z=16) in; + + Group sizes are ``1`` by default. If your compute shader doesn't specify + a size for a dimension or uses ``1`` as size you don't have to supply + this parameter. + + Args: + group_x: The number of work groups to be launched in the X dimension. + group_y: The number of work groups to be launched in the y dimension. + group_z: The number of work groups to be launched in the z dimension. + """ + self._use() + gl.glDispatchCompute(group_x, group_y, group_z) + + def __getitem__(self, item) -> Uniform | UniformBlock: + """Get a uniform or uniform block""" + try: + uniform = self._uniforms[item] + except KeyError: + raise KeyError(f"Uniform with the name `{item}` was not found.") + + return uniform.getter() + + def __setitem__(self, key, value): + """Set a uniform value""" + # Ensure we are setting the uniform on this program + # if self._ctx.active_program != self: + # self.use() + + try: + uniform = self._uniforms[key] + except KeyError: + raise KeyError(f"Uniform with the name `{key}` was not found.") + + uniform.setter(value) + + def __del__(self): + if self._ctx.gc_mode == "context_gc" and self._glo > 0: + self._ctx.objects.append(self) + + def delete(self) -> None: + """ + Destroy the internal compute shader object. + + This is normally not necessary, but depends on the + garbage collection configured in the context. + """ + GLComputeShader.delete_glo(self._ctx, self._glo) + self._glo = 0 + + @staticmethod + def delete_glo(ctx, prog_id): + """ + Low level method for destroying a compute shader by id. + + Args: + ctx: The context this program belongs to. + prog_id: The OpenGL id of the program. + """ + # Check to see if the context was already cleaned up from program + # shut down. If so, we don't need to delete the shaders. + if gl.current_context is None: + return + + gl.glDeleteProgram(prog_id) + # TODO: Count compute shaders + ctx.stats.decr("compute_shader") + + def _introspect_uniforms(self): + """Figure out what uniforms are available and build an internal map""" + # Number of active uniforms in the program + active_uniforms = gl.GLint(0) + gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORMS, byref(active_uniforms)) + + # Loop all the active uniforms + for index in range(active_uniforms.value): + # Query uniform information like name, type, size etc. + u_name, u_type, u_size = self._query_uniform(index) + u_location = gl.glGetUniformLocation(self._glo, u_name.encode()) + + # Skip uniforms that may be in Uniform Blocks + # TODO: We should handle all uniforms + if u_location == -1: + # print(f"Uniform {u_location} {u_name} {u_size} {u_type} skipped") + continue + + u_name = u_name.replace("[0]", "") # Remove array suffix + self._uniforms[u_name] = Uniform( + self._ctx, self._glo, u_location, u_name, u_type, u_size + ) + + def _introspect_uniform_blocks(self): + """Finds uniform blocks and maps the to python objectss""" + active_uniform_blocks = gl.GLint(0) + gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORM_BLOCKS, byref(active_uniform_blocks)) + # print('GL_ACTIVE_UNIFORM_BLOCKS', active_uniform_blocks) + + for loc in range(active_uniform_blocks.value): + index, size, name = self._query_uniform_block(loc) + block = UniformBlock(self._glo, index, size, name) + self._uniforms[name] = block + + def _query_uniform(self, location: int) -> tuple[str, int, int]: + """Retrieve Uniform information at given location. + + Returns the name, the type as a GLenum (GL_FLOAT, ...) and the size. Size is + greater than 1 only for Uniform arrays, like an array of floats or an array + of Matrices. + """ + u_size = gl.GLint() + u_type = gl.GLenum() + buf_size = 192 # max uniform character length + u_name = create_string_buffer(buf_size) + gl.glGetActiveUniform( + self._glo, # program to query + location, # location to query + buf_size, # size of the character/name buffer + None, # the number of characters actually written by OpenGL in the string + u_size, # size of the uniform variable + u_type, # data type of the uniform variable + u_name, # string buffer for storing the name + ) + return u_name.value.decode(), u_type.value, u_size.value + + def _query_uniform_block(self, location: int) -> tuple[int, int, str]: + """Query active uniform block by retrieving the name and index and size""" + # Query name + u_size = gl.GLint() + buf_size = 192 # max uniform character length + u_name = create_string_buffer(buf_size) + gl.glGetActiveUniformBlockName( + self._glo, # program to query + location, # location to query + 256, # max size if the name + u_size, # length + u_name, + ) + # Query index + index = gl.glGetUniformBlockIndex(self._glo, u_name) + # Query size + b_size = gl.GLint() + gl.glGetActiveUniformBlockiv(self._glo, index, gl.GL_UNIFORM_BLOCK_DATA_SIZE, b_size) + return index, b_size.value, u_name.value.decode() diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 9dffd0f30..14930f3f8 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -1,4 +1,4 @@ -from typing import List, Dict, Sequence +from typing import List, Dict, Iterable, Sequence, Tuple from arcade.gl.context import Context from arcade.context import ArcadeContext @@ -7,10 +7,14 @@ from arcade.types import BufferProtocol +from .types import PyGLenum + from .buffer import GLBuffer +from .compute_shader import GLComputeShader from .glsl import ShaderSource from .types import BufferDescription from .program import GLProgram +from .texture import GLTexture2D from .vertex_array import GLGeometry class GLContext(Context): @@ -90,6 +94,48 @@ def geometry( index_element_size=index_element_size, ) + def compute_shader(self, *, source: str, common: Iterable[str] = ()) -> GLComputeShader: + src = ShaderSource(self, source, common, pyglet.gl.GL_COMPUTE_SHADER) + return GLComputeShader(self, src.get_source()) + + def texture( + self, + size: Tuple[int, int], + *, + components: int = 4, + dtype: str = "f1", + data: BufferProtocol | None = None, + wrap_x: PyGLenum | None = None, + wrap_y: PyGLenum | None = None, + filter: Tuple[PyGLenum, PyGLenum] | None = None, + samples: int = 0, + immutable: bool = False, + internal_format: PyGLenum | None = None, + compressed: bool = False, + compressed_data: bool = False, + ) -> GLTexture2D: + compressed = compressed or compressed_data + + return GLTexture2D( + self, + size, + components=components, + data=data, + dtype=dtype, + wrap_x=wrap_x, + wrap_y=wrap_y, + filter=filter, + samples=samples, + immutable=immutable, + internal_format=internal_format, + compressed=compressed, + compressed_data=compressed_data, + ) + + def depth_texture( + self, size: Tuple[int, int], *, data: BufferProtocol | None = None + ) -> GLTexture2D: + return GLTexture2D(self, size, data=data, depth=True) class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): diff --git a/arcade/gl/backends/gl/texture.py b/arcade/gl/backends/gl/texture.py new file mode 100644 index 000000000..264fd5169 --- /dev/null +++ b/arcade/gl/backends/gl/texture.py @@ -0,0 +1,718 @@ +from __future__ import annotations + +import weakref +from ctypes import byref, string_at +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.gl.texture import Texture2D + +from arcade.types import BufferProtocol +from .buffer import Buffer +from .types import ( + BufferOrBufferProtocol, + PyGLuint, + compare_funcs, + pixel_formats, + swizzle_enum_to_str, + swizzle_str_to_enum, +) +from .utils import data_to_ctypes + +if TYPE_CHECKING: # handle import cycle caused by type hinting + from arcade.gl import Context + + +class GLTexture2D(Texture2D): + """ + An OpenGL 2D texture. + We can create an empty black texture or a texture from byte data. + A texture can also be created with different datatypes such as + float, integer or unsigned integer. + + The best way to create a texture instance is through :py:meth:`arcade.gl.Context.texture` + + Supported ``dtype`` values are:: + + # Float formats + 'f1': UNSIGNED_BYTE + 'f2': HALF_FLOAT + 'f4': FLOAT + # int formats + 'i1': BYTE + 'i2': SHORT + 'i4': INT + # uint formats + 'u1': UNSIGNED_BYTE + 'u2': UNSIGNED_SHORT + 'u4': UNSIGNED_INT + + Args: + ctx: + The context the object belongs to + size: + The size of the texture + components: + The number of components (1: R, 2: RG, 3: RGB, 4: RGBA) + dtype: + The data type of each component: f1, f2, f4 / i1, i2, i4 / u1, u2, u4 + data: + The texture data. Can be bytes or any object supporting + the buffer protocol. + filter: + The minification/magnification filter of the texture + wrap_x: + Wrap mode x + wrap_y: + Wrap mode y + target: + The texture type (Ignored. Legacy) + depth: + creates a depth texture if `True` + samples: + Creates a multisampled texture for values > 0. + This value will be clamped between 0 and the max + sample capability reported by the drivers. + immutable: + Make the storage (not the contents) immutable. This can sometimes be + required when using textures with compute shaders. + internal_format: + The internal format of the texture + compressed: + Is the texture compressed? + compressed_data: + The raw compressed data + """ + __slots__ = ( + "_glo", + "_target", + ) + + def __init__( + self, + ctx: Context, + size: tuple[int, int], + *, + components: int = 4, + dtype: str = "f1", + data: BufferProtocol | None = None, + filter: tuple[PyGLuint, PyGLuint] | None = None, + wrap_x: PyGLuint | None = None, + wrap_y: PyGLuint | None = None, + depth=False, + samples: int = 0, + immutable: bool = False, + internal_format: PyGLuint | None = None, + compressed: bool = False, + compressed_data: bool = False, + ): + super().__init__(ctx, size, components=components, dtype=dtype, data=data, filter=filter, wrap_x=wrap_x, wrap_y=wrap_y, depth=depth, samples=samples, immutable=immutable, internal_format = internal_format, compressed=compressed, compressed_data=compressed_data) + self._glo = glo = gl.GLuint() + + # Default filters for float and integer textures + # Integer textures should have NEAREST interpolation + # by default 3.3 core doesn't really support it consistently. + if "f" in self._dtype: + self._filter = gl.GL_LINEAR, gl.GL_LINEAR + else: + self._filter = gl.GL_NEAREST, gl.GL_NEAREST + + self._target = gl.GL_TEXTURE_2D if self._samples == 0 else gl.GL_TEXTURE_2D_MULTISAMPLE + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glGenTextures(1, byref(self._glo)) + + if self._glo.value == 0: + raise RuntimeError("Cannot create Texture. OpenGL failed to generate a texture id") + + gl.glBindTexture(self._target, self._glo) + + self._texture_2d(data) + + # Only set texture parameters on non-multisample textures + if self._samples == 0: + self.filter = filter or self._filter + self.wrap_x = wrap_x or self._wrap_x + self.wrap_y = wrap_y or self._wrap_y + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLTexture2D.delete_glo, self._ctx, glo) + + def resize(self, size: tuple[int, int]): + """ + Resize the texture. This will re-allocate the internal + memory and all pixel data will be lost. + + .. note:: Immutable textures cannot be resized. + + Args: + size: The new size of the texture + """ + if self._immutable: + raise ValueError("Immutable textures cannot be resized") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + self._width, self._height = size + + self._texture_2d(None) + + def __del__(self): + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: + self._ctx.objects.append(self) + + def _texture_2d(self, data): + """Create a 2D texture""" + # Start by resolving the texture format + try: + format_info = pixel_formats[self._dtype] + except KeyError: + raise ValueError( + f"dype '{self._dtype}' not support. Supported types are : " + f"{tuple(pixel_formats.keys())}" + ) + _format, _internal_format, self._type, self._component_size = format_info + if data is not None: + byte_length, data = data_to_ctypes(data) + self._validate_data_size(data, byte_length, self._width, self._height) + + # If we are dealing with a multisampled texture we have less options + if self._target == gl.GL_TEXTURE_2D_MULTISAMPLE: + gl.glTexImage2DMultisample( + self._target, + self._samples, + _internal_format[self._components], + self._width, + self._height, + True, # Fixed sample locations + ) + return + + # Make sure we unpack the pixel data with correct alignment + # or we'll end up with corrupted textures + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, self._alignment) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, self._alignment) + + # Create depth 2d texture + if self._depth: + gl.glTexImage2D( + self._target, + 0, # level + gl.GL_DEPTH_COMPONENT24, + self._width, + self._height, + 0, + gl.GL_DEPTH_COMPONENT, + gl.GL_UNSIGNED_INT, # gl.GL_FLOAT, + data, + ) + self.compare_func = "<=" + # Create normal 2d texture + else: + try: + self._format = _format[self._components] + if self._internal_format is None: + self._internal_format = _internal_format[self._components] + + if self._immutable: + # Specify immutable storage for this texture. + # glTexStorage2D can only be called once + gl.glTexStorage2D( + self._target, + 1, # Levels + self._internal_format, + self._width, + self._height, + ) + if data: + self.write(data) + else: + # glTexImage2D can be called multiple times to re-allocate storage + # Specify mutable storage for this texture. + if self._compressed_data is True: + gl.glCompressedTexImage2D( + self._target, # target + 0, # level + self._internal_format, # internal_format + self._width, # width + self._height, # height + 0, # border + len(data), # size + data, # data + ) + else: + gl.glTexImage2D( + self._target, # target + 0, # level + self._internal_format, # internal_format + self._width, # width + self._height, # height + 0, # border + self._format, # format + self._type, # type + data, # data + ) + except gl.GLException as ex: + raise gl.GLException( + ( + f"Unable to create texture: {ex} : dtype={self._dtype} " + f"size={self.size} components={self._components} " + f"MAX_TEXTURE_SIZE = {self.ctx.info.MAX_TEXTURE_SIZE}" + f": {ex}" + ) + ) + + @property + def ctx(self) -> Context: + """The context this texture belongs to.""" + return self._ctx + + @property + def glo(self) -> gl.GLuint: + """The OpenGL texture id""" + return self._glo + + @property + def compressed(self) -> bool: + """Is this using a compressed format?""" + return self._compressed + + @property + def width(self) -> int: + """The width of the texture in pixels""" + return self._width + + @property + def height(self) -> int: + """The height of the texture in pixels""" + return self._height + + @property + def dtype(self) -> str: + """The data type of each component""" + return self._dtype + + @property + def size(self) -> tuple[int, int]: + """The size of the texture as a tuple""" + return self._width, self._height + + @property + def samples(self) -> int: + """Number of samples if multisampling is enabled (read only)""" + return self._samples + + @property + def byte_size(self) -> int: + """The byte size of the texture.""" + return pixel_formats[self._dtype][3] * self._components * self.width * self.height + + @property + def components(self) -> int: + """Number of components in the texture""" + return self._components + + @property + def component_size(self) -> int: + """Size in bytes of each component""" + return self._component_size + + @property + def depth(self) -> bool: + """If this is a depth texture.""" + return self._depth + + @property + def immutable(self) -> bool: + """Does this texture have immutable storage?""" + return self._immutable + + @property + def swizzle(self) -> str: + """ + The swizzle mask of the texture (Default ``'RGBA'``). + + The swizzle mask change/reorder the ``vec4`` value returned by the ``texture()`` function + in a GLSL shaders. This is represented by a 4 character string were each + character can be:: + + 'R' GL_RED + 'G' GL_GREEN + 'B' GL_BLUE + 'A' GL_ALPHA + '0' GL_ZERO + '1' GL_ONE + + Example:: + + # Alpha channel will always return 1.0 + texture.swizzle = 'RGB1' + + # Only return the red component. The rest is masked to 0.0 + texture.swizzle = 'R000' + + # Reverse the components + texture.swizzle = 'ABGR' + """ + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + # Read the current swizzle values from the texture + swizzle_r = gl.GLint() + swizzle_g = gl.GLint() + swizzle_b = gl.GLint() + swizzle_a = gl.GLint() + + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_r) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_g) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_b) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_a) + + swizzle_str = "" + for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]: + swizzle_str += swizzle_enum_to_str[v.value] + + return swizzle_str + + @swizzle.setter + def swizzle(self, value: str): + if not isinstance(value, str): + raise ValueError(f"Swizzle must be a string, not '{type(str)}'") + + if len(value) != 4: + raise ValueError("Swizzle must be a string of length 4") + + swizzle_enums = [] + for c in value: + try: + c = c.upper() + swizzle_enums.append(swizzle_str_to_enum[c]) + except KeyError: + raise ValueError(f"Swizzle value '{c}' invalid. Must be one of RGBA01") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_enums[3]) + + @Texture2D.filter.setter + def filter(self, value: tuple[int, int]): + if not isinstance(value, tuple) or not len(value) == 2: + raise ValueError("Texture filter must be a 2 component tuple (min, mag)") + + self._filter = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + + @Texture2D.wrap_x.setter + def wrap_x(self, value: int): + self._wrap_x = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_S, value) + + @Texture2D.wrap_y.setter + def wrap_y(self, value: int): + self._wrap_y = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_T, value) + + @Texture2D.anisotropy.setter + def anisotropy(self, value): + self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + + @Texture2D.compare_func.setter + def compare_func(self, value: str | None): + if not self._depth: + raise ValueError("Depth comparison function can only be set on depth textures") + + if not isinstance(value, str) and value is not None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + func = compare_funcs.get(value, None) + if func is None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + self._compare_func = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + if value is None: + gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) + else: + gl.glTexParameteri( + self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE + ) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_FUNC, func) + + def read(self, level: int = 0, alignment: int = 1) -> bytes: + """ + Read the contents of the texture. + + Args: + level: + The texture level to read + alignment: + Alignment of the start of each row in memory in number of bytes. + Possible values: 1,2,4 + """ + if self._samples > 0: + raise ValueError("Multisampled textures cannot be read directly") + + if self._ctx.gl_api == "gl": + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, alignment) + + buffer = ( + 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)) + elif self._ctx.gl_api == "gles": + fbo = self._ctx.framebuffer(color_attachments=[self]) + return fbo.read(components=self._components, dtype=self._dtype) + else: + raise ValueError("Unknown gl_api: '{self._ctx.gl_api}'") + + def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> None: + """Write byte data from the passed source to the texture. + + The ``data`` value can be either an + :py:class:`arcade.gl.Buffer` or anything that implements the + `Buffer Protocol `_. + + The latter category includes ``bytes``, ``bytearray``, + ``array.array``, and more. You may need to use typing + workarounds for non-builtin types. See + :ref:`prog-guide-gl-buffer-protocol-typing` for more + information. + + Args: + data: + :class:`~arcade.gl.Buffer` or buffer protocol object with data to write. + level: + The texture level to write + viewport: + The area of the texture to write. 2 or 4 component tuple. + (x, y, w, h) or (w, h). Default is the full texture. + """ + # TODO: Support writing to layers using viewport + alignment + if self._samples > 0: + raise ValueError("Writing to multisampled textures not supported") + + x, y, w, h = 0, 0, self._width, self._height + if viewport: + if len(viewport) == 2: + w, h = viewport + elif len(viewport) == 4: + x, y, w, h = viewport + else: + raise ValueError("Viewport must be of length 2 or 4") + + if isinstance(data, Buffer): + gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, data.glo) + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glTexSubImage2D(self._target, level, x, y, w, h, self._format, self._type, 0) + gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) + else: + byte_size, data = data_to_ctypes(data) + self._validate_data_size(data, byte_size, w, h) + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glTexSubImage2D( + self._target, # target + level, # level + x, # x offset + y, # y offset + w, # width + h, # height + self._format, # format + self._type, # type + data, # pixel data + ) + + def _validate_data_size(self, byte_data, byte_size, width, height) -> None: + """Validate the size of the data to be written to the texture""" + # TODO: Validate data size for compressed textures + # This might be a bit tricky since the size of the compressed + # data would depend on the algorithm used. + if self._compressed is True: + return + + expected_size = width * height * self._component_size * self._components + if byte_size != expected_size: + raise ValueError( + f"Data size {len(byte_data)} does not match expected size {expected_size}" + ) + if len(byte_data) != byte_size: + raise ValueError( + f"Data size {len(byte_data)} does not match reported size {expected_size}" + ) + + def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: + """Generate mipmaps for this texture. + + The default values usually work well. + + Mipmaps are successively smaller versions of an original + texture with special filtering applied. Using mipmaps allows + OpenGL to render scaled versions of original textures with fewer + scaling artifacts. + + Mipmaps can be made for textures of any size. Each mipmap + version halves the width and height of the previous one (e.g. + 256 x 256, 128 x 128, 64 x 64, etc) down to a minimum of 1 x 1. + + .. note:: Mipmaps will only be used if a texture's filter is + configured with a mipmap-type minification:: + + # Set up linear interpolating minification filter + texture.filter = ctx.LINEAR_MIPMAP_LINEAR, ctx.LINEAR + + Args: + base: + Level the mipmaps start at (usually 0) + max_level: + The maximum number of levels to generate + + Also see: https://www.khronos.org/opengl/wiki/Texture#Mip_maps + """ + if self._samples > 0: + raise ValueError("Multisampled textures don't support mimpmaps") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(gl.GL_TEXTURE_2D, self._glo) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_BASE_LEVEL, base) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, max_level) + gl.glGenerateMipmap(gl.GL_TEXTURE_2D) + + def delete(self): + """ + Destroy the underlying OpenGL resource. + + Don't use this unless you know exactly what you are doing. + """ + GLTexture2D.delete_glo(self._ctx, self._glo) + self._glo.value = 0 + + @staticmethod + def delete_glo(ctx: "Context", glo: gl.GLuint): + """ + Destroy the texture. + + This is called automatically when the object is garbage collected. + + Args: + ctx: OpenGL Context + glo: The OpenGL texture id + """ + # If we have no context, then we are shutting down, so skip this + if gl.current_context is None: + return + + if glo.value != 0: + gl.glDeleteTextures(1, byref(glo)) + + ctx.stats.decr("texture") + + def use(self, unit: int = 0) -> None: + """Bind the texture to a channel, + + Args: + unit: The texture unit to bind the texture. + """ + gl.glActiveTexture(gl.GL_TEXTURE0 + unit) + gl.glBindTexture(self._target, self._glo) + + def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: int = 0): + """ + Bind textures to image units. + + Note that either or both ``read`` and ``write`` needs to be ``True``. + The supported modes are: read only, write only, read-write + + Args: + unit: The image unit + read: The compute shader intends to read from this image + write: The compute shader intends to write to this image + level: The mipmap level to bind + """ + if self._ctx.gl_api == "gles" and not self._immutable: + raise ValueError("Textures bound to image units must be created with immutable=True") + + access = gl.GL_READ_WRITE + if read and write: + access = gl.GL_READ_WRITE + elif read and not write: + access = gl.GL_READ_ONLY + elif not read and write: + access = gl.GL_WRITE_ONLY + else: + raise ValueError("Illegal access mode. The texture must at least be read or write only") + + gl.glBindImageTexture(unit, self._glo, level, 0, 0, access, self._internal_format) + + def get_handle(self, resident: bool = True) -> int: + """ + Get a handle for bindless texture access. + + Once a handle is created its parameters cannot be changed. + Attempting to do so will have no effect. (filter, wrap etc). + There is no way to undo this immutability. + + Handles cannot be used by shaders until they are resident. + This method can be called multiple times to move a texture + in and out of residency:: + + >> texture.get_handle(resident=False) + 4294969856 + >> texture.get_handle(resident=True) + 4294969856 + + Ths same handle is returned if the handle already exists. + + .. note:: Limitations from the OpenGL wiki + + The amount of storage available for resident images/textures may be less + than the total storage for textures that is available. As such, you should + attempt to minimize the time a texture spends being resident. Do not attempt + to take steps like making textures resident/un-resident every frame or something. + But if you are finished using a texture for some time, make it un-resident. + + Args: + resident: Make the texture resident. + """ + handle = gl.glGetTextureHandleARB(self._glo) + is_resident = gl.glIsTextureHandleResidentARB(handle) + + # Ensure we don't try to make a resident texture resident again + if resident: + if not is_resident: + gl.glMakeTextureHandleResidentARB(handle) + else: + if is_resident: + gl.glMakeTextureHandleNonResidentARB(handle) + + return handle + + def __repr__(self) -> str: + return "".format( + self._glo.value, self._width, self._height, self._components + ) diff --git a/arcade/gl/compute_shader.py b/arcade/gl/compute_shader.py index cffa27724..39ee38549 100644 --- a/arcade/gl/compute_shader.py +++ b/arcade/gl/compute_shader.py @@ -1,28 +1,13 @@ from __future__ import annotations -import weakref -from ctypes import ( - POINTER, - byref, - c_buffer, - c_char, - c_char_p, - c_int, - cast, - create_string_buffer, - pointer, -) +from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from pyglet import gl - -from .uniform import Uniform, UniformBlock - if TYPE_CHECKING: from arcade.gl import Context -class ComputeShader: +class ComputeShader(ABC): """ A higher level wrapper for an OpenGL compute shader. @@ -36,83 +21,10 @@ class ComputeShader: def __init__(self, ctx: Context, glsl_source: str) -> None: self._ctx = ctx self._source = glsl_source - self._uniforms: dict[str, UniformBlock | Uniform] = dict() - - from arcade.gl import ShaderException - - # Create the program - self._glo = glo = gl.glCreateProgram() - if not self._glo: - raise ShaderException("Failed to create program object") - - self._shader_obj = gl.glCreateShader(gl.GL_COMPUTE_SHADER) - if not self._shader_obj: - raise ShaderException("Failed to create compute shader object") - - # Set source - source_bytes = self._source.encode("utf-8") - strings = byref(cast(c_char_p(source_bytes), POINTER(c_char))) - lengths = pointer(c_int(len(source_bytes))) - gl.glShaderSource(self._shader_obj, 1, strings, lengths) - - # Compile and check result - gl.glCompileShader(self._shader_obj) - result = c_int() - gl.glGetShaderiv(self._shader_obj, gl.GL_COMPILE_STATUS, byref(result)) - if result.value == gl.GL_FALSE: - msg = create_string_buffer(512) - length = c_int() - gl.glGetShaderInfoLog(self._shader_obj, 512, byref(length), msg) - raise ShaderException( - ( - f"Error compiling compute shader " - f"({result.value}): {msg.value.decode('utf-8')}\n" - f"---- [compute shader] ---\n" - ) - + "\n".join( - f"{str(i + 1).zfill(3)}: {line} " - for i, line in enumerate(self._source.split("\n")) - ) - ) - - # Attach and link shader - gl.glAttachShader(self._glo, self._shader_obj) - gl.glLinkProgram(self._glo) - gl.glDeleteShader(self._shader_obj) - status = c_int() - gl.glGetProgramiv(self._glo, gl.GL_LINK_STATUS, status) - if not status.value: - length = c_int() - gl.glGetProgramiv(self._glo, gl.GL_INFO_LOG_LENGTH, length) - log = c_buffer(length.value) - gl.glGetProgramInfoLog(self._glo, len(log), None, log) - raise ShaderException("Program link error: {}".format(log.value.decode())) - - self._introspect_uniforms() - self._introspect_uniform_blocks() - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, ComputeShader.delete_glo, self._ctx, glo) ctx.stats.incr("compute_shader") - @property - def glo(self) -> int: - """The name/id of the OpenGL resource""" - return self._glo - - def _use(self) -> None: - """ - Use/activate the compute shader. - - .. Note:: - - This is not necessary to call in normal use cases - since ``run()`` already does this for you. - """ - gl.glUseProgram(self._glo) - self._ctx.active_program = self - + @abstractmethod def run(self, group_x=1, group_y=1, group_z=1) -> None: """ Run the compute shader. @@ -139,38 +51,12 @@ def run(self, group_x=1, group_y=1, group_z=1) -> None: group_y: The number of work groups to be launched in the y dimension. group_z: The number of work groups to be launched in the z dimension. """ - self._use() - gl.glDispatchCompute(group_x, group_y, group_z) - - def __getitem__(self, item) -> Uniform | UniformBlock: - """Get a uniform or uniform block""" - try: - uniform = self._uniforms[item] - except KeyError: - raise KeyError(f"Uniform with the name `{item}` was not found.") - - return uniform.getter() - - def __setitem__(self, key, value): - """Set a uniform value""" - # Ensure we are setting the uniform on this program - # if self._ctx.active_program != self: - # self.use() - - try: - uniform = self._uniforms[key] - except KeyError: - raise KeyError(f"Uniform with the name `{key}` was not found.") - - uniform.setter(value) + raise NotImplementedError("The enabled graphics backend does not support this method.") def __hash__(self) -> int: return id(self) - def __del__(self): - if self._ctx.gc_mode == "context_gc" and self._glo > 0: - self._ctx.objects.append(self) - + @abstractmethod def delete(self) -> None: """ Destroy the internal compute shader object. @@ -178,99 +64,4 @@ def delete(self) -> None: This is normally not necessary, but depends on the garbage collection configured in the context. """ - ComputeShader.delete_glo(self._ctx, self._glo) - self._glo = 0 - - @staticmethod - def delete_glo(ctx, prog_id): - """ - Low level method for destroying a compute shader by id. - - Args: - ctx: The context this program belongs to. - prog_id: The OpenGL id of the program. - """ - # Check to see if the context was already cleaned up from program - # shut down. If so, we don't need to delete the shaders. - if gl.current_context is None: - return - - gl.glDeleteProgram(prog_id) - # TODO: Count compute shaders - ctx.stats.decr("compute_shader") - - def _introspect_uniforms(self): - """Figure out what uniforms are available and build an internal map""" - # Number of active uniforms in the program - active_uniforms = gl.GLint(0) - gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORMS, byref(active_uniforms)) - - # Loop all the active uniforms - for index in range(active_uniforms.value): - # Query uniform information like name, type, size etc. - u_name, u_type, u_size = self._query_uniform(index) - u_location = gl.glGetUniformLocation(self._glo, u_name.encode()) - - # Skip uniforms that may be in Uniform Blocks - # TODO: We should handle all uniforms - if u_location == -1: - # print(f"Uniform {u_location} {u_name} {u_size} {u_type} skipped") - continue - - u_name = u_name.replace("[0]", "") # Remove array suffix - self._uniforms[u_name] = Uniform( - self._ctx, self._glo, u_location, u_name, u_type, u_size - ) - - def _introspect_uniform_blocks(self): - """Finds uniform blocks and maps the to python objectss""" - active_uniform_blocks = gl.GLint(0) - gl.glGetProgramiv(self._glo, gl.GL_ACTIVE_UNIFORM_BLOCKS, byref(active_uniform_blocks)) - # print('GL_ACTIVE_UNIFORM_BLOCKS', active_uniform_blocks) - - for loc in range(active_uniform_blocks.value): - index, size, name = self._query_uniform_block(loc) - block = UniformBlock(self._glo, index, size, name) - self._uniforms[name] = block - - def _query_uniform(self, location: int) -> tuple[str, int, int]: - """Retrieve Uniform information at given location. - - Returns the name, the type as a GLenum (GL_FLOAT, ...) and the size. Size is - greater than 1 only for Uniform arrays, like an array of floats or an array - of Matrices. - """ - u_size = gl.GLint() - u_type = gl.GLenum() - buf_size = 192 # max uniform character length - u_name = create_string_buffer(buf_size) - gl.glGetActiveUniform( - self._glo, # program to query - location, # location to query - buf_size, # size of the character/name buffer - None, # the number of characters actually written by OpenGL in the string - u_size, # size of the uniform variable - u_type, # data type of the uniform variable - u_name, # string buffer for storing the name - ) - return u_name.value.decode(), u_type.value, u_size.value - - def _query_uniform_block(self, location: int) -> tuple[int, int, str]: - """Query active uniform block by retrieving the name and index and size""" - # Query name - u_size = gl.GLint() - buf_size = 192 # max uniform character length - u_name = create_string_buffer(buf_size) - gl.glGetActiveUniformBlockName( - self._glo, # program to query - location, # location to query - 256, # max size if the name - u_size, # length - u_name, - ) - # Query index - index = gl.glGetUniformBlockIndex(self._glo, u_name) - # Query size - b_size = gl.GLint() - gl.glGetActiveUniformBlockiv(self._glo, index, gl.GL_UNIFORM_BLOCK_DATA_SIZE, b_size) - return index, b_size.value, u_name.value.decode() + raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/context.py b/arcade/gl/context.py index 4766b4e24..f9ec42290 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -29,7 +29,6 @@ from .buffer import Buffer from .compute_shader import ComputeShader from .framebuffer import DefaultFrameBuffer, Framebuffer -from .glsl import ShaderSource from .program import Program from .query import Query from .sampler import Sampler @@ -970,6 +969,7 @@ def framebuffer( self, color_attachments=color_attachments or [], depth_attachment=depth_attachment ) + @abstractmethod def texture( self, size: Tuple[int, int], @@ -1055,23 +1055,7 @@ def texture( Set to True if you are passing in raw compressed pixel data. This implies ``compressed=True``. """ - compressed = compressed or compressed_data - - return Texture2D( - self, - size, - components=components, - data=data, - dtype=dtype, - wrap_x=wrap_x, - wrap_y=wrap_y, - filter=filter, - samples=samples, - immutable=immutable, - internal_format=internal_format, - compressed=compressed, - compressed_data=compressed_data, - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") def texture_array( self, @@ -1107,6 +1091,7 @@ def texture_array( filter=filter, ) + @abstractmethod def depth_texture( self, size: Tuple[int, int], *, data: BufferProtocol | None = None ) -> Texture2D: @@ -1121,7 +1106,7 @@ def depth_texture( The texture data. Can be``bytes`` or any object supporting the buffer protocol. """ - return Texture2D(self, size, data=data, depth=True) + raise NotImplementedError("The enabled graphics backend does not support this method.") def sampler(self, texture: Texture2D) -> Sampler: """ @@ -1219,6 +1204,7 @@ def geometry( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def program( self, *, @@ -1277,6 +1263,7 @@ def query(self, *, samples=True, time=True, primitives=True) -> Query: """ return Query(self, samples=samples, time=time, primitives=primitives) + @abstractmethod def compute_shader(self, *, source: str, common: Iterable[str] = ()) -> ComputeShader: """ Create a compute shader. @@ -1287,8 +1274,7 @@ def compute_shader(self, *, source: str, common: Iterable[str] = ()) -> ComputeS common: Common / library source injected into compute shader """ - src = ShaderSource(self, source, common, gl.GL_COMPUTE_SHADER) - return ComputeShader(self, src.get_source()) + raise NotImplementedError("The enabled graphics backend does not support this method.") class ContextStats: diff --git a/arcade/gl/glsl.py b/arcade/gl/glsl.py deleted file mode 100644 index 61580bb6a..000000000 --- a/arcade/gl/glsl.py +++ /dev/null @@ -1,169 +0,0 @@ -import re -from typing import TYPE_CHECKING, Iterable - -from pyglet import gl - -if TYPE_CHECKING: - from .context import Context as ArcadeGlContext - -from .exceptions import ShaderException -from .types import SHADER_TYPE_NAMES, PyGLenum - - -class ShaderSource: - """ - GLSL source container for making source parsing simpler. - - We support locating out attributes, applying ``#defines`` values - and injecting common source. - - .. note::: - We do assume the source is neat enough to be parsed - this way and don't contain several statements on one line. - - Args: - ctx: - The context this framebuffer belongs to - source: - The GLSL source code - common: - Common source code to inject - source_type: - The shader type - """ - - def __init__( - self, - ctx: "ArcadeGlContext", - source: str, - common: Iterable[str] | None, - source_type: PyGLenum, - ): - self._source = source.strip() - self._type = source_type - self._lines = self._source.split("\n") if source else [] - self._out_attributes: list[str] = [] - - if not self._lines: - raise ValueError("Shader source is empty") - - self._version = self._find_glsl_version() - - # GLES specific modifications - if ctx.gl_api == "gles": - # TODO: Use the version from the context - self._lines[0] = "#version 310 es" - self._lines.insert(1, "precision mediump float;") - - if self._type == gl.GL_GEOMETRY_SHADER: - self._lines.insert(1, "#extension GL_EXT_geometry_shader : require") - - if self._type == gl.GL_COMPUTE_SHADER: - self._lines.insert(1, "precision mediump image2D;") - - self._version = self._find_glsl_version() - - # Inject common source - self.inject_common_sources(common) - - if self._type in [gl.GL_VERTEX_SHADER, gl.GL_GEOMETRY_SHADER]: - self._parse_out_attributes() - - @property - def version(self) -> int: - """The glsl version""" - return self._version - - @property - def out_attributes(self) -> list[str]: - """The out attributes for this program""" - return self._out_attributes - - def inject_common_sources(self, common: Iterable[str] | None) -> None: - """ - Inject common source code into the shader source. - - Args: - common: - A list of common source code strings to inject - """ - if not common: - return - - # Find the main function - for line_number, line in enumerate(self._lines): - if "main()" in line: - break - else: - raise ShaderException("No main() function found when injecting common source") - - # Insert all common sources - for source in common: - lines = source.split("\n") - self._lines = self._lines[:line_number] + lines + self._lines[line_number:] - - def get_source(self, *, defines: dict[str, str] | None = None) -> str: - """Return the shader source - - Args: - defines: Defines to replace in the source. - """ - if not defines: - return "\n".join(self._lines) - - lines = ShaderSource.apply_defines(self._lines, defines) - return "\n".join(lines) - - def _find_glsl_version(self) -> int: - if self._lines[0].strip().startswith("#version"): - try: - return int(self._lines[0].split()[1]) - except Exception: - pass - - source = "\n".join(f"{str(i + 1).zfill(3)}: {line} " for i, line in enumerate(self._lines)) - - raise ShaderException( - ( - "Cannot find #version in shader source. " - "Please provide at least a #version 330 statement in the beginning of the shader.\n" - f"---- [{SHADER_TYPE_NAMES[self._type]}] ---\n" - f"{source}" - ) - ) - - @staticmethod - def apply_defines(lines: list[str], defines: dict[str, str]) -> list[str]: - """Locate and apply #define values - - Args: - lines: - List of source lines - defines: - dict with ``name: value`` pairs. - """ - for nr, line in enumerate(lines): - line = line.strip() - if line.startswith("#define"): - try: - name = line.split()[1] - value = defines.get(name, None) - if value is None: - continue - - lines[nr] = "#define {} {}".format(name, str(value)) - except IndexError: - pass - - return lines - - def _parse_out_attributes(self): - """ - Locates out attributes so we don't have to manually supply them. - - Note that this currently doesn't work for structs. - """ - for line in self._lines: - res = re.match(r"(layout(.+)\))?(\s+)?(out)(\s+)(\w+)(\s+)(\w+)", line.strip()) - if res: - self._out_attributes.append(res.groups()[-1]) diff --git a/arcade/gl/texture.py b/arcade/gl/texture.py index 479fa23ce..8a51673bf 100644 --- a/arcade/gl/texture.py +++ b/arcade/gl/texture.py @@ -1,28 +1,22 @@ from __future__ import annotations -import weakref -from ctypes import byref, string_at +from abc import ABC, abstractmethod + from typing import TYPE_CHECKING from pyglet import gl from ..types import BufferProtocol -from .buffer import Buffer from .types import ( BufferOrBufferProtocol, - PyGLuint, - compare_funcs, pixel_formats, - swizzle_enum_to_str, - swizzle_str_to_enum, ) -from .utils import data_to_ctypes if TYPE_CHECKING: # handle import cycle caused by type hinting from arcade.gl import Context -class Texture2D: +class Texture2D(ABC): """ An OpenGL 2D texture. We can create an empty black texture or a texture from byte data. @@ -85,11 +79,9 @@ class Texture2D: __slots__ = ( "_ctx", - "_glo", "_width", "_height", "_dtype", - "_target", "_components", "_alignment", "_depth", @@ -117,25 +109,22 @@ def __init__( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - filter: tuple[PyGLuint, PyGLuint] | None = None, - wrap_x: PyGLuint | None = None, - wrap_y: PyGLuint | None = None, - target=gl.GL_TEXTURE_2D, + filter = None, # TODO: typing, should be tuple[PyGLuint, PyGLuint] + wrap_x = None, # TODO: typing, should be PyGLuint | None + wrap_y = None, # TODO: typing, should be PyGLuint | None depth=False, samples: int = 0, immutable: bool = False, - internal_format: PyGLuint | None = None, + internal_format = None, # TODO: typing, shouldb e PyGLuint | None compressed: bool = False, compressed_data: bool = False, ): - self._glo = glo = gl.GLuint() self._ctx = ctx self._width, self._height = size self._dtype = dtype self._components = components self._component_size = 0 self._alignment = 1 - self._target = target self._samples = min(max(0, samples), self._ctx.info.MAX_SAMPLES) self._depth = depth self._immutable = immutable @@ -144,13 +133,7 @@ def __init__( self._internal_format = internal_format self._compressed = compressed self._compressed_data = compressed_data - # Default filters for float and integer textures - # Integer textures should have NEAREST interpolation - # by default 3.3 core doesn't really support it consistently. - if "f" in self._dtype: - self._filter = gl.GL_LINEAR, gl.GL_LINEAR - else: - self._filter = gl.GL_NEAREST, gl.GL_NEAREST + self._wrap_x = gl.GL_REPEAT self._wrap_y = gl.GL_REPEAT @@ -162,29 +145,9 @@ def __init__( "Multisampled textures are not writable (cannot be initialized with data)" ) - self._target = gl.GL_TEXTURE_2D if self._samples == 0 else gl.GL_TEXTURE_2D_MULTISAMPLE - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glGenTextures(1, byref(self._glo)) - - if self._glo.value == 0: - raise RuntimeError("Cannot create Texture. OpenGL failed to generate a texture id") - - gl.glBindTexture(self._target, self._glo) - - self._texture_2d(data) - - # Only set texture parameters on non-multisample textures - if self._samples == 0: - self.filter = filter or self._filter - self.wrap_x = wrap_x or self._wrap_x - self.wrap_y = wrap_y or self._wrap_y - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, Texture2D.delete_glo, self._ctx, glo) - self.ctx.stats.incr("texture") + @abstractmethod def resize(self, size: tuple[int, int]): """ Resize the texture. This will re-allocate the internal @@ -195,132 +158,13 @@ def resize(self, size: tuple[int, int]): Args: size: The new size of the texture """ - if self._immutable: - raise ValueError("Immutable textures cannot be resized") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - self._width, self._height = size - - self._texture_2d(None) - - def __del__(self): - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: - self._ctx.objects.append(self) - - def _texture_2d(self, data): - """Create a 2D texture""" - # Start by resolving the texture format - try: - format_info = pixel_formats[self._dtype] - except KeyError: - raise ValueError( - f"dype '{self._dtype}' not support. Supported types are : " - f"{tuple(pixel_formats.keys())}" - ) - _format, _internal_format, self._type, self._component_size = format_info - if data is not None: - byte_length, data = data_to_ctypes(data) - self._validate_data_size(data, byte_length, self._width, self._height) - - # If we are dealing with a multisampled texture we have less options - if self._target == gl.GL_TEXTURE_2D_MULTISAMPLE: - gl.glTexImage2DMultisample( - self._target, - self._samples, - _internal_format[self._components], - self._width, - self._height, - True, # Fixed sample locations - ) - return - - # Make sure we unpack the pixel data with correct alignment - # or we'll end up with corrupted textures - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, self._alignment) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, self._alignment) - - # Create depth 2d texture - if self._depth: - gl.glTexImage2D( - self._target, - 0, # level - gl.GL_DEPTH_COMPONENT24, - self._width, - self._height, - 0, - gl.GL_DEPTH_COMPONENT, - gl.GL_UNSIGNED_INT, # gl.GL_FLOAT, - data, - ) - self.compare_func = "<=" - # Create normal 2d texture - else: - try: - self._format = _format[self._components] - if self._internal_format is None: - self._internal_format = _internal_format[self._components] - - if self._immutable: - # Specify immutable storage for this texture. - # glTexStorage2D can only be called once - gl.glTexStorage2D( - self._target, - 1, # Levels - self._internal_format, - self._width, - self._height, - ) - if data: - self.write(data) - else: - # glTexImage2D can be called multiple times to re-allocate storage - # Specify mutable storage for this texture. - if self._compressed_data is True: - gl.glCompressedTexImage2D( - self._target, # target - 0, # level - self._internal_format, # internal_format - self._width, # width - self._height, # height - 0, # border - len(data), # size - data, # data - ) - else: - gl.glTexImage2D( - self._target, # target - 0, # level - self._internal_format, # internal_format - self._width, # width - self._height, # height - 0, # border - self._format, # format - self._type, # type - data, # data - ) - except gl.GLException as ex: - raise gl.GLException( - ( - f"Unable to create texture: {ex} : dtype={self._dtype} " - f"size={self.size} components={self._components} " - f"MAX_TEXTURE_SIZE = {self.ctx.info.MAX_TEXTURE_SIZE}" - f": {ex}" - ) - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def ctx(self) -> Context: """The context this texture belongs to.""" return self._ctx - @property - def glo(self) -> gl.GLuint: - """The OpenGL texture id""" - return self._glo - @property def compressed(self) -> bool: """Is this using a compressed format?""" @@ -377,6 +221,7 @@ def immutable(self) -> bool: return self._immutable @property + @abstractmethod def swizzle(self) -> str: """ The swizzle mask of the texture (Default ``'RGBA'``). @@ -403,49 +248,12 @@ def swizzle(self) -> str: # Reverse the components texture.swizzle = 'ABGR' """ - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - # Read the current swizzle values from the texture - swizzle_r = gl.GLint() - swizzle_g = gl.GLint() - swizzle_b = gl.GLint() - swizzle_a = gl.GLint() - - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_r) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_g) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_b) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_a) - - swizzle_str = "" - for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]: - swizzle_str += swizzle_enum_to_str[v.value] - - return swizzle_str + raise NotImplementedError("The enabled graphics backend does not support this method.") @swizzle.setter + @abstractmethod def swizzle(self, value: str): - if not isinstance(value, str): - raise ValueError(f"Swizzle must be a string, not '{type(str)}'") - - if len(value) != 4: - raise ValueError("Swizzle must be a string of length 4") - - swizzle_enums = [] - for c in value: - try: - c = c.upper() - swizzle_enums.append(swizzle_str_to_enum[c]) - except KeyError: - raise ValueError(f"Swizzle value '{c}' invalid. Must be one of RGBA01") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_enums[3]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def filter(self) -> tuple[int, int]: @@ -479,15 +287,9 @@ def filter(self) -> tuple[int, int]: return self._filter @filter.setter + @abstractmethod def filter(self, value: tuple[int, int]): - if not isinstance(value, tuple) or not len(value) == 2: - raise ValueError("Texture filter must be a 2 component tuple (min, mag)") - - self._filter = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_x(self) -> int: @@ -512,11 +314,9 @@ def wrap_x(self) -> int: return self._wrap_x @wrap_x.setter + @abstractmethod def wrap_x(self, value: int): - self._wrap_x = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_S, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_y(self) -> int: @@ -541,11 +341,9 @@ def wrap_y(self) -> int: return self._wrap_y @wrap_y.setter + @abstractmethod def wrap_y(self, value: int): - self._wrap_y = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_T, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def anisotropy(self) -> float: @@ -553,11 +351,9 @@ def anisotropy(self) -> float: return self._anisotropy @anisotropy.setter + @abstractmethod def anisotropy(self, value): - self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def compare_func(self) -> str | None: @@ -577,28 +373,11 @@ def compare_func(self) -> str | None: return self._compare_func @compare_func.setter + @abstractmethod def compare_func(self, value: str | None): - if not self._depth: - raise ValueError("Depth comparison function can only be set on depth textures") - - if not isinstance(value, str) and value is not None: - raise ValueError(f"value must be as string: {self._compare_funcs.keys()}") - - func = compare_funcs.get(value, None) - if func is None: - raise ValueError(f"value must be as string: {compare_funcs.keys()}") - - self._compare_func = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - if value is None: - gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) - else: - gl.glTexParameteri( - self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE - ) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_FUNC, func) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def read(self, level: int = 0, alignment: int = 1) -> bytes: """ Read the contents of the texture. @@ -610,25 +389,9 @@ def read(self, level: int = 0, alignment: int = 1) -> bytes: Alignment of the start of each row in memory in number of bytes. Possible values: 1,2,4 """ - if self._samples > 0: - raise ValueError("Multisampled textures cannot be read directly") - - if self._ctx.gl_api == "gl": - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, alignment) - - buffer = ( - 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)) - elif self._ctx.gl_api == "gles": - fbo = self._ctx.framebuffer(color_attachments=[self]) - return fbo.read(components=self._components, dtype=self._dtype) - else: - raise ValueError("Unknown gl_api: '{self._ctx.gl_api}'") + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> None: """Write byte data from the passed source to the texture. @@ -651,64 +414,9 @@ def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> The area of the texture to write. 2 or 4 component tuple. (x, y, w, h) or (w, h). Default is the full texture. """ - # TODO: Support writing to layers using viewport + alignment - if self._samples > 0: - raise ValueError("Writing to multisampled textures not supported") - - x, y, w, h = 0, 0, self._width, self._height - if viewport: - if len(viewport) == 2: - w, h = viewport - elif len(viewport) == 4: - x, y, w, h = viewport - else: - raise ValueError("Viewport must be of length 2 or 4") - - if isinstance(data, Buffer): - gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, data.glo) - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glTexSubImage2D(self._target, level, x, y, w, h, self._format, self._type, 0) - gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) - else: - byte_size, data = data_to_ctypes(data) - self._validate_data_size(data, byte_size, w, h) - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glTexSubImage2D( - self._target, # target - level, # level - x, # x offset - y, # y offset - w, # width - h, # height - self._format, # format - self._type, # type - data, # pixel data - ) - - def _validate_data_size(self, byte_data, byte_size, width, height) -> None: - """Validate the size of the data to be written to the texture""" - # TODO: Validate data size for compressed textures - # This might be a bit tricky since the size of the compressed - # data would depend on the algorithm used. - if self._compressed is True: - return - - expected_size = width * height * self._component_size * self._components - if byte_size != expected_size: - raise ValueError( - f"Data size {len(byte_data)} does not match expected size {expected_size}" - ) - if len(byte_data) != byte_size: - raise ValueError( - f"Data size {len(byte_data)} does not match reported size {expected_size}" - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: """Generate mipmaps for this texture. @@ -737,53 +445,27 @@ def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: Also see: https://www.khronos.org/opengl/wiki/Texture#Mip_maps """ - if self._samples > 0: - raise ValueError("Multisampled textures don't support mimpmaps") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(gl.GL_TEXTURE_2D, self._glo) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_BASE_LEVEL, base) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAX_LEVEL, max_level) - gl.glGenerateMipmap(gl.GL_TEXTURE_2D) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def delete(self): """ Destroy the underlying OpenGL resource. Don't use this unless you know exactly what you are doing. """ - Texture2D.delete_glo(self._ctx, self._glo) - self._glo.value = 0 - - @staticmethod - def delete_glo(ctx: "Context", glo: gl.GLuint): - """ - Destroy the texture. - - This is called automatically when the object is garbage collected. - - Args: - ctx: OpenGL Context - glo: The OpenGL texture id - """ - # If we have no context, then we are shutting down, so skip this - if gl.current_context is None: - return - - if glo.value != 0: - gl.glDeleteTextures(1, byref(glo)) - - ctx.stats.decr("texture") + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def use(self, unit: int = 0) -> None: """Bind the texture to a channel, Args: unit: The texture unit to bind the texture. """ - gl.glActiveTexture(gl.GL_TEXTURE0 + unit) - gl.glBindTexture(self._target, self._glo) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: int = 0): """ Bind textures to image units. @@ -797,21 +479,9 @@ def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: write: The compute shader intends to write to this image level: The mipmap level to bind """ - if self._ctx.gl_api == "gles" and not self._immutable: - raise ValueError("Textures bound to image units must be created with immutable=True") - - access = gl.GL_READ_WRITE - if read and write: - access = gl.GL_READ_WRITE - elif read and not write: - access = gl.GL_READ_ONLY - elif not read and write: - access = gl.GL_WRITE_ONLY - else: - raise ValueError("Illegal access mode. The texture must at least be read or write only") - - gl.glBindImageTexture(unit, self._glo, level, 0, 0, access, self._internal_format) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def get_handle(self, resident: bool = True) -> int: """ Get a handle for bindless texture access. @@ -842,20 +512,4 @@ def get_handle(self, resident: bool = True) -> int: Args: resident: Make the texture resident. """ - handle = gl.glGetTextureHandleARB(self._glo) - is_resident = gl.glIsTextureHandleResidentARB(handle) - - # Ensure we don't try to make a resident texture resident again - if resident: - if not is_resident: - gl.glMakeTextureHandleResidentARB(handle) - else: - if is_resident: - gl.glMakeTextureHandleNonResidentARB(handle) - - return handle - - def __repr__(self) -> str: - return "".format( - self._glo.value, self._width, self._height, self._components - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") From 27ff1f343054586f0daa6a071476a4495d794676 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 8 Apr 2025 20:32:18 -0400 Subject: [PATCH 04/16] Framebuffer abstraction --- arcade/gl/backends/gl/context.py | 61 +++- arcade/gl/backends/gl/framebuffer.py | 420 ++++++++++++++++++++++++++ arcade/gl/context.py | 40 +-- arcade/gl/framebuffer.py | 320 +++----------------- arcade/gl/uniform.py | 422 --------------------------- 5 files changed, 522 insertions(+), 741 deletions(-) create mode 100644 arcade/gl/backends/gl/framebuffer.py delete mode 100644 arcade/gl/uniform.py diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 14930f3f8..4671d509f 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -4,6 +4,7 @@ from arcade.context import ArcadeContext import pyglet +from pyglet import gl from arcade.types import BufferProtocol @@ -11,16 +12,21 @@ from .buffer import GLBuffer from .compute_shader import GLComputeShader +from .framebuffer import GLFramebuffer, GLDefaultFrameBuffer from .glsl import ShaderSource from .types import BufferDescription from .program import GLProgram from .texture import GLTexture2D from .vertex_array import GLGeometry + class GLContext(Context): def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): super().__init__(window, gc_mode, gl_api) + def _create_default_framebuffer(self) -> GLDefaultFrameBuffer: + return GLDefaultFrameBuffer(self) + def buffer(self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> GLBuffer: return GLBuffer(self, data, reserve=reserve, usage=usage) @@ -37,24 +43,24 @@ def program( varyings: Sequence[str] | None = None, varyings_capture_mode: str = "interleaved", ) -> GLProgram: - source_vs = ShaderSource(self, vertex_shader, common, pyglet.gl.GL_VERTEX_SHADER) + source_vs = ShaderSource(self, vertex_shader, common, gl.GL_VERTEX_SHADER) source_fs = ( - ShaderSource(self, fragment_shader, common, pyglet.gl.GL_FRAGMENT_SHADER) + ShaderSource(self, fragment_shader, common, gl.GL_FRAGMENT_SHADER) if fragment_shader else None ) source_geo = ( - ShaderSource(self, geometry_shader, common, pyglet.gl.GL_GEOMETRY_SHADER) + ShaderSource(self, geometry_shader, common, gl.GL_GEOMETRY_SHADER) if geometry_shader else None ) source_tc = ( - ShaderSource(self, tess_control_shader, common, pyglet.gl.GL_TESS_CONTROL_SHADER) + ShaderSource(self, tess_control_shader, common, gl.GL_TESS_CONTROL_SHADER) if tess_control_shader else None ) source_te = ( - ShaderSource(self, tess_evaluation_shader, common, pyglet.gl.GL_TESS_EVALUATION_SHADER) + ShaderSource(self, tess_evaluation_shader, common, gl.GL_TESS_EVALUATION_SHADER) if tess_evaluation_shader else None ) @@ -137,6 +143,51 @@ def depth_texture( ) -> GLTexture2D: return GLTexture2D(self, size, data=data, depth=True) + def framebuffer( + self, + *, + color_attachments: GLTexture2D | List[GLTexture2D] | None = None, + depth_attachment: GLTexture2D | None = None, + ) -> GLFramebuffer: + return GLFramebuffer( + self, color_attachments=color_attachments or [], depth_attachment=depth_attachment + ) + + def copy_framebuffer( + self, + src: GLFramebuffer, + dst: GLFramebuffer, + src_attachment_index: int = 0, + depth: bool = True, + ): + # Set source and dest framebuffer + gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src.glo) + gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst.glo) + + # TODO: We can support blitting multiple layers here + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + src_attachment_index) + if dst.is_default: + gl.glDrawBuffer(gl.GL_BACK) + else: + gl.glDrawBuffer(gl.GL_COLOR_ATTACHMENT0) + + # gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, src._glo) + gl.glBlitFramebuffer( + 0, + 0, + src.width, + src.height, # Make source and dest size the same + 0, + 0, + src.width, + src.height, + gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT, + gl.GL_NEAREST, + ) + + # Reset states. We can also apply previous states here + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) diff --git a/arcade/gl/backends/gl/framebuffer.py b/arcade/gl/backends/gl/framebuffer.py new file mode 100644 index 000000000..478c2002c --- /dev/null +++ b/arcade/gl/backends/gl/framebuffer.py @@ -0,0 +1,420 @@ +from __future__ import annotations + +import weakref +from ctypes import Array, c_int, c_uint, string_at +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.types import RGBOrA255, RGBOrANormalized +from arcade.gl.framebuffer import DefaultFrameBuffer, Framebuffer + +from .texture import GLTexture2D +from .types import pixel_formats + +if TYPE_CHECKING: + from arcade.gl import Context + + +class GLFramebuffer(Framebuffer): + """ + An offscreen render target also called a Framebuffer Object in OpenGL. + This implementation is using texture attachments. When creating a + Framebuffer we supply it with textures we want our scene rendered into. + The advantage of using texture attachments is the ability we get + to keep working on the contents of the framebuffer. + + The best way to create framebuffer is through :py:meth:`arcade.gl.Context.framebuffer`:: + + # Create a 100 x 100 framebuffer with one attachment + ctx.framebuffer(color_attachments=[ctx.texture((100, 100), components=4)]) + + # Create a 100 x 100 framebuffer with two attachments + # Shaders can be configured writing to the different layers + ctx.framebuffer( + color_attachments=[ + ctx.texture((100, 100), components=4), + ctx.texture((100, 100), components=4), + ] + ) + + Args: + ctx: + The context this framebuffer belongs to + color_attachments: + A color attachment or a list of color attachments + depth_attachment: + A depth attachment + """ + __slots__ = ( + "_glo" + ) + + def __init__( + self, + ctx: "Context", + *, + color_attachments: GLTexture2D | list[GLTexture2D], + depth_attachment: GLTexture2D | None = None, + ): + super().__init__(ctx, color_attachments=color_attachments, depth_attachment=depth_attachment) + self._glo = fbo_id = gl.GLuint() # The OpenGL alias/name + + # Create the framebuffer object + gl.glGenFramebuffers(1, self._glo) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._glo) + + # Attach textures to it + for i, tex in enumerate(self._color_attachments): + # TODO: Possibly support attaching a specific mipmap level + # but we can read from specific mip levels from shaders. + gl.glFramebufferTexture2D( + gl.GL_FRAMEBUFFER, + gl.GL_COLOR_ATTACHMENT0 + i, + tex._target, + tex.glo, + 0, # Level 0 + ) + + if self.depth_attachment: + gl.glFramebufferTexture2D( + gl.GL_FRAMEBUFFER, + gl.GL_DEPTH_ATTACHMENT, + self.depth_attachment._target, + self.depth_attachment.glo, + 0, + ) + + # Ensure the framebuffer is sane! + self._check_completeness() + + # Set up draw buffers. This is simply a prepared list of attachments enums + # we use in the use() method to activate the different color attachment layers + layers = [gl.GL_COLOR_ATTACHMENT0 + i for i, _ in enumerate(self._color_attachments)] + # pyglet wants this as a ctypes thingy, so let's prepare it + self._draw_buffers: Array[c_uint] | None = (gl.GLuint * len(layers))(*layers) + + # Restore the original bound framebuffer to avoid confusion + self.ctx.active_framebuffer.use(force=True) + + if self._ctx.gc_mode == "auto" and not self.is_default: + weakref.finalize(self, GLFramebuffer.delete_glo, ctx, fbo_id) + + self.ctx.stats.incr("framebuffer") + + def __del__(self): + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and not self.is_default and self._glo.value > 0: + self._ctx.objects.append(self) + + @property + def glo(self) -> gl.GLuint: + """The OpenGL id/name of the framebuffer.""" + return self._glo + + @Framebuffer.viewport.setter + def viewport(self, value: tuple[int, int, int, int]): + if not isinstance(value, tuple) or len(value) != 4: + raise ValueError("viewport should be a 4-component tuple") + + self._viewport = value + + # If the framebuffer is bound we need to set the viewport. + # Otherwise it will be set on use() + if self._ctx.active_framebuffer == self: + gl.glViewport(*self._viewport) + if self._scissor is None: + gl.glScissor(*self._viewport) + else: + gl.glScissor(*self._scissor) + + @Framebuffer.scissor.setter + def scissor(self, value): + self._scissor = value + + if self._scissor is None: + if self._ctx.active_framebuffer == self: + gl.glScissor(*self._viewport) + else: + if self._ctx.active_framebuffer == self: + gl.glScissor(*self._scissor) + + @Framebuffer.depth_mask.setter + def depth_mask(self, value: bool): + self._depth_mask = value + # Set state if framebuffer is active + if self._ctx.active_framebuffer == self: + gl.glDepthMask(self._depth_mask) + + def _use(self, *, force: bool = False): + """Internal use that do not change the global active framebuffer""" + if self.ctx.active_framebuffer == self and not force: + return + + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._glo) + + # NOTE: gl.glDrawBuffer(GL_NONE) if no texture attachments (future) + # NOTE: Default framebuffer currently has this set to None + if self._draw_buffers: + gl.glDrawBuffers(len(self._draw_buffers), self._draw_buffers) + + gl.glDepthMask(self._depth_mask) + gl.glViewport(*self._viewport) + if self._scissor is not None: + gl.glScissor(*self._scissor) + else: + gl.glScissor(*self._viewport) + + def clear( + self, + *, + color: RGBOrA255 | None = None, + color_normalized: RGBOrANormalized | None = None, + depth: float = 1.0, + viewport: tuple[int, int, int, int] | None = None, + ): + """ + Clears the framebuffer:: + + # Clear the framebuffer using Arcade's colors (not normalized) + fb.clear(color=arcade.color.WHITE) + + # Clear framebuffer using the color red in normalized form + fbo.clear(color_normalized=(1.0, 0.0, 0.0, 1.0)) + + If the background color is an ``RGB`` value instead of ``RGBA``` + we assume alpha value 255. + + Args: + color: + A 3 or 4 component tuple containing the color + (prioritized over color_normalized) + color_normalized: + A 3 or 4 component tuple containing the color in normalized form + depth: + Value to clear the depth buffer (unused) + viewport: + The viewport range to clear + """ + with self.activate(): + scissor_values = self._scissor + + if viewport: + self.scissor = viewport + else: + self.scissor = None + + clear_color = 0.0, 0.0, 0.0, 0.0 + if color is not None: + if len(color) == 3: + clear_color = color[0] / 255, color[1] / 255, color[2] / 255, 1.0 + elif len(color) == 4: + clear_color = color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 + else: + raise ValueError("Color should be a 3 or 4 component tuple") + elif color_normalized is not None: + if len(color_normalized) == 3: + clear_color = color_normalized[0], color_normalized[1], color_normalized[2], 1.0 + elif len(color_normalized) == 4: + clear_color = color_normalized + else: + raise ValueError("Color should be a 3 or 4 component tuple") + + gl.glClearColor(*clear_color) + + if self.depth_attachment: + if self._ctx.gl_api == "gl": + gl.glClearDepth(depth) + else: # gles only supports glClearDepthf + gl.glClearDepthf(depth) + + gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) + else: + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + + self.scissor = scissor_values + + def read(self, *, viewport=None, components=3, attachment=0, dtype="f1") -> bytes: + """ + Read the raw framebuffer pixels. + + Reading data from a framebuffer is much more powerful than + reading date from textures. We can specify more or less + what format we want the data. It's not uncommon to throw + textures into framebuffers just to get access to this read + api. + + Args: + viewport: + The x, y, with, height area to read. + components: + The number of components to read. 1, 2, 3 or 4. + This will determine the format to read. + attachment: + The attachment id to read from + dtype: + The data type to read. Pixel data will be converted to this format. + """ + # TODO: use texture attachment info to determine read format? + try: + frmt = pixel_formats[dtype] + base_format = frmt[0][components] + pixel_type = frmt[2] + component_size = frmt[3] + except Exception: + raise ValueError(f"Invalid dtype '{dtype}'") + + with self.activate(): + # Configure attachment to read from. Does not work on default framebuffer. + if not self.is_default: + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) + + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + + if viewport: + x, y, width, height = viewport + else: + x, y, width, height = 0, 0, *self.size + + data = (gl.GLubyte * (components * component_size * width * height))(0) + gl.glReadPixels(x, y, width, height, base_format, pixel_type, data) + + if not self.is_default: + gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default + + return string_at(data, len(data)) + + def delete(self): + """ + Destroy the underlying OpenGL resource. + + .. warning:: Don't use this unless you know exactly what you are doing. + """ + GLFramebuffer.delete_glo(self._ctx, self._glo) + self._glo.value = 0 + + @staticmethod + def delete_glo(ctx, framebuffer_id): + """ + Destroys the framebuffer object + + Args: + ctx: + The context this framebuffer belongs to + framebuffer_id: + Framebuffer id destroy (glo) + """ + if gl.current_context is None: + return + + gl.glDeleteFramebuffers(1, framebuffer_id) + ctx.stats.decr("framebuffer") + + @staticmethod + def _check_completeness() -> None: + """ + Checks the completeness of the framebuffer. + + If the framebuffer is not complete, we cannot continue. + """ + # See completeness rules : https://www.khronos.org/opengl/wiki/Framebuffer_Object + states = { + gl.GL_FRAMEBUFFER_UNSUPPORTED: "Framebuffer unsupported. Try another format.", + gl.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: "Framebuffer incomplete attachment.", + gl.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: "Framebuffer missing attachment.", + gl.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: "Framebuffer unsupported dimension.", + gl.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: "Framebuffer incomplete formats.", + gl.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: "Framebuffer incomplete draw buffer.", + gl.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: "Framebuffer incomplete read buffer.", + gl.GL_FRAMEBUFFER_COMPLETE: "Framebuffer is complete.", + } + + status = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) + if status != gl.GL_FRAMEBUFFER_COMPLETE: + raise ValueError( + "Framebuffer is incomplete. {}".format(states.get(status, "Unknown error")) + ) + + def __repr__(self): + return "".format(self._glo.value) + + +class GLDefaultFrameBuffer(DefaultFrameBuffer, GLFramebuffer): + """ + Represents the default framebuffer. + + This is the framebuffer of the window itself and need some special handling. + + We are not allowed to destroy this framebuffer since it's owned by pyglet. + This framebuffer can also change size and pixel ratio at any point. + + We're doing some initial introspection to guess somewhat sane initial values. + Since this is a dynamic framebuffer we cannot trust the internal values. + We can only trust what the pyglet window itself reports related to window size + and framebuffer size. This should be updated in the ``on_resize`` callback. + """ + + is_default = True + """Is this the default framebuffer? (window buffer)""" + + def __init__(self, ctx: "Context"): + super().__init__(ctx) + + value = c_int() + gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING, value) + self._glo = gl.GLuint(value.value) + + # Query viewport values by inspecting the scissor box + values = (c_int * 4)() + gl.glGetIntegerv(gl.GL_SCISSOR_BOX, values) + x, y, width, height = list(values) + + self._viewport = x, y, width, height + self._scissor = None + self._width = width + self._height = height + + @DefaultFrameBuffer.viewport.setter + def viewport(self, value: tuple[int, int, int, int]): + if not isinstance(value, tuple) or len(value) != 4: + raise ValueError("viewport should be a 4-component tuple") + + ratio = self.ctx.window.get_pixel_ratio() + self._viewport = ( + int(value[0] * ratio), + int(value[1] * ratio), + int(value[2] * ratio), + int(value[3] * ratio), + ) + + # If the framebuffer is bound we need to set the viewport. + # Otherwise it will be set on use() + if self._ctx.active_framebuffer == self: + gl.glViewport(*self._viewport) + if self._scissor is None: + # FIXME: Probably should be set to the framebuffer size + gl.glScissor(*self._viewport) + else: + gl.glScissor(*self._scissor) + + @DefaultFrameBuffer.scissor.setter + def scissor(self, value): + if value is None: + # FIXME: Do we need to reset something here? + self._scissor = None + if self._ctx.active_framebuffer == self: + gl.glScissor(*self._viewport) + else: + ratio = self.ctx.window.get_pixel_ratio() + self._scissor = ( + int(value[0] * ratio), + int(value[1] * ratio), + int(value[2] * ratio), + int(value[3] * ratio), + ) + + # If the framebuffer is bound we need to set the scissor box. + # Otherwise it will be set on use() + if self._ctx.active_framebuffer == self: + gl.glScissor(*self._scissor) diff --git a/arcade/gl/context.py b/arcade/gl/context.py index f9ec42290..c074ba059 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -242,7 +242,7 @@ def __init__( self.default_texture_unit: int = self._info.MAX_TEXTURE_IMAGE_UNITS - 1 # Detect the default framebuffer - self._screen = DefaultFrameBuffer(self) + self._screen = self._create_default_framebuffer() # Tracking active program self.active_program: Program | ComputeShader | None = None # Tracking active framebuffer. On context creation the window is the default render target @@ -297,6 +297,10 @@ def __init__( #: This can be used during debugging. self.objects: Deque[Any] = deque() + @abstractmethod + def _create_default_framebuffer(self) -> DefaultFrameBuffer: + raise NotImplementedError("The enabled graphics backend does not support this method.") + @property def info(self) -> GLInfo: """ @@ -839,6 +843,7 @@ def flush(self) -> None: # Various utility methods + @abstractmethod def copy_framebuffer( self, src: Framebuffer, @@ -869,33 +874,7 @@ def copy_framebuffer( depth: Also copy depth attachment if present """ - # Set source and dest framebuffer - gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src._glo) - gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst._glo) - - # TODO: We can support blitting multiple layers here - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + src_attachment_index) - if dst.is_default: - gl.glDrawBuffer(gl.GL_BACK) - else: - gl.glDrawBuffer(gl.GL_COLOR_ATTACHMENT0) - - # gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, src._glo) - gl.glBlitFramebuffer( - 0, - 0, - src.width, - src.height, # Make source and dest size the same - 0, - 0, - src.width, - src.height, - gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT, - gl.GL_NEAREST, - ) - - # Reset states. We can also apply previous states here - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) + raise NotImplementedError("The enabled graphics backend does not support this method.") # --- Resource methods --- @@ -951,6 +930,7 @@ def buffer( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def framebuffer( self, *, @@ -965,9 +945,7 @@ def framebuffer( depth_attachment: Depth texture """ - return Framebuffer( - self, color_attachments=color_attachments or [], depth_attachment=depth_attachment - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod def texture( diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index 188c5a563..3cd7c2c51 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -1,22 +1,19 @@ from __future__ import annotations -import weakref +from abc import ABC, abstractmethod + from contextlib import contextmanager -from ctypes import Array, c_int, c_uint, string_at from typing import TYPE_CHECKING, Generator -from pyglet import gl - from arcade.types import RGBOrA255, RGBOrANormalized from .texture import Texture2D -from .types import pixel_formats if TYPE_CHECKING: from arcade.gl import Context -class Framebuffer: +class Framebuffer(ABC): """ An offscreen render target also called a Framebuffer Object in OpenGL. This implementation is using texture attachments. When creating a @@ -51,7 +48,6 @@ class Framebuffer: is_default = False __slots__ = ( "_ctx", - "_glo", "_width", "_height", "_color_attachments", @@ -72,7 +68,6 @@ def __init__( color_attachments: Texture2D | list[Texture2D], depth_attachment: Texture2D | None = None, ): - self._glo = fbo_id = gl.GLuint() # The OpenGL alias/name self._ctx = ctx if not color_attachments: raise ValueError("Framebuffer must at least have one color attachment") @@ -85,10 +80,6 @@ def __init__( self._depth_mask = True # Determines if the depth buffer should be affected self._prev_fbo = None - # Create the framebuffer object - gl.glGenFramebuffers(1, self._glo) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._glo) - # Ensure all attachments have the same size. # OpenGL do actually support different sizes, # but let's keep this simple with high compatibility. @@ -96,55 +87,10 @@ def __init__( self._viewport = 0, 0, self._width, self._height self._scissor: tuple[int, int, int, int] | None = None - # Attach textures to it - for i, tex in enumerate(self._color_attachments): - # TODO: Possibly support attaching a specific mipmap level - # but we can read from specific mip levels from shaders. - gl.glFramebufferTexture2D( - gl.GL_FRAMEBUFFER, - gl.GL_COLOR_ATTACHMENT0 + i, - tex._target, - tex.glo, - 0, # Level 0 - ) - - if self.depth_attachment: - gl.glFramebufferTexture2D( - gl.GL_FRAMEBUFFER, - gl.GL_DEPTH_ATTACHMENT, - self.depth_attachment._target, - self.depth_attachment.glo, - 0, - ) - - # Ensure the framebuffer is sane! - self._check_completeness() - - # Set up draw buffers. This is simply a prepared list of attachments enums - # we use in the use() method to activate the different color attachment layers - layers = [gl.GL_COLOR_ATTACHMENT0 + i for i, _ in enumerate(self._color_attachments)] - # pyglet wants this as a ctypes thingy, so let's prepare it - self._draw_buffers: Array[c_uint] | None = (gl.GLuint * len(layers))(*layers) - - # Restore the original bound framebuffer to avoid confusion - self.ctx.active_framebuffer.use(force=True) - - if self._ctx.gc_mode == "auto" and not self.is_default: - weakref.finalize(self, Framebuffer.delete_glo, ctx, fbo_id) - self.ctx.stats.incr("framebuffer") - def __del__(self): - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and not self.is_default and self._glo.value > 0: - self._ctx.objects.append(self) - @property - def glo(self) -> gl.GLuint: - """The OpenGL id/name of the framebuffer.""" - return self._glo - - def _get_viewport(self) -> tuple[int, int, int, int]: + def viewport(self) -> tuple[int, int, int, int]: """ Get or set the framebuffer's viewport. @@ -162,24 +108,13 @@ def _get_viewport(self) -> tuple[int, int, int, int]: """ return self._viewport - def _set_viewport(self, value: tuple[int, int, int, int]): - if not isinstance(value, tuple) or len(value) != 4: - raise ValueError("viewport should be a 4-component tuple") + @viewport.setter + @abstractmethod + def viewport(self, value: tuple[int, int, int, int]): + raise NotImplementedError("The enabled graphics backend does not support this method.") - self._viewport = value - - # If the framebuffer is bound we need to set the viewport. - # Otherwise it will be set on use() - if self._ctx.active_framebuffer == self: - gl.glViewport(*self._viewport) - if self._scissor is None: - gl.glScissor(*self._viewport) - else: - gl.glScissor(*self._scissor) - - viewport = property(_get_viewport, _set_viewport) - - def _get_scissor(self) -> tuple[int, int, int, int] | None: + @property + def scissor(self) -> tuple[int, int, int, int] | None: """ Get or set the scissor box for this framebuffer. @@ -199,17 +134,10 @@ def _get_scissor(self) -> tuple[int, int, int, int] | None: """ return self._scissor - def _set_scissor(self, value): - self._scissor = value - - if self._scissor is None: - if self._ctx.active_framebuffer == self: - gl.glScissor(*self._viewport) - else: - if self._ctx.active_framebuffer == self: - gl.glScissor(*self._scissor) - - scissor = property(_get_scissor, _set_scissor) + @scissor.setter + @abstractmethod + def scissor(self, value): + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def ctx(self) -> "Context": @@ -260,11 +188,9 @@ def depth_mask(self) -> bool: return self._depth_mask @depth_mask.setter + @abstractmethod def depth_mask(self, value: bool): - self._depth_mask = value - # Set state if framebuffer is active - if self._ctx.active_framebuffer == self: - gl.glDepthMask(self._depth_mask) + raise NotImplementedError("The enabled graphics backend does not support this method.") def __enter__(self): self._prev_fbo = self._ctx.active_framebuffer @@ -300,25 +226,12 @@ def use(self, *, force: bool = False): self._use(force=force) self._ctx.active_framebuffer = self + @abstractmethod def _use(self, *, force: bool = False): """Internal use that do not change the global active framebuffer""" - if self.ctx.active_framebuffer == self and not force: - return - - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._glo) - - # NOTE: gl.glDrawBuffer(GL_NONE) if no texture attachments (future) - # NOTE: Default framebuffer currently has this set to None - if self._draw_buffers: - gl.glDrawBuffers(len(self._draw_buffers), self._draw_buffers) - - gl.glDepthMask(self._depth_mask) - gl.glViewport(*self._viewport) - if self._scissor is not None: - gl.glScissor(*self._scissor) - else: - gl.glScissor(*self._viewport) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def clear( self, *, @@ -350,44 +263,9 @@ def clear( viewport: The viewport range to clear """ - with self.activate(): - scissor_values = self._scissor - - if viewport: - self.scissor = viewport - else: - self.scissor = None - - clear_color = 0.0, 0.0, 0.0, 0.0 - if color is not None: - if len(color) == 3: - clear_color = color[0] / 255, color[1] / 255, color[2] / 255, 1.0 - elif len(color) == 4: - clear_color = color[0] / 255, color[1] / 255, color[2] / 255, color[3] / 255 - else: - raise ValueError("Color should be a 3 or 4 component tuple") - elif color_normalized is not None: - if len(color_normalized) == 3: - clear_color = color_normalized[0], color_normalized[1], color_normalized[2], 1.0 - elif len(color_normalized) == 4: - clear_color = color_normalized - else: - raise ValueError("Color should be a 3 or 4 component tuple") - - gl.glClearColor(*clear_color) - - if self.depth_attachment: - if self._ctx.gl_api == "gl": - gl.glClearDepth(depth) - else: # gles only supports glClearDepthf - gl.glClearDepthf(depth) - - gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT) - else: - gl.glClear(gl.GL_COLOR_BUFFER_BIT) - - self.scissor = scissor_values + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def read(self, *, viewport=None, components=3, attachment=0, dtype="f1") -> bytes: """ Read the raw framebuffer pixels. @@ -409,35 +287,7 @@ def read(self, *, viewport=None, components=3, attachment=0, dtype="f1") -> byte dtype: The data type to read. Pixel data will be converted to this format. """ - # TODO: use texture attachment info to determine read format? - try: - frmt = pixel_formats[dtype] - base_format = frmt[0][components] - pixel_type = frmt[2] - component_size = frmt[3] - except Exception: - raise ValueError(f"Invalid dtype '{dtype}'") - - with self.activate(): - # Configure attachment to read from. Does not work on default framebuffer. - if not self.is_default: - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + attachment) - - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - if viewport: - x, y, width, height = viewport - else: - x, y, width, height = 0, 0, *self.size - - data = (gl.GLubyte * (components * component_size * width * height))(0) - gl.glReadPixels(x, y, width, height, base_format, pixel_type, data) - - if not self.is_default: - gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) # Reset to default - - return string_at(data, len(data)) + raise NotImplementedError("The enabled graphics backend does not support this method.") def resize(self): """ @@ -448,31 +298,14 @@ def resize(self): self._width, self._height = self._detect_size() self.viewport = 0, 0, self.width, self._height + @abstractmethod def delete(self): """ Destroy the underlying OpenGL resource. .. warning:: Don't use this unless you know exactly what you are doing. """ - Framebuffer.delete_glo(self._ctx, self._glo) - self._glo.value = 0 - - @staticmethod - def delete_glo(ctx, framebuffer_id): - """ - Destroys the framebuffer object - - Args: - ctx: - The context this framebuffer belongs to - framebuffer_id: - Framebuffer id destroy (glo) - """ - if gl.current_context is None: - return - - gl.glDeleteFramebuffers(1, framebuffer_id) - ctx.stats.decr("framebuffer") + raise NotImplementedError("The enabled graphics backend does not support this method.") def _detect_size(self) -> tuple[int, int]: """Detect the size of the framebuffer based on the attachments""" @@ -492,36 +325,7 @@ def _detect_size(self) -> tuple[int, int]: raise ValueError("All framebuffer attachments should have the same size") return expected_size - @staticmethod - def _check_completeness() -> None: - """ - Checks the completeness of the framebuffer. - - If the framebuffer is not complete, we cannot continue. - """ - # See completeness rules : https://www.khronos.org/opengl/wiki/Framebuffer_Object - states = { - gl.GL_FRAMEBUFFER_UNSUPPORTED: "Framebuffer unsupported. Try another format.", - gl.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: "Framebuffer incomplete attachment.", - gl.GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: "Framebuffer missing attachment.", - gl.GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: "Framebuffer unsupported dimension.", - gl.GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: "Framebuffer incomplete formats.", - gl.GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: "Framebuffer incomplete draw buffer.", - gl.GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: "Framebuffer incomplete read buffer.", - gl.GL_FRAMEBUFFER_COMPLETE: "Framebuffer is complete.", - } - - status = gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) - if status != gl.GL_FRAMEBUFFER_COMPLETE: - raise ValueError( - "Framebuffer is incomplete. {}".format(states.get(status, "Unknown error")) - ) - - def __repr__(self): - return "".format(self._glo.value) - - -class DefaultFrameBuffer(Framebuffer): +class DefaultFrameBuffer(Framebuffer, ABC): """ Represents the default framebuffer. @@ -539,8 +343,6 @@ class DefaultFrameBuffer(Framebuffer): is_default = True """Is this the default framebuffer? (window buffer)""" - __slots__ = () - def __init__(self, ctx: "Context"): self._ctx = ctx # TODO: Can we query this? @@ -550,10 +352,6 @@ def __init__(self, ctx: "Context"): self._depth_attachment = None self._depth_mask = True - value = c_int() - gl.glGetIntegerv(gl.GL_DRAW_FRAMEBUFFER_BINDING, value) - self._glo = gl.GLuint(value.value) - # Query draw buffers. This will most likely return GL_BACK # value = c_int() # gl.glGetIntegerv(gl.GL_DRAW_BUFFER0, value) @@ -562,16 +360,6 @@ def __init__(self, ctx: "Context"): # NOTE: Don't query for now self._draw_buffers = None - # Query viewport values by inspecting the scissor box - values = (c_int * 4)() - gl.glGetIntegerv(gl.GL_SCISSOR_BOX, values) - x, y, width, height = list(values) - - self._viewport = x, y, width, height - self._scissor = None - self._width = width - self._height = height - # HACK: Signal the default framebuffer having depth buffer self._depth_attachment = True # type: ignore @@ -594,7 +382,8 @@ def _get_framebuffer_size(self) -> tuple[int, int]: """Get the framebuffer size of the window""" return self._ctx.window.get_framebuffer_size() - def _get_viewport(self) -> tuple[int, int, int, int]: + @Framebuffer.viewport.getter + def viewport(self) -> tuple[int, int, int, int]: """ Get or set the framebuffer's viewport. The viewport parameter are ``(x, y, width, height)``. @@ -617,31 +406,13 @@ def _get_viewport(self) -> tuple[int, int, int, int]: int(self._viewport[3] / ratio), ) - def _set_viewport(self, value: tuple[int, int, int, int]): - if not isinstance(value, tuple) or len(value) != 4: - raise ValueError("viewport should be a 4-component tuple") - - ratio = self.ctx.window.get_pixel_ratio() - self._viewport = ( - int(value[0] * ratio), - int(value[1] * ratio), - int(value[2] * ratio), - int(value[3] * ratio), - ) - - # If the framebuffer is bound we need to set the viewport. - # Otherwise it will be set on use() - if self._ctx.active_framebuffer == self: - gl.glViewport(*self._viewport) - if self._scissor is None: - # FIXME: Probably should be set to the framebuffer size - gl.glScissor(*self._viewport) - else: - gl.glScissor(*self._scissor) - - viewport = property(_get_viewport, _set_viewport) + @Framebuffer.viewport.setter + @abstractmethod + def viewport(self, value: tuple[int, int, int, int]): + raise NotImplementedError("The enabled graphics backend does not support this method.") - def _get_scissor(self) -> tuple[int, int, int, int] | None: + @Framebuffer.scissor.getter + def scissor(self) -> tuple[int, int, int, int] | None: """ Get or set the scissor box for this framebuffer. @@ -667,24 +438,7 @@ def _get_scissor(self) -> tuple[int, int, int, int] | None: int(self._scissor[3] / ratio), ) - def _set_scissor(self, value): - if value is None: - # FIXME: Do we need to reset something here? - self._scissor = None - if self._ctx.active_framebuffer == self: - gl.glScissor(*self._viewport) - else: - ratio = self.ctx.window.get_pixel_ratio() - self._scissor = ( - int(value[0] * ratio), - int(value[1] * ratio), - int(value[2] * ratio), - int(value[3] * ratio), - ) - - # If the framebuffer is bound we need to set the scissor box. - # Otherwise it will be set on use() - if self._ctx.active_framebuffer == self: - gl.glScissor(*self._scissor) - - scissor = property(_get_scissor, _set_scissor) + @Framebuffer.scissor.setter + @abstractmethod + def scissor(self, value): + raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/uniform.py b/arcade/gl/uniform.py deleted file mode 100644 index 87d9e3a26..000000000 --- a/arcade/gl/uniform.py +++ /dev/null @@ -1,422 +0,0 @@ -import struct -from ctypes import POINTER, c_double, c_float, c_int, c_uint, cast -from typing import Callable - -from pyglet import gl - -from .exceptions import ShaderException - - -class Uniform: - """ - A Program uniform - - Args: - ctx: - The context - program_id: - The program id to which this uniform belongs - location: - The uniform location - name: - The uniform name - data_type: - The data type of the uniform - array_length: - The array length of the uniform - """ - - _type_to_struct = { - c_float: "f", - c_int: "i", - c_uint: "I", - c_double: "d", - } - - _uniform_getters = { - gl.GLint: gl.glGetUniformiv, - gl.GLuint: gl.glGetUniformuiv, - gl.GLfloat: gl.glGetUniformfv, - } - - _uniform_setters = { - # uniform type: (gl_type, setter, length, count) - # Integers 32 bit - gl.GL_INT: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_INT_VEC2: (gl.GLint, gl.glProgramUniform2iv, gl.glUniform2iv, 2, 1), - gl.GL_INT_VEC3: (gl.GLint, gl.glProgramUniform3iv, gl.glUniform3iv, 3, 1), - gl.GL_INT_VEC4: (gl.GLint, gl.glProgramUniform4iv, gl.glUniform4iv, 4, 1), - # Unsigned integers 32 bit - gl.GL_UNSIGNED_INT: (gl.GLuint, gl.glProgramUniform1uiv, gl.glUniform1uiv, 1, 1), - gl.GL_UNSIGNED_INT_VEC2: (gl.GLuint, gl.glProgramUniform2uiv, gl.glUniform2uiv, 2, 1), - gl.GL_UNSIGNED_INT_VEC3: (gl.GLuint, gl.glProgramUniform3uiv, gl.glUniform3uiv, 3, 1), - gl.GL_UNSIGNED_INT_VEC4: (gl.GLuint, gl.glProgramUniform4uiv, gl.glUniform4uiv, 4, 1), - # Integers 64 bit unsigned - gl.GL_INT64_ARB: (gl.GLint64, gl.glProgramUniform1i64vARB, gl.glUniform1i64vARB, 1, 1), - gl.GL_INT64_VEC2_ARB: (gl.GLint64, gl.glProgramUniform2i64vARB, gl.glUniform2i64vARB, 2, 1), - gl.GL_INT64_VEC3_ARB: (gl.GLint64, gl.glProgramUniform3i64vARB, gl.glUniform3i64vARB, 3, 1), - gl.GL_INT64_VEC4_ARB: (gl.GLint64, gl.glProgramUniform4i64vARB, gl.glUniform4i64vARB, 4, 1), - # Unsigned integers 64 bit - gl.GL_UNSIGNED_INT64_ARB: ( - gl.GLuint64, - gl.glProgramUniform1ui64vARB, - gl.glUniform1ui64vARB, - 1, - 1, - ), - gl.GL_UNSIGNED_INT64_VEC2_ARB: ( - gl.GLuint64, - gl.glProgramUniform2ui64vARB, - gl.glUniform2ui64vARB, - 2, - 1, - ), - gl.GL_UNSIGNED_INT64_VEC3_ARB: ( - gl.GLuint64, - gl.glProgramUniform3ui64vARB, - gl.glUniform3ui64vARB, - 3, - 1, - ), - gl.GL_UNSIGNED_INT64_VEC4_ARB: ( - gl.GLuint64, - gl.glProgramUniform4ui64vARB, - gl.glUniform4ui64vARB, - 4, - 1, - ), - # Bools - gl.GL_BOOL: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_BOOL_VEC2: (gl.GLint, gl.glProgramUniform2iv, gl.glUniform2iv, 2, 1), - gl.GL_BOOL_VEC3: (gl.GLint, gl.glProgramUniform3iv, gl.glUniform3iv, 3, 1), - gl.GL_BOOL_VEC4: (gl.GLint, gl.glProgramUniform4iv, gl.glUniform4iv, 4, 1), - # Floats 32 bit - gl.GL_FLOAT: (gl.GLfloat, gl.glProgramUniform1fv, gl.glUniform1fv, 1, 1), - gl.GL_FLOAT_VEC2: (gl.GLfloat, gl.glProgramUniform2fv, gl.glUniform2fv, 2, 1), - gl.GL_FLOAT_VEC3: (gl.GLfloat, gl.glProgramUniform3fv, gl.glUniform3fv, 3, 1), - gl.GL_FLOAT_VEC4: (gl.GLfloat, gl.glProgramUniform4fv, gl.glUniform4fv, 4, 1), - # Floats 64 bit - gl.GL_DOUBLE: (gl.GLdouble, gl.glProgramUniform1dv, gl.glUniform1dv, 1, 1), - gl.GL_DOUBLE_VEC2: (gl.GLdouble, gl.glProgramUniform2dv, gl.glUniform2dv, 2, 1), - gl.GL_DOUBLE_VEC3: (gl.GLdouble, gl.glProgramUniform3dv, gl.glUniform3dv, 3, 1), - gl.GL_DOUBLE_VEC4: (gl.GLdouble, gl.glProgramUniform4dv, gl.glUniform4dv, 4, 1), - # 1D Samplers - gl.GL_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_INT_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_UNSIGNED_INT_SAMPLER_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_TEXTURE_1D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - # 2D samplers - gl.GL_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_SAMPLER_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_INT_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_UNSIGNED_INT_SAMPLER_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_TEXTURE_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - # Array - gl.GL_SAMPLER_2D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY: ( - gl.GLint, - gl.glProgramUniform1iv, - gl.glUniform1iv, - 1, - 1, - ), - # 3D - gl.GL_SAMPLER_3D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - # Cube - gl.GL_SAMPLER_CUBE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_TEXTURE_CUBE_MAP_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - # Matrices - gl.GL_FLOAT_MAT2: (gl.GLfloat, gl.glProgramUniformMatrix2fv, gl.glUniformMatrix2fv, 4, 1), - gl.GL_FLOAT_MAT3: (gl.GLfloat, gl.glProgramUniformMatrix3fv, gl.glUniformMatrix3fv, 9, 1), - gl.GL_FLOAT_MAT4: (gl.GLfloat, gl.glProgramUniformMatrix4fv, gl.glUniformMatrix4fv, 16, 1), - # Image (compute shader) - gl.GL_IMAGE_1D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_2D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_2D_RECT: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_3D: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_CUBE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_1D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_2D_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_CUBE_MAP_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_2D_MULTISAMPLE: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_2D_MULTISAMPLE_ARRAY: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - gl.GL_IMAGE_BUFFER: (gl.GLint, gl.glProgramUniform1iv, gl.glUniform1iv, 1, 1), - # TODO: test/implement these: - # gl.GL_FLOAT_MAT2x3: glUniformMatrix2x3fv, - # gl.GL_FLOAT_MAT2x4: glUniformMatrix2x4fv, - # - # gl.GL_FLOAT_MAT3x2: glUniformMatrix3x2fv, - # gl.GL_FLOAT_MAT3x4: glUniformMatrix3x4fv, - # - # gl.GL_FLOAT_MAT4x2: glUniformMatrix4x2fv, - # gl.GL_FLOAT_MAT4x3: glUniformMatrix4x3fv, - } - - __slots__ = ( - "_program_id", - "_location", - "_name", - "_data_type", - "_array_length", - "_components", - "getter", - "setter", - "_ctx", - ) - - def __init__(self, ctx, program_id, location, name, data_type, array_length): - self._ctx = ctx - self._program_id = program_id - self._location = location - self._name = name - self._data_type = data_type - # Array length of the uniform (1 if no array) - self._array_length = array_length - # Number of components (including per array entry) - self._components = 0 - self.getter: Callable - """The getter function configured for this uniform""" - self.setter: Callable - """The setter function configured for this uniform""" - self._setup_getters_and_setters() - - @property - def location(self) -> int: - """The location of the uniform in the program""" - return self._location - - @property - def name(self) -> str: - """Name of the uniform""" - return self._name - - @property - def array_length(self) -> int: - """Length of the uniform array. If not an array 1 will be returned""" - return self._array_length - - @property - def components(self) -> int: - """ - How many components for the uniform. - - A vec4 will for example have 4 components. - """ - return self._components - - def _setup_getters_and_setters(self): - """Maps the right getter and setter functions for this uniform""" - try: - gl_type, gl_program_setter, gl_setter, length, count = self._uniform_setters[ - self._data_type - ] - self._components = length - except KeyError: - raise ShaderException(f"Unsupported Uniform type: {self._data_type}") - - gl_getter = self._uniform_getters[gl_type] - is_matrix = self._data_type in ( - gl.GL_FLOAT_MAT2, - gl.GL_FLOAT_MAT3, - gl.GL_FLOAT_MAT4, - ) - - # Create persistent mini c_array for getters and setters: - length = length * self._array_length # Increase buffer size to include arrays - c_array = (gl_type * length)() - ptr = cast(c_array, POINTER(gl_type)) - - # Create custom dedicated getters and setters for each uniform: - self.getter = Uniform._create_getter_func( - self._program_id, - self._location, - gl_getter, - c_array, - length, - ) - - self.setter = Uniform._create_setter_func( - self._ctx, - self._program_id, - self._location, - gl_program_setter, - gl_setter, - c_array, - gl_type, - length, - self._array_length, - count, - ptr, - is_matrix, - ) - - @staticmethod - def _create_getter_func(program_id, location, gl_getter, c_array, length): - """Create a function for getting/setting OpenGL data.""" - - def getter_func1(): - """Get single-element OpenGL uniform data.""" - gl_getter(program_id, location, c_array) - return c_array[0] - - def getter_func2(): - """Get list of OpenGL uniform data.""" - gl_getter(program_id, location, c_array) - return tuple(c_array) - - if length == 1: - return getter_func1 - else: - return getter_func2 - - @classmethod - def _create_setter_func( - cls, - ctx, - program_id, - location, - gl_program_setter, - gl_setter, - c_array, - gl_type, - length, - array_length, - count, - ptr, - is_matrix, - ): - """Create setters for OpenGL data.""" - # Matrix uniforms - if is_matrix: - if ctx._ext_separate_shader_objects_enabled: - - def setter_func(value): # type: ignore #conditional function variants must have identical signature - """Set OpenGL matrix uniform data.""" - try: - # FIXME: Configure the struct format on the uniform to support - # other types than float - fmt = cls._type_to_struct[gl_type] - c_array[:] = struct.unpack(f"{length}{fmt}", value) - except Exception: - c_array[:] = value - gl_program_setter(program_id, location, array_length, gl.GL_FALSE, ptr) - - else: - - def setter_func(value): # type: ignore #conditional function variants must have identical signature - """Set OpenGL matrix uniform data.""" - c_array[:] = value - gl.glUseProgram(program_id) - gl_setter(location, array_length, gl.GL_FALSE, ptr) - - # Single value uniforms - elif length == 1 and count == 1: - if ctx._ext_separate_shader_objects_enabled: - - def setter_func(value): # type: ignore #conditional function variants must have identical signature - """Set OpenGL uniform data value.""" - c_array[0] = value - gl_program_setter(program_id, location, array_length, ptr) - - else: - - def setter_func(value): # type: ignore #conditional function variants must have identical signature - """Set OpenGL uniform data value.""" - c_array[0] = value - gl.glUseProgram(program_id) - gl_setter(location, array_length, ptr) - - # Uniforms types with multiple components - elif length > 1 and count == 1: - if ctx._ext_separate_shader_objects_enabled: - - def setter_func(values): # type: ignore #conditional function variants must have identical signature - """Set list of OpenGL uniform data.""" - # Support buffer protocol - try: - # FIXME: Configure the struct format on the uniform to support - # other types than float - fmt = cls._type_to_struct[gl_type] - c_array[:] = struct.unpack(f"{length}{fmt}", values) - except Exception: - c_array[:] = values - - gl_program_setter(program_id, location, array_length, ptr) - - else: - - def setter_func(values): # type: ignore #conditional function variants must have identical signature - """Set list of OpenGL uniform data.""" - c_array[:] = values - gl.glUseProgram(program_id) - gl_setter(location, array_length, ptr) - - else: - raise NotImplementedError("Uniform type not yet supported.") - - return setter_func - - def __repr__(self) -> str: - return f"" - - -class UniformBlock: - """ - Wrapper for a uniform block in shaders. - - Args: - glo: - The OpenGL object handle - index: - The index of the uniform block - size: - The size of the uniform block - name: - The name of the uniform - """ - - __slots__ = ("glo", "index", "size", "name") - - def __init__(self, glo: int, index: int, size: int, name: str): - self.glo = glo - """The OpenGL object handle""" - - self.index = index - """The index of the uniform block""" - - self.size = size - """The size of the uniform block""" - - self.name = name - """The name of the uniform block""" - - @property - def binding(self) -> int: - """Get or set the binding index for this uniform block""" - binding = gl.GLint() - gl.glGetActiveUniformBlockiv(self.glo, self.index, gl.GL_UNIFORM_BLOCK_BINDING, binding) - return binding.value - - @binding.setter - def binding(self, binding: int): - gl.glUniformBlockBinding(self.glo, self.index, binding) - - def getter(self): - """ - The getter function for this uniform block. - - Returns self. - """ - return self - - def setter(self, value: int): - """ - The setter function for this uniform block. - - Args: - value: The binding index to set. - """ - self.binding = value - - def __str__(self) -> str: - return f"" From 0f70118f8bc1921c1b32c3da790d0c5ea9cd69d5 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 8 Apr 2025 21:07:19 -0400 Subject: [PATCH 05/16] Abstract samplers --- arcade/gl/backends/gl/context.py | 11 +++ arcade/gl/backends/gl/sampler.py | 145 +++++++++++++++++++++++++++++++ arcade/gl/context.py | 3 +- arcade/gl/sampler.py | 102 ++++++---------------- 4 files changed, 183 insertions(+), 78 deletions(-) create mode 100644 arcade/gl/backends/gl/sampler.py diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 4671d509f..772e05b11 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -16,6 +16,7 @@ from .glsl import ShaderSource from .types import BufferDescription from .program import GLProgram +from .sampler import GLSampler from .texture import GLTexture2D from .vertex_array import GLGeometry @@ -188,6 +189,16 @@ def copy_framebuffer( # Reset states. We can also apply previous states here gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0) + def sampler(self, texture: GLTexture2D) -> GLSampler: + """ + Create a sampler object for a texture. + + Args: + texture: + The texture to create a sampler for + """ + return GLSampler(self, texture) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) diff --git a/arcade/gl/backends/gl/sampler.py b/arcade/gl/backends/gl/sampler.py new file mode 100644 index 000000000..82f7ae16a --- /dev/null +++ b/arcade/gl/backends/gl/sampler.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import weakref +from ctypes import byref, c_uint32 +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.gl.sampler import Sampler + +from .types import PyGLuint, compare_funcs + +if TYPE_CHECKING: + from arcade.gl import Context, Texture2D + + +class GLSampler(Sampler): + """ + OpenGL sampler object. + + When bound to a texture unit it overrides all the + sampling parameters of the texture channel. + """ + + def __init__( + self, + ctx: Context, + texture: Texture2D, + *, + filter: tuple[PyGLuint, PyGLuint] | None = None, + wrap_x: PyGLuint | None = None, + wrap_y: PyGLuint | None = None, + ): + super().__init__(ctx, texture, filter=filter, wrap_x=wrap_x, wrap_y=wrap_y) + self._glo = -1 + + value = c_uint32() + gl.glGenSamplers(1, byref(value)) + self._glo = value.value + + # Default filters for float and integer textures + # Integer textures should have NEAREST interpolation + # by default 3.3 core doesn't really support it consistently. + if "f" in self.texture._dtype: + self._filter = gl.GL_LINEAR, gl.GL_LINEAR + else: + self._filter = gl.GL_NEAREST, gl.GL_NEAREST + + self._wrap_x = gl.GL_REPEAT + self._wrap_y = gl.GL_REPEAT + + # Only set texture parameters on non-multisample textures + if self.texture._samples == 0: + self.filter = filter or self._filter + self.wrap_x = wrap_x or self._wrap_x + self.wrap_y = wrap_y or self._wrap_y + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLSampler.delete_glo, self._glo) + + @property + def glo(self) -> PyGLuint: + """The OpenGL sampler id""" + return self._glo + + def use(self, unit: int): + """ + Bind the sampler to a texture unit + """ + gl.glBindSampler(unit, self._glo) + + def clear(self, unit: int): + """ + Unbind the sampler from a texture unit + """ + gl.glBindSampler(unit, 0) + + @Sampler.filter.setter + def filter(self, value: tuple[int, int]): + if not isinstance(value, tuple) or not len(value) == 2: + raise ValueError("Texture filter must be a 2 component tuple (min, mag)") + + self._filter = value + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + + @Sampler.wrap_x.setter + def wrap_x(self, value: int): + self._wrap_x = value + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_S, value) + + @Sampler.wrap_y.setter + def wrap_y(self, value: int): + self._wrap_y = value + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_T, value) + + @Sampler.anisotropy.setter + def anisotropy(self, value): + self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) + gl.glSamplerParameterf(self._glo, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + + @property + def compare_func(self) -> str | None: + """ + Get or set the compare function for a depth texture:: + + texture.compare_func = None # Disable depth comparison completely + texture.compare_func = '<=' # GL_LEQUAL + texture.compare_func = '<' # GL_LESS + texture.compare_func = '>=' # GL_GEQUAL + texture.compare_func = '>' # GL_GREATER + texture.compare_func = '==' # GL_EQUAL + texture.compare_func = '!=' # GL_NOTEQUAL + texture.compare_func = '0' # GL_NEVER + texture.compare_func = '1' # GL_ALWAYS + """ + return self._compare_func + + @Sampler.compare_func.setter + def compare_func(self, value: str | None): + if not self.texture._depth: + raise ValueError("Depth comparison function can only be set on depth textures") + + if not isinstance(value, str) and value is not None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + func = compare_funcs.get(value, None) + if func is None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + self._compare_func = value + if value is None: + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) + else: + gl.glSamplerParameteri( + self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE + ) + gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_FUNC, func) + + @staticmethod + def delete_glo(glo: int) -> None: + """ + Delete the OpenGL object + """ + gl.glDeleteSamplers(1, glo) diff --git a/arcade/gl/context.py b/arcade/gl/context.py index c074ba059..b68583173 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -1086,6 +1086,7 @@ def depth_texture( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def sampler(self, texture: Texture2D) -> Sampler: """ Create a sampler object for a texture. @@ -1094,7 +1095,7 @@ def sampler(self, texture: Texture2D) -> Sampler: texture: The texture to create a sampler for """ - return Sampler(self, texture) + raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod def geometry( diff --git a/arcade/gl/sampler.py b/arcade/gl/sampler.py index 7fb662fc4..e882911eb 100644 --- a/arcade/gl/sampler.py +++ b/arcade/gl/sampler.py @@ -1,18 +1,14 @@ from __future__ import annotations -import weakref -from ctypes import byref, c_uint32 -from typing import TYPE_CHECKING - -from pyglet import gl +from abc import ABC, abstractmethod -from .types import PyGLuint, compare_funcs +from typing import TYPE_CHECKING if TYPE_CHECKING: from arcade.gl import Context, Texture2D -class Sampler: +class Sampler(ABC): """ OpenGL sampler object. @@ -25,57 +21,37 @@ def __init__( ctx: "Context", texture: Texture2D, *, - filter: tuple[PyGLuint, PyGLuint] | None = None, - wrap_x: PyGLuint | None = None, - wrap_y: PyGLuint | None = None, + filter = None, # TODO: Typing, should be tuple[PyGLuint, PyGLuint] | None + wrap_x = None, # TODO: Typing, should be PyGLuint | None + wrap_y = None, # TODO: Typing, should be PyGLuint | None ): self._ctx = ctx - self._glo = -1 - value = c_uint32() - gl.glGenSamplers(1, byref(value)) - self._glo = value.value + # These three ultimately need to be set by the implementing backend, but we're creating them here first + # to trick some of the methods on the base class to being able to see them. + # So that we don't have to implement a getter on every backend + self._filter = None + self._wrap_x = None + self._wrap_y = None self.texture = texture - # Default filters for float and integer textures - # Integer textures should have NEAREST interpolation - # by default 3.3 core doesn't really support it consistently. - if "f" in self.texture._dtype: - self._filter = gl.GL_LINEAR, gl.GL_LINEAR - else: - self._filter = gl.GL_NEAREST, gl.GL_NEAREST - - self._wrap_x = gl.GL_REPEAT - self._wrap_y = gl.GL_REPEAT self._anisotropy = 1.0 self._compare_func: str | None = None - # Only set texture parameters on non-multisample textures - if self.texture._samples == 0: - self.filter = filter or self._filter - self.wrap_x = wrap_x or self._wrap_x - self.wrap_y = wrap_y or self._wrap_y - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, Sampler.delete_glo, self._glo) - - @property - def glo(self) -> PyGLuint: - """The OpenGL sampler id""" - return self._glo - + @abstractmethod def use(self, unit: int): """ Bind the sampler to a texture unit """ - gl.glBindSampler(unit, self._glo) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def clear(self, unit: int): """ Unbind the sampler from a texture unit """ - gl.glBindSampler(unit, 0) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def filter(self) -> tuple[int, int]: @@ -109,13 +85,9 @@ def filter(self) -> tuple[int, int]: return self._filter @filter.setter + @abstractmethod def filter(self, value: tuple[int, int]): - if not isinstance(value, tuple) or not len(value) == 2: - raise ValueError("Texture filter must be a 2 component tuple (min, mag)") - - self._filter = value - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_x(self) -> int: @@ -140,9 +112,9 @@ def wrap_x(self) -> int: return self._wrap_x @wrap_x.setter + @abstractmethod def wrap_x(self, value: int): - self._wrap_x = value - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_S, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_y(self) -> int: @@ -167,9 +139,9 @@ def wrap_y(self) -> int: return self._wrap_y @wrap_y.setter + @abstractmethod def wrap_y(self, value: int): - self._wrap_y = value - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_WRAP_T, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def anisotropy(self) -> float: @@ -178,8 +150,7 @@ def anisotropy(self) -> float: @anisotropy.setter def anisotropy(self, value): - self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) - gl.glSamplerParameterf(self._glo, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def compare_func(self) -> str | None: @@ -199,29 +170,6 @@ def compare_func(self) -> str | None: return self._compare_func @compare_func.setter + @abstractmethod def compare_func(self, value: str | None): - if not self.texture._depth: - raise ValueError("Depth comparison function can only be set on depth textures") - - if not isinstance(value, str) and value is not None: - raise ValueError(f"value must be as string: {self._compare_funcs.keys()}") - - func = compare_funcs.get(value, None) - if func is None: - raise ValueError(f"value must be as string: {compare_funcs.keys()}") - - self._compare_func = value - if value is None: - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) - else: - gl.glSamplerParameteri( - self._glo, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE - ) - gl.glSamplerParameteri(self._glo, gl.GL_TEXTURE_COMPARE_FUNC, func) - - @staticmethod - def delete_glo(glo: int) -> None: - """ - Delete the OpenGL object - """ - gl.glDeleteSamplers(1, glo) + raise NotImplementedError("The enabled graphics backend does not support this method.") From dec2c637d756370779f7edeff35214a5f7aa919e Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 8 Apr 2025 21:55:56 -0400 Subject: [PATCH 06/16] Abstract texture arrays --- arcade/gl/backends/gl/context.py | 23 + arcade/gl/backends/gl/texture_array.py | 662 +++++++++++++++++++++++++ arcade/gl/context.py | 12 +- arcade/gl/texture_array.py | 424 ++-------------- 4 files changed, 723 insertions(+), 398 deletions(-) create mode 100644 arcade/gl/backends/gl/texture_array.py diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 772e05b11..a5af797aa 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -18,6 +18,7 @@ from .program import GLProgram from .sampler import GLSampler from .texture import GLTexture2D +from .texture_array import GLTextureArray from .vertex_array import GLGeometry @@ -199,6 +200,28 @@ def sampler(self, texture: GLTexture2D) -> GLSampler: """ return GLSampler(self, texture) + def texture_array( + self, + size: Tuple[int, int, int], + *, + components: int = 4, + dtype: str = "f1", + data: BufferProtocol | None = None, + wrap_x: PyGLenum | None = None, + wrap_y: PyGLenum | None = None, + filter: Tuple[PyGLenum, PyGLenum] | None = None, + ) -> GLTextureArray: + return GLTextureArray( + self, + size, + components=components, + dtype=dtype, + data=data, + wrap_x=wrap_x, + wrap_y=wrap_y, + filter=filter, + ) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) diff --git a/arcade/gl/backends/gl/texture_array.py b/arcade/gl/backends/gl/texture_array.py new file mode 100644 index 000000000..8365fee75 --- /dev/null +++ b/arcade/gl/backends/gl/texture_array.py @@ -0,0 +1,662 @@ +from __future__ import annotations + +import weakref +from ctypes import byref, string_at +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.types import BufferProtocol +from arcade.gl.texture_array import TextureArray + +from .buffer import Buffer +from .types import ( + BufferOrBufferProtocol, + PyGLuint, + compare_funcs, + pixel_formats, + swizzle_enum_to_str, + swizzle_str_to_enum, +) +from .utils import data_to_ctypes + +if TYPE_CHECKING: # handle import cycle caused by type hinting + from arcade.gl import Context + + +class GLTextureArray(TextureArray): + """ + An OpenGL 2D texture array. + + We can create an empty black texture or a texture from byte data. + A texture can also be created with different datatypes such as + float, integer or unsigned integer. + + The best way to create a texture instance is through :py:meth:`arcade.gl.Context.texture` + + Supported ``dtype`` values are:: + + # Float formats + 'f1': UNSIGNED_BYTE + 'f2': HALF_FLOAT + 'f4': FLOAT + # int formats + 'i1': BYTE + 'i2': SHORT + 'i4': INT + # uint formats + 'u1': UNSIGNED_BYTE + 'u2': UNSIGNED_SHORT + 'u4': UNSIGNED_INT + + Args: + ctx: + The context the object belongs to + size: + The size of the texture (width, height, layers) + components: + The number of components (1: R, 2: RG, 3: RGB, 4: RGBA) + dtype: + The data type of each component: f1, f2, f4 / i1, i2, i4 / u1, u2, u4 + data: + The texture data. Can be bytes or any object supporting + the buffer protocol. + filter: + The minification/magnification filter of the texture + wrap_x: + Wrap mode x + wrap_y: + Wrap mode y + target: + The texture type (Ignored. Legacy) + depth: + creates a depth texture if `True` + samples: + Creates a multisampled texture for values > 0. + This value will be clamped between 0 and the max + sample capability reported by the drivers. + immutable: + Make the storage (not the contents) immutable. This can sometimes be + required when using textures with compute shaders. + internal_format: + The internal format of the texture + compressed: + Is the texture compressed? + compressed_data: + The raw compressed data + """ + + __slots__ = ( + "_glo", + "_target", + ) + + def __init__( + self, + ctx: Context, + size: tuple[int, int, int], + *, + components: int = 4, + dtype: str = "f1", + data: BufferProtocol | None = None, + filter: tuple[PyGLuint, PyGLuint] | None = None, + wrap_x: PyGLuint | None = None, + wrap_y: PyGLuint | None = None, + depth=False, + samples: int = 0, + immutable: bool = False, + internal_format: PyGLuint | None = None, + compressed: bool = False, + compressed_data: bool = False, + ): + self._glo = glo = gl.GLuint() + + # Default filters for float and integer textures + # Integer textures should have NEAREST interpolation + # by default 3.3 core doesn't really support it consistently. + if "f" in self._dtype: + self._filter = gl.GL_LINEAR, gl.GL_LINEAR + else: + self._filter = gl.GL_NEAREST, gl.GL_NEAREST + self._wrap_x = gl.GL_REPEAT + self._wrap_y = gl.GL_REPEAT + + self._target = ( + gl.GL_TEXTURE_2D_ARRAY if self._samples == 0 else gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY + ) + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glGenTextures(1, byref(self._glo)) + + if self._glo.value == 0: + raise RuntimeError("Cannot create Texture. OpenGL failed to generate a texture id") + + gl.glBindTexture(self._target, self._glo) + + self._texture_2d_array(data) + + # Only set texture parameters on non-multisample textures + if self._samples == 0: + self.filter = filter or self._filter + self.wrap_x = wrap_x or self._wrap_x + self.wrap_y = wrap_y or self._wrap_y + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLTextureArray.delete_glo, self._ctx, glo) + + def resize(self, size: tuple[int, int]): + """ + Resize the texture. This will re-allocate the internal + memory and all pixel data will be lost. + + .. note:: Immutable textures cannot be resized. + + Args: + size: The new size of the texture + """ + if self._immutable: + raise ValueError("Immutable textures cannot be resized") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + self._width, self._height = size + + self._texture_2d_array(None) + + def __del__(self): + # Intercept garbage collection if we are using Context.gc() + if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: + self._ctx.objects.append(self) + + def _texture_2d_array(self, data): + """Create a 2D texture""" + # Start by resolving the texture format + try: + format_info = pixel_formats[self._dtype] + except KeyError: + raise ValueError( + f"dype '{self._dtype}' not support. Supported types are : " + f"{tuple(pixel_formats.keys())}" + ) + _format, _internal_format, self._type, self._component_size = format_info + if data is not None: + byte_length, data = data_to_ctypes(data) + self._validate_data_size(data, byte_length, self._width, self._height, self._layers) + + # If we are dealing with a multisampled texture we have less options + if self._target == gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY: + gl.glTexImage3DMultisample( + self._target, + self._samples, + _internal_format[self._components], + self._width, + self._height, + self._layers, + True, # Fixed sample locations + ) + return + + # Make sure we unpack the pixel data with correct alignment + # or we'll end up with corrupted textures + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, self._alignment) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, self._alignment) + + # Create depth 2d texture + if self._depth: + gl.glTexImage3D( + self._target, + 0, # level + gl.GL_DEPTH_COMPONENT24, + self._width, + self._height, + self._layers, + 0, + gl.GL_DEPTH_COMPONENT, + gl.GL_UNSIGNED_INT, # gl.GL_FLOAT, + data, + ) + self.compare_func = "<=" + # Create normal 2d texture + else: + try: + self._format = _format[self._components] + if self._internal_format is None: + self._internal_format = _internal_format[self._components] + + if self._immutable: + # Specify immutable storage for this texture. + # glTexStorage2D can only be called once + gl.glTexStorage3D( + self._target, + 1, # Levels + self._internal_format, + self._width, + self._height, + self._layers, + ) + if data: + self.write(data) + else: + # glTexImage2D can be called multiple times to re-allocate storage + # Specify mutable storage for this texture. + if self._compressed_data is True: + gl.glCompressedTexImage3D( + self._target, # target + 0, # level + self._internal_format, # internal_format + self._width, # width + self._height, # height + self._layers, # layers + 0, # border + len(data), # size + data, # data + ) + else: + gl.glTexImage3D( + self._target, # target + 0, # level + self._internal_format, # internal_format + self._width, # width + self._height, # height + self._layers, # layers + 0, # border + self._format, # format + self._type, # type + data, # data + ) + except gl.GLException as ex: + raise gl.GLException( + ( + f"Unable to create texture: {ex} : dtype={self._dtype} " + f"size={self.size} components={self._components} " + f"MAX_TEXTURE_SIZE = {self.ctx.info.MAX_TEXTURE_SIZE}" + f": {ex}" + ) + ) + + @property + def glo(self) -> gl.GLuint: + """The OpenGL texture id""" + return self._glo + + @property + def swizzle(self) -> str: + """ + The swizzle mask of the texture (Default ``'RGBA'``). + + The swizzle mask change/reorder the ``vec4`` value returned by the ``texture()`` function + in a GLSL shaders. This is represented by a 4 character string were each + character can be:: + + 'R' GL_RED + 'G' GL_GREEN + 'B' GL_BLUE + 'A' GL_ALPHA + '0' GL_ZERO + '1' GL_ONE + + Example:: + + # Alpha channel will always return 1.0 + texture.swizzle = 'RGB1' + + # Only return the red component. The rest is masked to 0.0 + texture.swizzle = 'R000' + + # Reverse the components + texture.swizzle = 'ABGR' + """ + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + # Read the current swizzle values from the texture + swizzle_r = gl.GLint() + swizzle_g = gl.GLint() + swizzle_b = gl.GLint() + swizzle_a = gl.GLint() + + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_r) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_g) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_b) + gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_a) + + swizzle_str = "" + for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]: + swizzle_str += swizzle_enum_to_str[v.value] + + return swizzle_str + + @swizzle.setter + def swizzle(self, value: str): + if not isinstance(value, str): + raise ValueError(f"Swizzle must be a string, not '{type(str)}'") + + if len(value) != 4: + raise ValueError("Swizzle must be a string of length 4") + + swizzle_enums = [] + for c in value: + try: + c = c.upper() + swizzle_enums.append(swizzle_str_to_enum[c]) + except KeyError: + raise ValueError(f"Swizzle value '{c}' invalid. Must be one of RGBA01") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_enums[3]) + + @TextureArray.filter.setter + def filter(self, value: tuple[int, int]): + if not isinstance(value, tuple) or not len(value) == 2: + raise ValueError("Texture filter must be a 2 component tuple (min, mag)") + + self._filter = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + + @TextureArray.wrap_x.setter + def wrap_x(self, value: int): + self._wrap_x = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_S, value) + + @TextureArray.wrap_y.setter + def wrap_y(self, value: int): + self._wrap_y = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_T, value) + + @TextureArray.anisotropy.setter + def anisotropy(self, value): + self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + + @TextureArray.compare_func.setter + def compare_func(self, value: str | None): + if not self._depth: + raise ValueError("Depth comparison function can only be set on depth textures") + + if not isinstance(value, str) and value is not None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + func = compare_funcs.get(value, None) + if func is None: + raise ValueError(f"value must be as string: {compare_funcs.keys()}") + + self._compare_func = value + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + if value is None: + gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) + else: + gl.glTexParameteri( + self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE + ) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_FUNC, func) + + def read(self, level: int = 0, alignment: int = 1) -> bytes: + """ + Read the contents of the texture. + + Args: + level: + The texture level to read + alignment: + Alignment of the start of each row in memory in number of bytes. + Possible values: 1,2,4 + """ + if self._samples > 0: + raise ValueError("Multisampled textures cannot be read directly") + + if self._ctx.gl_api == "gl": + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, alignment) + + buffer = ( + gl.GLubyte + * (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)) + elif self._ctx.gl_api == "gles": + # 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") + else: + raise ValueError("Unknown gl_api: '{self._ctx.gl_api}'") + + def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> None: + """Write byte data into layers of the texture. + + The ``data`` value can be either an + :py:class:`arcade.gl.Buffer` or anything that implements the + `Buffer Protocol `_. + + The latter category includes ``bytes``, ``bytearray``, + ``array.array``, and more. You may need to use typing + workarounds for non-builtin types. See + :ref:`prog-guide-gl-buffer-protocol-typing` for more + information. + + Args: + data: + :class:`~arcade.gl.Buffer` or buffer protocol object with data to write. + level: + The texture level to write (LoD level, now layer) + viewport: + The area of the texture to write. Should be a 3 or 5-component tuple + `(x, y, layer, width, height)` writes to an area of a single layer. + If not provided the entire texture is written to. + """ + # TODO: Support writing to layers using viewport + alignment + if self._samples > 0: + raise ValueError("Writing to multisampled textures not supported") + + x, y, l, w, h = ( + 0, + 0, + 0, + self._width, + self._height, + ) + if viewport: + # TODO: Add more options here. For now we support writing to a single layer + # (width, hight, num_layers) is a suggestion from moderngl + # if len(viewport) == 3: + # w, h, l = viewport + if len(viewport) == 5: + x, y, l, w, h = viewport + else: + raise ValueError("Viewport must be of length 5") + + if isinstance(data, Buffer): + gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, data.glo) + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glTexSubImage3D(self._target, level, x, y, w, h, l, self._format, self._type, 0) + gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) + else: + byte_size, data = data_to_ctypes(data) + self._validate_data_size(data, byte_size, w, h, 1) # Single layer + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glTexSubImage3D( + self._target, # target + level, # level + x, # x offset + y, # y offset + l, # layer + w, # width + h, # height + 1, # depth (one layer) + self._format, # format + self._type, # type + data, # pixel data + ) + + def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: + """Generate mipmaps for this texture. + + The default values usually work well. + + Mipmaps are successively smaller versions of an original + texture with special filtering applied. Using mipmaps allows + OpenGL to render scaled versions of original textures with fewer + scaling artifacts. + + Mipmaps can be made for textures of any size. Each mipmap + version halves the width and height of the previous one (e.g. + 256 x 256, 128 x 128, 64 x 64, etc) down to a minimum of 1 x 1. + + .. note:: Mipmaps will only be used if a texture's filter is + configured with a mipmap-type minification:: + + # Set up linear interpolating minification filter + texture.filter = ctx.LINEAR_MIPMAP_LINEAR, ctx.LINEAR + + Args: + base: + Level the mipmaps start at (usually 0) + max_level: + The maximum number of levels to generate + + Also see: https://www.khronos.org/opengl/wiki/Texture#Mip_maps + """ + if self._samples > 0: + raise ValueError("Multisampled textures don't support mimpmaps") + + gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) + gl.glBindTexture(self._target, self._glo) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_BASE_LEVEL, base) + gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAX_LEVEL, max_level) + gl.glGenerateMipmap(self._target) + + def delete(self): + """ + Destroy the underlying OpenGL resource. + + Don't use this unless you know exactly what you are doing. + """ + self.delete_glo(self._ctx, self._glo) + self._glo.value = 0 + + @staticmethod + def delete_glo(ctx: "Context", glo: gl.GLuint): + """ + Destroy the texture. + + This is called automatically when the object is garbage collected. + + Args: + ctx: OpenGL Context + glo: The OpenGL texture id + """ + # If we have no context, then we are shutting down, so skip this + if gl.current_context is None: + return + + if glo.value != 0: + gl.glDeleteTextures(1, byref(glo)) + + ctx.stats.decr("texture") + + def use(self, unit: int = 0) -> None: + """Bind the texture to a channel, + + Args: + unit: The texture unit to bind the texture. + """ + gl.glActiveTexture(gl.GL_TEXTURE0 + unit) + gl.glBindTexture(self._target, self._glo) + + def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: int = 0): + """ + Bind textures to image units. + + Note that either or both ``read`` and ``write`` needs to be ``True``. + The supported modes are: read only, write only, read-write + + Args: + unit: The image unit + read: The compute shader intends to read from this image + write: The compute shader intends to write to this image + level: The mipmap level to bind + """ + if self._ctx.gl_api == "gles" and not self._immutable: + raise ValueError("Textures bound to image units must be created with immutable=True") + + access = gl.GL_READ_WRITE + if read and write: + access = gl.GL_READ_WRITE + elif read and not write: + access = gl.GL_READ_ONLY + elif not read and write: + access = gl.GL_WRITE_ONLY + else: + raise ValueError("Illegal access mode. The texture must at least be read or write only") + + gl.glBindImageTexture(unit, self._glo, level, 0, 0, access, self._internal_format) + + def get_handle(self, resident: bool = True) -> int: + """ + Get a handle for bindless texture access. + + Once a handle is created its parameters cannot be changed. + Attempting to do so will have no effect. (filter, wrap etc). + There is no way to undo this immutability. + + Handles cannot be used by shaders until they are resident. + This method can be called multiple times to move a texture + in and out of residency:: + + >> texture.get_handle(resident=False) + 4294969856 + >> texture.get_handle(resident=True) + 4294969856 + + Ths same handle is returned if the handle already exists. + + .. note:: Limitations from the OpenGL wiki + + The amount of storage available for resident images/textures may be less + than the total storage for textures that is available. As such, you should + attempt to minimize the time a texture spends being resident. Do not attempt + to take steps like making textures resident/un-resident every frame or something. + But if you are finished using a texture for some time, make it un-resident. + + Args: + resident: Make the texture resident. + """ + handle = gl.glGetTextureHandleARB(self._glo) + is_resident = gl.glIsTextureHandleResidentARB(handle) + + # Ensure we don't try to make a resident texture resident again + if resident: + if not is_resident: + gl.glMakeTextureHandleResidentARB(handle) + else: + if is_resident: + gl.glMakeTextureHandleNonResidentARB(handle) + + return handle + + def __repr__(self) -> str: + return "".format( + self._glo.value, self._width, self._layers, self._height, self._components + ) diff --git a/arcade/gl/context.py b/arcade/gl/context.py index b68583173..3a55d5b68 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -1035,6 +1035,7 @@ def texture( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def texture_array( self, size: Tuple[int, int, int], @@ -1058,16 +1059,7 @@ def texture_array( See :py:meth:`~arcade.gl.Context.texture` for arguments. """ - return TextureArray( - self, - size, - components=components, - dtype=dtype, - data=data, - wrap_x=wrap_x, - wrap_y=wrap_y, - filter=filter, - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod def depth_texture( diff --git a/arcade/gl/texture_array.py b/arcade/gl/texture_array.py index c75d666e8..88057b049 100644 --- a/arcade/gl/texture_array.py +++ b/arcade/gl/texture_array.py @@ -1,28 +1,19 @@ from __future__ import annotations -import weakref -from ctypes import byref, string_at +from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from pyglet import gl - from ..types import BufferProtocol -from .buffer import Buffer from .types import ( BufferOrBufferProtocol, - PyGLuint, - compare_funcs, pixel_formats, - swizzle_enum_to_str, - swizzle_str_to_enum, ) -from .utils import data_to_ctypes if TYPE_CHECKING: # handle import cycle caused by type hinting from arcade.gl import Context -class TextureArray: +class TextureArray(ABC): """ An OpenGL 2D texture array. @@ -86,12 +77,10 @@ class TextureArray: __slots__ = ( "_ctx", - "_glo", "_width", "_height", "_layers", "_dtype", - "_target", "_components", "_alignment", "_depth", @@ -119,25 +108,22 @@ def __init__( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - filter: tuple[PyGLuint, PyGLuint] | None = None, - wrap_x: PyGLuint | None = None, - wrap_y: PyGLuint | None = None, - target=gl.GL_TEXTURE_2D_ARRAY, + filter = None, + wrap_x = None, + wrap_y = None, depth=False, samples: int = 0, immutable: bool = False, - internal_format: PyGLuint | None = None, + internal_format = None, compressed: bool = False, compressed_data: bool = False, ): - self._glo = glo = gl.GLuint() self._ctx = ctx self._width, self._height, self._layers = size self._dtype = dtype self._components = components self._component_size = 0 self._alignment = 1 - self._target = target self._samples = min(max(0, samples), self._ctx.info.MAX_SAMPLES) self._depth = depth self._immutable = immutable @@ -146,15 +132,6 @@ def __init__( self._internal_format = internal_format self._compressed = compressed self._compressed_data = compressed_data - # Default filters for float and integer textures - # Integer textures should have NEAREST interpolation - # by default 3.3 core doesn't really support it consistently. - if "f" in self._dtype: - self._filter = gl.GL_LINEAR, gl.GL_LINEAR - else: - self._filter = gl.GL_NEAREST, gl.GL_NEAREST - self._wrap_x = gl.GL_REPEAT - self._wrap_y = gl.GL_REPEAT if self._components not in [1, 2, 3, 4]: raise ValueError("Components must be 1, 2, 3 or 4") @@ -164,31 +141,9 @@ def __init__( "Multisampled textures are not writable (cannot be initialized with data)" ) - self._target = ( - gl.GL_TEXTURE_2D_ARRAY if self._samples == 0 else gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY - ) - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glGenTextures(1, byref(self._glo)) - - if self._glo.value == 0: - raise RuntimeError("Cannot create Texture. OpenGL failed to generate a texture id") - - gl.glBindTexture(self._target, self._glo) - - self._texture_2d_array(data) - - # Only set texture parameters on non-multisample textures - if self._samples == 0: - self.filter = filter or self._filter - self.wrap_x = wrap_x or self._wrap_x - self.wrap_y = wrap_y or self._wrap_y - - if self._ctx.gc_mode == "auto": - weakref.finalize(self, TextureArray.delete_glo, self._ctx, glo) - self.ctx.stats.incr("texture") + @abstractmethod def resize(self, size: tuple[int, int]): """ Resize the texture. This will re-allocate the internal @@ -199,137 +154,13 @@ def resize(self, size: tuple[int, int]): Args: size: The new size of the texture """ - if self._immutable: - raise ValueError("Immutable textures cannot be resized") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - self._width, self._height = size - - self._texture_2d_array(None) - - def __del__(self): - # Intercept garbage collection if we are using Context.gc() - if self._ctx.gc_mode == "context_gc" and self._glo.value > 0: - self._ctx.objects.append(self) - - def _texture_2d_array(self, data): - """Create a 2D texture""" - # Start by resolving the texture format - try: - format_info = pixel_formats[self._dtype] - except KeyError: - raise ValueError( - f"dype '{self._dtype}' not support. Supported types are : " - f"{tuple(pixel_formats.keys())}" - ) - _format, _internal_format, self._type, self._component_size = format_info - if data is not None: - byte_length, data = data_to_ctypes(data) - self._validate_data_size(data, byte_length, self._width, self._height, self._layers) - - # If we are dealing with a multisampled texture we have less options - if self._target == gl.GL_TEXTURE_2D_MULTISAMPLE_ARRAY: - gl.glTexImage3DMultisample( - self._target, - self._samples, - _internal_format[self._components], - self._width, - self._height, - self._layers, - True, # Fixed sample locations - ) - return - - # Make sure we unpack the pixel data with correct alignment - # or we'll end up with corrupted textures - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, self._alignment) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, self._alignment) - - # Create depth 2d texture - if self._depth: - gl.glTexImage3D( - self._target, - 0, # level - gl.GL_DEPTH_COMPONENT24, - self._width, - self._height, - self._layers, - 0, - gl.GL_DEPTH_COMPONENT, - gl.GL_UNSIGNED_INT, # gl.GL_FLOAT, - data, - ) - self.compare_func = "<=" - # Create normal 2d texture - else: - try: - self._format = _format[self._components] - if self._internal_format is None: - self._internal_format = _internal_format[self._components] - - if self._immutable: - # Specify immutable storage for this texture. - # glTexStorage2D can only be called once - gl.glTexStorage3D( - self._target, - 1, # Levels - self._internal_format, - self._width, - self._height, - self._layers, - ) - if data: - self.write(data) - else: - # glTexImage2D can be called multiple times to re-allocate storage - # Specify mutable storage for this texture. - if self._compressed_data is True: - gl.glCompressedTexImage3D( - self._target, # target - 0, # level - self._internal_format, # internal_format - self._width, # width - self._height, # height - self._layers, # layers - 0, # border - len(data), # size - data, # data - ) - else: - gl.glTexImage3D( - self._target, # target - 0, # level - self._internal_format, # internal_format - self._width, # width - self._height, # height - self._layers, # layers - 0, # border - self._format, # format - self._type, # type - data, # data - ) - except gl.GLException as ex: - raise gl.GLException( - ( - f"Unable to create texture: {ex} : dtype={self._dtype} " - f"size={self.size} components={self._components} " - f"MAX_TEXTURE_SIZE = {self.ctx.info.MAX_TEXTURE_SIZE}" - f": {ex}" - ) - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def ctx(self) -> Context: """The context this texture belongs to.""" return self._ctx - @property - def glo(self) -> gl.GLuint: - """The OpenGL texture id""" - return self._glo - @property def compressed(self) -> bool: """Is this using a compressed format?""" @@ -391,6 +222,7 @@ def immutable(self) -> bool: return self._immutable @property + @abstractmethod def swizzle(self) -> str: """ The swizzle mask of the texture (Default ``'RGBA'``). @@ -417,49 +249,12 @@ def swizzle(self) -> str: # Reverse the components texture.swizzle = 'ABGR' """ - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - # Read the current swizzle values from the texture - swizzle_r = gl.GLint() - swizzle_g = gl.GLint() - swizzle_b = gl.GLint() - swizzle_a = gl.GLint() - - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_r) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_g) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_b) - gl.glGetTexParameteriv(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_a) - - swizzle_str = "" - for v in [swizzle_r, swizzle_g, swizzle_b, swizzle_a]: - swizzle_str += swizzle_enum_to_str[v.value] - - return swizzle_str + raise NotImplementedError("The enabled graphics backend does not support this method.") @swizzle.setter + @abstractmethod def swizzle(self, value: str): - if not isinstance(value, str): - raise ValueError(f"Swizzle must be a string, not '{type(str)}'") - - if len(value) != 4: - raise ValueError("Swizzle must be a string of length 4") - - swizzle_enums = [] - for c in value: - try: - c = c.upper() - swizzle_enums.append(swizzle_str_to_enum[c]) - except KeyError: - raise ValueError(f"Swizzle value '{c}' invalid. Must be one of RGBA01") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_R, swizzle_enums[0]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_G, swizzle_enums[1]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_B, swizzle_enums[2]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_SWIZZLE_A, swizzle_enums[3]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def filter(self) -> tuple[int, int]: @@ -493,15 +288,9 @@ def filter(self) -> tuple[int, int]: return self._filter @filter.setter + @abstractmethod def filter(self, value: tuple[int, int]): - if not isinstance(value, tuple) or not len(value) == 2: - raise ValueError("Texture filter must be a 2 component tuple (min, mag)") - - self._filter = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_MIN_FILTER, self._filter[0]) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAG_FILTER, self._filter[1]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_x(self) -> int: @@ -526,11 +315,9 @@ def wrap_x(self) -> int: return self._wrap_x @wrap_x.setter + @abstractmethod def wrap_x(self, value: int): - self._wrap_x = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_S, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wrap_y(self) -> int: @@ -555,11 +342,9 @@ def wrap_y(self) -> int: return self._wrap_y @wrap_y.setter + @abstractmethod def wrap_y(self, value: int): - self._wrap_y = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_WRAP_T, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def anisotropy(self) -> float: @@ -567,11 +352,9 @@ def anisotropy(self) -> float: return self._anisotropy @anisotropy.setter + @abstractmethod def anisotropy(self, value): - self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameterf(self._target, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def compare_func(self) -> str | None: @@ -591,28 +374,11 @@ def compare_func(self) -> str | None: return self._compare_func @compare_func.setter + @abstractmethod def compare_func(self, value: str | None): - if not self._depth: - raise ValueError("Depth comparison function can only be set on depth textures") - - if not isinstance(value, str) and value is not None: - raise ValueError(f"value must be as string: {self._compare_funcs.keys()}") - - func = compare_funcs.get(value, None) - if func is None: - raise ValueError(f"value must be as string: {compare_funcs.keys()}") - - self._compare_func = value - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - if value is None: - gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_NONE) - else: - gl.glTexParameteri( - self._target, gl.GL_TEXTURE_COMPARE_MODE, gl.GL_COMPARE_REF_TO_TEXTURE - ) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_COMPARE_FUNC, func) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def read(self, level: int = 0, alignment: int = 1) -> bytes: """ Read the contents of the texture. @@ -624,26 +390,9 @@ def read(self, level: int = 0, alignment: int = 1) -> bytes: Alignment of the start of each row in memory in number of bytes. Possible values: 1,2,4 """ - if self._samples > 0: - raise ValueError("Multisampled textures cannot be read directly") - - if self._ctx.gl_api == "gl": - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, alignment) - - buffer = ( - gl.GLubyte - * (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)) - elif self._ctx.gl_api == "gles": - # 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") - else: - raise ValueError("Unknown gl_api: '{self._ctx.gl_api}'") + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> None: """Write byte data into layers of the texture. @@ -667,55 +416,7 @@ def write(self, data: BufferOrBufferProtocol, level: int = 0, viewport=None) -> `(x, y, layer, width, height)` writes to an area of a single layer. If not provided the entire texture is written to. """ - # TODO: Support writing to layers using viewport + alignment - if self._samples > 0: - raise ValueError("Writing to multisampled textures not supported") - - x, y, l, w, h = ( - 0, - 0, - 0, - self._width, - self._height, - ) - if viewport: - # TODO: Add more options here. For now we support writing to a single layer - # (width, hight, num_layers) is a suggestion from moderngl - # if len(viewport) == 3: - # w, h, l = viewport - if len(viewport) == 5: - x, y, l, w, h = viewport - else: - raise ValueError("Viewport must be of length 5") - - if isinstance(data, Buffer): - gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, data.glo) - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glTexSubImage3D(self._target, level, x, y, w, h, l, self._format, self._type, 0) - gl.glBindBuffer(gl.GL_PIXEL_UNPACK_BUFFER, 0) - else: - byte_size, data = data_to_ctypes(data) - self._validate_data_size(data, byte_size, w, h, 1) # Single layer - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - gl.glTexSubImage3D( - self._target, # target - level, # level - x, # x offset - y, # y offset - l, # layer - w, # width - h, # height - 1, # depth (one layer) - self._format, # format - self._type, # type - data, # pixel data - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") def _validate_data_size( self, byte_data, byte_size: int, width: int, height: int, layers: int @@ -737,6 +438,7 @@ def _validate_data_size( f"Data size {len(byte_data)} does not match reported size {expected_size}" ) + @abstractmethod def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: """Generate mipmaps for this texture. @@ -765,53 +467,27 @@ def build_mipmaps(self, base: int = 0, max_level: int = 1000) -> None: Also see: https://www.khronos.org/opengl/wiki/Texture#Mip_maps """ - if self._samples > 0: - raise ValueError("Multisampled textures don't support mimpmaps") - - gl.glActiveTexture(gl.GL_TEXTURE0 + self._ctx.default_texture_unit) - gl.glBindTexture(self._target, self._glo) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_BASE_LEVEL, base) - gl.glTexParameteri(self._target, gl.GL_TEXTURE_MAX_LEVEL, max_level) - gl.glGenerateMipmap(self._target) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def delete(self): """ Destroy the underlying OpenGL resource. Don't use this unless you know exactly what you are doing. """ - self.delete_glo(self._ctx, self._glo) - self._glo.value = 0 - - @staticmethod - def delete_glo(ctx: "Context", glo: gl.GLuint): - """ - Destroy the texture. - - This is called automatically when the object is garbage collected. - - Args: - ctx: OpenGL Context - glo: The OpenGL texture id - """ - # If we have no context, then we are shutting down, so skip this - if gl.current_context is None: - return - - if glo.value != 0: - gl.glDeleteTextures(1, byref(glo)) - - ctx.stats.decr("texture") + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def use(self, unit: int = 0) -> None: """Bind the texture to a channel, Args: unit: The texture unit to bind the texture. """ - gl.glActiveTexture(gl.GL_TEXTURE0 + unit) - gl.glBindTexture(self._target, self._glo) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: int = 0): """ Bind textures to image units. @@ -825,21 +501,9 @@ def bind_to_image(self, unit: int, read: bool = True, write: bool = True, level: write: The compute shader intends to write to this image level: The mipmap level to bind """ - if self._ctx.gl_api == "gles" and not self._immutable: - raise ValueError("Textures bound to image units must be created with immutable=True") - - access = gl.GL_READ_WRITE - if read and write: - access = gl.GL_READ_WRITE - elif read and not write: - access = gl.GL_READ_ONLY - elif not read and write: - access = gl.GL_WRITE_ONLY - else: - raise ValueError("Illegal access mode. The texture must at least be read or write only") - - gl.glBindImageTexture(unit, self._glo, level, 0, 0, access, self._internal_format) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def get_handle(self, resident: bool = True) -> int: """ Get a handle for bindless texture access. @@ -870,20 +534,4 @@ def get_handle(self, resident: bool = True) -> int: Args: resident: Make the texture resident. """ - handle = gl.glGetTextureHandleARB(self._glo) - is_resident = gl.glIsTextureHandleResidentARB(handle) - - # Ensure we don't try to make a resident texture resident again - if resident: - if not is_resident: - gl.glMakeTextureHandleResidentARB(handle) - else: - if is_resident: - gl.glMakeTextureHandleNonResidentARB(handle) - - return handle - - def __repr__(self) -> str: - return "".format( - self._glo.value, self._width, self._layers, self._height, self._components - ) + raise NotImplementedError("The enabled graphics backend does not support this method.") From 0a32ecc4f8f8636b707b3501c582ef0e0aa3c7de Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 14 Apr 2025 21:21:35 -0400 Subject: [PATCH 07/16] Abstract queries --- arcade/gl/backends/gl/context.py | 4 + arcade/gl/backends/gl/query.py | 128 +++++++++++++++++++++++++++++++ arcade/gl/context.py | 3 +- arcade/gl/query.py | 83 ++------------------ 4 files changed, 142 insertions(+), 76 deletions(-) create mode 100644 arcade/gl/backends/gl/query.py diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index a5af797aa..a6ea4a743 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -16,6 +16,7 @@ from .glsl import ShaderSource from .types import BufferDescription from .program import GLProgram +from .query import GLQuery from .sampler import GLSampler from .texture import GLTexture2D from .texture_array import GLTextureArray @@ -222,6 +223,9 @@ def texture_array( filter=filter, ) + def query(self, *, samples=True, time=True, primitives=True) -> GLQuery: + return GLQuery(self, samples=samples, time=time, primitives=primitives) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) diff --git a/arcade/gl/backends/gl/query.py b/arcade/gl/backends/gl/query.py new file mode 100644 index 000000000..ba7ec6f8d --- /dev/null +++ b/arcade/gl/backends/gl/query.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import weakref +from typing import TYPE_CHECKING + +from pyglet import gl + +from arcade.gl.query import Query + +if TYPE_CHECKING: + from arcade.gl import Context + + +class GLQuery(Query): + """ + A query object to perform low level measurements of OpenGL rendering calls. + + The best way to create a program instance is through :py:meth:`arcade.gl.Context.query` + + Example usage:: + + query = ctx.query() + with query: + geometry.render(..) + + print('samples_passed:', query.samples_passed) + print('time_elapsed:', query.time_elapsed) + print('primitives_generated:', query.primitives_generated) + + Args: + ctx: + The context this query object belongs to + samples: + Enable counting written samples + time: + Enable measuring time elapsed + primitives: + Enable counting primitives + """ + + __slots__ = ( + "_glo_samples_passed", + "_glo_time_elapsed", + "_glo_primitives_generated", + "__weakref__", + ) + + def __init__(self, ctx: Context, samples=True, time=True, primitives=True): + super().__init__(ctx, samples, time, primitives) + + glos = [] + + self._glo_samples_passed = glo_samples_passed = gl.GLuint() + if self._samples_enabled: + gl.glGenQueries(1, self._glo_samples_passed) + glos.append(glo_samples_passed) + + self._glo_time_elapsed = glo_time_elapsed = gl.GLuint() + if self._time_enabled: + gl.glGenQueries(1, self._glo_time_elapsed) + glos.append(glo_time_elapsed) + + self._glo_primitives_generated = glo_primitives_generated = gl.GLuint() + if self._primitives_enabled: + gl.glGenQueries(1, self._glo_primitives_generated) + glos.append(glo_primitives_generated) + + if self._ctx.gc_mode == "auto": + weakref.finalize(self, GLQuery.delete_glo, self._ctx, glos) + + def __enter__(self): + if self._ctx.gl_api == "gl": + if self._samples_enabled: + gl.glBeginQuery(gl.GL_SAMPLES_PASSED, self._glo_samples_passed) + if self._time_enabled: + gl.glBeginQuery(gl.GL_TIME_ELAPSED, self._glo_time_elapsed) + if self._primitives_enabled: + gl.glBeginQuery(gl.GL_PRIMITIVES_GENERATED, self._glo_primitives_generated) + + def __exit__(self, exc_type, exc_val, exc_tb): + if self._ctx.gl_api == "gl": + if self._samples_enabled: + gl.glEndQuery(gl.GL_SAMPLES_PASSED) + value = gl.GLint() + gl.glGetQueryObjectiv(self._glo_samples_passed, gl.GL_QUERY_RESULT, value) + self._samples = value.value + + if self._time_enabled: + gl.glEndQuery(gl.GL_TIME_ELAPSED) + value = gl.GLint() + gl.glGetQueryObjectiv(self._glo_time_elapsed, gl.GL_QUERY_RESULT, value) + self._time = value.value + + if self._primitives_enabled: + gl.glEndQuery(gl.GL_PRIMITIVES_GENERATED) + value = gl.GLint() + gl.glGetQueryObjectiv(self._glo_primitives_generated, gl.GL_QUERY_RESULT, value) + self._primitives = value.value + + def delete(self): + """ + Destroy the underlying OpenGL resource. + + Don't use this unless you know exactly what you are doing. + """ + GLQuery.delete_glo( + self._ctx, + [ + self._glo_samples_passed, + self._glo_time_elapsed, + self._glo_primitives_generated, + ], + ) + + @staticmethod + def delete_glo(ctx, glos) -> None: + """ + Delete this query object. + + This is automatically called when the object is garbage collected. + """ + if gl.current_context is None: + return + + for glo in glos: + gl.glDeleteQueries(1, glo) + + ctx.stats.decr("query") diff --git a/arcade/gl/context.py b/arcade/gl/context.py index 3a55d5b68..61eed1bbc 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -1223,6 +1223,7 @@ def program( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def query(self, *, samples=True, time=True, primitives=True) -> Query: """ Create a query object for measuring rendering calls in opengl. @@ -1232,7 +1233,7 @@ def query(self, *, samples=True, time=True, primitives=True) -> Query: time: Measure rendering duration primitives: Collect the number of primitives emitted """ - return Query(self, samples=samples, time=time, primitives=primitives) + raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod def compute_shader(self, *, source: str, common: Iterable[str] = ()) -> ComputeShader: diff --git a/arcade/gl/query.py b/arcade/gl/query.py index 66dc53345..5709513e4 100644 --- a/arcade/gl/query.py +++ b/arcade/gl/query.py @@ -1,15 +1,13 @@ from __future__ import annotations -import weakref from typing import TYPE_CHECKING - -from pyglet import gl +from abc import ABC, abstractmethod if TYPE_CHECKING: from arcade.gl import Context -class Query: +class Query(ABC): """ A query object to perform low level measurements of OpenGL rendering calls. @@ -38,9 +36,6 @@ class Query: __slots__ = ( "_ctx", - "_glo_samples_passed", - "_glo_time_elapsed", - "_glo_primitives_generated", "__weakref__", "_samples_enabled", "_time_enabled", @@ -65,28 +60,8 @@ def __init__(self, ctx: Context, samples=True, time=True, primitives=True): self._time = 0 self._primitives = 0 - glos = [] - - self._glo_samples_passed = glo_samples_passed = gl.GLuint() - if self._samples_enabled: - gl.glGenQueries(1, self._glo_samples_passed) - glos.append(glo_samples_passed) - - self._glo_time_elapsed = glo_time_elapsed = gl.GLuint() - if self._time_enabled: - gl.glGenQueries(1, self._glo_time_elapsed) - glos.append(glo_time_elapsed) - - self._glo_primitives_generated = glo_primitives_generated = gl.GLuint() - if self._primitives_enabled: - gl.glGenQueries(1, self._glo_primitives_generated) - glos.append(glo_primitives_generated) - self.ctx.stats.incr("query") - if self._ctx.gc_mode == "auto": - weakref.finalize(self, Query.delete_glo, self._ctx, glos) - def __del__(self): if self._ctx.gc_mode == "context_gc": self._ctx.objects.append(self) @@ -120,61 +95,19 @@ def primitives_generated(self) -> int: """ return self._primitives + @abstractmethod def __enter__(self): - if self._ctx.gl_api == "gl": - if self._samples_enabled: - gl.glBeginQuery(gl.GL_SAMPLES_PASSED, self._glo_samples_passed) - if self._time_enabled: - gl.glBeginQuery(gl.GL_TIME_ELAPSED, self._glo_time_elapsed) - if self._primitives_enabled: - gl.glBeginQuery(gl.GL_PRIMITIVES_GENERATED, self._glo_primitives_generated) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def __exit__(self, exc_type, exc_val, exc_tb): - if self._ctx.gl_api == "gl": - if self._samples_enabled: - gl.glEndQuery(gl.GL_SAMPLES_PASSED) - value = gl.GLint() - gl.glGetQueryObjectiv(self._glo_samples_passed, gl.GL_QUERY_RESULT, value) - self._samples = value.value - - if self._time_enabled: - gl.glEndQuery(gl.GL_TIME_ELAPSED) - value = gl.GLint() - gl.glGetQueryObjectiv(self._glo_time_elapsed, gl.GL_QUERY_RESULT, value) - self._time = value.value - - if self._primitives_enabled: - gl.glEndQuery(gl.GL_PRIMITIVES_GENERATED) - value = gl.GLint() - gl.glGetQueryObjectiv(self._glo_primitives_generated, gl.GL_QUERY_RESULT, value) - self._primitives = value.value + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def delete(self): """ Destroy the underlying OpenGL resource. Don't use this unless you know exactly what you are doing. """ - Query.delete_glo( - self._ctx, - [ - self._glo_samples_passed, - self._glo_time_elapsed, - self._glo_primitives_generated, - ], - ) - - @staticmethod - def delete_glo(ctx, glos) -> None: - """ - Delete this query object. - - This is automatically called when the object is garbage collected. - """ - if gl.current_context is None: - return - - for glo in glos: - gl.glDeleteQueries(1, glo) - - ctx.stats.decr("query") + raise NotImplementedError("The enabled graphics backend does not support this method.") From 1a2745e20f4968382b8196fe2dab70fdc57a8c62 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Mon, 14 Apr 2025 23:55:16 -0400 Subject: [PATCH 08/16] Finished context/info abstraction --- arcade/gl/backends/gl/context.py | 304 +++++++++++++++++- arcade/gl/backends/gl/provider.py | 5 +- arcade/gl/backends/gl/query.py | 1 - arcade/gl/context.py | 492 ++++++++++-------------------- arcade/gl/enums.py | 72 ++++- arcade/gl/provider.py | 13 +- arcade/gl/utils.py | 35 --- 7 files changed, 541 insertions(+), 381 deletions(-) delete mode 100644 arcade/gl/utils.py diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index a6ea4a743..ca2b203fc 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -1,12 +1,14 @@ +from ctypes import c_int, c_float, c_char_p, cast from typing import List, Dict, Iterable, Sequence, Tuple -from arcade.gl.context import Context +from arcade.gl.context import Context, Info from arcade.context import ArcadeContext import pyglet from pyglet import gl from arcade.types import BufferProtocol +from arcade.gl import enums from .types import PyGLenum @@ -24,8 +26,185 @@ class GLContext(Context): + + #: The OpenGL api. Usually "gl" or "gles". + gl_api: str = "gl" + + _valid_apis = ("gl", "gles") + def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): - super().__init__(window, gc_mode, gl_api) + super().__init__(window, gc_mode) + + if gl_api not in self._valid_apis: + raise ValueError(f"Invalid gl_api. Options are: {self._valid_apis}") + self.gl_api = gl_api + + self._gl_version = (self._info.MAJOR_VERSION, self._info.MINOR_VERSION) + + # Hardcoded states + # This should always be enabled + # gl.glEnable(gl.GL_TEXTURE_CUBE_MAP_SEAMLESS) + # Set primitive restart index to -1 by default + if self.gl_api == "gles": + gl.glEnable(gl.GL_PRIMITIVE_RESTART_FIXED_INDEX) + else: + gl.glEnable(gl.GL_PRIMITIVE_RESTART) + + # Detect support for glProgramUniform. + # Assumed to be supported in gles + self._ext_separate_shader_objects_enabled = True + if self.gl_api == "gl": + have_ext = gl.gl_info.have_extension("GL_ARB_separate_shader_objects") + self._ext_separate_shader_objects_enabled = self.gl_version >= (4, 1) or have_ext + + # We enable scissor testing by default. + # This is always set to the same value as the viewport + # to avoid background color affecting areas outside the viewport + gl.glEnable(gl.GL_SCISSOR_TEST) + + @property + def gl_version(self) -> Tuple[int, int]: + """ + The OpenGL major and minor version as a tuple. + + This is the reported OpenGL version from + drivers and might be a higher version than + you requested. + """ + return self._gl_version + + @Context.extensions.getter + def extensions(self) -> set[str]: + return gl.gl_info.get_extensions() + + @property + def error(self) -> str | None: + """Check OpenGL error + + Returns a string representation of the occurring error + or ``None`` of no errors has occurred. + + Example:: + + err = ctx.error + if err: + raise RuntimeError("OpenGL error: {err}") + """ + err = gl.glGetError() + if err == enums.NO_ERROR: + return None + + return self._errors.get(err, "UNKNOWN_ERROR") + + def enable(self, *flags: int): + self._flags.update(flags) + + for flag in flags: + gl.glEnable(flag) + + def enable_only(self, *args: int): + self._flags = set(args) + + if self.BLEND in self._flags: + gl.glEnable(self.BLEND) + else: + gl.glDisable(self.BLEND) + + if self.DEPTH_TEST in self._flags: + gl.glEnable(self.DEPTH_TEST) + else: + gl.glDisable(self.DEPTH_TEST) + + if self.CULL_FACE in self._flags: + gl.glEnable(self.CULL_FACE) + else: + gl.glDisable(self.CULL_FACE) + + if self.gl_api == "gl": + if gl.GL_PROGRAM_POINT_SIZE in self._flags: + gl.glEnable(gl.GL_PROGRAM_POINT_SIZE) + else: + gl.glDisable(gl.GL_PROGRAM_POINT_SIZE) + + def disable(self, *args): + self._flags -= set(args) + + for flag in args: + gl.glDisable(flag) + + @Context.blend_func.setter + def blend_func(self, value: Tuple[int, int] | Tuple[int, int, int, int]): + self._blend_func = value + if len(value) == 2: + gl.glBlendFunc(*value) + elif len(value) == 4: + gl.glBlendFuncSeparate(*value) + else: + ValueError("blend_func takes a tuple of 2 or 4 values") + + @property + def front_face(self) -> str: + value = c_int() + gl.glGetIntegerv(gl.GL_FRONT_FACE, value) + return "cw" if value.value == gl.GL_CW else "ccw" + + @front_face.setter + def front_face(self, value: str): + if value not in ["cw", "ccw"]: + raise ValueError("front_face must be 'cw' or 'ccw'") + gl.glFrontFace(gl.GL_CW if value == "cw" else gl.GL_CCW) + + @property + def cull_face(self) -> str: + value = c_int() + gl.glGetIntegerv(gl.GL_CULL_FACE_MODE, value) + return self._cull_face_options_reverse[value.value] + + @cull_face.setter + def cull_face(self, value): + if value not in self._cull_face_options: + raise ValueError("cull_face must be", list(self._cull_face_options.keys())) + + gl.glCullFace(self._cull_face_options[value]) + + @Context.wireframe.setter + def wireframe(self, value: bool): + self._wireframe = value + if value: + gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) + else: + gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) + + @property + def patch_vertices(self) -> int: + value = c_int() + gl.glGetIntegerv(gl.GL_PATCH_VERTICES, value) + return value.value + + @patch_vertices.setter + def patch_vertices(self, value: int): + if not isinstance(value, int): + raise TypeError("patch_vertices must be an integer") + + gl.glPatchParameteri(gl.GL_PATCH_VERTICES, value) + + @Context.point_size.setter + def point_size(self, value: float): + if self.gl_api == "gl": + gl.glPointSize(self._point_size) + self._point_size = value + + @Context.primitive_restart_index.setter + def primitive_restart_index(self, value: int): + self._primitive_restart_index = value + if self.gl_api == "gl": + gl.glPrimitiveRestartIndex(value) + + def finish(self) -> None: + gl.glFinish() + + def flush(self) -> None: + gl.glFlush() def _create_default_framebuffer(self) -> GLDefaultFrameBuffer: return GLDefaultFrameBuffer(self) @@ -229,4 +408,123 @@ def query(self, *, samples=True, time=True, primitives=True) -> GLQuery: class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) - ArcadeContext.__init__(self, *args, **kwargs) \ No newline at end of file + ArcadeContext.__init__(self, *args, **kwargs) + +class GLInfo(Info): + """OpenGL info and capabilities""" + + def __init__(self, ctx): + super().__init__(ctx) + + self.MINOR_VERSION = self.get(gl.GL_MINOR_VERSION) + """Minor version number of the OpenGL API supported by the current context""" + + self.MAJOR_VERSION = self.get(gl.GL_MAJOR_VERSION) + """Major version number of the OpenGL API supported by the current context.""" + + self.MAX_COLOR_TEXTURE_SAMPLES = self.get(gl.GL_MAX_COLOR_TEXTURE_SAMPLES) + """Maximum number of samples in a color multisample texture""" + + self.MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS = self.get( + gl.GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS + ) + """Number of words for geometry shader uniform variables in all uniform blocks""" + + self.MAX_DEPTH_TEXTURE_SAMPLES = self.get(gl.GL_MAX_DEPTH_TEXTURE_SAMPLES) + """Maximum number of samples in a multisample depth or depth-stencil texture""" + + self.MAX_GEOMETRY_INPUT_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_INPUT_COMPONENTS) + """Maximum number of components of inputs read by a geometry shader""" + + self.MAX_GEOMETRY_OUTPUT_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_OUTPUT_COMPONENTS) + """Maximum number of components of outputs written by a geometry shader""" + + self.MAX_GEOMETRY_TEXTURE_IMAGE_UNITS = self.get(gl.GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS) + """ + Maximum supported texture image units that can be used to access texture + maps from the geometry shader + """ + + self.MAX_GEOMETRY_UNIFORM_BLOCKS = self.get(gl.GL_MAX_GEOMETRY_UNIFORM_BLOCKS) + """Maximum number of uniform blocks per geometry shader""" + + self.MAX_GEOMETRY_UNIFORM_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_UNIFORM_COMPONENTS) + """ + Maximum number of individual floating-point, integer, or boolean values that can + be held in uniform variable storage for a geometry shader + """ + + self.MAX_INTEGER_SAMPLES = self.get(gl.GL_MAX_INTEGER_SAMPLES) + """Maximum number of samples supported in integer format multisample buffers""" + + self.MAX_SAMPLE_MASK_WORDS = self.get(gl.GL_MAX_SAMPLE_MASK_WORDS) + """Maximum number of sample mask words""" + + self.POINT_SIZE_RANGE = self.get_int_tuple(gl.GL_POINT_SIZE_RANGE, 2) + """The minimum and maximum point size""" + + # This error checking doesn't actually need any implementation specific details + # However we need to do it here instead of the common class to catch all possible + # errors because of implementation specific gets. + err = self._ctx.error + if err: + from warnings import warn + + warn(f"Error happened while querying of limits. {err}") + + def get_int_tuple(self, enum, length: int): + """ + Get an enum as an int tuple + + Args: + enum: The enum to query + length: The length of the tuple + """ + try: + values = (c_int * length)() + gl.glGetIntegerv(enum, values) + return tuple(values) + except pyglet.gl.lib.GLException: + return tuple([0] * length) + + def get(self, enum, default=0) -> int: + """ + Get an integer limit. + + Args: + enum: The enum to query + default: The default value if the query fails + """ + try: + value = c_int() + gl.glGetIntegerv(enum, value) + return value.value + except pyglet.gl.lib.GLException: + return default + + def get_float(self, enum, default=0.0) -> float: + """ + Get a float limit + + Args: + enum: The enum to query + default: The default value if the query fails + """ + try: + value = c_float() + gl.glGetFloatv(enum, value) + return value.value + except pyglet.gl.lib.GLException: + return default + + def get_str(self, enum) -> str: + """ + Get a string limit. + + Args: + enum: The enum to query + """ + try: + return cast(gl.glGetString(enum), c_char_p).value.decode() # type: ignore + except pyglet.gl.lib.GLException: + return "Unknown" diff --git a/arcade/gl/backends/gl/provider.py b/arcade/gl/backends/gl/provider.py index 1af139fa7..5d3fc14f8 100644 --- a/arcade/gl/backends/gl/provider.py +++ b/arcade/gl/backends/gl/provider.py @@ -1,10 +1,13 @@ from arcade.gl.provider import BaseProvider -from .context import GLContext, GLArcadeContext +from .context import GLContext, GLArcadeContext, GLInfo class Provider(BaseProvider): def create_context(self, *args, **kwargs): return GLContext(*args, **kwargs) + def create_info(self, ctx): + return GLInfo(ctx) + def create_arcade_context(self, *args, **kwargs): return GLArcadeContext(*args, **kwargs) \ No newline at end of file diff --git a/arcade/gl/backends/gl/query.py b/arcade/gl/backends/gl/query.py index ba7ec6f8d..b1fd3d19f 100644 --- a/arcade/gl/backends/gl/query.py +++ b/arcade/gl/backends/gl/query.py @@ -42,7 +42,6 @@ class GLQuery(Query): "_glo_samples_passed", "_glo_time_elapsed", "_glo_primitives_generated", - "__weakref__", ) def __init__(self, ctx: Context, samples=True, time=True, primitives=True): diff --git a/arcade/gl/context.py b/arcade/gl/context.py index 61eed1bbc..ed5cbd4e4 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -6,7 +6,6 @@ import weakref from collections import deque from contextlib import contextmanager -from ctypes import c_char_p, c_float, c_int, cast from typing import ( Any, Deque, @@ -21,20 +20,19 @@ ) import pyglet -import pyglet.gl.lib -from pyglet import gl from pyglet.window import Window +from . import enums from ..types import BufferProtocol from .buffer import Buffer from .compute_shader import ComputeShader from .framebuffer import DefaultFrameBuffer, Framebuffer from .program import Program +from .provider import get_provider from .query import Query from .sampler import Sampler from .texture import Texture2D from .texture_array import TextureArray -from .types import BufferDescription, GLenumLike, PyGLenum from .vertex_array import Geometry LOG = logging.getLogger(__name__) @@ -58,184 +56,160 @@ class Context(ABC): active: Context | None = None """The active context""" - #: The OpenGL api. Usually "gl" or "gles". - gl_api: str = "gl" - # --- Store the most commonly used OpenGL constants # Texture - NEAREST = 0x2600 + NEAREST = enums.NEAREST """Texture interpolation - Nearest pixel""" - LINEAR = 0x2601 + LINEAR = enums.LINEAR """Texture interpolation - Linear interpolate""" - NEAREST_MIPMAP_NEAREST = 0x2700 + NEAREST_MIPMAP_NEAREST = enums.NEAREST_MIPMAP_NEAREST """Texture interpolation - Minification filter for mipmaps""" - LINEAR_MIPMAP_NEAREST = 0x2701 + LINEAR_MIPMAP_NEAREST = enums.LINEAR_MIPMAP_NEAREST """Texture interpolation - Minification filter for mipmaps""" - NEAREST_MIPMAP_LINEAR = 0x2702 + NEAREST_MIPMAP_LINEAR = enums.NEAREST_MIPMAP_LINEAR """Texture interpolation - Minification filter for mipmaps""" - LINEAR_MIPMAP_LINEAR = 0x2703 + LINEAR_MIPMAP_LINEAR = enums.LINEAR_MIPMAP_LINEAR """Texture interpolation - Minification filter for mipmaps""" - REPEAT = gl.GL_REPEAT + REPEAT = enums.REPEAT """Texture wrap mode - Repeat""" - CLAMP_TO_EDGE = gl.GL_CLAMP_TO_EDGE + CLAMP_TO_EDGE = enums.CLAMP_TO_EDGE """Texture wrap mode - Clamp to border pixel""" - CLAMP_TO_BORDER = gl.GL_CLAMP_TO_BORDER - """Texture wrap mode - Clamp to border color""" - - MIRRORED_REPEAT = gl.GL_MIRRORED_REPEAT + MIRRORED_REPEAT = enums.MIRRORED_REPEAT """Texture wrap mode - Repeat mirrored""" # Flags - BLEND = gl.GL_BLEND + BLEND = enums.BLEND """Context flag - Blending""" - DEPTH_TEST = gl.GL_DEPTH_TEST + DEPTH_TEST = enums.DEPTH_TEST """Context flag - Depth testing""" - CULL_FACE = gl.GL_CULL_FACE + CULL_FACE = enums.CULL_FACE """Context flag - Face culling""" - PROGRAM_POINT_SIZE = gl.GL_PROGRAM_POINT_SIZE - """ - Context flag - Enables ``gl_PointSize`` in vertex or geometry shaders. - - When enabled we can write to ``gl_PointSize`` in the vertex shader to specify the point size - for each individual point. - - If this value is not set in the shader the behavior is undefined. This means the points may - or may not appear depending if the drivers enforce some default value for ``gl_PointSize``. - - When disabled :py:attr:`point_size` is used. - """ - # Blend functions - ZERO = 0x0000 + ZERO = enums.ZERO """Blend function""" - ONE = 0x0001 + ONE = enums.ONE """Blend function""" - SRC_COLOR = 0x0300 + SRC_COLOR = enums.SRC_COLOR """Blend function""" - ONE_MINUS_SRC_COLOR = 0x0301 + ONE_MINUS_SRC_COLOR = enums.ONE_MINUS_SRC_COLOR """Blend function""" - SRC_ALPHA = 0x0302 + SRC_ALPHA = enums.SRC_ALPHA """Blend function""" - ONE_MINUS_SRC_ALPHA = 0x0303 + ONE_MINUS_SRC_ALPHA = enums.ONE_MINUS_SRC_ALPHA """Blend function""" - DST_ALPHA = 0x0304 + DST_ALPHA = enums.DST_ALPHA """Blend function""" - ONE_MINUS_DST_ALPHA = 0x0305 + ONE_MINUS_DST_ALPHA = enums.ONE_MINUS_DST_ALPHA """Blend function""" - DST_COLOR = 0x0306 + DST_COLOR = enums.DST_COLOR """Blend function""" - ONE_MINUS_DST_COLOR = 0x0307 + ONE_MINUS_DST_COLOR = enums.ONE_MINUS_DST_COLOR """Blend function""" # Blend equations - FUNC_ADD = 0x8006 + FUNC_ADD = enums.FUNC_ADD """Blend equation - source + destination""" - FUNC_SUBTRACT = 0x800A + FUNC_SUBTRACT = enums.FUNC_SUBTRACT """Blend equation - source - destination""" - FUNC_REVERSE_SUBTRACT = 0x800B + FUNC_REVERSE_SUBTRACT = enums.FUNC_REVERSE_SUBTRACT """Blend equation - destination - source""" - MIN = 0x8007 + MIN = enums.MIN """Blend equation - Minimum of source and destination""" - MAX = 0x8008 + MAX = enums.MAX """Blend equation - Maximum of source and destination""" # Blend mode shortcuts - BLEND_DEFAULT = 0x0302, 0x0303 + BLEND_DEFAULT = enums.BLEND_DEFAULT """Blend mode shortcut for default blend mode - ``SRC_ALPHA, ONE_MINUS_SRC_ALPHA``""" - BLEND_ADDITIVE = 0x0001, 0x0001 + BLEND_ADDITIVE = enums.BLEND_ADDITIVE """Blend mode shortcut for additive blending - ``ONE, ONE``""" - BLEND_PREMULTIPLIED_ALPHA = 0x0302, 0x0001 + BLEND_PREMULTIPLIED_ALPHA = enums.BLEND_PREMULTIPLIED_ALPHA """Blend mode shortcut for pre-multiplied alpha - ``SRC_ALPHA, ONE``""" # VertexArray: Primitives - POINTS = gl.GL_POINTS # 0 + POINTS = enums.POINTS # 0 """Primitive mode - points""" - LINES = gl.GL_LINES # 1 + LINES = enums.LINES # 1 """Primitive mode - lines""" - LINE_LOOP = gl.GL_LINE_LOOP # 2 + LINE_LOOP = enums.LINE_LOOP # 2 """Primitive mode - line loop""" - LINE_STRIP = gl.GL_LINE_STRIP # 3 + LINE_STRIP = enums.LINE_STRIP # 3 """Primitive mode - line strip""" - TRIANGLES = gl.GL_TRIANGLES # 4 + TRIANGLES = enums.TRIANGLES # 4 """Primitive mode - triangles""" - TRIANGLE_STRIP = gl.GL_TRIANGLE_STRIP # 5 + TRIANGLE_STRIP = enums.TRIANGLE_STRIP # 5 """Primitive mode - triangle strip""" - TRIANGLE_FAN = gl.GL_TRIANGLE_FAN # 6 + TRIANGLE_FAN = enums.TRIANGLE_FAN # 6 """Primitive mode - triangle fan""" - LINES_ADJACENCY = gl.GL_LINES_ADJACENCY # 10 + ##### ADJACENCY VALUES ARE NOT SUPPORTED BY WEBGL + ##### WE ARE LEAVING THESE VALUES IN THE COMMON IMPLEMENTATION + ##### TO MAKE IMPLEMENTATION EASIER, BECAUSE WEBGL WILL FAIL + ##### BEFORE USAGE OF THESE MATTERS + + LINES_ADJACENCY = enums.LINES_ADJACENCY # 10 """Primitive mode - lines with adjacency""" - LINE_STRIP_ADJACENCY = gl.GL_LINE_STRIP_ADJACENCY # 11 + LINE_STRIP_ADJACENCY = enums.LINE_STRIP_ADJACENCY # 11 """Primitive mode - line strip with adjacency""" - TRIANGLES_ADJACENCY = gl.GL_TRIANGLES_ADJACENCY # 12 + TRIANGLES_ADJACENCY = enums.TRIANGLES_ADJACENCY # 12 """Primitive mode - triangles with adjacency""" - TRIANGLE_STRIP_ADJACENCY = gl.GL_TRIANGLE_STRIP_ADJACENCY # 13 + TRIANGLE_STRIP_ADJACENCY = enums.TRIANGLE_STRIP_ADJACENCY # 13 """Primitive mode - triangle strip with adjacency""" - PATCHES = gl.GL_PATCHES - """Primitive mode - Patch (tessellation)""" - # The most common error enums _errors = { - gl.GL_INVALID_ENUM: "GL_INVALID_ENUM", - gl.GL_INVALID_VALUE: "GL_INVALID_VALUE", - gl.GL_INVALID_OPERATION: "GL_INVALID_OPERATION", - gl.GL_INVALID_FRAMEBUFFER_OPERATION: "GL_INVALID_FRAMEBUFFER_OPERATION", - gl.GL_OUT_OF_MEMORY: "GL_OUT_OF_MEMORY", - gl.GL_STACK_UNDERFLOW: "GL_STACK_UNDERFLOW", - gl.GL_STACK_OVERFLOW: "GL_STACK_OVERFLOW", + enums.INVALID_ENUM: "INVALID_ENUM", + enums.INVALID_VALUE: "INVALID_VALUE", + enums.INVALID_OPERATION: "INVALID_OPERATION", + enums.INVALID_FRAMEBUFFER_OPERATION: "INVALID_FRAMEBUFFER_OPERATION", + enums.OUT_OF_MEMORY: "OUT_OF_MEMORY", } - _valid_apis = ("gl", "gles") def __init__( self, window: pyglet.window.Window, # type: ignore gc_mode: str = "context_gc", - gl_api: str = "gl", ): self._window_ref = weakref.ref(window) - if gl_api not in self._valid_apis: - raise ValueError(f"Invalid gl_api. Options are: {self._valid_apis}") - self.gl_api = gl_api - self._info = GLInfo(self) - self._gl_version = (self._info.MAJOR_VERSION, self._info.MINOR_VERSION) + self._info = get_provider().create_info(self) + Context.activate(self) # Texture unit we use when doing operations on textures to avoid # affecting currently bound textures in the first units @@ -249,30 +223,9 @@ def __init__( self.active_framebuffer: Framebuffer = self._screen self._stats: ContextStats = ContextStats(warn_threshold=1000) - # Hardcoded states - # This should always be enabled - # gl.glEnable(gl.GL_TEXTURE_CUBE_MAP_SEAMLESS) - # Set primitive restart index to -1 by default - if self.gl_api == "gles": - gl.glEnable(gl.GL_PRIMITIVE_RESTART_FIXED_INDEX) - else: - gl.glEnable(gl.GL_PRIMITIVE_RESTART) - self._primitive_restart_index = -1 self.primitive_restart_index = self._primitive_restart_index - # Detect support for glProgramUniform. - # Assumed to be supported in gles - self._ext_separate_shader_objects_enabled = True - if self.gl_api == "gl": - have_ext = gl.gl_info.have_extension("GL_ARB_separate_shader_objects") - self._ext_separate_shader_objects_enabled = self.gl_version >= (4, 1) or have_ext - - # We enable scissor testing by default. - # This is always set to the same value as the viewport - # to avoid background color affecting areas outside the viewport - gl.glEnable(gl.GL_SCISSOR_TEST) - # States self._blend_func: Tuple[int, int] | Tuple[int, int, int, int] = self.BLEND_DEFAULT self._point_size = 1.0 @@ -280,14 +233,14 @@ def __init__( self._wireframe = False # Options for cull_face self._cull_face_options = { - "front": gl.GL_FRONT, - "back": gl.GL_BACK, - "front_and_back": gl.GL_FRONT_AND_BACK, + "front": enums.FRONT, + "back": enums.BACK, + "front_and_back": enums.FRONT_AND_BACK, } self._cull_face_options_reverse = { - gl.GL_FRONT: "front", - gl.GL_BACK: "back", - gl.GL_FRONT_AND_BACK: "front_and_back", + enums.FRONT: "front", + enums.BACK: "back", + enums.FRONT_AND_BACK: "front_and_back", } # Context GC as default. We need to call Context.gc() to free opengl resources @@ -302,7 +255,7 @@ def _create_default_framebuffer(self) -> DefaultFrameBuffer: raise NotImplementedError("The enabled graphics backend does not support this method.") @property - def info(self) -> GLInfo: + def info(self) -> Info: """ Get the info object for this context containing information about hardware/driver limits and other information. @@ -319,6 +272,7 @@ def info(self) -> GLInfo: return self._info @property + @abstractmethod def extensions(self) -> set[str]: """ Get a set of supported OpenGL extensions strings for this context. @@ -331,7 +285,7 @@ def extensions(self) -> set[str]: expected_extensions = {"GL_ARB_bindless_texture", "GL_ARB_get_program_binary"} ctx.extensions & expected_extensions == expected_extensions """ - return gl.gl_info.get_extensions() + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def stats(self) -> ContextStats: @@ -375,17 +329,6 @@ def fbo(self) -> Framebuffer: """ return self.active_framebuffer - @property - def gl_version(self) -> Tuple[int, int]: - """ - The OpenGL major and minor version as a tuple. - - This is the reported OpenGL version from - drivers and might be a higher version than - you requested. - """ - return self._gl_version - def gc(self) -> int: """ Run garbage collection of OpenGL objects for this context. @@ -432,6 +375,7 @@ def gc_mode(self, value: str): self._gc_mode = value @property + @abstractmethod def error(self) -> str | None: """Check OpenGL error @@ -444,11 +388,7 @@ def error(self) -> str | None: if err: raise RuntimeError("OpenGL error: {err}") """ - err = gl.glGetError() - if err == gl.GL_NO_ERROR: - return None - - return self._errors.get(err, "GL_UNKNOWN_ERROR") + raise NotImplementedError("The enabled graphics backend does not support this method.") @classmethod def activate(cls, ctx: Context): @@ -462,6 +402,7 @@ def activate(cls, ctx: Context): """ cls.active = ctx + @abstractmethod def enable(self, *flags: int): """ Enables one or more context flags:: @@ -474,11 +415,9 @@ def enable(self, *flags: int): Args: *flags: The flags to enable """ - self._flags.update(flags) - - for flag in flags: - gl.glEnable(flag) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def enable_only(self, *args: int): """ Enable only some flags. This will disable all other flags. @@ -495,28 +434,7 @@ def enable_only(self, *args: int): Args: *args: The flags to enable """ - self._flags = set(args) - - if self.BLEND in self._flags: - gl.glEnable(self.BLEND) - else: - gl.glDisable(self.BLEND) - - if self.DEPTH_TEST in self._flags: - gl.glEnable(self.DEPTH_TEST) - else: - gl.glDisable(self.DEPTH_TEST) - - if self.CULL_FACE in self._flags: - gl.glEnable(self.CULL_FACE) - else: - gl.glDisable(self.CULL_FACE) - - if self.gl_api == "gl": - if self.PROGRAM_POINT_SIZE in self._flags: - gl.glEnable(self.PROGRAM_POINT_SIZE) - else: - gl.glDisable(self.PROGRAM_POINT_SIZE) + raise NotImplementedError("The enabled graphics backend does not support this method.") @contextmanager def enabled(self, *flags): @@ -561,6 +479,7 @@ def enabled_only(self, *flags): finally: self.enable_only(*old_flags) + @abstractmethod def disable(self, *args): """ Disable one or more context flags:: @@ -570,10 +489,7 @@ def disable(self, *args): # Multiple flags ctx.disable(ctx.DEPTH_TEST, ctx.CULL_FACE) """ - self._flags -= set(args) - - for flag in args: - gl.glDisable(flag) + raise NotImplementedError("The enabled graphics backend does not support this method.") def is_enabled(self, flag) -> bool: """ @@ -683,19 +599,15 @@ def blend_func(self) -> Tuple[int, int] | Tuple[int, int, int, int]: return self._blend_func @blend_func.setter + @abstractmethod def blend_func(self, value: Tuple[int, int] | Tuple[int, int, int, int]): - self._blend_func = value - if len(value) == 2: - gl.glBlendFunc(*value) - elif len(value) == 4: - gl.glBlendFuncSeparate(*value) - else: - ValueError("blend_func takes a tuple of 2 or 4 values") + raise NotImplementedError("The enabled graphics backend does not support this method.") # def blend_equation(self) # Default is FUNC_ADD @property + @abstractmethod def front_face(self) -> str: """ Configure front face winding order of triangles. @@ -706,17 +618,15 @@ def front_face(self) -> str: ctx.front_face = "cw" ctx.front_face = "ccw" """ - value = c_int() - gl.glGetIntegerv(gl.GL_FRONT_FACE, value) - return "cw" if value.value == gl.GL_CW else "ccw" + raise NotImplementedError("The enabled graphics backend does not support this method.") @front_face.setter + @abstractmethod def front_face(self, value: str): - if value not in ["cw", "ccw"]: - raise ValueError("front_face must be 'cw' or 'ccw'") - gl.glFrontFace(gl.GL_CW if value == "cw" else gl.GL_CCW) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def cull_face(self) -> str: """ The face side to cull when face culling is enabled. @@ -728,16 +638,11 @@ def cull_face(self) -> str: ctx.cull_face = "back" ctx.cull_face = "front_and_back" """ - value = c_int() - gl.glGetIntegerv(gl.GL_CULL_FACE_MODE, value) - return self._cull_face_options_reverse[value.value] + raise NotImplementedError("The enabled graphics backend does not support this method.") @cull_face.setter def cull_face(self, value): - if value not in self._cull_face_options: - raise ValueError("cull_face must be", list(self._cull_face_options.keys())) - - gl.glCullFace(self._cull_face_options[value]) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def wireframe(self) -> bool: @@ -750,14 +655,12 @@ def wireframe(self) -> bool: return self._wireframe @wireframe.setter + @abstractmethod def wireframe(self, value: bool): - self._wireframe = value - if value: - gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_LINE) - else: - gl.glPolygonMode(gl.GL_FRONT_AND_BACK, gl.GL_FILL) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property + @abstractmethod def patch_vertices(self) -> int: """ Get or set number of vertices that will be used to make up a single patch primitive. @@ -765,16 +668,12 @@ def patch_vertices(self) -> int: Patch primitives are consumed by the tessellation control shader (if present) and subsequently used for tessellation. """ - value = c_int() - gl.glGetIntegerv(gl.GL_PATCH_VERTICES, value) - return value.value + raise NotImplementedError("The enabled graphics backend does not support this method.") @patch_vertices.setter + @abstractmethod def patch_vertices(self, value: int): - if not isinstance(value, int): - raise TypeError("patch_vertices must be an integer") - - gl.glPatchParameteri(gl.GL_PATCH_VERTICES, value) + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def point_size(self) -> float: @@ -798,10 +697,9 @@ def point_size(self) -> float: return self._point_size @point_size.setter + @abstractmethod def point_size(self, value: float): - if self.gl_api == "gl": - gl.glPointSize(self._point_size) - self._point_size = value + raise NotImplementedError("The enabled graphics backend does not support this method.") @property def primitive_restart_index(self) -> int: @@ -816,11 +714,11 @@ def primitive_restart_index(self) -> int: return self._primitive_restart_index @primitive_restart_index.setter + @abstractmethod def primitive_restart_index(self, value: int): - self._primitive_restart_index = value - if self.gl_api == "gl": - gl.glPrimitiveRestartIndex(value) + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def finish(self) -> None: """ Wait until all OpenGL rendering commands are completed. @@ -828,8 +726,9 @@ def finish(self) -> None: This function will actually stall until all work is done and may have severe performance implications. """ - gl.glFinish() + raise NotImplementedError("The enabled graphics backend does not support this method.") + @abstractmethod def flush(self) -> None: """ Flush the OpenGL command buffer. @@ -839,7 +738,7 @@ def flush(self) -> None: ensure that all commands are sent to the GPU before doing something else. """ - gl.glFlush() + raise NotImplementedError("The enabled graphics backend does not support this method.") # Various utility methods @@ -955,12 +854,12 @@ def texture( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - wrap_x: PyGLenum | None = None, - wrap_y: PyGLenum | None = None, - filter: Tuple[PyGLenum, PyGLenum] | None = None, + wrap_x = None, + wrap_y = None, + filter = None, samples: int = 0, immutable: bool = False, - internal_format: PyGLenum | None = None, + internal_format, compressed: bool = False, compressed_data: bool = False, ) -> Texture2D: @@ -1043,9 +942,9 @@ def texture_array( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - wrap_x: PyGLenum | None = None, - wrap_y: PyGLenum | None = None, - filter: Tuple[PyGLenum, PyGLenum] | None = None, + wrap_x = None, + wrap_y = None, + filter = None, ) -> TextureArray: """ Create a 2D Texture Array. @@ -1092,7 +991,7 @@ def sampler(self, texture: Texture2D) -> Sampler: @abstractmethod def geometry( self, - content: Sequence[BufferDescription] | None = None, + content = None, index_buffer: Buffer | None = None, mode: int | None = None, index_element_size: int = 4, @@ -1311,187 +1210,137 @@ def decr(self, key): setattr(self, key, (created, freed + 1)) -class GLInfo: +class Info(ABC): """OpenGL info and capabilities""" def __init__(self, ctx): self._ctx = ctx - self.MINOR_VERSION = self.get(gl.GL_MINOR_VERSION) - """Minor version number of the OpenGL API supported by the current context""" - - self.MAJOR_VERSION = self.get(gl.GL_MAJOR_VERSION) - """Major version number of the OpenGL API supported by the current context.""" - - self.VENDOR = self.get_str(gl.GL_VENDOR) + self.VENDOR = self.get_str(enums.VENDOR) """The vendor string. For example 'NVIDIA Corporation'""" - self.RENDERER = self.get_str(gl.GL_RENDERER) + self.RENDERER = self.get_str(enums.RENDERER) """The renderer things. For example "NVIDIA GeForce RTX 2080 SUPER/PCIe/SSE2""" - self.SAMPLE_BUFFERS = self.get(gl.GL_SAMPLE_BUFFERS) + self.SAMPLE_BUFFERS = self.get(enums.SAMPLE_BUFFERS) """Value indicating the number of sample buffers associated with the framebuffer""" - self.SUBPIXEL_BITS = self.get(gl.GL_SUBPIXEL_BITS) + self.SUBPIXEL_BITS = self.get(enums.SUBPIXEL_BITS) """ An estimate of the number of bits of subpixel resolution that are used to position rasterized geometry in window coordinates """ - self.UNIFORM_BUFFER_OFFSET_ALIGNMENT = self.get(gl.GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT) + self.UNIFORM_BUFFER_OFFSET_ALIGNMENT = self.get(enums.UNIFORM_BUFFER_OFFSET_ALIGNMENT) """Minimum required alignment for uniform buffer sizes and offset""" - self.MAX_ARRAY_TEXTURE_LAYERS = self.get(gl.GL_MAX_ARRAY_TEXTURE_LAYERS) + self.MAX_ARRAY_TEXTURE_LAYERS = self.get(enums.MAX_ARRAY_TEXTURE_LAYERS) """ Value indicates the maximum number of layers allowed in an array texture, and must be at least 256 """ - self.MAX_3D_TEXTURE_SIZE = self.get(gl.GL_MAX_3D_TEXTURE_SIZE) + self.MAX_3D_TEXTURE_SIZE = self.get(enums.MAX_3D_TEXTURE_SIZE) """ A rough estimate of the largest 3D texture that the GL can handle. The value must be at least 64 """ - self.MAX_COLOR_ATTACHMENTS = self.get(gl.GL_MAX_COLOR_ATTACHMENTS) + self.MAX_COLOR_ATTACHMENTS = self.get(enums.MAX_COLOR_ATTACHMENTS) """Maximum number of color attachments in a framebuffer""" - self.MAX_COLOR_TEXTURE_SAMPLES = self.get(gl.GL_MAX_COLOR_TEXTURE_SAMPLES) - """Maximum number of samples in a color multisample texture""" + self.MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = self.get( + enums.MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS + ) + """Number of words for vertex shader uniform variables in all uniform blocks""" self.MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = self.get( - gl.GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS + enums.MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS ) """the number of words for fragment shader uniform variables in all uniform blocks""" - self.MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS = self.get( - gl.GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS - ) - """Number of words for geometry shader uniform variables in all uniform blocks""" - - self.MAX_COMBINED_TEXTURE_IMAGE_UNITS = self.get(gl.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS) + self.MAX_COMBINED_TEXTURE_IMAGE_UNITS = self.get(enums.MAX_COMBINED_TEXTURE_IMAGE_UNITS) """ Maximum supported texture image units that can be used to access texture maps from the vertex shader """ - self.MAX_COMBINED_UNIFORM_BLOCKS = self.get(gl.GL_MAX_COMBINED_UNIFORM_BLOCKS) + self.MAX_COMBINED_UNIFORM_BLOCKS = self.get(enums.MAX_COMBINED_UNIFORM_BLOCKS) """Maximum number of uniform blocks per program""" - self.MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = self.get( - gl.GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS - ) - """Number of words for vertex shader uniform variables in all uniform blocks""" - - self.MAX_CUBE_MAP_TEXTURE_SIZE = self.get(gl.GL_MAX_CUBE_MAP_TEXTURE_SIZE) + self.MAX_CUBE_MAP_TEXTURE_SIZE = self.get(enums.MAX_CUBE_MAP_TEXTURE_SIZE) """A rough estimate of the largest cube-map texture that the GL can handle""" - self.MAX_DEPTH_TEXTURE_SAMPLES = self.get(gl.GL_MAX_DEPTH_TEXTURE_SAMPLES) - """Maximum number of samples in a multisample depth or depth-stencil texture""" - - self.MAX_DRAW_BUFFERS = self.get(gl.GL_MAX_DRAW_BUFFERS) + self.MAX_DRAW_BUFFERS = self.get(enums.MAX_DRAW_BUFFERS) """Maximum number of simultaneous outputs that may be written in a fragment shader""" - self.MAX_ELEMENTS_INDICES = self.get(gl.GL_MAX_ELEMENTS_INDICES) - """Recommended maximum number of vertex array indices""" - - self.MAX_ELEMENTS_VERTICES = self.get(gl.GL_MAX_ELEMENTS_VERTICES) + self.MAX_ELEMENTS_VERTICES = self.get(enums.MAX_ELEMENTS_VERTICES) """Recommended maximum number of vertex array vertices""" - self.MAX_FRAGMENT_INPUT_COMPONENTS = self.get(gl.GL_MAX_FRAGMENT_INPUT_COMPONENTS) + self.MAX_ELEMENTS_INDICES = self.get(enums.MAX_ELEMENTS_INDICES) + """Recommended maximum number of vertex array indices""" + + self.MAX_FRAGMENT_INPUT_COMPONENTS = self.get(enums.MAX_FRAGMENT_INPUT_COMPONENTS) """Maximum number of components of the inputs read by the fragment shader""" - self.MAX_FRAGMENT_UNIFORM_COMPONENTS = self.get(gl.GL_MAX_FRAGMENT_UNIFORM_COMPONENTS) + self.MAX_FRAGMENT_UNIFORM_COMPONENTS = self.get(enums.MAX_FRAGMENT_UNIFORM_COMPONENTS) """ Maximum number of individual floating-point, integer, or boolean values that can be held in uniform variable storage for a fragment shader """ - self.MAX_FRAGMENT_UNIFORM_VECTORS = self.get(gl.GL_MAX_FRAGMENT_UNIFORM_VECTORS) + self.MAX_FRAGMENT_UNIFORM_VECTORS = self.get(enums.MAX_FRAGMENT_UNIFORM_VECTORS) """ Maximum number of individual 4-vectors of floating-point, integer, or boolean values that can be held in uniform variable storage for a fragment shader """ - self.MAX_FRAGMENT_UNIFORM_BLOCKS = self.get(gl.GL_MAX_FRAGMENT_UNIFORM_BLOCKS) + self.MAX_FRAGMENT_UNIFORM_BLOCKS = self.get(enums.MAX_FRAGMENT_UNIFORM_BLOCKS) """Maximum number of uniform blocks per fragment shader.""" - self.MAX_GEOMETRY_INPUT_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_INPUT_COMPONENTS) - """Maximum number of components of inputs read by a geometry shader""" - - self.MAX_GEOMETRY_OUTPUT_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_OUTPUT_COMPONENTS) - """Maximum number of components of outputs written by a geometry shader""" - - self.MAX_GEOMETRY_TEXTURE_IMAGE_UNITS = self.get(gl.GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS) - """ - Maximum supported texture image units that can be used to access texture - maps from the geometry shader - """ - - self.MAX_GEOMETRY_UNIFORM_BLOCKS = self.get(gl.GL_MAX_GEOMETRY_UNIFORM_BLOCKS) - """Maximum number of uniform blocks per geometry shader""" - - self.MAX_GEOMETRY_UNIFORM_COMPONENTS = self.get(gl.GL_MAX_GEOMETRY_UNIFORM_COMPONENTS) - """ - Maximum number of individual floating-point, integer, or boolean values that can - be held in uniform variable storage for a geometry shader - """ - - self.MAX_INTEGER_SAMPLES = self.get(gl.GL_MAX_INTEGER_SAMPLES) - """Maximum number of samples supported in integer format multisample buffers""" - - self.MAX_SAMPLES = self.get(gl.GL_MAX_SAMPLES) + self.MAX_SAMPLES = self.get(enums.MAX_SAMPLES) """Maximum samples for a framebuffer""" - self.MAX_RENDERBUFFER_SIZE = self.get(gl.GL_MAX_RENDERBUFFER_SIZE) + self.MAX_RENDERBUFFER_SIZE = self.get(enums.MAX_RENDERBUFFER_SIZE) """Maximum supported size for renderbuffers""" - self.MAX_SAMPLE_MASK_WORDS = self.get(gl.GL_MAX_SAMPLE_MASK_WORDS) - """Maximum number of sample mask words""" - - self.MAX_UNIFORM_BUFFER_BINDINGS = self.get(gl.GL_MAX_UNIFORM_BUFFER_BINDINGS) - """Maximum number of uniform buffer binding points on the context""" - - self.MAX_UNIFORM_BUFFER_BINDINGS = self.get(gl.GL_MAX_UNIFORM_BUFFER_BINDINGS) + self.MAX_UNIFORM_BUFFER_BINDINGS = self.get(enums.MAX_UNIFORM_BUFFER_BINDINGS) """Maximum number of uniform buffer binding points on the context""" - self.MAX_TEXTURE_SIZE = self.get(gl.GL_MAX_TEXTURE_SIZE) + self.MAX_TEXTURE_SIZE = self.get(enums.MAX_TEXTURE_SIZE) """The value gives a rough estimate of the largest texture that the GL can handle""" - self.MAX_UNIFORM_BUFFER_BINDINGS = self.get(gl.GL_MAX_UNIFORM_BUFFER_BINDINGS) - """Maximum number of uniform buffer binding points on the context""" - - self.MAX_UNIFORM_BLOCK_SIZE = self.get(gl.GL_MAX_UNIFORM_BLOCK_SIZE) + self.MAX_UNIFORM_BLOCK_SIZE = self.get(enums.MAX_UNIFORM_BLOCK_SIZE) """Maximum size in basic machine units of a uniform block""" - self.MAX_VARYING_VECTORS = self.get(gl.GL_MAX_VARYING_VECTORS) + self.MAX_VARYING_VECTORS = self.get(enums.MAX_VARYING_VECTORS) """The number 4-vectors for varying variables""" - self.MAX_VERTEX_ATTRIBS = self.get(gl.GL_MAX_VERTEX_ATTRIBS) + self.MAX_VERTEX_ATTRIBS = self.get(enums.MAX_VERTEX_ATTRIBS) """Maximum number of 4-component generic vertex attributes accessible to a vertex shader.""" - self.MAX_VERTEX_TEXTURE_IMAGE_UNITS = self.get(gl.GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS) + self.MAX_VERTEX_TEXTURE_IMAGE_UNITS = self.get(enums.MAX_VERTEX_TEXTURE_IMAGE_UNITS) """ Maximum supported texture image units that can be used to access texture maps from the vertex shader. """ - self.MAX_VERTEX_UNIFORM_COMPONENTS = self.get(gl.GL_MAX_VERTEX_UNIFORM_COMPONENTS) + self.MAX_VERTEX_UNIFORM_COMPONENTS = self.get(enums.MAX_VERTEX_UNIFORM_COMPONENTS) """ Maximum number of individual floating-point, integer, or boolean values that can be held in uniform variable storage for a vertex shader """ - self.MAX_VERTEX_UNIFORM_VECTORS = self.get(gl.GL_MAX_VERTEX_UNIFORM_VECTORS) + self.MAX_VERTEX_UNIFORM_VECTORS = self.get(enums.MAX_VERTEX_UNIFORM_VECTORS) """ Maximum number of 4-vectors that may be held in uniform variable storage for the vertex shader """ - self.MAX_VERTEX_OUTPUT_COMPONENTS = self.get(gl.GL_MAX_VERTEX_OUTPUT_COMPONENTS) + self.MAX_VERTEX_OUTPUT_COMPONENTS = self.get(enums.MAX_VERTEX_OUTPUT_COMPONENTS) """Maximum number of components of output written by a vertex shader""" - self.MAX_VERTEX_UNIFORM_BLOCKS = self.get(gl.GL_MAX_VERTEX_UNIFORM_BLOCKS) + self.MAX_VERTEX_UNIFORM_BLOCKS = self.get(enums.MAX_VERTEX_UNIFORM_BLOCKS) """Maximum number of uniform blocks per vertex shader.""" # self.MAX_VERTEX_ATTRIB_RELATIVE_OFFSET = self.get( @@ -1499,42 +1348,34 @@ def __init__(self, ctx): # ) # self.MAX_VERTEX_ATTRIB_BINDINGS = self.get(gl.GL_MAX_VERTEX_ATTRIB_BINDINGS) - self.MAX_TEXTURE_IMAGE_UNITS = self.get(gl.GL_MAX_TEXTURE_IMAGE_UNITS) + self.MAX_TEXTURE_IMAGE_UNITS = self.get(enums.MAX_TEXTURE_IMAGE_UNITS) """Number of texture units""" - self.MAX_TEXTURE_MAX_ANISOTROPY = self.get_float(gl.GL_MAX_TEXTURE_MAX_ANISOTROPY, 1.0) + self.MAX_TEXTURE_MAX_ANISOTROPY = self.get_float(enums.MAX_TEXTURE_MAX_ANISOTROPY, 1.0) """The highest supported anisotropy value. Usually 8.0 or 16.0.""" - self.MAX_VIEWPORT_DIMS: Tuple[int, int] = self.get_int_tuple(gl.GL_MAX_VIEWPORT_DIMS, 2) + self.MAX_VIEWPORT_DIMS: Tuple[int, int] = self.get_int_tuple(enums.MAX_VIEWPORT_DIMS, 2) """ The maximum support window or framebuffer viewport. This is usually the same as the maximum texture size """ self.MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = self.get( - gl.GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS + enums.MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS ) """ How many buffers we can have as output when doing a transform(feedback). This is usually 4. """ - self.POINT_SIZE_RANGE = self.get_int_tuple(gl.GL_POINT_SIZE_RANGE, 2) - """The minimum and maximum point size""" - - err = self._ctx.error - if err: - from warnings import warn - - warn("Error happened while querying of limits. Moving on ..") - @overload - def get_int_tuple(self, enum: GLenumLike, length: Literal[2]) -> Tuple[int, int]: ... + def get_int_tuple(self, enum, length: Literal[2]) -> Tuple[int, int]: ... @overload - def get_int_tuple(self, enum: GLenumLike, length: int) -> Tuple[int, ...]: ... + def get_int_tuple(self, enum, length: int) -> Tuple[int, ...]: ... - def get_int_tuple(self, enum: GLenumLike, length: int): + @abstractmethod + def get_int_tuple(self, enum, length: int): """ Get an enum as an int tuple @@ -1542,14 +1383,10 @@ def get_int_tuple(self, enum: GLenumLike, length: int): enum: The enum to query length: The length of the tuple """ - try: - values = (c_int * length)() - gl.glGetIntegerv(enum, values) - return tuple(values) - except pyglet.gl.lib.GLException: - return tuple([0] * length) + raise NotImplementedError("The enabled graphics backend does not support this method.") - def get(self, enum: GLenumLike, default=0) -> int: + @abstractmethod + def get(self, enum, default=0) -> int: """ Get an integer limit. @@ -1557,14 +1394,10 @@ def get(self, enum: GLenumLike, default=0) -> int: enum: The enum to query default: The default value if the query fails """ - try: - value = c_int() - gl.glGetIntegerv(enum, value) - return value.value - except pyglet.gl.lib.GLException: - return default + raise NotImplementedError("The enabled graphics backend does not support this method.") - def get_float(self, enum: GLenumLike, default=0.0) -> float: + @abstractmethod + def get_float(self, enum, default=0.0) -> float: """ Get a float limit @@ -1572,21 +1405,14 @@ def get_float(self, enum: GLenumLike, default=0.0) -> float: enum: The enum to query default: The default value if the query fails """ - try: - value = c_float() - gl.glGetFloatv(enum, value) - return value.value - except pyglet.gl.lib.GLException: - return default + raise NotImplementedError("The enabled graphics backend does not support this method.") - def get_str(self, enum: GLenumLike) -> str: + @abstractmethod + def get_str(self, enum) -> str: """ Get a string limit. Args: enum: The enum to query """ - try: - return cast(gl.glGetString(enum), c_char_p).value.decode() # type: ignore - except pyglet.gl.lib.GLException: - return "Unknown" + raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/enums.py b/arcade/gl/enums.py index 775fec45a..6d0e0af24 100644 --- a/arcade/gl/enums.py +++ b/arcade/gl/enums.py @@ -1,4 +1,55 @@ -from pyglet import gl +""" +This module contains hard-coded hexadecimal enum values for OpenGL and WebGL values. +Only enums which are present in both OpenGL and WebGL are included, all of these enums, +based on the specifications, are the same. + +We are storing them here, so that we do not rely on importing the common subset of them from any +particular backend implementation's library(e.g. pyglet, JS via pyodide, etc). +""" + +# Get Parameters +VENDOR = 0x1F00 +RENDERER = 0x1F01 +VERSION = 0x1F02 +SAMPLE_BUFFERS = 0x80A8 +SUBPIXEL_BITS = 0x0D50 +UNIFORM_BUFFER_OFFSET_ALIGNMENT = 0x8A34 +MAX_ARRAY_TEXTURE_LAYERS = 0x88FF +MAX_3D_TEXTURE_SIZE = 0x8073 +MAX_COLOR_ATTACHMENTS = 0x8CDF +MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS = 0x8A31 +MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS = 0x8A33 +MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8B4D +MAX_COMBINED_UNIFORM_BLOCKS = 0x8A2E +MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C +MAX_DRAW_BUFFERS = 0x8824 +MAX_ELEMENTS_VERTICES = 0x80E8 +MAX_ELEMENTS_INDICES = 0x80E9 +MAX_FRAGMENT_INPUT_COMPONENTS = 0x9125 +MAX_FRAGMENT_UNIFORM_COMPONENTS = 0x8B49 +MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD +MAX_FRAGMENT_UNIFORM_BLOCKS = 0x8A2D +MAX_SAMPLES = 0x8D57 +MAX_RENDERBUFFER_SIZE = 0x84E8 +MAX_UNIFORM_BUFFER_BINDINGS = 0x8A2F +MAX_TEXTURE_SIZE = 0x0D33 +MAX_UNIFORM_BLOCK_SIZE = 0x8A30 +MAX_VARYING_VECTORS = 0x8DFC +MAX_VERTEX_ATTRIBS = 0x8869 +MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C +MAX_VERTEX_UNIFORM_COMPONENTS = 0x8B4A +MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB +MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B +MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122 +MAX_TEXTURE_IMAGE_UNITS = 0x8872 +MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF # Technically comes from EXT_texture_filter_anisotropic in WebGL, but it's widely available +MAX_VIEWPORT_DIMS = 0x0D3A +MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8C8B + +# Enable Flags +BLEND = 0x0BE2 +DEPTH_TEST = 0x0B71 +CULL_FACE = 0x0B44 # Texture min/mag filters NEAREST = 0x2600 @@ -9,10 +60,9 @@ LINEAR_MIPMAP_LINEAR = 0x2703 # Texture wrapping -REPEAT = gl.GL_REPEAT -CLAMP_TO_EDGE = gl.GL_CLAMP_TO_EDGE -CLAMP_TO_BORDER = gl.GL_CLAMP_TO_BORDER -MIRRORED_REPEAT = gl.GL_MIRRORED_REPEAT +REPEAT = 0x2901 +CLAMP_TO_EDGE = 0x812F +MIRRORED_REPEAT = 0x8370 # Blend functions ZERO = 0x0000 @@ -60,3 +110,15 @@ TRIANGLES_ADJACENCY = 12 TRIANGLE_STRIP_ADJACENCY = 13 PATCHES = 14 + +# Errors +NO_ERROR = 0 +INVALID_ENUM = 0x0500 +INVALID_VALUE = 0x0501 +INVALID_OPERATION = 0x0502 +INVALID_FRAMEBUFFER_OPERATION = 0x0506 +OUT_OF_MEMORY = 0x0505 + +FRONT = 0x0404 +BACK = 0x0405 +FRONT_AND_BACK = 0x0408 diff --git a/arcade/gl/provider.py b/arcade/gl/provider.py index 4143c43ef..a8bbb9d8b 100644 --- a/arcade/gl/provider.py +++ b/arcade/gl/provider.py @@ -1,12 +1,13 @@ from __future__ import annotations from abc import ABC, abstractmethod +from typing import TYPE_CHECKING import importlib -from arcade.context import ArcadeContext - -from .context import Context +if TYPE_CHECKING: + from arcade.context import ArcadeContext + from .context import Context _current_provider: BaseProvider = None @@ -20,6 +21,8 @@ def set_provider(provider_name: str): print(e) raise ImportError(f"arcade.gl Backend Provider '{provider_name}' not found") +def get_provider(): + return _current_provider def get_context(*args, **kwargs) -> Context: if _current_provider is None: @@ -39,6 +42,10 @@ class BaseProvider(ABC): def create_context(self, *args, **kwargs) -> Context: pass + @abstractmethod + def create_info(self, ctx: Context): + pass + @abstractmethod def create_arcade_context(self, *args, **kwargs) -> ArcadeContext: pass \ No newline at end of file diff --git a/arcade/gl/utils.py b/arcade/gl/utils.py deleted file mode 100644 index cf3249cb3..000000000 --- a/arcade/gl/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Various utility functions for the gl module. -""" - -from array import array -from ctypes import c_byte -from typing import Any - - -def data_to_ctypes(data: Any) -> tuple[int, Any]: - """ - Attempts to convert the data to ctypes if needed by using the buffer protocol. - - - bytes will be returned as is - - Tuples will be converted to array - - Other types will be converted to ctypes by using the buffer protocol - by creating a memoryview and then a ctypes array of bytes. - - Args: - data: The data to convert to ctypes. - Returns: - A tuple containing the size of the data in bytes - and the data object optionally converted to ctypes. - """ - if isinstance(data, bytes): - return len(data), data - else: - if isinstance(data, tuple): - data = array("f", data) - try: - m_view = memoryview(data) - c_bytes = c_byte * m_view.nbytes - return m_view.nbytes, c_bytes.from_buffer(m_view) - except Exception as ex: - raise TypeError(f"Failed to convert data to ctypes: {ex}") From 8795a3cfbd9ff8d63ad694d5437328ca4509ffca Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 15 Apr 2025 11:48:19 -0400 Subject: [PATCH 09/16] Types abstraction and more context cleanup --- arcade/application.py | 10 +- arcade/context.py | 7 +- arcade/gl/backends/gl/context.py | 6 +- arcade/gl/backends/gl/framebuffer.py | 2 +- arcade/gl/backends/gl/glsl.py | 2 +- arcade/gl/backends/gl/program.py | 2 +- arcade/gl/backends/gl/sampler.py | 3 +- arcade/gl/backends/gl/texture.py | 30 +- arcade/gl/backends/gl/texture_array.py | 27 +- arcade/gl/backends/gl/types.py | 536 ------------------------- arcade/gl/backends/gl/vertex_array.py | 2 +- arcade/gl/context.py | 1 + arcade/gl/enums.py | 111 +++++ arcade/gl/types.py | 254 ++++++------ arcade/utils.py | 4 +- 15 files changed, 296 insertions(+), 701 deletions(-) delete mode 100644 arcade/gl/backends/gl/types.py diff --git a/arcade/application.py b/arcade/application.py index 5155e1770..64b8bccb6 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -22,9 +22,9 @@ from arcade.color import BLACK from arcade.context import ArcadeContext from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255 -from arcade.utils import is_raspberry_pi +from arcade.utils import is_raspberry_pi, is_pyodide from arcade.window_commands import get_display_size, set_window -from arcade.gl.provider import get_arcade_context +from arcade.gl.provider import get_arcade_context, set_provider if TYPE_CHECKING: from arcade.camera import Projector @@ -167,6 +167,11 @@ def __init__( if os.environ.get("REPL_ID"): antialiasing = False + desired_gl_provider = "gl" + if is_pyodide(): + gl_api = "webgl" + desired_gl_provider = "webgl" + # Detect Raspberry Pi and switch to OpenGL ES 3.1 if is_raspberry_pi(): gl_version = 3, 1 @@ -275,6 +280,7 @@ def __init__( self.push_handlers(on_resize=self._on_resize) + set_provider(desired_gl_provider) self._ctx: ArcadeContext = get_arcade_context(self, gc_mode=gc_mode, gl_api=gl_api) #self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) self._background_color: Color = BLACK diff --git a/arcade/context.py b/arcade/context.py index 56164afc4..542c545d0 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -20,7 +20,6 @@ from arcade.gl.framebuffer import Framebuffer from arcade.gl.program import Program from arcade.gl.texture import Texture2D -from arcade.gl.types import PyGLenum from arcade.gl.vertex_array import Geometry from arcade.texture_atlas import DefaultTextureAtlas, TextureAtlasBase @@ -450,9 +449,9 @@ def load_texture( path: str | Path, *, flip: bool = True, - wrap_x: PyGLenum | None = None, - wrap_y: PyGLenum | None = None, - filter: tuple[PyGLenum, PyGLenum] | None = None, + wrap_x = None, + wrap_y = None, + filter = None, build_mipmaps: bool = False, internal_format: int | None = None, immutable: bool = False, diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index ca2b203fc..4c81a3317 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -9,14 +9,12 @@ from arcade.types import BufferProtocol from arcade.gl import enums - -from .types import PyGLenum +from arcade.gl.types import BufferDescription, PyGLenum from .buffer import GLBuffer from .compute_shader import GLComputeShader from .framebuffer import GLFramebuffer, GLDefaultFrameBuffer from .glsl import ShaderSource -from .types import BufferDescription from .program import GLProgram from .query import GLQuery from .sampler import GLSampler @@ -36,6 +34,8 @@ def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl super().__init__(window, gc_mode) if gl_api not in self._valid_apis: + if gl_api == "webgl": + raise ValueError(f"Tried to create a GLContext with webgl api selected. Valid options for this backend are: {self._valid_apis}") raise ValueError(f"Invalid gl_api. Options are: {self._valid_apis}") self.gl_api = gl_api diff --git a/arcade/gl/backends/gl/framebuffer.py b/arcade/gl/backends/gl/framebuffer.py index 478c2002c..57ff955a0 100644 --- a/arcade/gl/backends/gl/framebuffer.py +++ b/arcade/gl/backends/gl/framebuffer.py @@ -7,10 +7,10 @@ from pyglet import gl from arcade.types import RGBOrA255, RGBOrANormalized +from arcade.gl.types import pixel_formats from arcade.gl.framebuffer import DefaultFrameBuffer, Framebuffer from .texture import GLTexture2D -from .types import pixel_formats if TYPE_CHECKING: from arcade.gl import Context diff --git a/arcade/gl/backends/gl/glsl.py b/arcade/gl/backends/gl/glsl.py index f88fd44bb..f986d67eb 100644 --- a/arcade/gl/backends/gl/glsl.py +++ b/arcade/gl/backends/gl/glsl.py @@ -7,7 +7,7 @@ from .context import Context as ArcadeGlContext from arcade.gl.exceptions import ShaderException -from .types import SHADER_TYPE_NAMES, PyGLenum +from arcade.gl.types import SHADER_TYPE_NAMES, PyGLenum class ShaderSource: diff --git a/arcade/gl/backends/gl/program.py b/arcade/gl/backends/gl/program.py index 815f196bd..26114e8a1 100644 --- a/arcade/gl/backends/gl/program.py +++ b/arcade/gl/backends/gl/program.py @@ -19,8 +19,8 @@ from arcade.gl.exceptions import ShaderException from arcade.gl.program import Program +from arcade.gl.types import SHADER_TYPE_NAMES, AttribFormat, GLTypes, PyGLenum -from .types import SHADER_TYPE_NAMES, AttribFormat, GLTypes, PyGLenum from .uniform import Uniform, UniformBlock if TYPE_CHECKING: diff --git a/arcade/gl/backends/gl/sampler.py b/arcade/gl/backends/gl/sampler.py index 82f7ae16a..f21c9b2a8 100644 --- a/arcade/gl/backends/gl/sampler.py +++ b/arcade/gl/backends/gl/sampler.py @@ -7,8 +7,7 @@ from pyglet import gl from arcade.gl.sampler import Sampler - -from .types import PyGLuint, compare_funcs +from arcade.gl.types import PyGLuint, compare_funcs if TYPE_CHECKING: from arcade.gl import Context, Texture2D diff --git a/arcade/gl/backends/gl/texture.py b/arcade/gl/backends/gl/texture.py index 264fd5169..bb3ee1fc4 100644 --- a/arcade/gl/backends/gl/texture.py +++ b/arcade/gl/backends/gl/texture.py @@ -6,23 +6,41 @@ from pyglet import gl -from arcade.gl.texture import Texture2D - from arcade.types import BufferProtocol -from .buffer import Buffer -from .types import ( + +from arcade.gl.texture import Texture2D +from arcade.gl.types import ( BufferOrBufferProtocol, PyGLuint, compare_funcs, pixel_formats, - swizzle_enum_to_str, - swizzle_str_to_enum, ) + +from .buffer import Buffer from .utils import data_to_ctypes if TYPE_CHECKING: # handle import cycle caused by type hinting from arcade.gl import Context +#: Swizzle conversion lookup +swizzle_enum_to_str: dict[int, str] = { + gl.GL_RED: "R", + gl.GL_GREEN: "G", + gl.GL_BLUE: "B", + gl.GL_ALPHA: "A", + gl.GL_ZERO: "0", + gl.GL_ONE: "1", +} + +#: Swizzle conversion lookup +swizzle_str_to_enum: dict[str, int] = { + "R": gl.GL_RED, + "G": gl.GL_GREEN, + "B": gl.GL_BLUE, + "A": gl.GL_ALPHA, + "0": gl.GL_ZERO, + "1": gl.GL_ONE, +} class GLTexture2D(Texture2D): """ diff --git a/arcade/gl/backends/gl/texture_array.py b/arcade/gl/backends/gl/texture_array.py index 8365fee75..38999ed0f 100644 --- a/arcade/gl/backends/gl/texture_array.py +++ b/arcade/gl/backends/gl/texture_array.py @@ -8,21 +8,38 @@ from arcade.types import BufferProtocol from arcade.gl.texture_array import TextureArray - -from .buffer import Buffer -from .types import ( +from arcade.gl.types import ( BufferOrBufferProtocol, PyGLuint, compare_funcs, pixel_formats, - swizzle_enum_to_str, - swizzle_str_to_enum, ) + +from .buffer import Buffer from .utils import data_to_ctypes if TYPE_CHECKING: # handle import cycle caused by type hinting from arcade.gl import Context +#: Swizzle conversion lookup +swizzle_enum_to_str: dict[int, str] = { + gl.GL_RED: "R", + gl.GL_GREEN: "G", + gl.GL_BLUE: "B", + gl.GL_ALPHA: "A", + gl.GL_ZERO: "0", + gl.GL_ONE: "1", +} + +#: Swizzle conversion lookup +swizzle_str_to_enum: dict[str, int] = { + "R": gl.GL_RED, + "G": gl.GL_GREEN, + "B": gl.GL_BLUE, + "A": gl.GL_ALPHA, + "0": gl.GL_ZERO, + "1": gl.GL_ONE, +} class GLTextureArray(TextureArray): """ diff --git a/arcade/gl/backends/gl/types.py b/arcade/gl/backends/gl/types.py deleted file mode 100644 index 2af7fcbd6..000000000 --- a/arcade/gl/backends/gl/types.py +++ /dev/null @@ -1,536 +0,0 @@ -import re -from typing import Iterable, Sequence, Union - -from pyglet import gl -from typing_extensions import TypeAlias - -from arcade.types import BufferProtocol - -from .buffer import Buffer - -BufferOrBufferProtocol = Union[BufferProtocol, Buffer] - -GLenumLike = Union[gl.GLenum, int] -PyGLenum = int -GLuintLike = Union[gl.GLuint, int] -PyGLuint = int - - -OpenGlFilter: TypeAlias = tuple[PyGLenum, PyGLenum] -BlendFunction: TypeAlias = Union[ - tuple[PyGLenum, PyGLenum], tuple[PyGLenum, PyGLenum, PyGLenum, PyGLenum] -] - -#: Depth compare functions -compare_funcs: dict[str | None, int] = { - None: gl.GL_NONE, - "<=": gl.GL_LEQUAL, - "<": gl.GL_LESS, - ">=": gl.GL_GEQUAL, - ">": gl.GL_GREATER, - "==": gl.GL_EQUAL, - "!=": gl.GL_NOTEQUAL, - "0": gl.GL_NEVER, - "1": gl.GL_ALWAYS, -} - -#: Swizzle conversion lookup -swizzle_enum_to_str: dict[int, str] = { - gl.GL_RED: "R", - gl.GL_GREEN: "G", - gl.GL_BLUE: "B", - gl.GL_ALPHA: "A", - gl.GL_ZERO: "0", - gl.GL_ONE: "1", -} - -#: Swizzle conversion lookup -swizzle_str_to_enum: dict[str, int] = { - "R": gl.GL_RED, - "G": gl.GL_GREEN, - "B": gl.GL_BLUE, - "A": gl.GL_ALPHA, - "0": gl.GL_ZERO, - "1": gl.GL_ONE, -} - -_float_base_format = (0, gl.GL_RED, gl.GL_RG, gl.GL_RGB, gl.GL_RGBA) -_int_base_format = ( - 0, - gl.GL_RED_INTEGER, - gl.GL_RG_INTEGER, - gl.GL_RGB_INTEGER, - gl.GL_RGBA_INTEGER, -) -#: Pixel format lookup (base_format, internal_format, type, size) -pixel_formats = { - # float formats - "f1": ( - _float_base_format, - (0, gl.GL_R8, gl.GL_RG8, gl.GL_RGB8, gl.GL_RGBA8), - gl.GL_UNSIGNED_BYTE, - 1, - ), - "f2": ( - _float_base_format, - (0, gl.GL_R16F, gl.GL_RG16F, gl.GL_RGB16F, gl.GL_RGBA16F), - gl.GL_HALF_FLOAT, - 2, - ), - "f4": ( - _float_base_format, - (0, gl.GL_R32F, gl.GL_RG32F, gl.GL_RGB32F, gl.GL_RGBA32F), - gl.GL_FLOAT, - 4, - ), - # int formats - "i1": ( - _int_base_format, - (0, gl.GL_R8I, gl.GL_RG8I, gl.GL_RGB8I, gl.GL_RGBA8I), - gl.GL_BYTE, - 1, - ), - "i2": ( - _int_base_format, - (0, gl.GL_R16I, gl.GL_RG16I, gl.GL_RGB16I, gl.GL_RGBA16I), - gl.GL_SHORT, - 2, - ), - "i4": ( - _int_base_format, - (0, gl.GL_R32I, gl.GL_RG32I, gl.GL_RGB32I, gl.GL_RGBA32I), - gl.GL_INT, - 4, - ), - # uint formats - "u1": ( - _int_base_format, - (0, gl.GL_R8UI, gl.GL_RG8UI, gl.GL_RGB8UI, gl.GL_RGBA8UI), - gl.GL_UNSIGNED_BYTE, - 1, - ), - "u2": ( - _int_base_format, - (0, gl.GL_R16UI, gl.GL_RG16UI, gl.GL_RGB16UI, gl.GL_RGBA16UI), - gl.GL_UNSIGNED_SHORT, - 2, - ), - "u4": ( - _int_base_format, - (0, gl.GL_R32UI, gl.GL_RG32UI, gl.GL_RGB32UI, gl.GL_RGBA32UI), - gl.GL_UNSIGNED_INT, - 4, - ), -} - - -#: String representation of a shader types -SHADER_TYPE_NAMES = { - gl.GL_VERTEX_SHADER: "vertex shader", - gl.GL_FRAGMENT_SHADER: "fragment shader", - gl.GL_GEOMETRY_SHADER: "geometry shader", - gl.GL_TESS_CONTROL_SHADER: "tessellation control shader", - gl.GL_TESS_EVALUATION_SHADER: "tessellation evaluation shader", -} - -#: Lookup table for OpenGL type names -GL_NAMES = { - gl.GL_HALF_FLOAT: "GL_HALF_FLOAT", - gl.GL_FLOAT: "GL_FLOAT", - gl.GL_DOUBLE: "GL_DOUBLE", - gl.GL_INT: "GL_INT", - gl.GL_UNSIGNED_INT: "GL_UNSIGNED_INT", - gl.GL_SHORT: "GL_SHORT", - gl.GL_UNSIGNED_SHORT: "GL_UNSIGNED_SHORT", - gl.GL_BYTE: "GL_BYTE", - gl.GL_UNSIGNED_BYTE: "GL_UNSIGNED_BYTE", -} - - -def gl_name(gl_type: PyGLenum | None) -> str | PyGLenum | None: - """Return the name of a gl type""" - if gl_type is None: - return None - return GL_NAMES.get(gl_type, gl_type) - - -class AttribFormat: - """ - Represents a vertex attribute in a BufferDescription / Program. - This is attribute metadata used when attempting to map vertex - shader inputs. - - Args: - name: - Name of the attribute - gl_type: - The OpenGL type such as GL_FLOAT, GL_HALF_FLOAT etc. - bytes_per_component: - Number of bytes for a single component - offset: - Offset for BufferDescription - location: - Location for program attribute - """ - - __slots__ = ( - "name", - "gl_type", - "components", - "bytes_per_component", - "offset", - "location", - ) - - def __init__( - self, - name: str | None, - gl_type: PyGLenum | None, - components: int, - bytes_per_component: int, - offset=0, - location=0, - ): - self.name = name - """The name of the attribute in the program""" - self.gl_type = gl_type - """The OpenGL type of the attribute""" - self.components = components - """Number of components for this attribute (1, 2, 3 or 4)""" - self.bytes_per_component = bytes_per_component - """How many bytes for a single component""" - self.offset = offset - """Offset of the attribute in the buffer""" - self.location = location - """Location of the attribute in the program""" - - @property - def bytes_total(self) -> int: - """Total number of bytes for this attribute""" - return self.components * self.bytes_per_component - - def __repr__(self): - return ( - f"" - ) - - -class BufferDescription: - """Buffer Object description used with :py:class:`arcade.gl.Geometry`. - - This class provides a Buffer object with a description of its content, allowing the - a :py:class:`~arcade.gl.Geometry` object to correctly map shader attributes - to a program/shader. - - The formats is a string providing the number and type of each attribute. Currently - we only support f (float), i (integer) and B (unsigned byte). - - ``normalized`` enumerates the attributes which must have their values normalized. - This is useful for instance for colors attributes given as unsigned byte and - normalized to floats with values between 0.0 and 1.0. - - ``instanced`` allows this buffer to be used as instanced buffer. Each value will - be used once for the whole geometry. The geometry will be repeated a number of - times equal to the number of items in the Buffer. - - .. code-block:: python - - # Describe my_buffer - # It contains two floating point numbers being a 2d position - # and two floating point numbers being texture coordinates. - # We expect the shader using this buffer to have an in_pos and in_uv attribute (exact name) - BufferDescription( - my_buffer, - '2f 2f', - ['in_pos', 'in_uv'], - ) - - Args: - buffer: The buffer to describe - formats: The format of each attribute - attributes: List of attributes names (strings) - normalized: list of attribute names that should be normalized - instanced: ``True`` if this is per instance data - """ - - # Describe all variants of a format string to simplify parsing (single component) - # format: gl_type, byte_size - _formats: dict[str, tuple[PyGLenum | None, int]] = { - # (gl enum, byte size) - # Floats - "f": (gl.GL_FLOAT, 4), - "f1": (gl.GL_UNSIGNED_BYTE, 1), - "f2": (gl.GL_HALF_FLOAT, 2), - "f4": (gl.GL_FLOAT, 4), - "f8": (gl.GL_DOUBLE, 8), - # Unsigned integers - "u": (gl.GL_UNSIGNED_INT, 4), - "u1": (gl.GL_UNSIGNED_BYTE, 1), - "u2": (gl.GL_UNSIGNED_SHORT, 2), - "u4": (gl.GL_UNSIGNED_INT, 4), - # Signed integers - "i": (gl.GL_INT, 4), - "i1": (gl.GL_BYTE, 1), - "i2": (gl.GL_SHORT, 2), - "i4": (gl.GL_INT, 4), - # Padding (1, 2, 4, 8 bytes) - "x": (None, 1), - "x1": (None, 1), - "x2": (None, 2), - "x4": (None, 4), - "x8": (None, 8), - } - - __slots__ = ( - "buffer", - "attributes", - "normalized", - "instanced", - "formats", - "stride", - "num_vertices", - ) - - def __init__( - self, - buffer: Buffer, - formats: str, - attributes: Sequence[str], - normalized: Iterable[str] | None = None, - instanced: bool = False, - ): - #: The :py:class:`~arcade.gl.Buffer` this description object describes - self.buffer = buffer # type: Buffer - #: List of string attributes - self.attributes = attributes - #: List of normalized attributes - self.normalized: set[str] = set() if normalized is None else set(normalized) - #: Instanced flag (bool) - self.instanced: bool = instanced - #: Formats of each attribute - self.formats: list[AttribFormat] = [] - #: The byte stride of the buffer - self.stride: int = -1 - #: Number of vertices in the buffer - self.num_vertices: int = -1 - - if not isinstance(buffer, Buffer): - raise ValueError("buffer parameter must be an arcade.gl.Buffer") - - if not isinstance(self.attributes, (list, tuple)): - raise ValueError("Attributes must be a list or tuple") - - if self.normalized > set(self.attributes): - raise ValueError("Normalized attribute not found in attributes.") - - formats_list = formats.split(" ") - non_padded_formats = [f for f in formats_list if "x" not in f] - - if len(non_padded_formats) != len(self.attributes): - raise ValueError( - f"Different lengths of formats ({len(non_padded_formats)}) and " - f"attributes ({len(self.attributes)})" - ) - - def zip_attrs(formats: list[str], attributes: Sequence[str]): - """Join together formats and attribute names taking padding into account""" - attr_index = 0 - for f in formats: - if "x" in f: - yield f, None - else: - yield f, attributes[attr_index] - attr_index += 1 - - self.stride = 0 - for attr_fmt, attr_name in zip_attrs(formats_list, self.attributes): - # Automatically make f1 attributes normalized - if attr_name is not None and "f1" in attr_fmt: - self.normalized.add(attr_name) - try: - components_str, data_type_str, data_size_str = re.split(r"([fiux])", attr_fmt) - data_type = f"{data_type_str}{data_size_str}" if data_size_str else data_type_str - components = int(components_str) if components_str else 1 # 1 component is default - data_size = ( - int(data_size_str) if data_size_str else 4 - ) # 4 byte float and integer types are default - # Limit components to 4 for non-padded formats - if components > 4 and data_size is not None: - raise ValueError("Number of components must be 1, 2, 3 or 4") - except Exception as ex: - raise ValueError(f"Could not parse attribute format: '{attr_fmt} : {ex}'") - - gl_type, byte_size = self._formats[data_type] - self.formats.append( - AttribFormat(attr_name, gl_type, components, byte_size, offset=self.stride) - ) - - self.stride += byte_size * components - - if self.buffer.size % self.stride != 0: - raise ValueError( - f"Buffer size must align by {self.stride} bytes. " - f"{self.buffer} size={self.buffer.size}" - ) - - # Estimate number of vertices for this buffer - self.num_vertices = self.buffer.size // self.stride - - def __repr__(self) -> str: - return f"" - - def __eq__(self, other) -> bool: - if not isinstance(other, BufferDescription): - raise ValueError( - f"The only logical comparison to a BufferDescription" - f"is a BufferDescription not {type(other)}" - ) - - # Equal if we share the same attribute - return len(set(self.attributes) & set(other.attributes)) > 0 - - -class TypeInfo: - """ - Describes an opengl type. - - Args: - name: - The string representation of this type - enum: - The enum of this type - gl_type: - The base enum of this type - gl_size: - byte size if the gl_type - components: - Number of components for this enum - """ - - __slots__ = "name", "enum", "gl_type", "gl_size", "components" - - def __init__( - self, name: str, enum: GLenumLike, gl_type: PyGLenum, gl_size: int, components: int - ): - self.name = name - """The string representation of this type""" - self.enum = enum - """The OpenEL enum of this type""" - self.gl_type = gl_type - """The base OpenGL data type""" - self.gl_size = gl_size - """The size of the base OpenGL data type""" - self.components = components - """The number of components (1, 2, 3 or 4)""" - - @property - def size(self) -> int: - """The total size of this type in bytes""" - return self.gl_size * self.components - - def __repr__(self) -> str: - return ( - f"" - ) - - -class GLTypes: - """ - Detailed Information about all attribute type. - - During introspection we often just get integers telling us what type is used. - This can for example be ``35664`` telling us it's a ``GL_FLOAT_VEC2``. - - During introspection we need to know the exact datatype of the attribute. - It's not enough to know it's a float, we need to know if it's a vec2, vec3, vec4 - or any other type that OpenGL supports. - - Examples of types are:: - - GL_FLOAT_VEC2 - GL_DOUBLE_VEC4 - GL_INT_VEC3 - GL_UNSIGNED_INT_VEC2 - GL_UNSIGNED_BYTE - GL_FLOAT - GL_DOUBLE - GL_INT - GL_UNSIGNED_INT - ... - """ - - types = { - # Floats - gl.GL_FLOAT: TypeInfo("GL_FLOAT", gl.GL_FLOAT, gl.GL_FLOAT, 4, 1), - gl.GL_FLOAT_VEC2: TypeInfo("GL_FLOAT_VEC2", gl.GL_FLOAT_VEC2, gl.GL_FLOAT, 4, 2), - gl.GL_FLOAT_VEC3: TypeInfo("GL_FLOAT_VEC3", gl.GL_FLOAT_VEC3, gl.GL_FLOAT, 4, 3), - gl.GL_FLOAT_VEC4: TypeInfo("GL_FLOAT_VEC4", gl.GL_FLOAT_VEC4, gl.GL_FLOAT, 4, 4), - # Doubles - gl.GL_DOUBLE: TypeInfo("GL_DOUBLE", gl.GL_DOUBLE, gl.GL_DOUBLE, 8, 1), - gl.GL_DOUBLE_VEC2: TypeInfo("GL_DOUBLE_VEC2", gl.GL_DOUBLE_VEC2, gl.GL_DOUBLE, 8, 2), - gl.GL_DOUBLE_VEC3: TypeInfo("GL_DOUBLE_VEC3", gl.GL_DOUBLE_VEC3, gl.GL_DOUBLE, 8, 3), - gl.GL_DOUBLE_VEC4: TypeInfo("GL_DOUBLE_VEC4", gl.GL_DOUBLE_VEC4, gl.GL_DOUBLE, 8, 4), - # Booleans (ubyte) - gl.GL_BOOL: TypeInfo("GL_BOOL", gl.GL_BOOL, gl.GL_BOOL, 1, 1), - gl.GL_BOOL_VEC2: TypeInfo("GL_BOOL_VEC2", gl.GL_BOOL_VEC2, gl.GL_BOOL, 1, 2), - gl.GL_BOOL_VEC3: TypeInfo("GL_BOOL_VEC3", gl.GL_BOOL_VEC3, gl.GL_BOOL, 1, 3), - gl.GL_BOOL_VEC4: TypeInfo("GL_BOOL_VEC4", gl.GL_BOOL_VEC4, gl.GL_BOOL, 1, 4), - # Integers - gl.GL_INT: TypeInfo("GL_INT", gl.GL_INT, gl.GL_INT, 4, 1), - gl.GL_INT_VEC2: TypeInfo("GL_INT_VEC2", gl.GL_INT_VEC2, gl.GL_INT, 4, 2), - gl.GL_INT_VEC3: TypeInfo("GL_INT_VEC3", gl.GL_INT_VEC3, gl.GL_INT, 4, 3), - gl.GL_INT_VEC4: TypeInfo("GL_INT_VEC4", gl.GL_INT_VEC4, gl.GL_INT, 4, 4), - # Unsigned Integers - gl.GL_UNSIGNED_INT: TypeInfo( - "GL_UNSIGNED_INT", gl.GL_UNSIGNED_INT, gl.GL_UNSIGNED_INT, 4, 1 - ), - gl.GL_UNSIGNED_INT_VEC2: TypeInfo( - "GL_UNSIGNED_INT_VEC2", gl.GL_UNSIGNED_INT_VEC2, gl.GL_UNSIGNED_INT, 4, 2 - ), - gl.GL_UNSIGNED_INT_VEC3: TypeInfo( - "GL_UNSIGNED_INT_VEC3", gl.GL_UNSIGNED_INT_VEC3, gl.GL_UNSIGNED_INT, 4, 3 - ), - gl.GL_UNSIGNED_INT_VEC4: TypeInfo( - "GL_UNSIGNED_INT_VEC4", gl.GL_UNSIGNED_INT_VEC4, gl.GL_UNSIGNED_INT, 4, 4 - ), - # Unsigned Short (mostly used for short index buffers) - gl.GL_UNSIGNED_SHORT: TypeInfo( - "GL.GL_UNSIGNED_SHORT", gl.GL_UNSIGNED_SHORT, gl.GL_UNSIGNED_SHORT, 2, 2 - ), - # Byte - gl.GL_BYTE: TypeInfo("GL_BYTE", gl.GL_BYTE, gl.GL_BYTE, 1, 1), - gl.GL_UNSIGNED_BYTE: TypeInfo( - "GL_UNSIGNED_BYTE", gl.GL_UNSIGNED_BYTE, gl.GL_UNSIGNED_BYTE, 1, 1 - ), - # Matrices - gl.GL_FLOAT_MAT2: TypeInfo("GL_FLOAT_MAT2", gl.GL_FLOAT_MAT2, gl.GL_FLOAT, 4, 4), - gl.GL_FLOAT_MAT3: TypeInfo("GL_FLOAT_MAT3", gl.GL_FLOAT_MAT3, gl.GL_FLOAT, 4, 9), - gl.GL_FLOAT_MAT4: TypeInfo("GL_FLOAT_MAT4", gl.GL_FLOAT_MAT4, gl.GL_FLOAT, 4, 16), - gl.GL_FLOAT_MAT2x3: TypeInfo("GL_FLOAT_MAT2x3", gl.GL_FLOAT_MAT2x3, gl.GL_FLOAT, 4, 6), - gl.GL_FLOAT_MAT2x4: TypeInfo("GL_FLOAT_MAT2x4", gl.GL_FLOAT_MAT2x4, gl.GL_FLOAT, 4, 8), - gl.GL_FLOAT_MAT3x2: TypeInfo("GL_FLOAT_MAT3x2", gl.GL_FLOAT_MAT3x2, gl.GL_FLOAT, 4, 6), - gl.GL_FLOAT_MAT3x4: TypeInfo("GL_FLOAT_MAT3x4", gl.GL_FLOAT_MAT3x4, gl.GL_FLOAT, 4, 12), - gl.GL_FLOAT_MAT4x2: TypeInfo("GL_FLOAT_MAT4x2", gl.GL_FLOAT_MAT4x2, gl.GL_FLOAT, 4, 8), - gl.GL_FLOAT_MAT4x3: TypeInfo("GL_FLOAT_MAT4x3", gl.GL_FLOAT_MAT4x3, gl.GL_FLOAT, 4, 12), - # Double matrices - gl.GL_DOUBLE_MAT2: TypeInfo("GL_DOUBLE_MAT2", gl.GL_DOUBLE_MAT2, gl.GL_DOUBLE, 8, 4), - gl.GL_DOUBLE_MAT3: TypeInfo("GL_DOUBLE_MAT3", gl.GL_DOUBLE_MAT3, gl.GL_DOUBLE, 8, 9), - gl.GL_DOUBLE_MAT4: TypeInfo("GL_DOUBLE_MAT4", gl.GL_DOUBLE_MAT4, gl.GL_DOUBLE, 8, 16), - gl.GL_DOUBLE_MAT2x3: TypeInfo("GL_DOUBLE_MAT2x3", gl.GL_DOUBLE_MAT2x3, gl.GL_DOUBLE, 8, 6), - gl.GL_DOUBLE_MAT2x4: TypeInfo("GL_DOUBLE_MAT2x4", gl.GL_DOUBLE_MAT2x4, gl.GL_DOUBLE, 8, 8), - gl.GL_DOUBLE_MAT3x2: TypeInfo("GL_DOUBLE_MAT3x2", gl.GL_DOUBLE_MAT3x2, gl.GL_DOUBLE, 8, 6), - gl.GL_DOUBLE_MAT3x4: TypeInfo("GL_DOUBLE_MAT3x4", gl.GL_DOUBLE_MAT3x4, gl.GL_DOUBLE, 8, 12), - gl.GL_DOUBLE_MAT4x2: TypeInfo("GL_DOUBLE_MAT4x2", gl.GL_DOUBLE_MAT4x2, gl.GL_DOUBLE, 8, 8), - gl.GL_DOUBLE_MAT4x3: TypeInfo("GL_DOUBLE_MAT4x3", gl.GL_DOUBLE_MAT4x3, gl.GL_DOUBLE, 8, 12), - # TODO: Add sampler types if needed. Only needed for better uniform introspection. - } - - @classmethod - def get(cls, enum: int) -> TypeInfo: - """Get the TypeInfo for a given""" - try: - return cls.types[enum] - except KeyError: - raise ValueError(f"Unknown gl type {enum}. Someone needs to add it") diff --git a/arcade/gl/backends/gl/vertex_array.py b/arcade/gl/backends/gl/vertex_array.py index 5c1fed58d..18128d348 100644 --- a/arcade/gl/backends/gl/vertex_array.py +++ b/arcade/gl/backends/gl/vertex_array.py @@ -6,11 +6,11 @@ from pyglet import gl +from arcade.gl.types import BufferDescription, GLenumLike, GLuintLike, gl_name from arcade.gl.vertex_array import VertexArray, Geometry from .buffer import Buffer from .program import Program -from .types import BufferDescription, GLenumLike, GLuintLike, gl_name if TYPE_CHECKING: from arcade.gl import Context diff --git a/arcade/gl/context.py b/arcade/gl/context.py index ed5cbd4e4..fd00c856a 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -206,6 +206,7 @@ def __init__( self, window: pyglet.window.Window, # type: ignore gc_mode: str = "context_gc", + gl_api: str = "gl" # This is ignored here, but used in implementation classes ): self._window_ref = weakref.ref(window) self._info = get_provider().create_info(self) diff --git a/arcade/gl/enums.py b/arcade/gl/enums.py index 6d0e0af24..a685bc1ed 100644 --- a/arcade/gl/enums.py +++ b/arcade/gl/enums.py @@ -7,6 +7,16 @@ particular backend implementation's library(e.g. pyglet, JS via pyodide, etc). """ +NONE = 0 +NEVER = 0x0200 +LESS = 0x0201 +EQUAL = 0x0202 +LEQUAL = 0x0203 +GREATER = 0x0204 +NOTEQUAL = 0x0205 +GEQUAL = 0x0206 +ALWAYS = 0x0207 + # Get Parameters VENDOR = 0x1F00 RENDERER = 0x1F01 @@ -122,3 +132,104 @@ FRONT = 0x0404 BACK = 0x0405 FRONT_AND_BACK = 0x0408 + +RED = 0x1903 +RG = 0x8227 +RGB = 0x1907 +RGBA = 0x1908 +RED_INTEGER = 0x8D94 +RG_INTEGER = 0x8228 +RGB_INTEGER = 0x8D98 +RGBA_INTEGER = 0x8D99 + +R8 = 0x8229 +RG8 = 0x822B +RGB8 = 0x8051 +RGBA8 = 0x8058 +R16F = 0x822D +RG16F = 0x822F +RGB16F = 0x881B +RGBA16F = 0x881A +R32F = 0x822E +RG32F = 0x8230 +RGB32F = 0x8815 +RGBA32F = 0x8814 +R8I = 0x8231 +RG8I = 0x8237 +RGB8I = 0x8D8F +RGBA8I = 0x8D8E +R16I = 0x8233 +RG16I = 0x8239 +RGB16I = 0x8D89 +RGBA16I = 0x8D88 +R32I = 0x8235 +RG32I = 0x823B +RGB32I = 0x8D83 +RGBA32I = 0x8D82 +R8UI = 0x8232 +RG8UI = 0x8238 +RGB8UI = 0x8D7D +RGBA8UI = 0x8D7C +R16UI = 0x8234 +RG16UI = 0x823A +RGB16UI = 0x8D77 +RGBA16UI = 0x8D76 +R32UI = 0x8236 +RG32UI = 0x823C +RGB32UI = 0x8D71 +RGBA32UI = 0x8D70 + +BYTE = 0x1400 +UNSIGNED_BYTE = 0x1401 +SHORT = 0x1402 +UNSIGNED_SHORT = 0x1403 +INT = 0x1404 +UNSIGNED_INT = 0x1405 +FLOAT = 0x1406 +HALF_FLOAT = 0x140B +DOUBLE = 0x140A # Not supported in WebGL, but left in common to make implementation easier + +FLOAT_VEC2 = 0x8B50 +FLOAT_VEC3 = 0x8B51 +FLOAT_VEC4 = 0x8B52 +INT_VEC2 = 0x8B53 +INT_VEC3 = 0x8B54 +INT_VEC4 = 0x8B55 +BOOL = 0x8B56 +BOOL_VEC2 = 0x8B57 +BOOL_VEC3 = 0x8B58 +BOOL_VEC4 = 0x8B59 +UNSIGNED_INT_VEC2 = 0x8DC6 +UNSIGNED_INT_VEC3 = 0x8DC7 +UNSIGNED_INT_VEC4 = 0x8DC8 +FLOAT_MAT2 = 0x8B5A +FLOAT_MAT3 = 0x8B5B +FLOAT_MAT4 = 0x8B5C +FLOAT_MAT2x3 = 0x8B65 +FLOAT_MAT2x4 = 0x8B66 +FLOAT_MAT3x2 = 0x8B67 +FLOAT_MAT3x4 = 0x8B68 +FLOAT_MAT4x2 = 0x8B69 +FLOAT_MAT4x3 = 0x8B6A + +# Double Vectors - Unsupported by WebGL +DOUBLE_VEC2 = 0x8FFC +DOUBLE_VEC3 = 0x8FFD +DOUBLE_VEC4 = 0x8FFE + +# Double Matrices - Unsupported by WebGL +DOUBLE_MAT2 = 0x8F46 +DOUBLE_MAT3 = 0x8F47 +DOUBLE_MAT4 = 0x8F48 +DOUBLE_MAT2x3 = 0x8F49 +DOUBLE_MAT2x4 = 0x8F4A +DOUBLE_MAT3x2 = 0x8F4B +DOUBLE_MAT3x4 = 0x8F4C +DOUBLE_MAT4x2 = 0x8F4D +DOUBLE_MAT4x3 = 0x8F4E + +VERTEX_SHADER = 0x8B31 +FRAGMENT_SHADER = 0x8B30 +GEOMETRY_SHADER = 0x8DD9 # Not supported in WebGL +TESS_CONTROL_SHADER = 0x8E88 # Not supported in WebGL +TESS_EVALUATION_SHADER = 0x8E87 # Not supported in WebGL \ No newline at end of file diff --git a/arcade/gl/types.py b/arcade/gl/types.py index 2af7fcbd6..ad7ccd70b 100644 --- a/arcade/gl/types.py +++ b/arcade/gl/types.py @@ -1,18 +1,16 @@ import re -from typing import Iterable, Sequence, Union - -from pyglet import gl -from typing_extensions import TypeAlias +from typing import Iterable, Sequence, TypeAlias, Union from arcade.types import BufferProtocol from .buffer import Buffer +from . import enums BufferOrBufferProtocol = Union[BufferProtocol, Buffer] -GLenumLike = Union[gl.GLenum, int] +GLenumLike = int PyGLenum = int -GLuintLike = Union[gl.GLuint, int] +GLuintLike = int PyGLuint = int @@ -23,102 +21,82 @@ #: Depth compare functions compare_funcs: dict[str | None, int] = { - None: gl.GL_NONE, - "<=": gl.GL_LEQUAL, - "<": gl.GL_LESS, - ">=": gl.GL_GEQUAL, - ">": gl.GL_GREATER, - "==": gl.GL_EQUAL, - "!=": gl.GL_NOTEQUAL, - "0": gl.GL_NEVER, - "1": gl.GL_ALWAYS, -} - -#: Swizzle conversion lookup -swizzle_enum_to_str: dict[int, str] = { - gl.GL_RED: "R", - gl.GL_GREEN: "G", - gl.GL_BLUE: "B", - gl.GL_ALPHA: "A", - gl.GL_ZERO: "0", - gl.GL_ONE: "1", -} - -#: Swizzle conversion lookup -swizzle_str_to_enum: dict[str, int] = { - "R": gl.GL_RED, - "G": gl.GL_GREEN, - "B": gl.GL_BLUE, - "A": gl.GL_ALPHA, - "0": gl.GL_ZERO, - "1": gl.GL_ONE, + None: enums.NONE, + "<=": enums.LEQUAL, + "<": enums.LESS, + ">=": enums.GEQUAL, + ">": enums.GREATER, + "==": enums.EQUAL, + "!=": enums.NOTEQUAL, + "0": enums.NEVER, + "1": enums.ALWAYS, } -_float_base_format = (0, gl.GL_RED, gl.GL_RG, gl.GL_RGB, gl.GL_RGBA) +_float_base_format = (0, enums.RED, enums.RG, enums.RGB, enums.RGBA) _int_base_format = ( 0, - gl.GL_RED_INTEGER, - gl.GL_RG_INTEGER, - gl.GL_RGB_INTEGER, - gl.GL_RGBA_INTEGER, + enums.RED_INTEGER, + enums.RG_INTEGER, + enums.RGB_INTEGER, + enums.RGBA_INTEGER, ) #: Pixel format lookup (base_format, internal_format, type, size) pixel_formats = { # float formats "f1": ( _float_base_format, - (0, gl.GL_R8, gl.GL_RG8, gl.GL_RGB8, gl.GL_RGBA8), - gl.GL_UNSIGNED_BYTE, + (0, enums.R8, enums.RG8, enums.RGB8, enums.RGBA8), + enums.UNSIGNED_BYTE, 1, ), "f2": ( _float_base_format, - (0, gl.GL_R16F, gl.GL_RG16F, gl.GL_RGB16F, gl.GL_RGBA16F), - gl.GL_HALF_FLOAT, + (0, enums.R16F, enums.RG16F, enums.RGB16F, enums.RGBA16F), + enums.HALF_FLOAT, 2, ), "f4": ( _float_base_format, - (0, gl.GL_R32F, gl.GL_RG32F, gl.GL_RGB32F, gl.GL_RGBA32F), - gl.GL_FLOAT, + (0, enums.R32F, enums.RG32F, enums.RGB32F, enums.RGBA32F), + enums.FLOAT, 4, ), # int formats "i1": ( _int_base_format, - (0, gl.GL_R8I, gl.GL_RG8I, gl.GL_RGB8I, gl.GL_RGBA8I), - gl.GL_BYTE, + (0, enums.R8I, enums.RG8I, enums.RGB8I, enums.RGBA8I), + enums.BYTE, 1, ), "i2": ( _int_base_format, - (0, gl.GL_R16I, gl.GL_RG16I, gl.GL_RGB16I, gl.GL_RGBA16I), - gl.GL_SHORT, + (0, enums.R16I, enums.RG16I, enums.RGB16I, enums.RGBA16I), + enums.SHORT, 2, ), "i4": ( _int_base_format, - (0, gl.GL_R32I, gl.GL_RG32I, gl.GL_RGB32I, gl.GL_RGBA32I), - gl.GL_INT, + (0, enums.R32I, enums.RG32I, enums.RGB32I, enums.RGBA32I), + enums.INT, 4, ), # uint formats "u1": ( _int_base_format, - (0, gl.GL_R8UI, gl.GL_RG8UI, gl.GL_RGB8UI, gl.GL_RGBA8UI), - gl.GL_UNSIGNED_BYTE, + (0, enums.R8UI, enums.RG8UI, enums.RGB8UI, enums.RGBA8UI), + enums.UNSIGNED_BYTE, 1, ), "u2": ( _int_base_format, - (0, gl.GL_R16UI, gl.GL_RG16UI, gl.GL_RGB16UI, gl.GL_RGBA16UI), - gl.GL_UNSIGNED_SHORT, + (0, enums.R16UI, enums.RG16UI, enums.RGB16UI, enums.RGBA16UI), + enums.UNSIGNED_SHORT, 2, ), "u4": ( _int_base_format, - (0, gl.GL_R32UI, gl.GL_RG32UI, gl.GL_RGB32UI, gl.GL_RGBA32UI), - gl.GL_UNSIGNED_INT, + (0, enums.R32UI, enums.RG32UI, enums.RGB32UI, enums.RGBA32UI), + enums.UNSIGNED_INT, 4, ), } @@ -126,28 +104,28 @@ #: String representation of a shader types SHADER_TYPE_NAMES = { - gl.GL_VERTEX_SHADER: "vertex shader", - gl.GL_FRAGMENT_SHADER: "fragment shader", - gl.GL_GEOMETRY_SHADER: "geometry shader", - gl.GL_TESS_CONTROL_SHADER: "tessellation control shader", - gl.GL_TESS_EVALUATION_SHADER: "tessellation evaluation shader", + enums.VERTEX_SHADER: "vertex shader", + enums.FRAGMENT_SHADER: "fragment shader", + enums.GEOMETRY_SHADER: "geometry shader", # Not supported in WebGL + enums.TESS_CONTROL_SHADER: "tessellation control shader", # Not supported in WebGL + enums.TESS_EVALUATION_SHADER: "tessellation evaluation shader", # Not supported in WebGL } #: Lookup table for OpenGL type names GL_NAMES = { - gl.GL_HALF_FLOAT: "GL_HALF_FLOAT", - gl.GL_FLOAT: "GL_FLOAT", - gl.GL_DOUBLE: "GL_DOUBLE", - gl.GL_INT: "GL_INT", - gl.GL_UNSIGNED_INT: "GL_UNSIGNED_INT", - gl.GL_SHORT: "GL_SHORT", - gl.GL_UNSIGNED_SHORT: "GL_UNSIGNED_SHORT", - gl.GL_BYTE: "GL_BYTE", - gl.GL_UNSIGNED_BYTE: "GL_UNSIGNED_BYTE", + enums.HALF_FLOAT: "GL_HALF_FLOAT", + enums.FLOAT: "GL_FLOAT", + enums.DOUBLE: "GL_DOUBLE", # Double not supported in WebGL + enums.INT: "GL_INT", + enums.UNSIGNED_INT: "GL_UNSIGNED_INT", + enums.SHORT: "GL_SHORT", + enums.UNSIGNED_SHORT: "GL_UNSIGNED_SHORT", + enums.BYTE: "GL_BYTE", + enums.UNSIGNED_BYTE: "GL_UNSIGNED_BYTE", } -def gl_name(gl_type: PyGLenum | None) -> str | PyGLenum | None: +def gl_name(gl_type): """Return the name of a gl type""" if gl_type is None: return None @@ -185,7 +163,7 @@ class AttribFormat: def __init__( self, name: str | None, - gl_type: PyGLenum | None, + gl_type, components: int, bytes_per_component: int, offset=0, @@ -256,24 +234,24 @@ class BufferDescription: # Describe all variants of a format string to simplify parsing (single component) # format: gl_type, byte_size - _formats: dict[str, tuple[PyGLenum | None, int]] = { + _formats: dict[str, tuple] = { # (gl enum, byte size) # Floats - "f": (gl.GL_FLOAT, 4), - "f1": (gl.GL_UNSIGNED_BYTE, 1), - "f2": (gl.GL_HALF_FLOAT, 2), - "f4": (gl.GL_FLOAT, 4), - "f8": (gl.GL_DOUBLE, 8), + "f": (enums.FLOAT, 4), + "f1": (enums.UNSIGNED_BYTE, 1), + "f2": (enums.HALF_FLOAT, 2), + "f4": (enums.FLOAT, 4), + "f8": (enums.DOUBLE, 8), # Double unsupported by WebGL # Unsigned integers - "u": (gl.GL_UNSIGNED_INT, 4), - "u1": (gl.GL_UNSIGNED_BYTE, 1), - "u2": (gl.GL_UNSIGNED_SHORT, 2), - "u4": (gl.GL_UNSIGNED_INT, 4), + "u": (enums.UNSIGNED_INT, 4), + "u1": (enums.UNSIGNED_BYTE, 1), + "u2": (enums.UNSIGNED_SHORT, 2), + "u4": (enums.UNSIGNED_INT, 4), # Signed integers - "i": (gl.GL_INT, 4), - "i1": (gl.GL_BYTE, 1), - "i2": (gl.GL_SHORT, 2), - "i4": (gl.GL_INT, 4), + "i": (enums.INT, 4), + "i1": (enums.BYTE, 1), + "i2": (enums.SHORT, 2), + "i4": (enums.INT, 4), # Padding (1, 2, 4, 8 bytes) "x": (None, 1), "x1": (None, 1), @@ -411,7 +389,7 @@ class TypeInfo: __slots__ = "name", "enum", "gl_type", "gl_size", "components" def __init__( - self, name: str, enum: GLenumLike, gl_type: PyGLenum, gl_size: int, components: int + self, name: str, enum, gl_type, gl_size: int, components: int ): self.name = name """The string representation of this type""" @@ -463,67 +441,67 @@ class GLTypes: types = { # Floats - gl.GL_FLOAT: TypeInfo("GL_FLOAT", gl.GL_FLOAT, gl.GL_FLOAT, 4, 1), - gl.GL_FLOAT_VEC2: TypeInfo("GL_FLOAT_VEC2", gl.GL_FLOAT_VEC2, gl.GL_FLOAT, 4, 2), - gl.GL_FLOAT_VEC3: TypeInfo("GL_FLOAT_VEC3", gl.GL_FLOAT_VEC3, gl.GL_FLOAT, 4, 3), - gl.GL_FLOAT_VEC4: TypeInfo("GL_FLOAT_VEC4", gl.GL_FLOAT_VEC4, gl.GL_FLOAT, 4, 4), - # Doubles - gl.GL_DOUBLE: TypeInfo("GL_DOUBLE", gl.GL_DOUBLE, gl.GL_DOUBLE, 8, 1), - gl.GL_DOUBLE_VEC2: TypeInfo("GL_DOUBLE_VEC2", gl.GL_DOUBLE_VEC2, gl.GL_DOUBLE, 8, 2), - gl.GL_DOUBLE_VEC3: TypeInfo("GL_DOUBLE_VEC3", gl.GL_DOUBLE_VEC3, gl.GL_DOUBLE, 8, 3), - gl.GL_DOUBLE_VEC4: TypeInfo("GL_DOUBLE_VEC4", gl.GL_DOUBLE_VEC4, gl.GL_DOUBLE, 8, 4), + enums.FLOAT: TypeInfo("GL_FLOAT", enums.FLOAT, enums.FLOAT, 4, 1), + enums.FLOAT_VEC2: TypeInfo("GL_FLOAT_VEC2", enums.FLOAT_VEC2, enums.FLOAT, 4, 2), + enums.FLOAT_VEC3: TypeInfo("GL_FLOAT_VEC3", enums.FLOAT_VEC3, enums.FLOAT, 4, 3), + enums.FLOAT_VEC4: TypeInfo("GL_FLOAT_VEC4", enums.FLOAT_VEC4, enums.FLOAT, 4, 4), + # Doubles -- Unsupported by WebGL + enums.DOUBLE: TypeInfo("GL_DOUBLE", enums.DOUBLE, enums.DOUBLE, 8, 1), + enums.DOUBLE_VEC2: TypeInfo("GL_DOUBLE_VEC2", enums.DOUBLE_VEC2, enums.DOUBLE, 8, 2), + enums.DOUBLE_VEC3: TypeInfo("GL_DOUBLE_VEC3", enums.DOUBLE_VEC3, enums.DOUBLE, 8, 3), + enums.DOUBLE_VEC4: TypeInfo("GL_DOUBLE_VEC4", enums.DOUBLE_VEC4, enums.DOUBLE, 8, 4), # Booleans (ubyte) - gl.GL_BOOL: TypeInfo("GL_BOOL", gl.GL_BOOL, gl.GL_BOOL, 1, 1), - gl.GL_BOOL_VEC2: TypeInfo("GL_BOOL_VEC2", gl.GL_BOOL_VEC2, gl.GL_BOOL, 1, 2), - gl.GL_BOOL_VEC3: TypeInfo("GL_BOOL_VEC3", gl.GL_BOOL_VEC3, gl.GL_BOOL, 1, 3), - gl.GL_BOOL_VEC4: TypeInfo("GL_BOOL_VEC4", gl.GL_BOOL_VEC4, gl.GL_BOOL, 1, 4), + enums.BOOL: TypeInfo("GL_BOOL", enums.BOOL, enums.BOOL, 1, 1), + enums.BOOL_VEC2: TypeInfo("GL_BOOL_VEC2", enums.BOOL_VEC2, enums.BOOL, 1, 2), + enums.BOOL_VEC3: TypeInfo("GL_BOOL_VEC3", enums.BOOL_VEC3, enums.BOOL, 1, 3), + enums.BOOL_VEC4: TypeInfo("GL_BOOL_VEC4", enums.BOOL_VEC4, enums.BOOL, 1, 4), # Integers - gl.GL_INT: TypeInfo("GL_INT", gl.GL_INT, gl.GL_INT, 4, 1), - gl.GL_INT_VEC2: TypeInfo("GL_INT_VEC2", gl.GL_INT_VEC2, gl.GL_INT, 4, 2), - gl.GL_INT_VEC3: TypeInfo("GL_INT_VEC3", gl.GL_INT_VEC3, gl.GL_INT, 4, 3), - gl.GL_INT_VEC4: TypeInfo("GL_INT_VEC4", gl.GL_INT_VEC4, gl.GL_INT, 4, 4), + enums.INT: TypeInfo("GL_INT", enums.INT, enums.INT, 4, 1), + enums.INT_VEC2: TypeInfo("GL_INT_VEC2", enums.INT_VEC2, enums.INT, 4, 2), + enums.INT_VEC3: TypeInfo("GL_INT_VEC3", enums.INT_VEC3, enums.INT, 4, 3), + enums.INT_VEC4: TypeInfo("GL_INT_VEC4", enums.INT_VEC4, enums.INT, 4, 4), # Unsigned Integers - gl.GL_UNSIGNED_INT: TypeInfo( - "GL_UNSIGNED_INT", gl.GL_UNSIGNED_INT, gl.GL_UNSIGNED_INT, 4, 1 + enums.UNSIGNED_INT: TypeInfo( + "GL_UNSIGNED_INT", enums.UNSIGNED_INT, enums.UNSIGNED_INT, 4, 1 ), - gl.GL_UNSIGNED_INT_VEC2: TypeInfo( - "GL_UNSIGNED_INT_VEC2", gl.GL_UNSIGNED_INT_VEC2, gl.GL_UNSIGNED_INT, 4, 2 + enums.UNSIGNED_INT_VEC2: TypeInfo( + "GL_UNSIGNED_INT_VEC2", enums.UNSIGNED_INT_VEC2, enums.UNSIGNED_INT, 4, 2 ), - gl.GL_UNSIGNED_INT_VEC3: TypeInfo( - "GL_UNSIGNED_INT_VEC3", gl.GL_UNSIGNED_INT_VEC3, gl.GL_UNSIGNED_INT, 4, 3 + enums.UNSIGNED_INT_VEC3: TypeInfo( + "GL_UNSIGNED_INT_VEC3", enums.UNSIGNED_INT_VEC3, enums.UNSIGNED_INT, 4, 3 ), - gl.GL_UNSIGNED_INT_VEC4: TypeInfo( - "GL_UNSIGNED_INT_VEC4", gl.GL_UNSIGNED_INT_VEC4, gl.GL_UNSIGNED_INT, 4, 4 + enums.UNSIGNED_INT_VEC4: TypeInfo( + "GL_UNSIGNED_INT_VEC4", enums.UNSIGNED_INT_VEC4, enums.UNSIGNED_INT, 4, 4 ), # Unsigned Short (mostly used for short index buffers) - gl.GL_UNSIGNED_SHORT: TypeInfo( - "GL.GL_UNSIGNED_SHORT", gl.GL_UNSIGNED_SHORT, gl.GL_UNSIGNED_SHORT, 2, 2 + enums.UNSIGNED_SHORT: TypeInfo( + "GL.GL_UNSIGNED_SHORT", enums.UNSIGNED_SHORT, enums.UNSIGNED_SHORT, 2, 2 ), # Byte - gl.GL_BYTE: TypeInfo("GL_BYTE", gl.GL_BYTE, gl.GL_BYTE, 1, 1), - gl.GL_UNSIGNED_BYTE: TypeInfo( - "GL_UNSIGNED_BYTE", gl.GL_UNSIGNED_BYTE, gl.GL_UNSIGNED_BYTE, 1, 1 + enums.BYTE: TypeInfo("GL_BYTE", enums.BYTE, enums.BYTE, 1, 1), + enums.UNSIGNED_BYTE: TypeInfo( + "GL_UNSIGNED_BYTE", enums.UNSIGNED_BYTE, enums.UNSIGNED_BYTE, 1, 1 ), # Matrices - gl.GL_FLOAT_MAT2: TypeInfo("GL_FLOAT_MAT2", gl.GL_FLOAT_MAT2, gl.GL_FLOAT, 4, 4), - gl.GL_FLOAT_MAT3: TypeInfo("GL_FLOAT_MAT3", gl.GL_FLOAT_MAT3, gl.GL_FLOAT, 4, 9), - gl.GL_FLOAT_MAT4: TypeInfo("GL_FLOAT_MAT4", gl.GL_FLOAT_MAT4, gl.GL_FLOAT, 4, 16), - gl.GL_FLOAT_MAT2x3: TypeInfo("GL_FLOAT_MAT2x3", gl.GL_FLOAT_MAT2x3, gl.GL_FLOAT, 4, 6), - gl.GL_FLOAT_MAT2x4: TypeInfo("GL_FLOAT_MAT2x4", gl.GL_FLOAT_MAT2x4, gl.GL_FLOAT, 4, 8), - gl.GL_FLOAT_MAT3x2: TypeInfo("GL_FLOAT_MAT3x2", gl.GL_FLOAT_MAT3x2, gl.GL_FLOAT, 4, 6), - gl.GL_FLOAT_MAT3x4: TypeInfo("GL_FLOAT_MAT3x4", gl.GL_FLOAT_MAT3x4, gl.GL_FLOAT, 4, 12), - gl.GL_FLOAT_MAT4x2: TypeInfo("GL_FLOAT_MAT4x2", gl.GL_FLOAT_MAT4x2, gl.GL_FLOAT, 4, 8), - gl.GL_FLOAT_MAT4x3: TypeInfo("GL_FLOAT_MAT4x3", gl.GL_FLOAT_MAT4x3, gl.GL_FLOAT, 4, 12), - # Double matrices - gl.GL_DOUBLE_MAT2: TypeInfo("GL_DOUBLE_MAT2", gl.GL_DOUBLE_MAT2, gl.GL_DOUBLE, 8, 4), - gl.GL_DOUBLE_MAT3: TypeInfo("GL_DOUBLE_MAT3", gl.GL_DOUBLE_MAT3, gl.GL_DOUBLE, 8, 9), - gl.GL_DOUBLE_MAT4: TypeInfo("GL_DOUBLE_MAT4", gl.GL_DOUBLE_MAT4, gl.GL_DOUBLE, 8, 16), - gl.GL_DOUBLE_MAT2x3: TypeInfo("GL_DOUBLE_MAT2x3", gl.GL_DOUBLE_MAT2x3, gl.GL_DOUBLE, 8, 6), - gl.GL_DOUBLE_MAT2x4: TypeInfo("GL_DOUBLE_MAT2x4", gl.GL_DOUBLE_MAT2x4, gl.GL_DOUBLE, 8, 8), - gl.GL_DOUBLE_MAT3x2: TypeInfo("GL_DOUBLE_MAT3x2", gl.GL_DOUBLE_MAT3x2, gl.GL_DOUBLE, 8, 6), - gl.GL_DOUBLE_MAT3x4: TypeInfo("GL_DOUBLE_MAT3x4", gl.GL_DOUBLE_MAT3x4, gl.GL_DOUBLE, 8, 12), - gl.GL_DOUBLE_MAT4x2: TypeInfo("GL_DOUBLE_MAT4x2", gl.GL_DOUBLE_MAT4x2, gl.GL_DOUBLE, 8, 8), - gl.GL_DOUBLE_MAT4x3: TypeInfo("GL_DOUBLE_MAT4x3", gl.GL_DOUBLE_MAT4x3, gl.GL_DOUBLE, 8, 12), + enums.FLOAT_MAT2: TypeInfo("GL_FLOAT_MAT2", enums.FLOAT_MAT2, enums.FLOAT, 4, 4), + enums.FLOAT_MAT3: TypeInfo("GL_FLOAT_MAT3", enums.FLOAT_MAT3, enums.FLOAT, 4, 9), + enums.FLOAT_MAT4: TypeInfo("GL_FLOAT_MAT4", enums.FLOAT_MAT4, enums.FLOAT, 4, 16), + enums.FLOAT_MAT2x3: TypeInfo("GL_FLOAT_MAT2x3", enums.FLOAT_MAT2x3, enums.FLOAT, 4, 6), + enums.FLOAT_MAT2x4: TypeInfo("GL_FLOAT_MAT2x4", enums.FLOAT_MAT2x4, enums.FLOAT, 4, 8), + enums.FLOAT_MAT3x2: TypeInfo("GL_FLOAT_MAT3x2", enums.FLOAT_MAT3x2, enums.FLOAT, 4, 6), + enums.FLOAT_MAT3x4: TypeInfo("GL_FLOAT_MAT3x4", enums.FLOAT_MAT3x4, enums.FLOAT, 4, 12), + enums.FLOAT_MAT4x2: TypeInfo("GL_FLOAT_MAT4x2", enums.FLOAT_MAT4x2, enums.FLOAT, 4, 8), + enums.FLOAT_MAT4x3: TypeInfo("GL_FLOAT_MAT4x3", enums.FLOAT_MAT4x3, enums.FLOAT, 4, 12), + # Double matrices -- unsupported by WebGL + enums.DOUBLE_MAT2: TypeInfo("GL_DOUBLE_MAT2", enums.DOUBLE_MAT2, enums.DOUBLE, 8, 4), + enums.DOUBLE_MAT3: TypeInfo("GL_DOUBLE_MAT3", enums.DOUBLE_MAT3, enums.DOUBLE, 8, 9), + enums.DOUBLE_MAT4: TypeInfo("GL_DOUBLE_MAT4", enums.DOUBLE_MAT4, enums.DOUBLE, 8, 16), + enums.DOUBLE_MAT2x3: TypeInfo("GL_DOUBLE_MAT2x3", enums.DOUBLE_MAT2x3, enums.DOUBLE, 8, 6), + enums.DOUBLE_MAT2x4: TypeInfo("GL_DOUBLE_MAT2x4", enums.DOUBLE_MAT2x4, enums.DOUBLE, 8, 8), + enums.DOUBLE_MAT3x2: TypeInfo("GL_DOUBLE_MAT3x2", enums.DOUBLE_MAT3x2, enums.DOUBLE, 8, 6), + enums.DOUBLE_MAT3x4: TypeInfo("GL_DOUBLE_MAT3x4", enums.DOUBLE_MAT3x4, enums.DOUBLE, 8, 12), + enums.DOUBLE_MAT4x2: TypeInfo("GL_DOUBLE_MAT4x2", enums.DOUBLE_MAT4x2, enums.DOUBLE, 8, 8), + enums.DOUBLE_MAT4x3: TypeInfo("GL_DOUBLE_MAT4x3", enums.DOUBLE_MAT4x3, enums.DOUBLE, 8, 12), # TODO: Add sampler types if needed. Only needed for better uniform introspection. } diff --git a/arcade/utils.py b/arcade/utils.py index 547820d47..141d4f6e1 100644 --- a/arcade/utils.py +++ b/arcade/utils.py @@ -21,6 +21,7 @@ "is_nonstr_iterable", "is_str_or_noniterable", "grow_sequence", + "is_pyodide", "is_raspberry_pi", "get_raspberry_pi_info", ] @@ -254,12 +255,13 @@ def __deepcopy__(self, memo): # noqa return decorated_type +def is_pyodide() -> bool: + return False def is_raspberry_pi() -> bool: """Determine if the host is a raspberry pi.""" return get_raspberry_pi_info()[0] - def get_raspberry_pi_info() -> tuple[bool, str, str]: """ Determine if the host is a raspberry pi with additional info. From e3b80a8c3f57d361e95968158689e25b4679c257 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Tue, 15 Apr 2025 18:20:45 -0400 Subject: [PATCH 10/16] replace pyglet.gl with enums --- arcade/gl/texture.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/arcade/gl/texture.py b/arcade/gl/texture.py index 8a51673bf..d324f86c6 100644 --- a/arcade/gl/texture.py +++ b/arcade/gl/texture.py @@ -4,8 +4,7 @@ from typing import TYPE_CHECKING -from pyglet import gl - +from . import enums from ..types import BufferProtocol from .types import ( BufferOrBufferProtocol, @@ -134,8 +133,8 @@ def __init__( self._compressed = compressed self._compressed_data = compressed_data - self._wrap_x = gl.GL_REPEAT - self._wrap_y = gl.GL_REPEAT + self._wrap_x = enums.REPEAT + self._wrap_y = enums.REPEAT if self._components not in [1, 2, 3, 4]: raise ValueError("Components must be 1, 2, 3 or 4") From 8bbb0db0a1f54469b52e118302f678ef10da43c4 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:18:03 -0400 Subject: [PATCH 11/16] Work on tests --- arcade/application.py | 2 ++ arcade/gl/backends/gl/framebuffer.py | 2 -- arcade/gl/backends/gl/texture_array.py | 1 + pyproject.toml | 4 ++++ tests/conftest.py | 23 ++++++++++++++++++- tests/unit/gl/__init__.py | 0 tests/unit/gl/backends/__init__.py | 0 tests/unit/gl/backends/gl/__init__.py | 0 .../gl/{ => backends/gl}/test_gl_program.py | 5 ++-- tests/unit/gl/test_gl_context.py | 2 +- tests/unit/gl/test_gl_texture.py | 4 ++-- 11 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 tests/unit/gl/__init__.py create mode 100644 tests/unit/gl/backends/__init__.py create mode 100644 tests/unit/gl/backends/gl/__init__.py rename tests/unit/gl/{ => backends/gl}/test_gl_program.py (98%) diff --git a/arcade/application.py b/arcade/application.py index 64b8bccb6..4694a9a91 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -170,6 +170,8 @@ def __init__( desired_gl_provider = "gl" if is_pyodide(): gl_api = "webgl" + + if gl_api == "webgl": desired_gl_provider = "webgl" # Detect Raspberry Pi and switch to OpenGL ES 3.1 diff --git a/arcade/gl/backends/gl/framebuffer.py b/arcade/gl/backends/gl/framebuffer.py index 57ff955a0..78b07c15e 100644 --- a/arcade/gl/backends/gl/framebuffer.py +++ b/arcade/gl/backends/gl/framebuffer.py @@ -100,8 +100,6 @@ def __init__( if self._ctx.gc_mode == "auto" and not self.is_default: weakref.finalize(self, GLFramebuffer.delete_glo, ctx, fbo_id) - self.ctx.stats.incr("framebuffer") - def __del__(self): # Intercept garbage collection if we are using Context.gc() if self._ctx.gc_mode == "context_gc" and not self.is_default and self._glo.value > 0: diff --git a/arcade/gl/backends/gl/texture_array.py b/arcade/gl/backends/gl/texture_array.py index 38999ed0f..ee425461c 100644 --- a/arcade/gl/backends/gl/texture_array.py +++ b/arcade/gl/backends/gl/texture_array.py @@ -126,6 +126,7 @@ def __init__( compressed: bool = False, compressed_data: bool = False, ): + super().__init__(ctx, size, components=components, dtype=dtype, data=data, filter=filter, wrap_x=wrap_x, wrap_y=wrap_y, depth=depth, samples=samples, immutable=immutable, internal_format=internal_format, compressed=compressed, compressed_data=compressed_data) self._glo = glo = gl.GLuint() # Default filters for float and integer textures diff --git a/pyproject.toml b/pyproject.toml index 3a995d57c..e3a1de143 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,6 +136,10 @@ norecursedirs = [ "dist", "tempt", ] +markers = [ + "backendgl: Run OpenGL (or OpenGL ES) backend specific tests", + "backendwebgl: Run WebGL backend specific tests" +] [tool.pyright] include = ["arcade"] diff --git a/tests/conftest.py b/tests/conftest.py index d11069917..8ccfe2f7c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -26,12 +26,33 @@ FIXTURE_ROOT = PROJECT_ROOT / "tests" / "fixtures" arcade.resources.add_resource_handle("fixtures", FIXTURE_ROOT) REAL_WINDOW_CLASS = arcade.Window +GL_BACKEND = "gl" WINDOW = None OFFSCREEN = None +POSSIBLE_BACKENDS = [ + "backendgl", + "backendwebgl" +] + arcade.resources.load_kenney_fonts() +def pytest_addoption(parser): + parser.addoption("--gl-backend", default="gl") + +def pytest_configure(config): + global GL_BACKEND + GL_BACKEND = config.option.gl_backend + +def pytest_collection_modifyitems(config, items): + desired_backend = "backend" + GL_BACKEND + for item in items: + for backend in POSSIBLE_BACKENDS: + if backend in item.keywords: + if backend != desired_backend: + item.add_marker(pytest.mark.skip(f"Skipping GL backend specific test for {backend}")) + def make_window_caption(request=None, prefix="Testing", sep=" - ") -> str: """Centralizes test name customization. @@ -51,7 +72,7 @@ def create_window(width=1280, height=720, caption="Testing", **kwargs): global WINDOW if not WINDOW: WINDOW = REAL_WINDOW_CLASS( - width=width, height=height, title=caption, vsync=False, antialiasing=False + width=width, height=height, title=caption, vsync=False, antialiasing=False, gl_api = GL_BACKEND ) WINDOW.set_vsync(False) # This value is being monkey-patched into the Window class so that tests can identify if we are using diff --git a/tests/unit/gl/__init__.py b/tests/unit/gl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/gl/backends/__init__.py b/tests/unit/gl/backends/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/gl/backends/gl/__init__.py b/tests/unit/gl/backends/gl/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/unit/gl/test_gl_program.py b/tests/unit/gl/backends/gl/test_gl_program.py similarity index 98% rename from tests/unit/gl/test_gl_program.py rename to tests/unit/gl/backends/gl/test_gl_program.py index 2873e2dae..47305da76 100644 --- a/tests/unit/gl/test_gl_program.py +++ b/tests/unit/gl/backends/gl/test_gl_program.py @@ -4,9 +4,10 @@ from pyglet import gl from pyglet.math import Mat4, Mat3 from arcade.gl import ShaderException -from arcade.gl.uniform import UniformBlock -from arcade.gl.glsl import ShaderSource +from arcade.gl.backends.gl.uniform import UniformBlock +from arcade.gl.backends.gl.glsl import ShaderSource +pytestmark = pytest.mark.backendgl def test_shader_source(ctx): """Test shader source parsing""" diff --git a/tests/unit/gl/test_gl_context.py b/tests/unit/gl/test_gl_context.py index ede56b34b..3448dc3c9 100644 --- a/tests/unit/gl/test_gl_context.py +++ b/tests/unit/gl/test_gl_context.py @@ -77,7 +77,7 @@ def test_enable_disable(ctx): assert ctx.is_enabled(ctx.BLEND) is False assert len(ctx._flags) == 2 - ctx.enable_only(ctx.BLEND, ctx.CULL_FACE, ctx.DEPTH_TEST, ctx.PROGRAM_POINT_SIZE) + ctx.enable_only(ctx.BLEND, ctx.CULL_FACE, ctx.DEPTH_TEST) def test_enabled(ctx): diff --git a/tests/unit/gl/test_gl_texture.py b/tests/unit/gl/test_gl_texture.py index 2e41a6302..a78a9b31d 100644 --- a/tests/unit/gl/test_gl_texture.py +++ b/tests/unit/gl/test_gl_texture.py @@ -26,9 +26,9 @@ def test_properties(ctx): with pytest.raises(ValueError): texture.filter = None - texture.wrap_x = ctx.CLAMP_TO_BORDER + texture.wrap_x = ctx.CLAMP_TO_EDGE texture.wrap_y = ctx.CLAMP_TO_EDGE - assert texture.wrap_x == ctx.CLAMP_TO_BORDER + assert texture.wrap_x == ctx.CLAMP_TO_EDGE assert texture.wrap_y == ctx.CLAMP_TO_EDGE From 0dbf35c03fe8ccadcc6b521c372045a57aa0c113 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:43:47 -0400 Subject: [PATCH 12/16] Kind of hacky fix for tessellation example --- arcade/examples/gl/tessellation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/arcade/examples/gl/tessellation.py b/arcade/examples/gl/tessellation.py index c506cd91a..8a3efb3ee 100644 --- a/arcade/examples/gl/tessellation.py +++ b/arcade/examples/gl/tessellation.py @@ -10,6 +10,7 @@ import arcade from arcade.gl import BufferDescription +import pyglet.gl WINDOW_WIDTH = 1280 WINDOW_HEIGHT = 720 @@ -106,7 +107,7 @@ def __init__(self, width, height, title): def on_draw(self): self.clear() self.program["time"] = self.time - self.geometry.render(self.program, mode=self.ctx.PATCHES) + self.geometry.render(self.program, mode=pyglet.gl.GL_PATCHES) if __name__ == "__main__": From 905e4d294adacbf2908ab8dcbd3e0a242f5cdf89 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:44:03 -0400 Subject: [PATCH 13/16] Fix stats counting for compute shaders --- arcade/gl/backends/gl/compute_shader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/arcade/gl/backends/gl/compute_shader.py b/arcade/gl/backends/gl/compute_shader.py index f47fc5ade..3a5322376 100644 --- a/arcade/gl/backends/gl/compute_shader.py +++ b/arcade/gl/backends/gl/compute_shader.py @@ -95,8 +95,6 @@ def __init__(self, ctx: Context, glsl_source: str) -> None: if self._ctx.gc_mode == "auto": weakref.finalize(self, GLComputeShader.delete_glo, self._ctx, glo) - ctx.stats.incr("compute_shader") - @property def glo(self) -> int: """The name/id of the OpenGL resource""" From c4c274139623fab72b56b8267afb20c1cdb44edc Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:45:04 -0400 Subject: [PATCH 14/16] ruff format --- arcade/application.py | 6 +-- arcade/context.py | 6 +-- arcade/gl/backends/gl/buffer.py | 4 +- arcade/gl/backends/gl/context.py | 52 ++++++++++++++------------ arcade/gl/backends/gl/framebuffer.py | 13 ++++--- arcade/gl/backends/gl/provider.py | 5 ++- arcade/gl/backends/gl/texture.py | 22 +++++++++-- arcade/gl/backends/gl/texture_array.py | 20 +++++++++- arcade/gl/backends/gl/vertex_array.py | 4 +- arcade/gl/buffer.py | 1 - arcade/gl/context.py | 21 +++++------ arcade/gl/enums.py | 8 ++-- arcade/gl/framebuffer.py | 2 +- arcade/gl/program.py | 11 ++++-- arcade/gl/provider.py | 12 ++++-- arcade/gl/query.py | 2 +- arcade/gl/sampler.py | 7 ++-- arcade/gl/texture.py | 11 +++--- arcade/gl/texture_array.py | 8 ++-- arcade/gl/types.py | 16 ++++---- arcade/gl/vertex_array.py | 29 ++++++++------ arcade/utils.py | 3 ++ 22 files changed, 156 insertions(+), 107 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 4694a9a91..db0c725a8 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -21,10 +21,10 @@ from arcade.clock import GLOBAL_CLOCK, GLOBAL_FIXED_CLOCK, _setup_clock, _setup_fixed_clock from arcade.color import BLACK from arcade.context import ArcadeContext +from arcade.gl.provider import get_arcade_context, set_provider from arcade.types import LBWH, Color, Rect, RGBANormalized, RGBOrA255 -from arcade.utils import is_raspberry_pi, is_pyodide +from arcade.utils import is_pyodide, is_raspberry_pi from arcade.window_commands import get_display_size, set_window -from arcade.gl.provider import get_arcade_context, set_provider if TYPE_CHECKING: from arcade.camera import Projector @@ -284,7 +284,7 @@ def __init__( set_provider(desired_gl_provider) self._ctx: ArcadeContext = get_arcade_context(self, gc_mode=gc_mode, gl_api=gl_api) - #self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) + # self._ctx: ArcadeContext = ArcadeContext(self, gc_mode=gc_mode, gl_api=gl_api) self._background_color: Color = BLACK self._current_view: View | None = None diff --git a/arcade/context.py b/arcade/context.py index 542c545d0..79ef31915 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -449,9 +449,9 @@ def load_texture( path: str | Path, *, flip: bool = True, - wrap_x = None, - wrap_y = None, - filter = None, + wrap_x=None, + wrap_y=None, + filter=None, build_mipmaps: bool = False, internal_format: int | None = None, immutable: bool = False, diff --git a/arcade/gl/backends/gl/buffer.py b/arcade/gl/backends/gl/buffer.py index 1324d7a69..1d8284fec 100644 --- a/arcade/gl/backends/gl/buffer.py +++ b/arcade/gl/backends/gl/buffer.py @@ -6,12 +6,11 @@ from pyglet import gl +from arcade.gl.buffer import Buffer from arcade.types import BufferProtocol from .utils import data_to_ctypes -from arcade.gl.buffer import Buffer - if TYPE_CHECKING: from arcade.gl import Context @@ -52,7 +51,6 @@ class GLBuffer(Buffer): __slots__ = "_glo", "_usage" - def __init__( self, ctx: Context, diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 4c81a3317..7ee68b926 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -1,19 +1,18 @@ -from ctypes import c_int, c_float, c_char_p, cast -from typing import List, Dict, Iterable, Sequence, Tuple - -from arcade.gl.context import Context, Info -from arcade.context import ArcadeContext +from ctypes import c_char_p, c_float, c_int, cast +from typing import Dict, Iterable, List, Sequence, Tuple import pyglet from pyglet import gl -from arcade.types import BufferProtocol +from arcade.context import ArcadeContext from arcade.gl import enums +from arcade.gl.context import Context, Info from arcade.gl.types import BufferDescription, PyGLenum +from arcade.types import BufferProtocol from .buffer import GLBuffer from .compute_shader import GLComputeShader -from .framebuffer import GLFramebuffer, GLDefaultFrameBuffer +from .framebuffer import GLDefaultFrameBuffer, GLFramebuffer from .glsl import ShaderSource from .program import GLProgram from .query import GLQuery @@ -24,18 +23,21 @@ class GLContext(Context): - #: The OpenGL api. Usually "gl" or "gles". gl_api: str = "gl" _valid_apis = ("gl", "gles") - def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): + def __init__( + self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl" + ): super().__init__(window, gc_mode) if gl_api not in self._valid_apis: if gl_api == "webgl": - raise ValueError(f"Tried to create a GLContext with webgl api selected. Valid options for this backend are: {self._valid_apis}") + raise ValueError( + f"Tried to create a GLContext with webgl api selected. Valid options for this backend are: {self._valid_apis}" + ) raise ValueError(f"Invalid gl_api. Options are: {self._valid_apis}") self.gl_api = gl_api @@ -209,21 +211,23 @@ def flush(self) -> None: def _create_default_framebuffer(self) -> GLDefaultFrameBuffer: return GLDefaultFrameBuffer(self) - def buffer(self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> GLBuffer: + def buffer( + self, *, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static" + ) -> GLBuffer: return GLBuffer(self, data, reserve=reserve, usage=usage) def program( - self, - *, - vertex_shader: str, - fragment_shader: str | None = None, - geometry_shader: str | None = None, - tess_control_shader: str | None = None, - tess_evaluation_shader: str | None = None, - common: List[str] | None = None, - defines: Dict[str, str] | None = None, - varyings: Sequence[str] | None = None, - varyings_capture_mode: str = "interleaved", + self, + *, + vertex_shader: str, + fragment_shader: str | None = None, + geometry_shader: str | None = None, + tess_control_shader: str | None = None, + tess_evaluation_shader: str | None = None, + common: List[str] | None = None, + defines: Dict[str, str] | None = None, + varyings: Sequence[str] | None = None, + varyings_capture_mode: str = "interleaved", ) -> GLProgram: source_vs = ShaderSource(self, vertex_shader, common, gl.GL_VERTEX_SHADER) source_fs = ( @@ -321,7 +325,7 @@ def texture( ) def depth_texture( - self, size: Tuple[int, int], *, data: BufferProtocol | None = None + self, size: Tuple[int, int], *, data: BufferProtocol | None = None ) -> GLTexture2D: return GLTexture2D(self, size, data=data, depth=True) @@ -405,11 +409,13 @@ def texture_array( def query(self, *, samples=True, time=True, primitives=True) -> GLQuery: return GLQuery(self, samples=samples, time=time, primitives=primitives) + class GLArcadeContext(ArcadeContext, GLContext): def __init__(self, *args, **kwargs): GLContext.__init__(self, *args, **kwargs) ArcadeContext.__init__(self, *args, **kwargs) + class GLInfo(Info): """OpenGL info and capabilities""" diff --git a/arcade/gl/backends/gl/framebuffer.py b/arcade/gl/backends/gl/framebuffer.py index 78b07c15e..12b9f52af 100644 --- a/arcade/gl/backends/gl/framebuffer.py +++ b/arcade/gl/backends/gl/framebuffer.py @@ -6,9 +6,9 @@ from pyglet import gl -from arcade.types import RGBOrA255, RGBOrANormalized -from arcade.gl.types import pixel_formats from arcade.gl.framebuffer import DefaultFrameBuffer, Framebuffer +from arcade.gl.types import pixel_formats +from arcade.types import RGBOrA255, RGBOrANormalized from .texture import GLTexture2D @@ -46,9 +46,8 @@ class GLFramebuffer(Framebuffer): depth_attachment: A depth attachment """ - __slots__ = ( - "_glo" - ) + + __slots__ = "_glo" def __init__( self, @@ -57,7 +56,9 @@ def __init__( color_attachments: GLTexture2D | list[GLTexture2D], depth_attachment: GLTexture2D | None = None, ): - super().__init__(ctx, color_attachments=color_attachments, depth_attachment=depth_attachment) + super().__init__( + ctx, color_attachments=color_attachments, depth_attachment=depth_attachment + ) self._glo = fbo_id = gl.GLuint() # The OpenGL alias/name # Create the framebuffer object diff --git a/arcade/gl/backends/gl/provider.py b/arcade/gl/backends/gl/provider.py index 5d3fc14f8..0a76924ae 100644 --- a/arcade/gl/backends/gl/provider.py +++ b/arcade/gl/backends/gl/provider.py @@ -1,6 +1,7 @@ from arcade.gl.provider import BaseProvider -from .context import GLContext, GLArcadeContext, GLInfo +from .context import GLArcadeContext, GLContext, GLInfo + class Provider(BaseProvider): def create_context(self, *args, **kwargs): @@ -10,4 +11,4 @@ def create_info(self, ctx): return GLInfo(ctx) def create_arcade_context(self, *args, **kwargs): - return GLArcadeContext(*args, **kwargs) \ No newline at end of file + return GLArcadeContext(*args, **kwargs) diff --git a/arcade/gl/backends/gl/texture.py b/arcade/gl/backends/gl/texture.py index bb3ee1fc4..62ed3bafd 100644 --- a/arcade/gl/backends/gl/texture.py +++ b/arcade/gl/backends/gl/texture.py @@ -6,8 +6,6 @@ from pyglet import gl -from arcade.types import BufferProtocol - from arcade.gl.texture import Texture2D from arcade.gl.types import ( BufferOrBufferProtocol, @@ -15,6 +13,7 @@ compare_funcs, pixel_formats, ) +from arcade.types import BufferProtocol from .buffer import Buffer from .utils import data_to_ctypes @@ -42,6 +41,7 @@ "1": gl.GL_ONE, } + class GLTexture2D(Texture2D): """ An OpenGL 2D texture. @@ -102,6 +102,7 @@ class GLTexture2D(Texture2D): compressed_data: The raw compressed data """ + __slots__ = ( "_glo", "_target", @@ -125,7 +126,22 @@ def __init__( compressed: bool = False, compressed_data: bool = False, ): - super().__init__(ctx, size, components=components, dtype=dtype, data=data, filter=filter, wrap_x=wrap_x, wrap_y=wrap_y, depth=depth, samples=samples, immutable=immutable, internal_format = internal_format, compressed=compressed, compressed_data=compressed_data) + super().__init__( + ctx, + size, + components=components, + dtype=dtype, + data=data, + filter=filter, + wrap_x=wrap_x, + wrap_y=wrap_y, + depth=depth, + samples=samples, + immutable=immutable, + internal_format=internal_format, + compressed=compressed, + compressed_data=compressed_data, + ) self._glo = glo = gl.GLuint() # Default filters for float and integer textures diff --git a/arcade/gl/backends/gl/texture_array.py b/arcade/gl/backends/gl/texture_array.py index ee425461c..98c2da07e 100644 --- a/arcade/gl/backends/gl/texture_array.py +++ b/arcade/gl/backends/gl/texture_array.py @@ -6,7 +6,6 @@ from pyglet import gl -from arcade.types import BufferProtocol from arcade.gl.texture_array import TextureArray from arcade.gl.types import ( BufferOrBufferProtocol, @@ -14,6 +13,7 @@ compare_funcs, pixel_formats, ) +from arcade.types import BufferProtocol from .buffer import Buffer from .utils import data_to_ctypes @@ -41,6 +41,7 @@ "1": gl.GL_ONE, } + class GLTextureArray(TextureArray): """ An OpenGL 2D texture array. @@ -126,7 +127,22 @@ def __init__( compressed: bool = False, compressed_data: bool = False, ): - super().__init__(ctx, size, components=components, dtype=dtype, data=data, filter=filter, wrap_x=wrap_x, wrap_y=wrap_y, depth=depth, samples=samples, immutable=immutable, internal_format=internal_format, compressed=compressed, compressed_data=compressed_data) + super().__init__( + ctx, + size, + components=components, + dtype=dtype, + data=data, + filter=filter, + wrap_x=wrap_x, + wrap_y=wrap_y, + depth=depth, + samples=samples, + immutable=immutable, + internal_format=internal_format, + compressed=compressed, + compressed_data=compressed_data, + ) self._glo = glo = gl.GLuint() # Default filters for float and integer textures diff --git a/arcade/gl/backends/gl/vertex_array.py b/arcade/gl/backends/gl/vertex_array.py index 18128d348..6389a4f96 100644 --- a/arcade/gl/backends/gl/vertex_array.py +++ b/arcade/gl/backends/gl/vertex_array.py @@ -7,7 +7,7 @@ from pyglet import gl from arcade.gl.types import BufferDescription, GLenumLike, GLuintLike, gl_name -from arcade.gl.vertex_array import VertexArray, Geometry +from arcade.gl.vertex_array import Geometry, VertexArray from .buffer import Buffer from .program import Program @@ -461,6 +461,7 @@ class GLGeometry(Geometry): Byte size of the index buffer datatype. Can be 1, 2 or 4 (8, 16 or 32bit integer) """ + def __init__( self, ctx: "Context", @@ -471,7 +472,6 @@ def __init__( ) -> None: super().__init__(ctx, content, index_buffer, mode, index_element_size) - def _generate_vao(self, program: Program) -> VertexArray: """ Create a new VertexArray for the given program. diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index 214f9bf7c..294c3730b 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import ABC, abstractmethod - from typing import TYPE_CHECKING from arcade.types import BufferProtocol diff --git a/arcade/gl/context.py b/arcade/gl/context.py index fd00c856a..84e1b3f4a 100644 --- a/arcade/gl/context.py +++ b/arcade/gl/context.py @@ -1,9 +1,8 @@ from __future__ import annotations -from abc import ABC, abstractmethod - import logging import weakref +from abc import ABC, abstractmethod from collections import deque from contextlib import contextmanager from typing import ( @@ -22,8 +21,8 @@ import pyglet from pyglet.window import Window -from . import enums from ..types import BufferProtocol +from . import enums from .buffer import Buffer from .compute_shader import ComputeShader from .framebuffer import DefaultFrameBuffer, Framebuffer @@ -206,7 +205,7 @@ def __init__( self, window: pyglet.window.Window, # type: ignore gc_mode: str = "context_gc", - gl_api: str = "gl" # This is ignored here, but used in implementation classes + gl_api: str = "gl", # This is ignored here, but used in implementation classes ): self._window_ref = weakref.ref(window) self._info = get_provider().create_info(self) @@ -855,9 +854,9 @@ def texture( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - wrap_x = None, - wrap_y = None, - filter = None, + wrap_x=None, + wrap_y=None, + filter=None, samples: int = 0, immutable: bool = False, internal_format, @@ -943,9 +942,9 @@ def texture_array( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - wrap_x = None, - wrap_y = None, - filter = None, + wrap_x=None, + wrap_y=None, + filter=None, ) -> TextureArray: """ Create a 2D Texture Array. @@ -992,7 +991,7 @@ def sampler(self, texture: Texture2D) -> Sampler: @abstractmethod def geometry( self, - content = None, + content=None, index_buffer: Buffer | None = None, mode: int | None = None, index_element_size: int = 4, diff --git a/arcade/gl/enums.py b/arcade/gl/enums.py index a685bc1ed..01f7637ac 100644 --- a/arcade/gl/enums.py +++ b/arcade/gl/enums.py @@ -52,7 +52,7 @@ MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122 MAX_TEXTURE_IMAGE_UNITS = 0x8872 -MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF # Technically comes from EXT_texture_filter_anisotropic in WebGL, but it's widely available +MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF # Technically comes from EXT_texture_filter_anisotropic in WebGL, but it's widely available MAX_VIEWPORT_DIMS = 0x0D3A MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8C8B @@ -187,7 +187,7 @@ UNSIGNED_INT = 0x1405 FLOAT = 0x1406 HALF_FLOAT = 0x140B -DOUBLE = 0x140A # Not supported in WebGL, but left in common to make implementation easier +DOUBLE = 0x140A # Not supported in WebGL, but left in common to make implementation easier FLOAT_VEC2 = 0x8B50 FLOAT_VEC3 = 0x8B51 @@ -230,6 +230,6 @@ VERTEX_SHADER = 0x8B31 FRAGMENT_SHADER = 0x8B30 -GEOMETRY_SHADER = 0x8DD9 # Not supported in WebGL +GEOMETRY_SHADER = 0x8DD9 # Not supported in WebGL TESS_CONTROL_SHADER = 0x8E88 # Not supported in WebGL -TESS_EVALUATION_SHADER = 0x8E87 # Not supported in WebGL \ No newline at end of file +TESS_EVALUATION_SHADER = 0x8E87 # Not supported in WebGL diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index 3cd7c2c51..cc4f5143e 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import ABC, abstractmethod - from contextlib import contextmanager from typing import TYPE_CHECKING, Generator @@ -325,6 +324,7 @@ def _detect_size(self) -> tuple[int, int]: raise ValueError("All framebuffer attachments should have the same size") return expected_size + class DefaultFrameBuffer(Framebuffer, ABC): """ Represents the default framebuffer. diff --git a/arcade/gl/program.py b/arcade/gl/program.py index 2976af8d5..ecae23d85 100644 --- a/arcade/gl/program.py +++ b/arcade/gl/program.py @@ -1,8 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, Iterable - from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Iterable if TYPE_CHECKING: from arcade.gl import Context @@ -73,7 +72,9 @@ def ctx(self) -> "Context": @property @abstractmethod - def attributes(self) -> Iterable: # TODO: Typing on this Iterable, need generic type for AttribFormat? + def attributes( + self, + ) -> Iterable: # TODO: Typing on this Iterable, need generic type for AttribFormat? """List of attribute information.""" raise NotImplementedError("The enabled graphics backend does not support this method.") @@ -144,7 +145,9 @@ def delete(self): raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod - def __getitem__(self, item): # TODO: typing, this should return Uniform | UniformBlock, but need generic options for those: + def __getitem__( + self, item + ): # TODO: typing, this should return Uniform | UniformBlock, but need generic options for those: """Get a uniform or uniform block""" raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/provider.py b/arcade/gl/provider.py index a8bbb9d8b..b6ba8fb3e 100644 --- a/arcade/gl/provider.py +++ b/arcade/gl/provider.py @@ -1,16 +1,17 @@ from __future__ import annotations +import importlib from abc import ABC, abstractmethod from typing import TYPE_CHECKING -import importlib - if TYPE_CHECKING: from arcade.context import ArcadeContext + from .context import Context _current_provider: BaseProvider = None + def set_provider(provider_name: str): global _current_provider @@ -21,23 +22,26 @@ def set_provider(provider_name: str): print(e) raise ImportError(f"arcade.gl Backend Provider '{provider_name}' not found") + def get_provider(): return _current_provider + def get_context(*args, **kwargs) -> Context: if _current_provider is None: set_provider("gl") return _current_provider.create_context(*args, **kwargs) + def get_arcade_context(*args, **kwargs) -> ArcadeContext: if _current_provider is None: set_provider("gl") return _current_provider.create_arcade_context(*args, **kwargs) -class BaseProvider(ABC): +class BaseProvider(ABC): @abstractmethod def create_context(self, *args, **kwargs) -> Context: pass @@ -48,4 +52,4 @@ def create_info(self, ctx: Context): @abstractmethod def create_arcade_context(self, *args, **kwargs) -> ArcadeContext: - pass \ No newline at end of file + pass diff --git a/arcade/gl/query.py b/arcade/gl/query.py index 5709513e4..dacf2eed9 100644 --- a/arcade/gl/query.py +++ b/arcade/gl/query.py @@ -1,7 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING from abc import ABC, abstractmethod +from typing import TYPE_CHECKING if TYPE_CHECKING: from arcade.gl import Context diff --git a/arcade/gl/sampler.py b/arcade/gl/sampler.py index e882911eb..96dc1429a 100644 --- a/arcade/gl/sampler.py +++ b/arcade/gl/sampler.py @@ -1,7 +1,6 @@ from __future__ import annotations from abc import ABC, abstractmethod - from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -21,9 +20,9 @@ def __init__( ctx: "Context", texture: Texture2D, *, - filter = None, # TODO: Typing, should be tuple[PyGLuint, PyGLuint] | None - wrap_x = None, # TODO: Typing, should be PyGLuint | None - wrap_y = None, # TODO: Typing, should be PyGLuint | None + filter=None, # TODO: Typing, should be tuple[PyGLuint, PyGLuint] | None + wrap_x=None, # TODO: Typing, should be PyGLuint | None + wrap_y=None, # TODO: Typing, should be PyGLuint | None ): self._ctx = ctx diff --git a/arcade/gl/texture.py b/arcade/gl/texture.py index d324f86c6..428509e89 100644 --- a/arcade/gl/texture.py +++ b/arcade/gl/texture.py @@ -1,11 +1,10 @@ from __future__ import annotations from abc import ABC, abstractmethod - from typing import TYPE_CHECKING -from . import enums from ..types import BufferProtocol +from . import enums from .types import ( BufferOrBufferProtocol, pixel_formats, @@ -108,13 +107,13 @@ def __init__( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - filter = None, # TODO: typing, should be tuple[PyGLuint, PyGLuint] - wrap_x = None, # TODO: typing, should be PyGLuint | None - wrap_y = None, # TODO: typing, should be PyGLuint | None + filter=None, # TODO: typing, should be tuple[PyGLuint, PyGLuint] + wrap_x=None, # TODO: typing, should be PyGLuint | None + wrap_y=None, # TODO: typing, should be PyGLuint | None depth=False, samples: int = 0, immutable: bool = False, - internal_format = None, # TODO: typing, shouldb e PyGLuint | None + internal_format=None, # TODO: typing, shouldb e PyGLuint | None compressed: bool = False, compressed_data: bool = False, ): diff --git a/arcade/gl/texture_array.py b/arcade/gl/texture_array.py index 88057b049..93a165f73 100644 --- a/arcade/gl/texture_array.py +++ b/arcade/gl/texture_array.py @@ -108,13 +108,13 @@ def __init__( components: int = 4, dtype: str = "f1", data: BufferProtocol | None = None, - filter = None, - wrap_x = None, - wrap_y = None, + filter=None, + wrap_x=None, + wrap_y=None, depth=False, samples: int = 0, immutable: bool = False, - internal_format = None, + internal_format=None, compressed: bool = False, compressed_data: bool = False, ): diff --git a/arcade/gl/types.py b/arcade/gl/types.py index ad7ccd70b..ec63583b3 100644 --- a/arcade/gl/types.py +++ b/arcade/gl/types.py @@ -3,8 +3,8 @@ from arcade.types import BufferProtocol -from .buffer import Buffer from . import enums +from .buffer import Buffer BufferOrBufferProtocol = Union[BufferProtocol, Buffer] @@ -106,16 +106,16 @@ SHADER_TYPE_NAMES = { enums.VERTEX_SHADER: "vertex shader", enums.FRAGMENT_SHADER: "fragment shader", - enums.GEOMETRY_SHADER: "geometry shader", # Not supported in WebGL - enums.TESS_CONTROL_SHADER: "tessellation control shader", # Not supported in WebGL - enums.TESS_EVALUATION_SHADER: "tessellation evaluation shader", # Not supported in WebGL + enums.GEOMETRY_SHADER: "geometry shader", # Not supported in WebGL + enums.TESS_CONTROL_SHADER: "tessellation control shader", # Not supported in WebGL + enums.TESS_EVALUATION_SHADER: "tessellation evaluation shader", # Not supported in WebGL } #: Lookup table for OpenGL type names GL_NAMES = { enums.HALF_FLOAT: "GL_HALF_FLOAT", enums.FLOAT: "GL_FLOAT", - enums.DOUBLE: "GL_DOUBLE", # Double not supported in WebGL + enums.DOUBLE: "GL_DOUBLE", # Double not supported in WebGL enums.INT: "GL_INT", enums.UNSIGNED_INT: "GL_UNSIGNED_INT", enums.SHORT: "GL_SHORT", @@ -241,7 +241,7 @@ class BufferDescription: "f1": (enums.UNSIGNED_BYTE, 1), "f2": (enums.HALF_FLOAT, 2), "f4": (enums.FLOAT, 4), - "f8": (enums.DOUBLE, 8), # Double unsupported by WebGL + "f8": (enums.DOUBLE, 8), # Double unsupported by WebGL # Unsigned integers "u": (enums.UNSIGNED_INT, 4), "u1": (enums.UNSIGNED_BYTE, 1), @@ -388,9 +388,7 @@ class TypeInfo: __slots__ = "name", "enum", "gl_type", "gl_size", "components" - def __init__( - self, name: str, enum, gl_type, gl_size: int, components: int - ): + def __init__(self, name: str, enum, gl_type, gl_size: int, components: int): self.name = name """The string representation of this type""" self.enum = enum diff --git a/arcade/gl/vertex_array.py b/arcade/gl/vertex_array.py index e3190e3fc..ca65d11f6 100644 --- a/arcade/gl/vertex_array.py +++ b/arcade/gl/vertex_array.py @@ -1,9 +1,8 @@ from __future__ import annotations import weakref -from typing import TYPE_CHECKING, Sequence - from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Sequence from .buffer import Buffer from .program import Program @@ -11,6 +10,7 @@ if TYPE_CHECKING: from arcade.gl import Context + class VertexArray(ABC): """ Wrapper for Vertex Array Objects (VAOs). @@ -47,7 +47,7 @@ def __init__( self, ctx: Context, program: Program, - content: Sequence, # TODO: typing, this should be Sequence[BufferDescription] need generic BufferDescription though + content: Sequence, # TODO: typing, this should be Sequence[BufferDescription] need generic BufferDescription though index_buffer: Buffer | None = None, index_element_size: int = 4, ) -> None: @@ -92,7 +92,11 @@ def delete(self) -> None: @abstractmethod def render( - self, mode: int, first: int = 0, vertices: int = 0, instances: int = 1 # TODO: typing, technically mode can also be a ctypes uint in GL backend + self, + mode: int, + first: int = 0, + vertices: int = 0, + instances: int = 1, # TODO: typing, technically mode can also be a ctypes uint in GL backend ) -> None: """ Render the VertexArray to the currently active framebuffer. @@ -110,7 +114,9 @@ def render( raise NotImplementedError("The enabled graphics backend does not support this method.") @abstractmethod - def render_indirect(self, buffer: Buffer, mode: int, count, first, stride) -> None: # TODO: typing, technically mode can also be a ctypes uint in GL backend + def render_indirect( + self, buffer: Buffer, mode: int, count, first, stride + ) -> None: # TODO: typing, technically mode can also be a ctypes uint in GL backend """ Render the VertexArray to the framebuffer using indirect rendering. @@ -135,8 +141,8 @@ def render_indirect(self, buffer: Buffer, mode: int, count, first, stride) -> No def transform_interleaved( self, buffer: Buffer, - mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet - output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet first: int = 0, vertices: int = 0, instances: int = 1, @@ -194,6 +200,7 @@ def transform_separate( """ raise NotImplementedError("The enabled graphics backend does not support this method.") + class Geometry(ABC): """A higher level abstraction of the VertexArray. @@ -234,7 +241,7 @@ class Geometry(ABC): def __init__( self, ctx: Context, - content: Sequence | None, # TODO: typing, this should be Sequence[BufferDescription] + content: Sequence | None, # TODO: typing, this should be Sequence[BufferDescription] index_buffer: Buffer | None = None, mode: int | None = None, index_element_size: int = 4, @@ -290,7 +297,7 @@ def num_vertices(self) -> int: def num_vertices(self, value: int): self._num_vertices = value - def append_buffer_description(self, descr): # TODO: typing, descr should be BufferDescription + def append_buffer_description(self, descr): # TODO: typing, descr should be BufferDescription """ Append a new BufferDescription to the existing Geometry. @@ -318,7 +325,7 @@ def render( self, program: Program, *, - mode = None, # TODO: typing, mode should be GLenumLike | None + mode=None, # TODO: typing, mode should be GLenumLike | None first: int = 0, vertices: int | None = None, instances: int = 1, @@ -396,7 +403,7 @@ def render_indirect( program: Program, buffer: Buffer, *, - mode = None, # TODO: typing, mode should be GLuintLike | None + mode=None, # TODO: typing, mode should be GLuintLike | None count: int = -1, first: int = 0, stride: int = 0, diff --git a/arcade/utils.py b/arcade/utils.py index 141d4f6e1..797d9f1f7 100644 --- a/arcade/utils.py +++ b/arcade/utils.py @@ -255,13 +255,16 @@ def __deepcopy__(self, memo): # noqa return decorated_type + def is_pyodide() -> bool: return False + def is_raspberry_pi() -> bool: """Determine if the host is a raspberry pi.""" return get_raspberry_pi_info()[0] + def get_raspberry_pi_info() -> tuple[bool, str, str]: """ Determine if the host is a raspberry pi with additional info. From ef788f8bf3ed21b1dc5478adf32d2af55032de09 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:53:52 -0400 Subject: [PATCH 15/16] Fix some linting problems with getter/setters --- arcade/gl/backends/gl/sampler.py | 17 ----------------- arcade/gl/framebuffer.py | 8 ++++---- 2 files changed, 4 insertions(+), 21 deletions(-) diff --git a/arcade/gl/backends/gl/sampler.py b/arcade/gl/backends/gl/sampler.py index f21c9b2a8..76962aae5 100644 --- a/arcade/gl/backends/gl/sampler.py +++ b/arcade/gl/backends/gl/sampler.py @@ -98,23 +98,6 @@ def anisotropy(self, value): self._anisotropy = max(1.0, min(value, self._ctx.info.MAX_TEXTURE_MAX_ANISOTROPY)) gl.glSamplerParameterf(self._glo, gl.GL_TEXTURE_MAX_ANISOTROPY, self._anisotropy) - @property - def compare_func(self) -> str | None: - """ - Get or set the compare function for a depth texture:: - - texture.compare_func = None # Disable depth comparison completely - texture.compare_func = '<=' # GL_LEQUAL - texture.compare_func = '<' # GL_LESS - texture.compare_func = '>=' # GL_GEQUAL - texture.compare_func = '>' # GL_GREATER - texture.compare_func = '==' # GL_EQUAL - texture.compare_func = '!=' # GL_NOTEQUAL - texture.compare_func = '0' # GL_NEVER - texture.compare_func = '1' # GL_ALWAYS - """ - return self._compare_func - @Sampler.compare_func.setter def compare_func(self, value: str | None): if not self.texture._depth: diff --git a/arcade/gl/framebuffer.py b/arcade/gl/framebuffer.py index cc4f5143e..65d109161 100644 --- a/arcade/gl/framebuffer.py +++ b/arcade/gl/framebuffer.py @@ -382,7 +382,7 @@ def _get_framebuffer_size(self) -> tuple[int, int]: """Get the framebuffer size of the window""" return self._ctx.window.get_framebuffer_size() - @Framebuffer.viewport.getter + @property def viewport(self) -> tuple[int, int, int, int]: """ Get or set the framebuffer's viewport. @@ -406,12 +406,12 @@ def viewport(self) -> tuple[int, int, int, int]: int(self._viewport[3] / ratio), ) - @Framebuffer.viewport.setter + @viewport.setter @abstractmethod def viewport(self, value: tuple[int, int, int, int]): raise NotImplementedError("The enabled graphics backend does not support this method.") - @Framebuffer.scissor.getter + @property def scissor(self) -> tuple[int, int, int, int] | None: """ Get or set the scissor box for this framebuffer. @@ -438,7 +438,7 @@ def scissor(self) -> tuple[int, int, int, int] | None: int(self._scissor[3] / ratio), ) - @Framebuffer.scissor.setter + @scissor.setter @abstractmethod def scissor(self, value): raise NotImplementedError("The enabled graphics backend does not support this method.") From 7fb6de6327a3a86ab7b0e6a1e2889575825ec902 Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 4 May 2025 13:58:05 -0400 Subject: [PATCH 16/16] line length linting --- arcade/gl/backends/gl/context.py | 3 ++- arcade/gl/enums.py | 5 ++++- arcade/gl/program.py | 2 +- arcade/gl/sampler.py | 7 ++++--- arcade/gl/vertex_array.py | 14 +++++++------- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/arcade/gl/backends/gl/context.py b/arcade/gl/backends/gl/context.py index 7ee68b926..850ad37fb 100644 --- a/arcade/gl/backends/gl/context.py +++ b/arcade/gl/backends/gl/context.py @@ -36,7 +36,8 @@ def __init__( if gl_api not in self._valid_apis: if gl_api == "webgl": raise ValueError( - f"Tried to create a GLContext with webgl api selected. Valid options for this backend are: {self._valid_apis}" + "Tried to create a GLContext with webgl api selected. " + + f"Valid options for this backend are: {self._valid_apis}" ) raise ValueError(f"Invalid gl_api. Options are: {self._valid_apis}") self.gl_api = gl_api diff --git a/arcade/gl/enums.py b/arcade/gl/enums.py index 01f7637ac..4b410dd7d 100644 --- a/arcade/gl/enums.py +++ b/arcade/gl/enums.py @@ -52,7 +52,10 @@ MAX_VERTEX_UNIFORM_BLOCKS = 0x8A2B MAX_VERTEX_OUTPUT_COMPONENTS = 0x9122 MAX_TEXTURE_IMAGE_UNITS = 0x8872 -MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF # Technically comes from EXT_texture_filter_anisotropic in WebGL, but it's widely available + +# Technically comes from EXT_texture_filter_anisotropic in WebGL, but it's widely available +MAX_TEXTURE_MAX_ANISOTROPY = 0x84FF + MAX_VIEWPORT_DIMS = 0x0D3A MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS = 0x8C8B diff --git a/arcade/gl/program.py b/arcade/gl/program.py index ecae23d85..9a1d69073 100644 --- a/arcade/gl/program.py +++ b/arcade/gl/program.py @@ -147,7 +147,7 @@ def delete(self): @abstractmethod def __getitem__( self, item - ): # TODO: typing, this should return Uniform | UniformBlock, but need generic options for those: + ): # TODO: typing, this should return Uniform | UniformBlock """Get a uniform or uniform block""" raise NotImplementedError("The enabled graphics backend does not support this method.") diff --git a/arcade/gl/sampler.py b/arcade/gl/sampler.py index 96dc1429a..d0ee1b76f 100644 --- a/arcade/gl/sampler.py +++ b/arcade/gl/sampler.py @@ -26,9 +26,10 @@ def __init__( ): self._ctx = ctx - # These three ultimately need to be set by the implementing backend, but we're creating them here first - # to trick some of the methods on the base class to being able to see them. - # So that we don't have to implement a getter on every backend + # These three ultimately need to be set by the implementing backend. + # We're creating them here first to trick some of the methods on the + # base class to being able to see them. So that we don't have to + # implement a getter on every backend self._filter = None self._wrap_x = None self._wrap_y = None diff --git a/arcade/gl/vertex_array.py b/arcade/gl/vertex_array.py index ca65d11f6..5a8a764c9 100644 --- a/arcade/gl/vertex_array.py +++ b/arcade/gl/vertex_array.py @@ -47,7 +47,7 @@ def __init__( self, ctx: Context, program: Program, - content: Sequence, # TODO: typing, this should be Sequence[BufferDescription] need generic BufferDescription though + content: Sequence, # TODO: typing, this should be Sequence[BufferDescription] index_buffer: Buffer | None = None, index_element_size: int = 4, ) -> None: @@ -96,7 +96,7 @@ def render( mode: int, first: int = 0, vertices: int = 0, - instances: int = 1, # TODO: typing, technically mode can also be a ctypes uint in GL backend + instances: int = 1, # TODO: typing, mode can also be a ctypes uint in GL backend ) -> None: """ Render the VertexArray to the currently active framebuffer. @@ -116,7 +116,7 @@ def render( @abstractmethod def render_indirect( self, buffer: Buffer, mode: int, count, first, stride - ) -> None: # TODO: typing, technically mode can also be a ctypes uint in GL backend + ) -> None: # TODO: typing, mode can also be a ctypes uint in GL backend """ Render the VertexArray to the framebuffer using indirect rendering. @@ -141,8 +141,8 @@ def render_indirect( def transform_interleaved( self, buffer: Buffer, - mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet - output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + mode, # TODO, typing. This should be GLenumLike type + output_mode, # TODO, typing. This should be GLenumLike type first: int = 0, vertices: int = 0, instances: int = 1, @@ -172,8 +172,8 @@ def transform_interleaved( def transform_separate( self, buffers: list[Buffer], - mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet - output_mode, # TODO, typing. This should be GLenumLike type but idk how to handle that generically yet + mode, # TODO, typing. This should be GLenumLike type + output_mode, # TODO, typing. This should be GLenumLike type first: int = 0, vertices: int = 0, instances: int = 1,