diff options
Diffstat (limited to 'silx/gui/_glutils')
-rw-r--r-- | silx/gui/_glutils/FramebufferTexture.py | 3 | ||||
-rw-r--r-- | silx/gui/_glutils/OpenGLWidget.py | 14 | ||||
-rw-r--r-- | silx/gui/_glutils/Texture.py | 319 | ||||
-rw-r--r-- | silx/gui/_glutils/utils.py | 30 |
4 files changed, 200 insertions, 166 deletions
diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py index cc05080..e065030 100644 --- a/silx/gui/_glutils/FramebufferTexture.py +++ b/silx/gui/_glutils/FramebufferTexture.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -62,6 +62,7 @@ class FramebufferTexture(object): **kwargs): self._texture = Texture(internalFormat, shape=shape, **kwargs) + self._texture.prepare() self._previousFramebuffer = 0 # Used by with statement diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py index 1f7bfae..5e3fcb8 100644 --- a/silx/gui/_glutils/OpenGLWidget.py +++ b/silx/gui/_glutils/OpenGLWidget.py @@ -329,6 +329,20 @@ class OpenGLWidget(qt.QWidget): else: return self.__openGLWidget.getDevicePixelRatio() + def getDotsPerInch(self): + """Returns current screen resolution as device pixels per inch. + + :rtype: float + """ + screen = self.window().windowHandle().screen() + if screen is not None: + # TODO check if this is correct on different OS/screen + # OK on macOS10.12/qt5.13.2 + dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio() + else: # Fallback + dpi = 96. * self.getDevicePixelRatio() + return dpi + def getOpenGLVersion(self): """Returns the available OpenGL version. diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py index a7fd44b..c72135a 100644 --- a/silx/gui/_glutils/Texture.py +++ b/silx/gui/_glutils/Texture.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility +# Copyright (c) 2014-2020 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -81,20 +81,23 @@ class Texture(object): else: shape = data.shape + self._deferredUpdates = [(format_, data, None)] + assert len(shape) in (2, 3) self._shape = tuple(shape) self._ndim = len(shape) self.texUnit = texUnit - self._name = gl.glGenTextures(1) - self.bind(self.texUnit) + self._texParameterUpdates = {} # Store texture params to update + + self._minFilter = minFilter if minFilter is not None else gl.GL_NEAREST + self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = self._minFilter - self._minFilter = None - self.minFilter = minFilter if minFilter is not None else gl.GL_NEAREST + self._magFilter = magFilter if magFilter is not None else gl.GL_LINEAR + self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = self._magFilter - self._magFilter = None - self.magFilter = magFilter if magFilter is not None else gl.GL_LINEAR + self._name = None # Store texture ID if wrap is not None: if not isinstance(wrap, abc.Iterable): @@ -102,69 +105,10 @@ class Texture(object): assert len(wrap) == self.ndim - gl.glTexParameter(self.target, - gl.GL_TEXTURE_WRAP_S, - wrap[-1]) - gl.glTexParameter(self.target, - gl.GL_TEXTURE_WRAP_T, - wrap[-2]) + self._texParameterUpdates[gl.GL_TEXTURE_WRAP_S] = wrap[-1] + self._texParameterUpdates[gl.GL_TEXTURE_WRAP_T] = wrap[-2] if self.ndim == 3: - gl.glTexParameter(self.target, - gl.GL_TEXTURE_WRAP_R, - wrap[0]) - - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - # This are the defaults, useless to set if not modified - # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) - - if data is None: - data = c_void_p(0) - type_ = gl.GL_UNSIGNED_BYTE - else: - type_ = utils.numpyToGLType(data.dtype) - - if self.ndim == 2: - _logger.debug( - 'Creating 2D texture shape: (%d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage2D( - gl.GL_TEXTURE_2D, - 0, - self.internalFormat, - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - else: - _logger.debug( - 'Creating 3D texture shape: (%d, %d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], self.shape[2], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage3D( - gl.GL_TEXTURE_3D, - 0, - self.internalFormat, - self.shape[2], - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - - gl.glBindTexture(self.target, 0) + self._texParameterUpdates[gl.GL_TEXTURE_WRAP_R] = wrap[0] @property def target(self): @@ -188,12 +132,11 @@ class Texture(object): @property def name(self): - """OpenGL texture name""" - if self._name is not None: - return self._name - else: - raise RuntimeError( - "No OpenGL texture resource, discard has already been called") + """OpenGL texture name. + + It is None if not initialized or already discarded. + """ + return self._name @property def minFilter(self): @@ -204,10 +147,7 @@ class Texture(object): def minFilter(self, minFilter): if minFilter != self.minFilter: self._minFilter = minFilter - self.bind() - gl.glTexParameter(self.target, - gl.GL_TEXTURE_MIN_FILTER, - self.minFilter) + self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = minFilter @property def magFilter(self): @@ -218,20 +158,112 @@ class Texture(object): def magFilter(self, magFilter): if magFilter != self.magFilter: self._magFilter = magFilter - self.bind() - gl.glTexParameter(self.target, - gl.GL_TEXTURE_MAG_FILTER, - self.magFilter) + self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = magFilter - def discard(self): - """Delete associated OpenGL texture""" - if self._name is not None: - gl.glDeleteTextures(self._name) - self._name = None - else: - _logger.warning("Discard as already been called") + def _isPrepareRequired(self) -> bool: + """Returns True if OpenGL texture needs to be updated. - def bind(self, texUnit=None): + :rtype: bool + """ + return (self._name is None or + self._texParameterUpdates or + self._deferredUpdates) + + def _prepareAndBind(self, texUnit=None): + """Synchronizes the OpenGL texture""" + if self._name is None: + self._name = gl.glGenTextures(1) + + self._bind(texUnit) + + # Synchronizes texture parameters + for pname, param in self._texParameterUpdates.items(): + gl.glTexParameter(self.target, pname, param) + self._texParameterUpdates = {} + + # Copy data to texture + for format_, data, offset in self._deferredUpdates: + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + + # This are the defaults, useless to set if not modified + # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) + # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) + # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) + # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) + # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) + + if data is None: + data = c_void_p(0) + type_ = gl.GL_UNSIGNED_BYTE + else: + type_ = utils.numpyToGLType(data.dtype) + + if offset is None: # Initialize texture + if self.ndim == 2: + _logger.debug( + 'Creating 2D texture shape: (%d, %d),' + ' internal format: %s, format: %s, type: %s', + self.shape[0], self.shape[1], + str(self.internalFormat), str(format_), str(type_)) + + gl.glTexImage2D( + gl.GL_TEXTURE_2D, + 0, + self.internalFormat, + self.shape[1], + self.shape[0], + 0, + format_, + type_, + data) + + else: + _logger.debug( + 'Creating 3D texture shape: (%d, %d, %d),' + ' internal format: %s, format: %s, type: %s', + self.shape[0], self.shape[1], self.shape[2], + str(self.internalFormat), str(format_), str(type_)) + + gl.glTexImage3D( + gl.GL_TEXTURE_3D, + 0, + self.internalFormat, + self.shape[2], + self.shape[1], + self.shape[0], + 0, + format_, + type_, + data) + + else: # Update already existing texture + if self.ndim == 2: + gl.glTexSubImage2D(gl.GL_TEXTURE_2D, + 0, + offset[1], + offset[0], + data.shape[1], + data.shape[0], + format_, + type_, + data) + + else: + gl.glTexSubImage3D(gl.GL_TEXTURE_3D, + 0, + offset[2], + offset[1], + offset[0], + data.shape[2], + data.shape[1], + data.shape[0], + format_, + type_, + data) + + self._deferredUpdates = [] + + def _bind(self, texUnit=None): """Bind the texture to a texture unit. :param int texUnit: The texture unit to use @@ -241,73 +273,80 @@ class Texture(object): gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) gl.glBindTexture(self.target, self.name) + def _unbind(self, texUnit=None): + """Reset texture binding to a texture unit. + + :param int texUnit: The texture unit to use + """ + if texUnit is None: + texUnit = self.texUnit + gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) + gl.glBindTexture(self.target, 0) + + def prepare(self): + """Synchronizes the OpenGL texture. + + This method must be called with a current OpenGL context. + """ + if self._isPrepareRequired(): + self._prepareAndBind() + self._unbind() + + def bind(self, texUnit=None): + """Bind the texture to a texture unit. + + The OpenGL texture is updated if needed. + + This method must be called with a current OpenGL context. + + :param int texUnit: The texture unit to use + """ + if self._isPrepareRequired(): + self._prepareAndBind(texUnit) + else: + self._bind(texUnit) + + def discard(self): + """Delete associated OpenGL texture. + + This method must be called with a current OpenGL context. + """ + if self._name is not None: + gl.glDeleteTextures(self._name) + self._name = None + else: + _logger.warning("Texture not initialized or already discarded") + # with statement def __enter__(self): self.bind() def __exit__(self, exc_type, exc_val, exc_tb): - gl.glActiveTexture(gl.GL_TEXTURE0 + self.texUnit) - gl.glBindTexture(self.target, 0) + self._unbind() - def update(self, - format_, - data, - offset=(0, 0, 0), - texUnit=None): + def update(self, format_, data, offset=(0, 0, 0), copy=True): """Update the content of the texture. Texture is not resized, so data must fit into texture with the given offset. + This update is performed lazily during next call to + :meth:`prepare` or :meth:`bind`. + Data MUST not be changed until then. + :param format_: The OpenGL format of the data :param data: The data to use to update the texture - :param offset: The offset in the texture where to copy the data - :type offset: List[int] - :param int texUnit: - The texture unit to use (default: the one provided at init) + :param List[int] offset: Offset in the texture where to copy the data + :param bool copy: + True (default) to copy data, False to use as is (do not modify) """ - data = numpy.array(data, copy=False, order='C') + data = numpy.array(data, copy=copy, order='C') + offset = tuple(offset) assert data.ndim == self.ndim assert len(offset) >= self.ndim for i in range(self.ndim): assert offset[i] + data.shape[i] <= self.shape[i] - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - # This are the defaults, useless to set if not modified - # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) - - self.bind(texUnit) - - type_ = utils.numpyToGLType(data.dtype) - - if self.ndim == 2: - gl.glTexSubImage2D(gl.GL_TEXTURE_2D, - 0, - offset[1], - offset[0], - data.shape[1], - data.shape[0], - format_, - type_, - data) - gl.glBindTexture(gl.GL_TEXTURE_2D, 0) - else: - gl.glTexSubImage3D(gl.GL_TEXTURE_3D, - 0, - offset[2], - offset[1], - offset[0], - data.shape[2], - data.shape[1], - data.shape[0], - format_, - type_, - data) - gl.glBindTexture(gl.GL_TEXTURE_3D, 0) + self._deferredUpdates.append((format_, data, offset)) diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py index 35cf819..d5627ef 100644 --- a/silx/gui/_glutils/utils.py +++ b/silx/gui/_glutils/utils.py @@ -29,45 +29,25 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "10/01/2017" -from . import gl import numpy - -_GL_TYPE_SIZES = { - gl.GL_FLOAT: 4, - gl.GL_BYTE: 1, - gl.GL_SHORT: 2, - gl.GL_INT: 4, - gl.GL_UNSIGNED_BYTE: 1, - gl.GL_UNSIGNED_SHORT: 2, - gl.GL_UNSIGNED_INT: 4, -} +from OpenGL.constants import BYTE_SIZES as _BYTE_SIZES +from OpenGL.constants import ARRAY_TO_GL_TYPE_MAPPING as _ARRAY_TO_GL_TYPE_MAPPING def sizeofGLType(type_): """Returns the size in bytes of an element of type `type_`""" - return _GL_TYPE_SIZES[type_] - - -_TYPE_CONVERTER = { - numpy.dtype(numpy.float32): gl.GL_FLOAT, - numpy.dtype(numpy.int8): gl.GL_BYTE, - numpy.dtype(numpy.int16): gl.GL_SHORT, - numpy.dtype(numpy.int32): gl.GL_INT, - numpy.dtype(numpy.uint8): gl.GL_UNSIGNED_BYTE, - numpy.dtype(numpy.uint16): gl.GL_UNSIGNED_SHORT, - numpy.dtype(numpy.uint32): gl.GL_UNSIGNED_INT, -} + return _BYTE_SIZES[type_] def isSupportedGLType(type_): """Test if a numpy type or dtype can be converted to a GL type.""" - return numpy.dtype(type_) in _TYPE_CONVERTER + return numpy.dtype(type_).char in _ARRAY_TO_GL_TYPE_MAPPING def numpyToGLType(type_): """Returns the GL type corresponding the provided numpy type or dtype.""" - return _TYPE_CONVERTER[numpy.dtype(type_)] + return _ARRAY_TO_GL_TYPE_MAPPING[numpy.dtype(type_).char] def segmentTrianglesIntersection(segment, triangles): |