diff --git a/arcade/gl/backends/gl/__init__.py b/arcade/gl/backends/gl/__init__.py new file mode 100644 index 0000000000..9886b9e9e6 --- /dev/null +++ b/arcade/gl/backends/gl/__init__.py @@ -0,0 +1,2 @@ +from .context import GLContext +from .buffer import GLBuffer \ No newline at end of file diff --git a/arcade/gl/backends/gl/buffer.py b/arcade/gl/backends/gl/buffer.py new file mode 100644 index 0000000000..192ba4f3f3 --- /dev/null +++ b/arcade/gl/backends/gl/buffer.py @@ -0,0 +1,302 @@ +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.new import Buffer, Context + +from .utils import data_to_ctypes + +if TYPE_CHECKING: + from arcade.gl.backends.gl import GLContext + + +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__ = "_ctx", "_glo", "_size", "_usage", "__weakref__" + _usages = { + "static": gl.GL_STATIC_DRAW, + "dynamic": gl.GL_DYNAMIC_DRAW, + "stream": gl.GL_STREAM_DRAW, + } + + def __init__( + self, + ctx: Context, + data: BufferProtocol | None = None, + reserve: int = 0, + usage: str = "static", + ) -> None: + assert isinstance(ctx, GLContext) + self._ctx = ctx + self._glo = glo = gl.GLuint() + self._size = -1 + self._usage = GLBuffer._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, GLBuffer.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.""" + return self._size + + @property + def ctx(self) -> GLContext: + """The context this resource belongs to.""" + return self._ctx + + @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: GLContext, 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) -> None: + """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 + """ + assert isinstance(source, GLBuffer) + # 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: int = 0, offset: int = 0, size: int = -1) -> None: + """ + 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 0000000000..44e83f2090 --- /dev/null +++ b/arcade/gl/backends/gl/context.py @@ -0,0 +1,1678 @@ +from __future__ import annotations + +import logging +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, + Dict, + Iterable, + List, + Literal, + Sequence, + Set, + Tuple, + overload, +) + +import pyglet +import pyglet.gl.lib +from pyglet import gl +from pyglet.window import Window + +from arcade.types import BufferProtocol +from arcade.gl.new import Buffer, Context, Geometry, Program + +from .buffer import GLBuffer +from .compute_shader import ComputeShader +from .framebuffer import DefaultFrameBuffer, Framebuffer +from .glsl import ShaderSource +from .program import GLProgram +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 GLGeometry + +LOG = logging.getLogger(__name__) + + +class GLContext(Context): + """ + Represents an OpenGL context. This context belongs to a pyglet window. + normally accessed through ``window.ctx``. + + The Context class contains methods for creating resources, + global states and commonly used enums. All enums also exist + in the ``gl`` module. (``ctx.BLEND`` or ``arcade.gl.BLEND``). + + Args: + window: The pyglet window this context belongs to + gc_mode: The garbage collection mode. Default is "context_gc" + gl_api: The OpenGL api. Default is "gl" + """ + + active: GLContext | 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 + """Texture interpolation - Nearest pixel""" + + LINEAR = 0x2601 + """Texture interpolation - Linear interpolate""" + + NEAREST_MIPMAP_NEAREST = 0x2700 + """Texture interpolation - Minification filter for mipmaps""" + + LINEAR_MIPMAP_NEAREST = 0x2701 + """Texture interpolation - Minification filter for mipmaps""" + + NEAREST_MIPMAP_LINEAR = 0x2702 + """Texture interpolation - Minification filter for mipmaps""" + + LINEAR_MIPMAP_LINEAR = 0x2703 + """Texture interpolation - Minification filter for mipmaps""" + + REPEAT = gl.GL_REPEAT + """Texture wrap mode - Repeat""" + + CLAMP_TO_EDGE = gl.GL_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 + """Texture wrap mode - Repeat mirrored""" + + # Flags + + BLEND = gl.GL_BLEND + """Context flag - Blending""" + + DEPTH_TEST = gl.GL_DEPTH_TEST + """Context flag - Depth testing""" + + CULL_FACE = gl.GL_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 + """Blend function""" + + ONE = 0x0001 + """Blend function""" + + SRC_COLOR = 0x0300 + """Blend function""" + + ONE_MINUS_SRC_COLOR = 0x0301 + """Blend function""" + + SRC_ALPHA = 0x0302 + """Blend function""" + + ONE_MINUS_SRC_ALPHA = 0x0303 + """Blend function""" + + DST_ALPHA = 0x0304 + """Blend function""" + + ONE_MINUS_DST_ALPHA = 0x0305 + """Blend function""" + + DST_COLOR = 0x0306 + """Blend function""" + + ONE_MINUS_DST_COLOR = 0x0307 + """Blend function""" + + # Blend equations + FUNC_ADD = 0x8006 + """Blend equation - source + destination""" + + FUNC_SUBTRACT = 0x800A + """Blend equation - source - destination""" + + FUNC_REVERSE_SUBTRACT = 0x800B + """Blend equation - destination - source""" + + MIN = 0x8007 + """Blend equation - Minimum of source and destination""" + + MAX = 0x8008 + """Blend equation - Maximum of source and destination""" + + # Blend mode shortcuts + BLEND_DEFAULT = 0x0302, 0x0303 + """Blend mode shortcut for default blend mode - ``SRC_ALPHA, ONE_MINUS_SRC_ALPHA``""" + + BLEND_ADDITIVE = 0x0001, 0x0001 + """Blend mode shortcut for additive blending - ``ONE, ONE``""" + + BLEND_PREMULTIPLIED_ALPHA = 0x0302, 0x0001 + """Blend mode shortcut for pre-multiplied alpha - ``SRC_ALPHA, ONE``""" + + # VertexArray: Primitives + POINTS = gl.GL_POINTS # 0 + """Primitive mode - points""" + + LINES = gl.GL_LINES # 1 + """Primitive mode - lines""" + + LINE_LOOP = gl.GL_LINE_LOOP # 2 + """Primitive mode - line loop""" + + LINE_STRIP = gl.GL_LINE_STRIP # 3 + """Primitive mode - line strip""" + + TRIANGLES = gl.GL_TRIANGLES # 4 + """Primitive mode - triangles""" + + TRIANGLE_STRIP = gl.GL_TRIANGLE_STRIP # 5 + """Primitive mode - triangle strip""" + + TRIANGLE_FAN = gl.GL_TRIANGLE_FAN # 6 + """Primitive mode - triangle fan""" + + LINES_ADJACENCY = gl.GL_LINES_ADJACENCY # 10 + """Primitive mode - lines with adjacency""" + + LINE_STRIP_ADJACENCY = gl.GL_LINE_STRIP_ADJACENCY # 11 + """Primitive mode - line strip with adjacency""" + + TRIANGLES_ADJACENCY = gl.GL_TRIANGLES_ADJACENCY # 12 + """Primitive mode - triangles with adjacency""" + + TRIANGLE_STRIP_ADJACENCY = gl.GL_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", + } + _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) + GLContext.activate(self) + # Texture unit we use when doing operations on textures to avoid + # affecting currently bound textures in the first units + self.default_texture_unit: int = self._info.MAX_TEXTURE_IMAGE_UNITS - 1 + + # Detect the default framebuffer + self._screen = DefaultFrameBuffer(self) + # Tracking active program + self.active_program: GLProgram | ComputeShader | None = None + # Tracking active framebuffer. On context creation the window is the default render target + 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 + self._flags: Set[int] = set() + 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, + } + self._cull_face_options_reverse = { + gl.GL_FRONT: "front", + gl.GL_BACK: "back", + gl.GL_FRONT_AND_BACK: "front_and_back", + } + + # Context GC as default. We need to call Context.gc() to free opengl resources + self._gc_mode = "context_gc" + self.gc_mode = gc_mode + #: Collected objects to gc when gc_mode is "context_gc". + #: This can be used during debugging. + self.objects: Deque[Any] = deque() + + @property + def info(self) -> GLInfo: + """ + Get the info object for this context containing information + about hardware/driver limits and other information. + + Example:: + + >> ctx.info.MAX_TEXTURE_SIZE + (16384, 16384) + >> ctx.info.VENDOR + NVIDIA Corporation + >> ctx.info.RENDERER + NVIDIA GeForce RTX 2080 SUPER/PCIe/SSE2 + """ + return self._info + + @property + def extensions(self) -> set[str]: + """ + Get a set of supported OpenGL extensions strings for this context. + + This can be used to check if a specific extension is supported:: + + # Check if bindless textures are supported + "GL_ARB_bindless_texture" in ctx.extensions + # Check for multiple extensions + expected_extensions = {"GL_ARB_bindless_texture", "GL_ARB_get_program_binary"} + ctx.extensions & expected_extensions == expected_extensions + """ + return gl.gl_info.get_extensions() + + @property + def stats(self) -> ContextStats: + """ + Get the stats instance containing runtime information + about creation and destruction of OpenGL objects. + + This can be useful for debugging and profiling. + Creating and throwing away OpenGL objects can be detrimental + to performance. + + Example:: + + # Show the created and freed resource count + >> ctx.stats.texture + (100, 10) + >> ctx.framebuffer + (1, 0) + >> ctx.buffer + (10, 0) + """ + return self._stats + + @property + def window(self) -> Window: + """The window this context belongs to (read only).""" + window_ref = self._window_ref() + if window_ref is None: + raise Exception("Window not available, lost reference.") + return window_ref + + @property + def screen(self) -> Framebuffer: + """The framebuffer for the window (read only)""" + return self._screen + + @property + def fbo(self) -> Framebuffer: + """ + Get the currently active framebuffer (read only). + """ + 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. + This is only needed when ``gc_mode`` is ``context_gc``. + + Returns: + The number of resources destroyed + """ + # Loop the array until all objects are gone. + # Deleting one object might add new ones so we need + # to loop until the deque is empty + num_objects = 0 + + while len(self.objects): + obj = self.objects.popleft() + obj.delete() + num_objects += 1 + + return num_objects + + @property + def gc_mode(self) -> str: + """ + Set the garbage collection mode for OpenGL resources. + Supported modes are:: + + # Default: + # Defer garbage collection until ctx.gc() is called + # This can be useful to enforce the main thread to + # run garbage collection of opengl resources + ctx.gc_mode = "context_gc" + + # Auto collect is similar to python garbage collection. + # This is a risky mode. Know what you are doing before using this. + ctx.gc_mode = "auto" + """ + return self._gc_mode + + @gc_mode.setter + def gc_mode(self, value: str): + modes = ["auto", "context_gc"] + if value not in modes: + raise ValueError("Unsupported gc_mode. Supported modes are:", modes) + self._gc_mode = value + + @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 == gl.GL_NO_ERROR: + return None + + return self._errors.get(err, "GL_UNKNOWN_ERROR") + + @classmethod + def activate(cls, ctx: Context): + """ + Mark a context as the currently active one. + + .. Warning:: Never call this unless you know exactly what you are doing. + + Args: + ctx: The context to activate + """ + cls.active = ctx + + def enable(self, *flags: int): + """ + Enables one or more context flags:: + + # Single flag + ctx.enable(ctx.BLEND) + # Multiple flags + ctx.enable(ctx.DEPTH_TEST, ctx.CULL_FACE) + + Args: + *flags: The flags to enable + """ + self._flags.update(flags) + + for flag in flags: + gl.glEnable(flag) + + def enable_only(self, *args: int): + """ + Enable only some flags. This will disable all other flags. + This is a simple way to ensure that context flag states + are not lingering from other sections of your code base:: + + # Ensure all flags are disabled (enable no flags) + ctx.enable_only() + # Make sure only blending is enabled + ctx.enable_only(ctx.BLEND) + # Make sure only depth test and culling is enabled + ctx.enable_only(ctx.DEPTH_TEST, ctx.CULL_FACE) + + 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) + + @contextmanager + def enabled(self, *flags): + """ + Temporarily change enabled flags. + + Flags that was enabled initially will stay enabled. + Only new enabled flags will be reversed when exiting + the context. + + Example:: + + with ctx.enabled(ctx.BLEND, ctx.CULL_FACE): + # Render something + """ + flags = set(flags) + new_flags = flags - self._flags + + self.enable(*flags) + try: + yield + finally: + self.disable(*new_flags) + + @contextmanager + def enabled_only(self, *flags): + """ + Temporarily change enabled flags. + + Only the supplied flags with be enabled in in the context. When exiting + the context the old flags will be restored. + + Example:: + + with ctx.enabled_only(ctx.BLEND, ctx.CULL_FACE): + # Render something + """ + old_flags = self._flags + self.enable_only(*flags) + try: + yield + finally: + self.enable_only(*old_flags) + + def disable(self, *args): + """ + Disable one or more context flags:: + + # Single flag + ctx.disable(ctx.BLEND) + # Multiple flags + ctx.disable(ctx.DEPTH_TEST, ctx.CULL_FACE) + """ + self._flags -= set(args) + + for flag in args: + gl.glDisable(flag) + + def is_enabled(self, flag) -> bool: + """ + Check if a context flag is enabled. + + .. Warning:: + + This only tracks states set through this context instance. + It does not query the actual OpenGL state. If you change context + flags by calling ``glEnable`` or ``glDisable`` directly you + are on your own. + """ + return flag in self._flags + + @property + def viewport(self) -> Tuple[int, int, int, int]: + """ + Get or set the viewport for the currently active framebuffer. + The viewport simply describes what pixels of the screen + OpenGL should render to. Normally it would be the size of + the window's framebuffer:: + + # 4:3 screen + ctx.viewport = 0, 0, 800, 600 + # 1080p + ctx.viewport = 0, 0, 1920, 1080 + # Using the current framebuffer size + ctx.viewport = 0, 0, *ctx.screen.size + """ + return self.active_framebuffer.viewport + + @viewport.setter + def viewport(self, value: Tuple[int, int, int, int]): + self.active_framebuffer.viewport = value + + @property + def scissor(self) -> Tuple[int, int, int, int] | None: + """ + Get or set the scissor box for the active framebuffer. + This is a shortcut for :py:meth:`~arcade.gl.Framebuffer.scissor`. + + By default the scissor box is disabled and has no effect + and will have an initial value of ``None``. The scissor + box is enabled when setting a value and disabled when + set to ``None``. + + Example:: + + # Set and enable scissor box only drawing + # in a 100 x 100 pixel lower left area + ctx.scissor = 0, 0, 100, 100 + # Disable scissoring + ctx.scissor = None + """ + return self.fbo.scissor + + @scissor.setter + def scissor(self, value): + self.fbo.scissor = value + + @property + def blend_func(self) -> Tuple[int, int] | Tuple[int, int, int, int]: + """ + Get or set the blend function. + This is tuple specifying how the color and + alpha blending factors are computed for the source + and destination pixel. + + When using a two component tuple you specify the + blend function for the source and the destination. + + When using a four component tuple you specify the + blend function for the source color, source alpha + destination color and destination alpha. (separate blend + functions for color and alpha) + + Supported blend functions are:: + + ZERO + ONE + SRC_COLOR + ONE_MINUS_SRC_COLOR + DST_COLOR + ONE_MINUS_DST_COLOR + SRC_ALPHA + ONE_MINUS_SRC_ALPHA + DST_ALPHA + ONE_MINUS_DST_ALPHA + + # Shortcuts + DEFAULT_BLENDING # (SRC_ALPHA, ONE_MINUS_SRC_ALPHA) + ADDITIVE_BLENDING # (ONE, ONE) + PREMULTIPLIED_ALPHA # (SRC_ALPHA, ONE) + + These enums can be accessed in the ``arcade.gl`` + module or simply as attributes of the context object. + The raw enums from ``pyglet.gl`` can also be used. + + Example:: + + # Using constants from the context object + ctx.blend_func = ctx.ONE, ctx.ONE + # from the gl module + from arcade import gl + ctx.blend_func = gl.ONE, gl.ONE + """ + return self._blend_func + + @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") + + # def blend_equation(self) + # Default is FUNC_ADD + + @property + def front_face(self) -> str: + """ + Configure front face winding order of triangles. + + By default the counter-clockwise winding side is the front face. + This can be set set to clockwise or counter-clockwise:: + + 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" + + @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: + """ + The face side to cull when face culling is enabled. + + By default the back face is culled. This can be set to + front, back or front_and_back:: + + ctx.cull_face = "front" + 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] + + @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]) + + @property + def wireframe(self) -> bool: + """ + Get or set the wireframe mode. + + When enabled all primitives will be rendered as lines + by changing the polygon mode. + """ + return self._wireframe + + @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: + """ + Get or set number of vertices that will be used to make up a single patch primitive. + + 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 + + @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) + + @property + def point_size(self) -> float: + """ + Set or get the point size. Default is `1.0`. + + Point size changes the pixel size of rendered points. The min and max values + are limited by :py:attr:`~arcade.gl.context.Limits.POINT_SIZE_RANGE`. + This value usually at least ``(1, 100)``, but this depends on the drivers/vendors. + + If variable point size is needed you can enable + :py:attr:`~arcade.gl.Context.PROGRAM_POINT_SIZE` and write to ``gl_PointSize`` + in the vertex or geometry shader. + + .. Note:: + + Using a geometry shader to create triangle strips from points is often a safer + way to render large points since you don't have have any size restrictions + and it offers more flexibility. + """ + return self._point_size + + @point_size.setter + def point_size(self, value: float): + if self.gl_api == "gl": + gl.glPointSize(self._point_size) + self._point_size = value + + @property + def primitive_restart_index(self) -> int: + """ + Get or set the primitive restart index. Default is ``-1``. + + The primitive restart index can be used in index buffers + to restart a primitive. This is for example useful when you + use triangle strips or line strips and want to start on + a new strip in the same buffer / draw call. + """ + return self._primitive_restart_index + + @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: + """ + Wait until all OpenGL rendering commands are completed. + + This function will actually stall until all work is done + and may have severe performance implications. + """ + gl.glFinish() + + def flush(self) -> None: + """ + Flush the OpenGL command buffer. + + This will send all queued commands to the GPU but will not wait + until they are completed. This is useful when you want to + ensure that all commands are sent to the GPU before doing + something else. + """ + gl.glFlush() + + # Various utility methods + + def copy_framebuffer( + self, + src: Framebuffer, + dst: Framebuffer, + src_attachment_index: int = 0, + depth: bool = True, + ): + """ + Copies/blits a framebuffer to another one. + We can select one color attachment to copy plus + an optional depth attachment. + + This operation has many restrictions to ensure it works across + different platforms and drivers: + + * The source and destination framebuffer must be the same size + * The formats of the attachments must be the same + * Only the source framebuffer can be multisampled + * Framebuffers cannot have integer attachments + + Args: + src: + The framebuffer to copy from + dst: + The framebuffer we copy to + src_attachment_index: + The color attachment to copy from + 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) + + # --- Resource methods --- + + def buffer(self, *args, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> Buffer: + """ + Create an OpenGL Buffer object. The buffer will contain all zero-bytes if + no data is supplied. + + Examples:: + + # Create 1024 byte buffer + ctx.buffer(reserve=1024) + # Create a buffer with 1000 float values using python's array.array + from array import array + ctx.buffer(data=array('f', [i for in in range(1000)]) + # Create a buffer with 1000 random 32 bit floats using numpy + self.ctx.buffer(data=np.random.random(1000).astype("f4")) + + 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. + + The ``usage`` parameter enables the GL implementation to make more intelligent + decisions that may impact buffer object performance. It does not add any restrictions. + If in doubt, skip this parameter and revisit when optimizing. The result + are likely to be different between vendors/drivers or may not have any effect. + Always use the default static usage for buffers that don't change. + + The available values mean the following:: + + stream + The data contents will be modified once and used at most a few times. + static + The data contents will be modified once and used many times. + dynamic + The data contents will be modified repeatedly and used many times. + + Args: + data: + The buffer data. This can be a ``bytes`` instance or any + any other object supporting the buffer protocol. + reserve: + The number of bytes to reserve + usage: + Buffer usage. 'static', 'dynamic' or 'stream' + """ + return GLBuffer(self, data, reserve=reserve, usage=usage) + + def framebuffer( + self, + *, + color_attachments: Texture2D | List[Texture2D] | None = None, + depth_attachment: Texture2D | None = None, + ) -> Framebuffer: + """Create a Framebuffer. + + Args: + color_attachments: + List of textures we want to render into + depth_attachment: + Depth texture + """ + return Framebuffer( + self, color_attachments=color_attachments or [], depth_attachment=depth_attachment + ) + + 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, + ) -> Texture2D: + """ + Create a 2D Texture. + + Example:: + + # Create a 1024 x 1024 RGBA texture + image = PIL.Image.open("my_texture.png") + ctx.texture(size=(1024, 1024), components=4, data=image.tobytes()) + + # Create and compress a texture. The compression format is set by the internal_format + image = PIL.Image.open("my_texture.png") + ctx.texture( + size=(1024, 1024), + components=4, + compressed=True, + internal_format=gl.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, + data=image.tobytes(), + ) + + # Create a compressed texture from raw compressed data. This is an extremely + # fast way to load a large number of textures. + image_bytes = "" + ctx.texture( + size=(1024, 1024), + components=4, + internal_format=gl.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, + compressed_data=True, + data=image_bytes, + ) + + Wrap modes: ``GL_REPEAT``, ``GL_MIRRORED_REPEAT``, ``GL_CLAMP_TO_EDGE``, + ``GL_CLAMP_TO_BORDER`` + + Minifying filters: ``GL_NEAREST``, ``GL_LINEAR``, ``GL_NEAREST_MIPMAP_NEAREST``, + ``GL_LINEAR_MIPMAP_NEAREST`` ``GL_NEAREST_MIPMAP_LINEAR``, ``GL_LINEAR_MIPMAP_LINEAR`` + + Magnifying filters: ``GL_NEAREST``, ``GL_LINEAR`` + + Args: + size: + The size of the texture + components: + 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. + wrap_x: + How the texture wraps in x direction + wrap_y: + How the texture wraps in y direction + filter: + Minification and magnification filter + samples: + Creates a multisampled texture for values > 0 + 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. This can be used to + enable sRGB or texture compression. + compressed: + Set to True if you want the texture to be compressed. + This assumes you have set a internal_format to a compressed format. + compressed_data: + 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, + ) + + 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, + ) -> TextureArray: + """ + Create a 2D Texture Array. + + This is a 2D texture with multiple layers. This is useful for + storing multiple textures in a single texture object. This can + be used for texture atlases or storing multiple frames of an + animation in a single texture or equally sized tile textures. + + Note that ``size`` is a 3-tuple where the last value is the number of layers. + + 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, + ) + + def depth_texture( + self, size: Tuple[int, int], *, data: BufferProtocol | None = None + ) -> Texture2D: + """ + Create a 2D depth texture. Can be used as a depth attachment + in a :py:class:`~arcade.gl.Framebuffer`. + + Args: + size: + The size of the texture + data: + The texture data. Can be``bytes`` or any object + supporting the buffer protocol. + """ + return Texture2D(self, size, data=data, depth=True) + + def sampler(self, texture: Texture2D) -> Sampler: + """ + Create a sampler object for a texture. + + Args: + texture: + The texture to create a sampler for + """ + return Sampler(self, texture) + + 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 + mainly responsible for automatically map buffer inputs to your shader(s) + and provide various methods for rendering or processing this geometry, + + The same geometry can be rendered with different + programs as long as your shader is using one or more of the input attribute. + This means geometry with positions and colors can be rendered with a program + only using the positions. We will automatically map what is necessary and + cache these mappings internally for performance. + + In short, the geometry object is a light object that describes what buffers + contains and automatically negotiate with shaders/programs. This is a very + complex field in OpenGL so the Geometry object provides substantial time + savings and greatly reduces the complexity of your code. + + Geometry also provide rendering methods supporting the following: + + * Rendering geometry with and without index buffer + * Rendering your geometry using instancing. Per instance buffers can be provided + or the current instance can be looked up using ``gl_InstanceID`` in shaders. + * Running transform feedback shaders that writes to buffers instead the screen. + This can write to one or multiple buffer. + * Render your geometry with indirect rendering. This means packing + multiple meshes into the same buffer(s) and batch drawing them. + + Examples:: + + # Single buffer geometry with a vec2 vertex position attribute + ctx.geometry([BufferDescription(buffer, '2f', ["in_vert"])], mode=ctx.TRIANGLES) + + # Single interleaved buffer with two attributes. A vec2 position and vec2 velocity + ctx.geometry([ + BufferDescription(buffer, '2f 2f', ["in_vert", "in_velocity"]) + ], + mode=ctx.POINTS, + ) + + # Geometry with index buffer + ctx.geometry( + [BufferDescription(buffer, '2f', ["in_vert"])], + index_buffer=ibo, + mode=ctx.TRIANGLES, + ) + + # Separate buffers + ctx.geometry([ + BufferDescription(buffer_pos, '2f', ["in_vert"]) + BufferDescription(buffer_vel, '2f', ["in_velocity"]) + ], + mode=ctx.POINTS, + ) + + # Providing per-instance data for instancing + ctx.geometry([ + BufferDescription(buffer_pos, '2f', ["in_vert"]) + BufferDescription(buffer_instance_pos, '2f', ["in_offset"], instanced=True) + ], + mode=ctx.POINTS, + ) + + Args: + content: + List of :py:class:`~arcade.gl.BufferDescription` + index_buffer: + Index/element buffer + mode: + The default draw mode + mode: + The default draw mode + index_element_size: + Byte size of a single index/element in the index buffer. + 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) + """ + assert isinstance(index_buffer, GLBuffer) + return GLGeometry( + self, + content, + index_buffer=index_buffer, + mode=mode, + index_element_size=index_element_size, + ) + + def program( + self, + *args, + vertex_shader: str | None = None, + 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", + ) -> Program: + """ + Create a :py:class:`~arcade.gl.Program` given shader sources + and other settings. + + Args: + 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 + common: + Common shader sources injected into all shaders + defines: + Substitute #defines values in the source + varyings: + The name of the out attributes in a transform shader. + This is normally not necessary since we auto detect them, + but some more complex out structures we can't detect. + 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. + """ + assert vertex_shader is not None + 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 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 query(self, *, samples=True, time=True, primitives=True) -> Query: + """ + Create a query object for measuring rendering calls in opengl. + + Args: + samples: Collect written samples + time: Measure rendering duration + primitives: Collect the number of primitives emitted + """ + return Query(self, samples=samples, time=time, primitives=primitives) + + def compute_shader(self, *, source: str, common: Iterable[str] = ()) -> ComputeShader: + """ + Create a compute shader. + + Args: + source: + The glsl source + common: + Common / library source injected into compute shader + """ + src = ShaderSource(self, source, common, gl.GL_COMPUTE_SHADER) + return ComputeShader(self, src.get_source()) + + +class ContextStats: + """ + Runtime allocation statistics of OpenGL objects. + """ + + def __init__(self, warn_threshold=100): + self.warn_threshold = warn_threshold + + self.texture = (0, 0) + """Textures (created, freed)""" + + self.framebuffer = (0, 0) + """Framebuffers (created, freed)""" + + self.buffer = (0, 0) + """Buffers (created, freed)""" + + self.program = (0, 0) + """Programs (created, freed)""" + + self.vertex_array = (0, 0) + """Vertex Arrays (created, freed)""" + + self.geometry = (0, 0) + """Geometry (created, freed)""" + + self.compute_shader = (0, 0) + """Compute Shaders (created, freed)""" + + self.query = (0, 0) + """Queries (created, freed)""" + + def incr(self, key: str) -> None: + """ + Increments a counter. + + Args: + key: The attribute name / counter to increment. + """ + created, freed = getattr(self, key) + setattr(self, key, (created + 1, freed)) + if created % self.warn_threshold == 0 and created > 0: + LOG.debug( + "%s allocations passed threshold (%s) [created = %s] [freed = %s] [active = %s]", + key, + self.warn_threshold, + created, + freed, + created - freed, + ) + + def decr(self, key): + """ + Decrement a counter. + + Args: + key: The attribute name / counter to decrement. + """ + created, freed = getattr(self, key) + setattr(self, key, (created, freed + 1)) + + +class GLInfo: + """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) + """The vendor string. For example 'NVIDIA Corporation'""" + + self.RENDERER = self.get_str(gl.GL_RENDERER) + """The renderer things. For example "NVIDIA GeForce RTX 2080 SUPER/PCIe/SSE2""" + + self.SAMPLE_BUFFERS = self.get(gl.GL_SAMPLE_BUFFERS) + """Value indicating the number of sample buffers associated with the framebuffer""" + + self.SUBPIXEL_BITS = self.get(gl.GL_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) + """Minimum required alignment for uniform buffer sizes and offset""" + + self.MAX_ARRAY_TEXTURE_LAYERS = self.get(gl.GL_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) + """ + 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) + """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_FRAGMENT_UNIFORM_COMPONENTS = self.get( + gl.GL_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) + """ + 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) + """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) + """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) + """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) + """Recommended maximum number of vertex array vertices""" + + self.MAX_FRAGMENT_INPUT_COMPONENTS = self.get(gl.GL_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) + """ + 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) + """ + 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) + """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) + """Maximum samples for a framebuffer""" + + self.MAX_RENDERBUFFER_SIZE = self.get(gl.GL_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) + """Maximum number of uniform buffer binding points on the context""" + + self.MAX_TEXTURE_SIZE = self.get(gl.GL_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) + """Maximum size in basic machine units of a uniform block""" + + self.MAX_VARYING_VECTORS = self.get(gl.GL_MAX_VARYING_VECTORS) + """The number 4-vectors for varying variables""" + + self.MAX_VERTEX_ATTRIBS = self.get(gl.GL_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) + """ + 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) + """ + 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) + """ + 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) + """Maximum number of components of output written by a vertex shader""" + + self.MAX_VERTEX_UNIFORM_BLOCKS = self.get(gl.GL_MAX_VERTEX_UNIFORM_BLOCKS) + """Maximum number of uniform blocks per vertex shader.""" + + # self.MAX_VERTEX_ATTRIB_RELATIVE_OFFSET = self.get( + # gl.GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET + # ) + # 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) + """Number of texture units""" + + self.MAX_TEXTURE_MAX_ANISOTROPY = self.get_float(gl.GL_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) + """ + 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 + ) + """ + 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]: ... + + @overload + def get_int_tuple(self, enum: GLenumLike, length: int) -> Tuple[int, ...]: ... + + def get_int_tuple(self, enum: GLenumLike, 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: GLenumLike, 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: GLenumLike, 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: GLenumLike) -> 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/program.py b/arcade/gl/backends/gl/program.py new file mode 100644 index 0000000000..7e1b004204 --- /dev/null +++ b/arcade/gl/backends/gl/program.py @@ -0,0 +1,554 @@ +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, cast + +from pyglet import gl + +from arcade.gl.exceptions import ShaderException + +from arcade.gl.new import Context, Program + +from .types import SHADER_TYPE_NAMES, AttribFormat, GLTypes, PyGLenum +from .uniform import Uniform, UniformBlock + +if TYPE_CHECKING: + from .context import GLContext + + +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__ = ( + "_ctx", + "_glo", + "_uniforms", + "_varyings", + "_varyings_capture_mode", + "_geometry_info", + "_attributes", + "attribute_key", + "__weakref__", + ) + + _valid_capture_modes = ("interleaved", "separate") + + def __init__( + self, + ctx: GLContext, + *, + 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 = 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) + + 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) + + @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 0000000000..cc263c09e8 --- /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 GLBuffer + +BufferOrBufferProtocol = Union[BufferProtocol, GLBuffer] + +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: GLBuffer, + 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: GLBuffer + #: 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, GLBuffer): + raise ValueError("buffer parameter must be an arcade.gl.backends.gl.GLBuffer") + + 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 0000000000..f2ce13f8dd --- /dev/null +++ b/arcade/gl/backends/gl/uniform.py @@ -0,0 +1,423 @@ +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 + +# TODO: does this need to publicly usable? Or does all uniform interaction happen via program/context? +# Basically, do we need a protocol for this in the abstract API or can this exist solely in implementations? +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/utils.py b/arcade/gl/backends/gl/utils.py new file mode 100644 index 0000000000..cf3249cb3f --- /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/backends/gl/vertex_array.py b/arcade/gl/backends/gl/vertex_array.py new file mode 100644 index 0000000000..c01e5bef78 --- /dev/null +++ b/arcade/gl/backends/gl/vertex_array.py @@ -0,0 +1,835 @@ +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.new import Context, Geometry, Program, VertexArray + +from .buffer import GLBuffer +from .program import GLProgram +from .types import BufferDescription, GLenumLike, GLuintLike, gl_name + +if TYPE_CHECKING: + from .context import GLContext + +# 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__ = ( + "_ctx", + "glo", + "_program", + "_content", + "_ibo", + "_index_element_size", + "_index_element_type", + "_num_vertices", + "__weakref__", + ) + + def __init__( + self, + ctx: GLContext, + program: GLProgram, + content: Sequence[BufferDescription], + index_buffer: GLBuffer | None = None, + index_element_size: int = 4, + ) -> None: + self._ctx = ctx + 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, GLVertexArray.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.""" + return self._ctx + + @property + def program(self) -> Program: + """The assigned program.""" + return self._program + + @property + def ibo(self) -> GLBuffer | None: + """Element/index buffer.""" + return self._ibo + + @property + def num_vertices(self) -> int: + """The number of vertices.""" + return self._num_vertices + + 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: GLContext, 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: GLProgram, content: Sequence[BufferDescription], index_buffer: GLBuffer | 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: GLBuffer, 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: GLBuffer, + 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[GLBuffer], + 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) + """ + + __slots__ = ( + "_ctx", + "_content", + "_index_buffer", + "_index_element_size", + "_mode", + "_vao_cache", + "_num_vertices", + "__weakref__", + ) + + def __init__( + self, + ctx: GLContext, + content: Sequence[BufferDescription] | None, + index_buffer: GLBuffer | None = None, + mode: int | None = None, + index_element_size: int = 4, + ) -> None: + self._ctx = ctx + self._content = list(content or []) + self._index_buffer = index_buffer + self._index_element_size = index_element_size + self._mode = mode if mode is not None else ctx.TRIANGLES + self._vao_cache: dict[str, GLVertexArray] = {} + self._num_vertices: int = -1 + + if self._index_buffer and self._index_element_size not in (1, 2, 4): + raise ValueError("index_element_size must be 1, 2, or 4") + + if content: + # Calculate vertices. Use the minimum for now + if self._index_buffer: + self._num_vertices = self._index_buffer.size // self._index_element_size + else: + self._num_vertices = content[0].num_vertices + for descr in self._content: + if descr.instanced: + continue + self._num_vertices = min(self._num_vertices, descr.num_vertices) + + # No cleanup is needed, but we want to count them + weakref.finalize(self, GLGeometry._release, self._ctx) + self._ctx.stats.incr("geometry") + + @property + def ctx(self) -> Context: + """The context this geometry belongs to.""" + return self._ctx + + @property + def index_buffer(self) -> GLBuffer | None: + """Index/element buffer if supplied at creation.""" + return self._index_buffer + + @property + def num_vertices(self) -> int: + """ + Get or set the number of vertices. + + Be careful when modifying this properly + and be absolutely sure what you are doing. + """ + # TODO: Calculate this better... + return self._num_vertices + + @num_vertices.setter + def num_vertices(self, value: int): + self._num_vertices = value + + def append_buffer_description(self, descr: BufferDescription): + """ + Append a new BufferDescription to the existing Geometry. + + .. Warning:: Geometry cannot contain two BufferDescriptions which share an attribute name. + """ + for other_descr in self._content: + if other_descr == descr: + raise ValueError( + "Geometry cannot contain two BufferDescriptions which share an " + f"attribute name. Found a conflict in {descr} and {other_descr}" + ) + self._content.append(descr) + + def instance(self, program: GLProgram) -> GLVertexArray: + """ + Get the :py:class:`arcade.gl.VertexArray` compatible with this program. + """ + vao = self._vao_cache.get(program.attribute_key) + if vao: + return vao + + return self._generate_vao(program) + + def render( + self, + program: GLProgram, + *, + mode: GLenumLike | None = None, + first: int = 0, + vertices: int | None = None, + instances: int = 1, + ) -> None: + """Render the geometry with a specific program. + + The geometry object will know how many vertices your buffers contains + so overriding vertices is not needed unless you have a special case + or have resized the buffers after the geometry instance was created. + + Args: + program: + The Program to render with + mode: + Override what primitive mode should be used + first: + Offset start vertex + vertices: + Override the number of vertices to render + instances: + Number of instances to render + """ + program.use() + vao = self.instance(program) + + mode = self._mode if mode is None else mode + + # If we have a geometry shader we need to sanity check that + # the primitive mode is supported + if program.geometry_vertices > 0: + if program.geometry_input == self._ctx.POINTS: + mode = program.geometry_input + if program.geometry_input == self._ctx.LINES: + if mode not in [ + self._ctx.LINES, + self._ctx.LINE_STRIP, + self._ctx.LINE_LOOP, + self._ctx.LINES_ADJACENCY, + ]: + raise ValueError( + "Geometry shader expects LINES, LINE_STRIP, LINE_LOOP " + " or LINES_ADJACENCY as input" + ) + if program.geometry_input == self._ctx.LINES_ADJACENCY: + if mode not in [self._ctx.LINES_ADJACENCY, self._ctx.LINE_STRIP_ADJACENCY]: + raise ValueError( + "Geometry shader expects LINES_ADJACENCY or LINE_STRIP_ADJACENCY as input" + ) + if program.geometry_input == self._ctx.TRIANGLES: + if mode not in [ + self._ctx.TRIANGLES, + self._ctx.TRIANGLE_STRIP, + self._ctx.TRIANGLE_FAN, + ]: + raise ValueError( + "Geometry shader expects GL_TRIANGLES, GL_TRIANGLE_STRIP " + "or GL_TRIANGLE_FAN as input" + ) + if program.geometry_input == self._ctx.TRIANGLES_ADJACENCY: + if mode not in [self._ctx.TRIANGLES_ADJACENCY, self._ctx.TRIANGLE_STRIP_ADJACENCY]: + raise ValueError( + "Geometry shader expects GL_TRIANGLES_ADJACENCY or " + "GL_TRIANGLE_STRIP_ADJACENCY as input" + ) + + vao.render( + mode=mode, + first=first, + vertices=vertices or self._num_vertices, + instances=instances, + ) + + def render_indirect( + self, + program: GLProgram, + buffer: GLBuffer, + *, + mode: GLuintLike | None = None, + count: int = -1, + first: int = 0, + stride: int = 0, + ) -> None: + """ + Render the VertexArray to the framebuffer using indirect rendering. + + .. Warning:: This requires OpenGL 4.3 + + The following structs are expected for the buffer:: + + // Array rendering - no index buffer (16 bytes) + typedef struct { + uint count; + uint instanceCount; + uint first; + uint baseInstance; + } DrawArraysIndirectCommand; + + // Index rendering - with index buffer 20 bytes + typedef struct { + GLuint count; + GLuint instanceCount; + GLuint firstIndex; + GLuint baseVertex; + GLuint baseInstance; + } DrawElementsIndirectCommand; + + The ``stride`` is the byte stride between every rendering command + in the buffer. By default we assume this is 16 for array rendering + (no index buffer) and 20 for indexed rendering (with index buffer) + + Args: + program: + The program to execute + 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. + If omitted all draw commands in the buffer will be executed. + 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. + """ + program.use() + vao = self.instance(program) + + mode = self._mode if mode is None else mode + vao.render_indirect(buffer, mode, count, first, stride) + + def transform( + self, + program: GLProgram, + buffer: GLBuffer | list[GLBuffer], + *, + first: int = 0, + vertices: int | None = None, + instances: int = 1, + buffer_offset: int = 0, + ) -> None: + """ + Render with transform feedback. Instead of rendering to the screen + or a framebuffer the result will instead end up in the ``buffer`` we supply. + + If a geometry shader is used the output primitive mode is automatically detected. + + Args: + program: + The Program to render with + buffer: + The buffer(s) we transform into. + This depends on the programs ``varyings_capture_mode``. We can transform + into one buffer interleaved or transform each attribute into separate buffers. + first: + Offset start vertex + vertices: + Number of vertices to render + instances: + Number of instances to render + buffer_offset: + Byte offset for the buffer + """ + program.use() + vao = self.instance(program) + if program._varyings_capture_mode == "interleaved": + if not isinstance(buffer, GLBuffer): + raise ValueError( + ( + "Buffer must be a single Buffer object " + "because the capture mode of the program is: " + f"{program.varyings_capture_mode}" + ) + ) + vao.transform_interleaved( + buffer, + mode=program.geometry_input, + output_mode=program.geometry_output, + first=first, + vertices=vertices or self._num_vertices, + instances=instances, + buffer_offset=buffer_offset, + ) + else: + if not isinstance(buffer, list): + raise ValueError( + ( + "buffer must be a list of Buffer object " + "because the capture mode of the program is: " + f"{program.varyings_capture_mode}" + ) + ) + vao.transform_separate( + buffer, + mode=program.geometry_input, + output_mode=program.geometry_output, + first=first, + vertices=vertices or self._num_vertices, + instances=instances, + buffer_offset=buffer_offset, + ) + + def flush(self) -> None: + """ + Flush all the internally generated VertexArrays. + + The Geometry instance will store a VertexArray + for every unique set of input attributes it + stumbles over when rendering and transform calls + are issued. This data is usually pretty light weight + and usually don't need flushing. + """ + self._vao_cache = {} + + def _generate_vao(self, program: GLProgram) -> GLVertexArray: + """ + 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 + + @staticmethod + def _release(ctx) -> None: + """Mainly here to count destroyed instances""" + ctx.stats.decr("geometry") diff --git a/arcade/gl/buffer.py b/arcade/gl/buffer.py index 7504bc46bd..172144464e 100644 --- a/arcade/gl/buffer.py +++ b/arcade/gl/buffer.py @@ -1,299 +1,21 @@ from __future__ import annotations -import weakref -from ctypes import byref, string_at -from typing import TYPE_CHECKING - -from pyglet import gl +from typing import Protocol, runtime_checkable, TYPE_CHECKING from arcade.types import BufferProtocol -from .utils import data_to_ctypes - if TYPE_CHECKING: - from arcade.gl import Context - - -class 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__ = "_ctx", "_glo", "_size", "_usage", "__weakref__" - _usages = { - "static": gl.GL_STATIC_DRAW, - "dynamic": gl.GL_DYNAMIC_DRAW, - "stream": gl.GL_STREAM_DRAW, - } - - def __init__( - self, - ctx: Context, - data: BufferProtocol | None = None, - reserve: int = 0, - 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.""" - return self._size - - @property - 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 - - def delete(self) -> None: - """ - Destroy the underlying OpenGL 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") - - 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) + from .context import Context + +@runtime_checkable +class Buffer(Protocol): + + def size(self) -> int: ... + def ctx(self) -> Context: ... + def delete(self) -> None: ... + def read(self, size: int = -1, offset: int = 0) -> bytes: ... + def write(self, data: BufferProtocol, offset: int = 0) -> None: ... + def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0) -> None: ... + def orphan(self, size: int = -1, double: bool = False) -> None: ... + def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1) -> None: ... + def bind_to_storage_buffer(self, *, binding=0, offset=0, size=-1) -> None: ... \ No newline at end of file diff --git a/arcade/gl/new/__init__.py b/arcade/gl/new/__init__.py new file mode 100644 index 0000000000..5f859eab7e --- /dev/null +++ b/arcade/gl/new/__init__.py @@ -0,0 +1,4 @@ +from .buffer import Buffer +from .context import Context +from .program import Program +from .vertex_array import Geometry, VertexArray \ No newline at end of file diff --git a/arcade/gl/new/buffer.py b/arcade/gl/new/buffer.py new file mode 100644 index 0000000000..24576a8d22 --- /dev/null +++ b/arcade/gl/new/buffer.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from typing import Protocol, runtime_checkable, TYPE_CHECKING + +from arcade.types import BufferProtocol + +if TYPE_CHECKING: + from .context import Context + +@runtime_checkable +class Buffer(Protocol): + + @property + def ctx(self) -> Context: ... + def delete(self) -> None: ... + + @property + def size(self) -> int: ... + + def read(self, size: int = -1, offset: int = 0) -> bytes: ... + def write(self, data: BufferProtocol, offset: int = 0) -> None: ... + def copy_from_buffer(self, source: Buffer, size=-1, offset=0, source_offset=0) -> None: ... + def orphan(self, size: int = -1, double: bool = False) -> None: ... + def bind_to_uniform_block(self, binding: int = 0, offset: int = 0, size: int = -1) -> None: ... + def bind_to_storage_buffer(self, binding: int = 0, offset: int = 0, size: int = -1) -> None: ... \ No newline at end of file diff --git a/arcade/gl/new/context.py b/arcade/gl/new/context.py new file mode 100644 index 0000000000..7a3a8ed99e --- /dev/null +++ b/arcade/gl/new/context.py @@ -0,0 +1,37 @@ +from typing import Protocol, runtime_checkable, Any, TypeVar, List, Dict, Sequence + +from .buffer import Buffer +from .program import Program +from .vertex_array import Geometry + +from arcade.types import BufferProtocol +from arcade.gl.backends.gl.types import BufferDescription + +@runtime_checkable +class Context(Protocol): + + def buffer(self, *args, data: BufferProtocol | None = None, reserve: int = 0, usage: str = "static") -> Buffer: ... + + def program( + self, + *args, + vertex_shader: str | None = None, + 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", + ) -> Program: ... + + def geometry( + self, + content: Sequence[BufferDescription] | None = None, # TODO: Hack for not having generic BufferDescription yet + index_buffer: Buffer | None = None, + mode: int | None = None, + index_element_size: int = 4, + ) -> Geometry: ... + +T = TypeVar("T", bound=Context) \ No newline at end of file diff --git a/arcade/gl/new/program.py b/arcade/gl/new/program.py new file mode 100644 index 0000000000..b4f92eb0bc --- /dev/null +++ b/arcade/gl/new/program.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from typing import Protocol, runtime_checkable, TYPE_CHECKING + +if TYPE_CHECKING: + from .context import Context + + +@runtime_checkable +class Program(Protocol): + + @property + def ctx(self) -> Context: ... diff --git a/arcade/gl/new/types.py b/arcade/gl/new/types.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/arcade/gl/new/vertex_array.py b/arcade/gl/new/vertex_array.py new file mode 100644 index 0000000000..88d0e80da1 --- /dev/null +++ b/arcade/gl/new/vertex_array.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Protocol, runtime_checkable, TYPE_CHECKING + +if TYPE_CHECKING: + from .context import Context + from .program import Program + +@runtime_checkable +class VertexArray(Protocol): + + def ctx(self) -> Context: ... + def program(self) -> Program: ... + + +@runtime_checkable +class Geometry(Protocol): + + def ctx(self) -> Context: ... \ No newline at end of file