# Copyright 2017 The dm_control Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ============================================================================ """Base class for OpenGL context handlers. `ContextBase` defines a common API that OpenGL rendering contexts should conform to. In addition, it provides a `make_current` context manager that: 1. Makes this OpenGL context current within the appropriate rendering thread. 2. Yields an object exposing a `call` method that can be used to execute OpenGL calls within the rendering thread. See the docstring for `dm_control.utils.render_executor` for further details regarding rendering threads. """ import abc import atexit import collections import contextlib import sys import weakref from absl import logging from dm_control._render import executor import numpy as np _CURRENT_CONTEXT_FOR_THREAD = collections.defaultdict(lambda: None) _CURRENT_THREAD_FOR_CONTEXT = collections.defaultdict(lambda: None) class ContextBase(metaclass=abc.ABCMeta): """Base class for managing OpenGL contexts.""" def __init__(self, max_width, max_height, render_executor_class=executor.RenderExecutor): """Initializes this context.""" logging.debug('Using render executor class: %s', render_executor_class.__name__) self._render_executor = render_executor_class() self._refcount = 0 self_weakref = weakref.ref(self) def _free_at_exit(): if self_weakref(): self_weakref()._free_unconditionally() # pylint: disable=protected-access atexit.register(_free_at_exit) with self._render_executor.execution_context() as ctx: ctx.call(self._platform_init, max_width, max_height) self._patients = [] def keep_alive(self, obj): self._patients.append(obj) def dont_keep_alive(self, obj): try: self._patients.remove(obj) except ValueError: pass def increment_refcount(self): self._refcount += 1 def decrement_refcount(self): self._refcount -= 1 @property def terminated(self): return self._render_executor.terminated @property def thread(self): return self._render_executor.thread def _free_on_executor_thread(self): # pylint: disable=missing-function-docstring current_ctx = _CURRENT_CONTEXT_FOR_THREAD[self._render_executor.thread] if current_ctx is not None: del _CURRENT_THREAD_FOR_CONTEXT[current_ctx] del _CURRENT_CONTEXT_FOR_THREAD[self._render_executor.thread] self._platform_make_current() try: dummy = [] while self._patients: patient = self._patients.pop() assert sys.getrefcount(patient) == sys.getrefcount(dummy) if hasattr(patient, 'free'): patient.free() del patient finally: self._platform_free() def free(self): """Frees resources associated with this context if its refcount is zero.""" if self._refcount == 0: self._free_unconditionally() def _free_unconditionally(self): self._render_executor.terminate(self._free_on_executor_thread) def __del__(self): self._free_unconditionally() @contextlib.contextmanager def make_current(self): """Context manager that makes this Renderer's OpenGL context current. Yields: An object that exposes a `call` method that can be used to call a function on the dedicated rendering thread. Raises: RuntimeError: If this context is already current on another thread. """ with self._render_executor.execution_context() as ctx: if _CURRENT_CONTEXT_FOR_THREAD[self._render_executor.thread] != id(self): if _CURRENT_THREAD_FOR_CONTEXT[id(self)]: raise RuntimeError( 'Cannot make context {!r} current on thread {!r}: ' 'this context is already current on another thread {!r}.' .format(self, self._render_executor.thread, _CURRENT_THREAD_FOR_CONTEXT[id(self)])) else: current_context = ( _CURRENT_CONTEXT_FOR_THREAD[self._render_executor.thread]) if current_context: del _CURRENT_THREAD_FOR_CONTEXT[current_context] _CURRENT_THREAD_FOR_CONTEXT[id(self)] = self._render_executor.thread _CURRENT_CONTEXT_FOR_THREAD[self._render_executor.thread] = id(self) ctx.call(self._platform_make_current) yield ctx def to_pixels(self, buffer): """Converts the buffer to pixels.""" return np.flipud(buffer) @abc.abstractmethod def _platform_init(self, max_width, max_height): """Performs an implementation-specific context initialization.""" @abc.abstractmethod def _platform_make_current(self): """Make the OpenGL context current on the executing thread.""" @abc.abstractmethod def _platform_free(self): """Performs an implementation-specific context cleanup."""