Source code for libretro.drivers.video.opengl.moderngl

"""
:class:`.VideoDriver` implementation backed by :mod:`moderngl` and PyOpenGL.

.. seealso::

    :class:`.VideoDriver`
        The protocol this driver implements.
"""

# pyright: reportUnknownMemberType=false, reportMissingTypeStubs=false
# ModernGL's typeshed stubs are very incomplete,
# and PyOpenGL doesn't have any at all.
# These two warnings silence a lot of noise.

from __future__ import annotations

import struct
import warnings
from array import array
from collections.abc import Iterator, Sequence, Set
from copy import deepcopy
from importlib import resources
from sys import modules
from typing import TYPE_CHECKING, cast, final, override

import moderngl
from OpenGL import GL
from OpenGL.error import GLError

from libretro.api.proc import retro_proc_address_t

if TYPE_CHECKING:
    import moderngl_window
    from moderngl_window.context.base import BaseWindow
else:
    try:
        import moderngl_window
        from moderngl_window.context.base import BaseWindow

        # These features require moderngl_window,
        # but I don't want to require that it be installed
    except ImportError:
        moderngl_window = None
        BaseWindow = None

from moderngl import (
    Buffer,
    Context,
    Framebuffer,
    Renderbuffer,
    Texture,
    Uniform,
    VertexArray,
    create_context,
)

from libretro.api.av import retro_game_geometry, retro_system_av_info
from libretro.api.video import (
    HardwareContext,
    MemoryAccess,
    PixelFormat,
    Rotation,
    retro_framebuffer,
    retro_hw_render_callback,
    retro_hw_render_interface,
)

from ..driver import (
    FrameBufferSpecial,
    Screenshot,
    UnsupportedContextError,
    VideoDriver,
)

_CONTEXTS = frozenset((HardwareContext.NONE, HardwareContext.OPENGL_CORE, HardwareContext.OPENGL))

_DEFAULT_VERT_FILENAME = "moderngl_vertex.glsl"
_DEFAULT_FRAG_FILENAME = "moderngl_frag.glsl"

_vertex = struct.Struct("2f 2f")  # 4 floats (one vec2 for screen coords, one for vec2 coords)
_POSITION_NORTHWEST = (-1, 1)
_POSITION_NORTHEAST = (1, 1)
_POSITION_SOUTHWEST = (-1, -1)
_POSITION_SOUTHEAST = (1, -1)
_TEXCOORD_NORTHWEST = (0, 1)
_TEXCOORD_NORTHEAST = (1, 1)
_TEXCOORD_SOUTHWEST = (0, 0)
_TEXCOORD_SOUTHEAST = (1, 0)

_NORTHWEST = _vertex.pack(*_POSITION_NORTHWEST, *_TEXCOORD_NORTHWEST)
_NORTHEAST = _vertex.pack(*_POSITION_NORTHEAST, *_TEXCOORD_NORTHEAST)
_SOUTHWEST = _vertex.pack(*_POSITION_SOUTHWEST, *_TEXCOORD_SOUTHWEST)
_SOUTHEAST = _vertex.pack(*_POSITION_SOUTHEAST, *_TEXCOORD_SOUTHEAST)

_VERTEXES = b"".join((_NORTHWEST, _SOUTHWEST, _NORTHEAST, _SOUTHEAST))

_DEFAULT_WINDOW_IMPL = "pyglet"
_IDENTITY_MAT4 = array("f", [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])

GL_RGBA = 0x1908


def _create_orthogonal_projection(
    left: float,
    right: float,
    bottom: float,
    top: float,
    near: float,
    far: float,
) -> array[float]:
    rml = right - left
    tmb = top - bottom
    fmn = far - near

    A = 2.0 / rml
    B = 2.0 / tmb
    C = -2.0 / fmn
    Tx = -(right + left) / rml
    Ty = -(top + bottom) / tmb
    Tz = -(far + near) / fmn

    return array(
        "f",
        (
            A,
            0.0,
            0.0,
            0.0,
            0.0,
            B,
            0.0,
            0.0,
            0.0,
            0.0,
            C,
            0.0,
            Tx,
            Ty,
            Tz,
            1.0,
        ),
    )


def _get_gl_error() -> int:
    try:
        return cast(int, GL.glGetError())
    except GLError as e:
        return cast(int, e.err)


