"""
Types for delegating to different :class:`.VideoDriver` implementations at runtime,
depending on the player's preferences
and available hardware resources.
"""
from collections.abc import Callable, Mapping, Set
from copy import deepcopy
from types import MappingProxyType
from typing import final, override
from libretro.api.av import retro_game_geometry, retro_system_av_info
from libretro.api.proc import retro_proc_address_t
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
from .software import ArrayVideoDriver
DriverMap = Mapping[HardwareContext, Callable[[], VideoDriver]]
_default_driver_map: dict[HardwareContext, Callable[[], VideoDriver]] = {
HardwareContext.NONE: ArrayVideoDriver
}
try:
from libretro.drivers.video.opengl import ModernGlVideoDriver
_default_driver_map[HardwareContext.OPENGL_CORE] = ModernGlVideoDriver
_default_driver_map[HardwareContext.OPENGL] = ModernGlVideoDriver
except ImportError:
ModernGlVideoDriver = None
DEFAULT_DRIVER_MAP: DriverMap = MappingProxyType(_default_driver_map)
"""
The default mapping from context types to :class:`.VideoDriver` constructors.
Defaults may vary based on the platform and installed packages.
They are as follows:
:attr:`.HardwareContext.NONE`
Always available and mapped to :class:`.ArrayVideoDriver`.
:attr:`~.HardwareContext.OPENGL_CORE`, :attr:`~.HardwareContext.OPENGL`
Mapped to :class:`.ModernGlVideoDriver` if :py:mod:`moderngl` is installed, absent if not.
:class:`.VideoDriver` s for other graphics APIs have not yet been implemented.
"""
[docs]
@final
class MultiVideoDriver(VideoDriver):
"""
A video driver that delegates to one of several possible :class:`.VideoDriver` s,
depending on what the core or frontend requests.
This class is useful for a :class:`.Core` that supports multiple graphics APIs,
especially if it can switch between them at runtime.
"""
[docs]
def __init__(
self,
drivers: DriverMap = DEFAULT_DRIVER_MAP,
preferred: HardwareContext = HardwareContext.NONE,
):
"""
Initialize a new multi-video driver with the preferred :class:`.HardwareContext`.
:param drivers: A map of hardware context types to callables;
each callable should accept no arguments
and return a new video driver instance.
Defaults to :data:`DEFAULT_DRIVER_MAP`.
:param preferred: The initial hardware context type to use.
:raises TypeError: If any parameter is not consistent with its documented types.
:raises ValueError: If ``drivers`` doesn't contain an entry for both ``preferred`` and :attr:`.HardwareContext.NONE`.
"""
if not isinstance(drivers, Mapping):
raise TypeError(
f"Expected drivers to be a Mapping[HardwareContext, () -> VideoDriver], got {type(drivers).__name__}"
)
if preferred not in HardwareContext:
raise ValueError(f"Invalid hardware context: {preferred}")
if preferred not in drivers:
raise ValueError(f"No video driver for preferred hardware context: {preferred}")
if not all(isinstance(k, HardwareContext) for k in drivers.keys()):
raise TypeError("All keys in 'drivers' must be HardwareContext instances")
if not all(callable(v) for v in drivers.values()):
raise TypeError("All values in 'drivers' must be callable")
self._preferred = preferred
self._drivers = dict(drivers)
self._supported_contexts = frozenset(self._drivers.keys())
self._pixel_format = PixelFormat.RGB1555
self._current: VideoDriver | None = None
self._rotation: Rotation = Rotation.NONE
self._system_av_info: retro_system_av_info | None = None
self._callback = retro_hw_render_callback(context_type=HardwareContext.NONE)
self._shared_context = False
self._next_hw_context: HardwareContext | None = HardwareContext.NONE
[docs]
@override
def refresh(
self, data: memoryview | FrameBufferSpecial, width: int, height: int, pitch: int
) -> None:
"""Delegate to the active video driver's :meth:`.VideoDriver.refresh` method."""
if self._current is None:
raise RuntimeError("No active video driver")
self._current.refresh(data, width, height, pitch)
@property
@override
def needs_reinit(self) -> bool:
"""
:obj:`True` if no underlying :class:`.VideoDriver` has been initialized,
a new graphics API has been requested with :meth:`~.MultiVideoDriver.set_context`,
or the active driver's :attr:`~.VideoDriver.needs_reinit` is :obj:`True`.
"""
if self._current is None:
return True
if self._next_hw_context is not None:
return True
return self._current.needs_reinit
[docs]
@override
def reinit(self) -> None:
"""
Initialize a new :class:`.VideoDriver` instance based on the most recent graphics API request,
or reinitializes the current one if no new context has been requested.
If initializing a new :class:`.VideoDriver` then
its :attr:`~.VideoDriver.pixel_format`, :attr:`~.VideoDriver.system_av_info`,
:attr:`~.VideoDriver.rotation`, and :attr:`~.VideoDriver.shared_context`
are set to the values used by the existing driver (if any) where supported.
.. note::
Does not use :attr:`~.VideoDriver.preferred_context`.
"""
if self._current is not None and self._current.active_context == self._next_hw_context:
# If we're not switching to a whole new video driver...
self._current.reinit() # ...then just let the driver reinit itself
elif self._next_hw_context is not None:
# If we're switching to another hardware rendering API...
driver = self._drivers[self._next_hw_context]()
if not driver:
raise RuntimeError(f"Video driver for {self._next_hw_context} not initialized")
if self._current is not None:
# Use the settings of the existing video driver if we're switching to a new one
pixel_format = self._current.pixel_format
system_av_info = self._current.system_av_info
rotation = self._current.rotation
shared = self._current.shared_context
else:
pixel_format = self._pixel_format
system_av_info = self._system_av_info
rotation = self._rotation
shared = self._shared_context
try:
driver.pixel_format = pixel_format
except NotImplementedError:
self._pixel_format = PixelFormat.RGB1555
try:
driver.rotation = rotation
except NotImplementedError:
self._rotation = Rotation.NONE
try:
driver.shared_context = shared
except NotImplementedError:
self._shared_context = False
old_driver = self._current
self._current = driver
# Must set the callback before setting the system AV info,
# as setting the system AV info reinitializes the video driver immediately
# and that requires calling the core-provided callbacks
driver.set_context(self._callback)
if system_av_info is not None:
driver.system_av_info = system_av_info
# No need to call driver.reinit(); setting the system AV info should do that
del old_driver
# TODO: If initializing the new driver fails, keep the old one
self._next_hw_context = None
@property
@override
def supported_contexts(self) -> Set[HardwareContext]:
return self._supported_contexts
@property
@override
def active_context(self) -> HardwareContext:
return self._current.active_context if self._current else HardwareContext.NONE
@property
@override
def preferred_context(self) -> HardwareContext | None:
return self._preferred
@preferred_context.setter
@override
def preferred_context(self, context: HardwareContext) -> None:
if not isinstance(context, HardwareContext):
raise TypeError(f"Expected a HardwareContext, got {type(context).__name__}")
if context not in self._supported_contexts:
raise ValueError(f"Unsupported hardware context: {context}")
self._preferred = context
@preferred_context.deleter
@override
def preferred_context(self) -> None:
self._preferred = None
[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 self._supported_contexts:
raise UnsupportedContextError(f"Unsupported hardware context: {context_type}")
self._next_hw_context = context_type
self._callback = deepcopy(callback)
# TODO: If requesting NONE explicitly, check _callback.cache_context;
# if true, keep using this hardware context for software rendering.
# If not, switch to whatever driver is registered for NONE.
@property
@override
def current_framebuffer(self) -> int:
if self._current is None:
return 0
framebuffer = self._current.current_framebuffer
if framebuffer is None:
return 0
return framebuffer
[docs]
@override
def get_proc_address(self, sym: bytes) -> retro_proc_address_t | None:
if self._current is None:
return None
return self._current.get_proc_address(sym)
@property
@override
def rotation(self) -> Rotation:
return self._rotation
@rotation.setter
@override
def rotation(self, rotation: Rotation) -> None:
self._rotation = rotation
if self._current:
self._current.rotation = rotation
@property
@override
def can_dupe(self) -> bool | None:
if self._current is not None:
return self._current.can_dupe
@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__}")
self._pixel_format = format
if self._current is not None:
self._current.pixel_format = format
@property
@override
def system_av_info(self) -> retro_system_av_info:
if self._current is not None:
system_av_info = self._current.system_av_info
if system_av_info is not None:
return system_av_info
if self._system_av_info is None:
raise RuntimeError("System AV info not set")
return 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 retro_system_av_info, got {type(av_info).__name__}")
self._system_av_info = deepcopy(av_info)
self.reinit()
@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 retro_game_geometry, got {type(geometry).__name__}")
if self._system_av_info: # In case this gets called before the initial AV info get
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
if self._current is not None:
self._current.geometry = geometry
[docs]
@override
def get_software_framebuffer(
self, width: int, height: int, flags: MemoryAccess
) -> retro_framebuffer | None:
if self._current:
return self._current.get_software_framebuffer(width, height, flags)
return None
@property
@override
def hw_render_interface(self) -> retro_hw_render_interface | None:
if self._current:
return self._current.hw_render_interface
return None
@property
@override
def shared_context(self) -> bool:
if self._current:
return self._current.shared_context
return self._shared_context
@shared_context.setter
@override
def shared_context(self, value: bool) -> None:
if not isinstance(value, bool):
raise TypeError(f"Expected bool, got {type(value).__name__}")
self._shared_context = value
if self._current:
self._current.shared_context = value
[docs]
@override
def screenshot(self, prerotate: bool = True) -> Screenshot | None:
return self._current.screenshot(prerotate) if self._current else None
__all__ = [
"MultiVideoDriver",
"DriverMap",
"DEFAULT_DRIVER_MAP",
]