def _extract_gl_errors() -> Iterator[int]:
    # glGetError does _not_ return the most error;
    # it turns out OpenGL maintains a queue of errors,
    # and glGetError pops the most recent one.
    err: int
    while (err := _get_gl_error()) != GL.GL_NO_ERROR:
        yield err


def _gl_error_str(code: int) -> str:
    match code:
        case GL.GL_NO_ERROR:
            return "GL_NO_ERROR"
        case GL.GL_INVALID_ENUM:
            return "GL_INVALID_ENUM"
        case GL.GL_INVALID_VALUE:
            return "GL_INVALID_VALUE"
        case GL.GL_INVALID_OPERATION:
            return "GL_INVALID_OPERATION"
        case GL.GL_STACK_OVERFLOW:
            return "GL_STACK_OVERFLOW"
        case GL.GL_STACK_UNDERFLOW:
            return "GL_STACK_UNDERFLOW"
        case GL.GL_OUT_OF_MEMORY:
            return "GL_OUT_OF_MEMORY"
        case GL.GL_INVALID_FRAMEBUFFER_OPERATION:
            return "GL_INVALID_FRAMEBUFFER_OPERATION"
        case GL.GL_CONTEXT_LOST:
            return "GL_CONTEXT_LOST"
        case _:
            return f"0x{code:X}"


def _warn_unhandled_gl_errors():
    # Should be called as soon as possible after entering Python from the core;
    # unhandled OpenGL errors from the core must not hamper the frontend.
    unhandled_errors = tuple(_extract_gl_errors())
    if unhandled_errors:
        error_string = ", ".join(_gl_error_str(e) for e in unhandled_errors)
        warnings.warn(f"Core did not handle the following OpenGL errors: {error_string}")


[docs] @final class ModernGlVideoDriver(VideoDriver): """ A video driver that exposes an OpenGL context to the :term:`core` via :mod:`moderngl` and PyOpenGL. """
[docs] def __init__( self, vertex_shader: str | None = None, fragment_shader: str | None = None, varyings: Sequence[str] = ("transformedTexCoord",), window: str | None = None, # TODO: Add ability to configure the OpenGL message callback # TODO: Add ability to configure the OpenGL debug group # TODO: Add ability to force an OpenGL version ): """ Initialize the video driver. Does not create an OpenGL context; that will occur when :meth:`reinit` is called. This driver uses a basic shader program, but custom shaders can be provided. :warning: The shaders are not compiled or linked until the OpenGL context is created, so GLSL errors won't be detected until then. :param vertex_shader: The GLSL source of the vertex shader to use for rendering, or :obj:`None` to use the built-in default. :param fragment_shader: The GLSL source of the fragment shader to use for rendering, or :obj:`None` to use the built-in default. :param varyings: The names of the "varyings" (vertex value outputs) to use. """ package_files = resources.files(modules[__name__].__package__) # TODO: Support passing SPIR-V shaders as bytes match vertex_shader: case str(): self._vertex_shader = vertex_shader case None: self._vertex_shader = (package_files / _DEFAULT_VERT_FILENAME).read_text() case _: raise TypeError(f"Expected a str or None, got {type(vertex_shader).__name__}") match fragment_shader: case str(): self._fragment_shader = fragment_shader case None: self._fragment_shader = (package_files / _DEFAULT_FRAG_FILENAME).read_text() case _: raise TypeError(f"Expected a str or None, got {type(fragment_shader).__name__}") if not isinstance(varyings, Sequence): raise TypeError(f"Expected a sequence of str, got {type(varyings).__name__}") if not all(isinstance(v, str) for v in varyings): raise TypeError("All elements of 'varyings' must be str") self._varyings = tuple(varyings) self._callback: retro_hw_render_callback | None = None self._pixel_format = PixelFormat.RGB1555 self._system_av_info: retro_system_av_info | None = None self._shared = False self._context: Context | None = None self._shader_program: moderngl.Program | None = None self._vao: VertexArray | None = None self._vbo: Buffer | None = None self._last_size: tuple[int, int] | None = None self._rotation: Rotation = Rotation.NONE # Framebuffer, color, and depth attachments for the "default" framebuffer # (equivalent to what a window would provide) self._fbo: Framebuffer | None = None self._color: Texture | None = None self._depth: Renderbuffer | None = None # Framebuffer, color, and depth attachments for the framebuffer # that the core will directly render to. # Will be copied to the default framebuffer for "display". self._hw_render_fbo: Framebuffer | None = None self._hw_render_color: Texture | None = None self._hw_render_depth: Renderbuffer | None = None # Texture for CPU-rendered output self._cpu_color: Texture | None = None # Objects for the window, if requested self._window: BaseWindow | None = None self._window_class: type[BaseWindow] | None = None # TODO: Honor os.environ.get("MODERNGL_WINDOW") if window is not None and moderngl_window is not None: window_mode = _DEFAULT_WINDOW_IMPL if window == "default" else window if not isinstance(window, str): raise TypeError(f"Expected a str or None, got {type(window).__name__}") self._window_class = moderngl_window.get_local_window_cls(window_mode)
[docs] def __del__(self): """Clean up allocated OpenGL resources and the underlying context.""" if self._cpu_color: del self._cpu_color if self._hw_render_depth: del self._hw_render_depth if self._hw_render_color: del self._hw_render_color if self._hw_render_fbo: del self._hw_render_fbo if self._depth: del self._depth if self._color: del self._color if self._fbo: del self._fbo if self._vbo: del self._vbo if self._vao: del self._vao if self._shader_program: del self._shader_program if self._context: del self._context
[docs] @override def set_context(self, callback: retro_hw_render_callback) -> None: if callback is None: return None if not isinstance(callback, retro_hw_render_callback): raise TypeError(f"Expected a retro_hw_render_callback, got {type(callback).__name__}") context_type = HardwareContext(callback.context_type) if context_type not in _CONTEXTS: raise UnsupportedContextError( f"Unsupported hardware context: {context_type} (must be one of: {_CONTEXTS})" ) self._callback = deepcopy(callback)
@property @override def current_framebuffer(self) -> int | None: if self._hw_render_fbo: return self._hw_render_fbo.glo return 0
[docs] @override def get_proc_address(self, sym: bytes) -> retro_proc_address_t | None: if not sym: return None if not self._context: raise RuntimeError("OpenGL context not initialized") # See here https://github.com/moderngl/glcontext?tab=readme-ov-file#structure proc: int | None = self._context.mglo._context.load_opengl_function(sym.decode()) if proc is None: return None return retro_proc_address_t(proc)
[docs] @override def refresh( self, data: memoryview[int] | FrameBufferSpecial, width: int, height: int, pitch: int ) -> None: if not self._context: raise RuntimeError("OpenGL context not initialized") _warn_unhandled_gl_errors() with self._context.debug_scope("libretro.ModernGlVideoDriver.refresh"): match data: case FrameBufferSpecial.DUPE: # Do nothing, we're re-rendering the previous frame # TODO: Re-render whichever framebuffer was most recently used pass case FrameBufferSpecial.HARDWARE: assert self._color is not None, ( "Color buffer should have been initialized in reinit() by now" ) assert self._hw_render_fbo is not None, ( "Hardware render FBO should have been initialized in reinit() by now" ) assert self._hw_render_color is not None, ( "Hardware render color buffer should have been initialized in reinit() by now" ) self._context.copy_framebuffer(self._color, self._hw_render_fbo) self._hw_render_color.use() case memoryview(): self.__update_cpu_texture(data, width, height, pitch) assert self._cpu_color is not None, ( "CPU-rendered color buffer should have been initialized in reinit() by now" ) self._cpu_color.use() self._context.viewport = (0, 0, width, height) matrix = _create_orthogonal_projection(-1, 1, 1, -1, -1, 1) assert self._shader_program is not None, ( "Shader program should have been initialized in reinit() by now" ) mvp_uniform = self._shader_program.get("mvp", None) assert isinstance(mvp_uniform, Uniform), ( "shader should've been verified to have an 'mvp' uniform in reinit() by now" ) mvp_uniform.write(matrix) assert self._fbo is not None, "FBO should have been initialized in reinit() by now" assert self._color is not None, ( "Color buffer should have been initialized in reinit() by now" ) assert self._vao is not None, "VAO should have been initialized in reinit() by now" self._fbo.use() self._color.use(1) self._vao.render(moderngl.TRIANGLE_STRIP) if self._window: with self._context.debug_scope( "libretro.ModernGlVideoDriver.refresh.swap_buffers" ): self._window.fbo.use() self._context.copy_framebuffer(self._window.fbo, self._fbo) self._window.swap_buffers() self._context.finish() self._last_size = (width, height)
@property @override def needs_reinit(self) -> bool: if not self._context: return True if not self._vao: return True return False
[docs] @override def reinit(self) -> None: if not self._system_av_info: raise RuntimeError("System AV info not set") if self._callback: context_type = HardwareContext(self._callback.context_type) if context_type not in _CONTEXTS: raise RuntimeError(f"Unsupported hardware context: {context_type}") else: context_type = HardwareContext.NONE # TODO: Honor cache_context; try to avoid reinitializing the context if self._context: self._context.clear_errors() if self._callback and self._callback.context_destroy: # If the core wants to clean up before the context is destroyed... with self._context.debug_scope( "libretro.ModernGlVideoDriver.reinit.context_destroy" ): self._callback.context_destroy() _warn_unhandled_gl_errors() if self._window: self._window.destroy() del self._window self._context.release() del self._context del self._hw_render_depth del self._hw_render_color del self._hw_render_fbo del self._vao del self._fbo del self._shader_program del self._vbo del self._cpu_color # Destroy the OpenGL context and create a new one geometry = self._system_av_info.geometry match context_type, self._window_class: case HardwareContext.NONE, type() as window_class: self._window = window_class( title="libretro.py", size=(geometry.base_width, geometry.base_height), resizable=False, visible=True, vsync=False, ) moderngl_window.activate_context(self._window) self._context = self._window.ctx case HardwareContext.OPENGL, type() as window_class: self._window = window_class( title="libretro.py", gl_version=(2, 1), size=(geometry.base_width, geometry.base_height), resizable=False, visible=True, vsync=False, ) moderngl_window.activate_context(self._window) self._context = self._window.ctx case HardwareContext.OPENGL_CORE, type() as window_class: assert self._callback is not None, ( "Should've been set by the core, or else this branch wouldn't have been taken" ) self._window = window_class( title="libretro.py", gl_version=(self._callback.version_major, self._callback.version_minor), size=(geometry.base_width, geometry.base_height), resizable=False, visible=True, vsync=False, ) moderngl_window.activate_context(self._window) self._context = self._window.ctx case HardwareContext.NONE, None: # Create a default context with OpenGL 3.3 core profile; # do not expose it to the core, only use it for software rendering self._context = create_context(standalone=True) case HardwareContext.OPENGL, None: self._context = create_context(require=210, standalone=True, share=self._shared) case HardwareContext.OPENGL_CORE, None: assert self._callback is not None, ( "Should've been set by the core, or else this branch wouldn't have been taken" ) ver = self._callback.version_major * 100 + self._callback.version_minor * 10 self._context = create_context(require=ver, standalone=True, share=self._shared) self._context.clear_errors() if self._context.version_code >= 430: self._context.enable_direct(cast(int, GL.GL_DEBUG_OUTPUT)) self._context.enable_direct(cast(int, GL.GL_DEBUG_OUTPUT_SYNCHRONOUS)) elif "GL_KHR_debug" in self._context.extensions and GL.glPushDebugGroupKHR: self._context.enable_direct(cast(int, GL.GL_DEBUG_OUTPUT_KHR)) self._context.enable_direct(cast(int, GL.GL_DEBUG_OUTPUT_SYNCHRONOUS_KHR)) # TODO: Contribute this stuff to moderngl self._context.clear_errors() with self._context.debug_scope("libretro.ModernGlVideoDriver.reinit"): self._context.gc_mode = "auto" self.__init_fbo() self._shader_program = self._context.program( vertex_shader=self._vertex_shader, fragment_shader=self._fragment_shader, varyings=self._varyings, fragment_outputs={"pixelColor": 0}, ) mvp = array("f", _IDENTITY_MAT4) if not self._callback or not self._callback.bottom_left_origin: # If we're only using software rendering, or if we want the origin at the top-left... mvp[5] = -1 # ...then flip the screen vertically by negating the Y scale mvp_uniform = self._shader_program.get("mvp", None) if not isinstance(mvp_uniform, Uniform): raise RuntimeError( f"Expected shader program to have an 'mvp' uniform, but it was a {type(mvp_uniform).__name__} or not present at all" ) mvp_uniform.write(mvp) self._vbo = self._context.buffer(_VERTEXES) self._vao = self._context.vertex_array( self._shader_program, self._vbo, "vertexCoord", "texCoord" ) # TODO: Make the particular names configurable self._shader_program.label = "libretro.py Shader Program" self._vbo.label = "libretro.py Screen VBO" self._vao.label = "libretro.py Screen VAO" # TODO: Honor debug_context; enable debugging features if requested if self._callback is not None and context_type != HardwareContext.NONE: # If the core specifically wants to render with the OpenGL API... self.__init_hw_render() if self._callback.context_reset: # If the core wants to set up resources after the context is created... self._context.clear_errors() with self._context.debug_scope( "libretro.ModernGlVideoDriver.reinit.context_reset" ): self._callback.context_reset() _warn_unhandled_gl_errors()
@property @override def supported_contexts(self) -> Set[HardwareContext]: return _CONTEXTS @property @override def preferred_context(self) -> HardwareContext | None: return HardwareContext.OPENGL_CORE @property @override def active_context(self) -> HardwareContext: if self._context and self._callback: return HardwareContext(self._callback.context_type) return HardwareContext.NONE @property @override def geometry(self) -> retro_game_geometry | None: return deepcopy(self._system_av_info.geometry) if self._system_av_info else None @geometry.setter @override def geometry(self, geometry: retro_game_geometry) -> None: if not isinstance(geometry, retro_game_geometry): raise TypeError(f"Expected a retro_game_geometry, got {type(geometry).__name__}") if not self._system_av_info: raise RuntimeError("No system AV info has been set") self._system_av_info.geometry.base_width = geometry.base_width self._system_av_info.geometry.base_height = geometry.base_height self._system_av_info.geometry.aspect_ratio = geometry.aspect_ratio # TODO: Set the OpenGL viewport if necessary @property @override def pixel_format(self) -> PixelFormat: return self._pixel_format @pixel_format.setter @override def pixel_format(self, format: PixelFormat) -> None: if not isinstance(format, PixelFormat): raise TypeError(f"Expected a PixelFormat, got {type(format).__name__}") if format not in PixelFormat: raise ValueError(f"Invalid pixel format: {format}") if self._pixel_format != format: self._pixel_format = format if self._cpu_color: self._cpu_color.release() self._cpu_color = None @property @override def rotation(self) -> Rotation: return self._rotation @rotation.setter @override def rotation(self, rotation: Rotation) -> None: if not isinstance(rotation, (Rotation, int)): raise TypeError(f"Expected a Rotation, got {type(rotation).__name__}") if rotation not in Rotation: raise ValueError(f"Invalid rotation: {rotation}") self._rotation = rotation # TODO: Set the rotation matrix in the shader @property @override def system_av_info(self) -> retro_system_av_info | None: if not self._system_av_info: return None return deepcopy(self._system_av_info) @system_av_info.setter @override def system_av_info(self, av_info: retro_system_av_info) -> None: if not isinstance(av_info, retro_system_av_info): raise TypeError(f"Expected a retro_system_av_info, got {type(av_info).__name__}") self._system_av_info = deepcopy(av_info) self.reinit()
[docs] @override def screenshot(self, prerotate: bool = True) -> Screenshot | None: if self._system_av_info is None: return None if not self._context: return None if not self._last_size: return None self._context.clear_errors() with self._context.debug_scope("libretro.ModernGlVideoDriver.screenshot"): if self._window: frame = self._window.fbo.read(self._last_size, 4) else: if not self._fbo: return None frame = self._fbo.read(self._last_size, 4) if frame is None: return None self._context.clear_errors() if not self._callback or not self._callback.bottom_left_origin: # If we're using software rendering or the origin is at the bottom-left... bytes_per_row = self._last_size[0] * self._pixel_format.bytes_per_pixel reversed_frame = array("B", frame) reversed_frame_view = memoryview(reversed_frame) frame_view = memoryview(frame) frame_len = len(frame) for i in range(self._last_size[1]): # For each row... start = i * bytes_per_row end = start + bytes_per_row reversed_frame_view[start:end] = frame_view[ frame_len - end : frame_len - start ] # ...copy row number (height - i) to row i frame = reversed_frame_view return Screenshot( memoryview(frame), self._last_size[0], self._last_size[1], self._rotation, self._pixel_format, )
@property @override def shared_context(self) -> bool: return self._shared @shared_context.setter @override def shared_context(self, value: bool) -> None: self._shared = bool(value) @property @override def can_dupe(self) -> bool: return True
[docs] @override def get_software_framebuffer( self, width: int, height: int, flags: MemoryAccess ) -> retro_framebuffer | None: # TODO: Map the OpenGL texture to a software framebuffer pass
@property @override def hw_render_interface(self) -> retro_hw_render_interface | None: # libretro doesn't define one of these for OpenGL, so no need return None def __get_framebuffer_size(self) -> tuple[int, int]: assert self._context is not None assert self._system_av_info is not None # Equivalent to glGetIntegerv max_fbo_size = self._context.info["GL_MAX_TEXTURE_SIZE"] max_rb_size = self._context.info["GL_MAX_RENDERBUFFER_SIZE"] geometry = self._system_av_info.geometry if ( geometry.max_width > max_rb_size or geometry.max_height > max_fbo_size or geometry.max_height > max_rb_size or geometry.max_width > max_fbo_size ): warnings.warn( f"Core-provided framebuffer size ({geometry.max_width}x{geometry.max_height}) exceeds GL_MAX_TEXTURE_SIZE ({max_fbo_size}) or GL_MAX_RENDERBUFFER_SIZE ({max_rb_size})" ) width = min(geometry.max_width, max_fbo_size, max_rb_size) height = min(geometry.max_height, max_fbo_size, max_rb_size) return width, height def __init_fbo(self): assert self._context is not None with self._context.debug_scope("libretro.ModernGlVideoDriver.__init_fbo"): assert self._system_av_info is not None del self._fbo del self._color del self._depth geometry = self._system_av_info.geometry size = self.__get_framebuffer_size() # Similar to glGenTextures, glBindTexture, and glTexImage2D self._color = self._context.texture(size, 4) self._depth = self._context.depth_renderbuffer(size) # Similar to glGenFramebuffers, glBindFramebuffer, and glFramebufferTexture2D self._fbo = self._context.framebuffer(self._color, self._depth) self._color.label = "libretro.py Main FBO Color Attachment" self._depth.label = "libretro.py Main FBO Depth Attachment" self._fbo.label = "libretro.py Main FBO" self._fbo.viewport = (0, 0, geometry.base_width, geometry.base_height) self._fbo.scissor = (0, 0, geometry.base_width, geometry.base_height) self._fbo.clear() self._context.clear_errors() def __init_hw_render(self): assert self._context is not None with self._context.debug_scope("libretro.ModernGlVideoDriver.__init_hw_render"): assert self._callback is not None assert self._system_av_info is not None self._hw_render_fbo = None self._hw_render_color = None self._hw_render_depth = None size = self.__get_framebuffer_size() # Similar to glGenTextures, glBindTexture, and glTexImage2D self._hw_render_color = self._context.texture(size, 4) if self._callback.depth: # If the core is asking for a depth attachment... # Similar to glGenRenderbuffers, glBindRenderbuffer, and glRenderbufferStorage self._hw_render_depth = self._context.depth_renderbuffer(size) if self._callback.stencil: warnings.warn( "Core requested stencil attachment, but moderngl lacks support; ignoring" ) # TODO: Implement stencil buffer support in moderngl # Similar to glGenFramebuffers, glBindFramebuffer, and glFramebufferTexture2D self._hw_render_fbo = self._context.framebuffer( self._hw_render_color, self._hw_render_depth ) self._hw_render_fbo.clear() self._hw_render_fbo.label = "libretro.py Hardware Rendering FBO" self._hw_render_color.label = "libretro.py Hardware Rendering FBO Color Attachment" if self._hw_render_depth: self._hw_render_depth.label = "libretro.py Hardware Rendering FBO Depth Attachment" def __update_cpu_texture(self, data: memoryview[int], width: int, height: int, _pitch: int): assert self._context is not None with self._context.debug_scope("libretro.ModernGlVideoDriver.__update_cpu_texture"): if self._cpu_color and self._cpu_color.size == (width, height): # If we have a texture for CPU-rendered output, and it's the right size... self._cpu_color.write(data) else: del self._cpu_color # Equivalent to glGenTextures, glBindTexture, glTexImage2D, and glTexParameteri match self._pixel_format: case PixelFormat.XRGB8888: self._cpu_color = self._context.texture( (width, height), 4, data ) # GL_RGBA8 self._cpu_color.swizzle = "BGR1" case PixelFormat.RGB565: GL_RGB565 = 0x8D62 self._cpu_color = self._context.texture( (width, height), 3, data, internal_format=GL_RGB565 ) # moderngl can't natively express GL_RGB565 case PixelFormat.RGB1555: GL_RGB5 = 0x8050 self._cpu_color = self._context.texture( (width, height), 3, data, internal_format=GL_RGB5 ) # moderngl can't natively express GL_RGB5 self._cpu_color.label = "libretro.py CPU-Rendered Frame"
__all__ = ["ModernGlVideoDriver"]