1# Copyright (c) 2019 Ultimaker B.V.
2# Uranium is released under the terms of the LGPLv3 or higher.
3
4import sys
5import ctypes   # type: ignore
6
7from PyQt5.QtGui import QOpenGLVersionProfile, QOpenGLContext, QOpenGLFramebufferObject, QOpenGLBuffer
8from PyQt5.QtWidgets import QMessageBox
9from typing import Any, TYPE_CHECKING, cast
10
11from UM.Logger import Logger
12
13from UM.Version import Version
14from UM.View.GL.FrameBufferObject import FrameBufferObject
15from UM.View.GL.ShaderProgram import ShaderProgram
16from UM.View.GL.ShaderProgram import InvalidShaderProgramError
17from UM.View.GL.Texture import Texture
18from UM.View.GL.OpenGLContext import OpenGLContext
19from UM.i18n import i18nCatalog  # To make dialogs translatable.
20i18n_catalog = i18nCatalog("uranium")
21
22import OpenGL.GL as gl
23
24if TYPE_CHECKING:
25    from UM.Mesh.MeshData import MeshData
26
27
28class OpenGL:
29    """Convenience methods for dealing with OpenGL.
30
31    This class simplifies dealing with OpenGL and different Python OpenGL bindings. It
32    mostly describes an interface that should be implemented for dealing with basic OpenGL
33    functionality using these different OpenGL bindings. Additionally, it provides singleton
34    handling. The implementation-defined subclass must be set as singleton instance as soon
35    as possible so that any calls to getInstance() return a proper object.
36    """
37    VertexBufferProperty = "__vertex_buffer"
38    IndexBufferProperty = "__index_buffer"
39
40    class Vendor:
41        """Different OpenGL chipset vendors."""
42        NVidia = 1
43        AMD = 2
44        Intel = 3
45        Other = 4
46
47    def __init__(self) -> None:
48        if OpenGL.__instance is not None:
49            raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
50        OpenGL.__instance = self
51
52        super().__init__()
53
54        profile = QOpenGLVersionProfile()
55        profile.setVersion(OpenGLContext.major_version, OpenGLContext.minor_version)
56        profile.setProfile(OpenGLContext.profile)
57
58        context = QOpenGLContext.currentContext()
59        if not context:
60            Logger.log("e", "Startup failed due to OpenGL context creation failing")
61            QMessageBox.critical(None, i18n_catalog.i18nc("@message", "Failed to Initialize OpenGL", "Could not initialize an OpenGL context. This program requires OpenGL 2.0 or higher. Please check your video card drivers."))
62            sys.exit(1)
63        self._gl = gl
64#        self._gl = context.versionFunctions(profile) # type: Any #It's actually a protected class in PyQt that depends on the implementation of your graphics card.
65        if not self._gl:
66            Logger.log("e", "Startup failed due to OpenGL initialization failing")
67            QMessageBox.critical(None, i18n_catalog.i18nc("@message", "Failed to Initialize OpenGL", "Could not initialize OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers."))
68            sys.exit(1)
69
70        # It would be nice to be able to not necessarily need OpenGL FrameBuffer Object support, but
71        # due to a limitation in PyQt, currently glReadPixels or similar methods are not available.
72        # This means we can only get frame buffer contents through methods that indirectly call
73        # those methods, in this case primarily QOpenGLFrameBufferObject::toImage(), making us
74        # hard-depend on FrameBuffer Objects.
75        if not self.hasFrameBufferObjects():
76            Logger.log("e", "Startup failed, OpenGL does not support Frame Buffer Objects")
77            QMessageBox.critical(None, i18n_catalog.i18nc("Critical OpenGL Extensions Missing", "Critical OpenGL extensions are missing. This program requires support for Framebuffer Objects. Please check your video card drivers."))
78            sys.exit(1)
79
80#        self._gl.initializeOpenGLFunctions()
81
82        self._gpu_vendor = OpenGL.Vendor.Other #type: int
83        vendor_string = self._gl.glGetString(self._gl.GL_VENDOR).decode("utf-8")
84        if vendor_string is None:
85            vendor_string = "Unknown"
86        vendor_string = vendor_string.lower()
87        if "nvidia" in vendor_string:
88            self._gpu_vendor = OpenGL.Vendor.NVidia
89        elif "amd" in vendor_string or "ati" in vendor_string:
90            self._gpu_vendor = OpenGL.Vendor.AMD
91        elif "intel" in vendor_string:
92            self._gpu_vendor = OpenGL.Vendor.Intel
93
94        self._gpu_type = "Unknown"  # type: str
95        # WORKAROUND: Cura/#1117 Cura-packaging/12
96        # Some Intel GPU chipsets return a string, which is not undecodable via PyQt5.
97        # This workaround makes the code fall back to a "Unknown" renderer in these cases.
98        try:
99            self._gpu_type = self._gl.glGetString(self._gl.GL_RENDERER)
100        except UnicodeDecodeError:
101            Logger.log("e", "DecodeError while getting GL_RENDERER via glGetString!")
102
103        self._opengl_version = self._gl.glGetString(self._gl.GL_VERSION) #type: str
104
105        self._opengl_shading_language_version = Version("0.0")  # type: Version
106        try:
107            self._opengl_shading_language_version = Version(self._gl.glGetString(self._gl.GL_SHADING_LANGUAGE_VERSION))
108        except:
109            self._opengl_shading_language_version = Version("1.0")
110
111        if not self.hasFrameBufferObjects():
112            Logger.log("w", "No frame buffer support, falling back to texture copies.")
113
114        Logger.log("d", "Initialized OpenGL subsystems.")
115        Logger.log("d", "OpenGL Version:  %s", self._opengl_version)
116        Logger.log("d", "OpenGL Vendor:   %s", self._gl.glGetString(self._gl.GL_VENDOR))
117        Logger.log("d", "OpenGL Renderer: %s", self._gpu_type)
118        Logger.log("d", "GLSL Version:    %s", self._opengl_shading_language_version)
119
120    def hasFrameBufferObjects(self) -> bool:
121        """Check if the current OpenGL implementation supports FrameBuffer Objects.
122
123        :return: True if FBOs are supported, False if not.
124        """
125        return QOpenGLFramebufferObject.hasOpenGLFramebufferObjects()
126
127    def getOpenGLVersion(self) -> str:
128        """Get the current OpenGL version.
129
130        :return: Version of OpenGL
131        """
132        return self._opengl_version
133
134    def getOpenGLShadingLanguageVersion(self) -> "Version":
135        """Get the current OpenGL shading language version.
136
137        :return: Shading language version of OpenGL
138        """
139        return self._opengl_shading_language_version
140
141    def getGPUVendorName(self) -> str:
142        """Get the current GPU vendor name.
143
144        :return: Name of the vendor of current GPU
145        """
146        return self._gl.glGetString(self._gl.GL_VENDOR)
147
148    def getGPUVendor(self) -> int:
149        """Get the current GPU vendor.
150
151        :return: One of the items of OpenGL.Vendor.
152        """
153        return self._gpu_vendor
154
155    def getGPUType(self) -> str:
156        """Get a string describing the current GPU type.
157
158        This effectively should return the OpenGL renderer string.
159        """
160        return self._gpu_type
161
162    def getBindingsObject(self) -> Any:
163        """Get the OpenGL bindings object.
164
165        This should return an object that has all supported OpenGL functions
166        as methods and additionally defines all OpenGL constants. This object
167        is used to make direct OpenGL calls so should match OpenGL as closely
168        as possible.
169        """
170        return self._gl
171
172    def createFrameBufferObject(self, width: int, height: int) -> FrameBufferObject:
173        """Create a FrameBuffer Object.
174
175        This should return an implementation-specifc FrameBufferObject subclass.
176        """
177        return FrameBufferObject(width, height)
178
179    def createTexture(self) -> Texture:
180        """Create a Texture Object.
181
182        This should return an implementation-specific Texture subclass.
183        """
184        return Texture(self._gl)
185
186    def createShaderProgram(self, file_name: str) -> ShaderProgram:
187        """Create a ShaderProgram Object.
188
189        This should return an implementation-specifc ShaderProgram subclass.
190        """
191        shader = ShaderProgram()
192        # The version_string must match the keys in shader files.
193        if OpenGLContext.isLegacyOpenGL():
194            version_string = ""  # Nothing is added to "fragment" and "vertex"
195        else:
196            version_string = "41core"
197
198        try:
199            shader.load(file_name, version = version_string)
200        except InvalidShaderProgramError:
201            # If the loading failed, it could be that there is no specific shader for this version.
202            # Try again without a version nr to get the generic one.
203            if version_string != "":
204                shader.load(file_name, version = "")
205        return shader
206
207    def createVertexBuffer(self, mesh: "MeshData", **kwargs: Any) -> QOpenGLBuffer:
208        """Create a Vertex buffer for a mesh.
209
210        This will create a vertex buffer object that is filled with the
211        vertex data of the mesh.
212
213        By default, the associated vertex buffer should be cached using a
214        custom property on the mesh. This should use the VertexBufferProperty
215        property name.
216
217        :param mesh: The mesh to create a vertex buffer for.
218        :param kwargs: Keyword arguments.
219        Possible values:
220        - force_recreate: Ignore the cached value if set and always create a new buffer.
221        """
222        if not kwargs.get("force_recreate", False) and hasattr(mesh, OpenGL.VertexBufferProperty):
223            return getattr(mesh, OpenGL.VertexBufferProperty)
224
225        buffer = QOpenGLBuffer(QOpenGLBuffer.VertexBuffer)
226        buffer.create()
227        buffer.bind()
228
229        float_size = ctypes.sizeof(ctypes.c_float)
230        int_size = ctypes.sizeof(ctypes.c_int)
231
232        buffer_size = mesh.getVertexCount() * 3 * float_size # Vertex count * number of components * sizeof(float32)
233        if mesh.hasNormals():
234            buffer_size += mesh.getVertexCount() * 3 * float_size # Vertex count * number of components * sizeof(float32)
235        if mesh.hasColors():
236            buffer_size += mesh.getVertexCount() * 4 * float_size # Vertex count * number of components * sizeof(float32)
237        if mesh.hasUVCoordinates():
238            buffer_size += mesh.getVertexCount() * 2 * float_size # Vertex count * number of components * sizeof(float32)
239        for attribute_name in mesh.attributeNames():
240            attribute = mesh.getAttribute(attribute_name)
241            if attribute["opengl_type"] == "vector2f":
242                buffer_size += mesh.getVertexCount() * 2 * float_size
243            elif attribute["opengl_type"] == "vector4f":
244                buffer_size += mesh.getVertexCount() * 4 * float_size
245            elif attribute["opengl_type"] == "int":
246                buffer_size += mesh.getVertexCount() * int_size
247            elif attribute["opengl_type"] == "float":
248                buffer_size += mesh.getVertexCount() * float_size
249            else:
250                Logger.log(
251                    "e", "Could not determine buffer size for attribute [%s] with type [%s]" % (attribute_name, attribute["opengl_type"]))
252        buffer.allocate(buffer_size)
253
254        offset = 0
255        vertices = mesh.getVerticesAsByteArray()
256        if vertices is not None:
257            buffer.write(0, vertices, len(vertices))
258            offset += len(vertices)
259
260        if mesh.hasNormals():
261            normals = cast(bytes, mesh.getNormalsAsByteArray())
262            buffer.write(offset, normals, len(normals))
263            offset += len(normals)
264
265        if mesh.hasColors():
266            colors = cast(bytes, mesh.getColorsAsByteArray())
267            buffer.write(offset, colors, len(colors))
268            offset += len(colors)
269
270        if mesh.hasUVCoordinates():
271            uvs = cast(bytes, mesh.getUVCoordinatesAsByteArray())
272            buffer.write(offset, uvs, len(uvs))
273            offset += len(uvs)
274
275        for attribute_name in mesh.attributeNames():
276            attribute = mesh.getAttribute(attribute_name)
277            attribute_byte_array = attribute["value"].tostring()
278            buffer.write(offset, attribute_byte_array, len(attribute_byte_array))
279            offset += len(attribute_byte_array)
280
281        buffer.release()
282
283        setattr(mesh, OpenGL.VertexBufferProperty, buffer)
284        return buffer
285
286    def createIndexBuffer(self, mesh: "MeshData", **kwargs: Any):
287        """Create an index buffer for a mesh.
288
289        This will create an index buffer object that is filled with the
290        index data of the mesh.
291
292        By default, the associated index buffer should be cached using a
293        custom property on the mesh. This should use the IndexBufferProperty
294        property name.
295
296        :param mesh: The mesh to create an index buffer for.
297        :param kwargs: Keyword arguments.
298            Possible values:
299            - force_recreate: Ignore the cached value if set and always create a new buffer.
300        """
301        if not mesh.hasIndices():
302            return None
303
304        if not kwargs.get("force_recreate", False) and hasattr(mesh, OpenGL.IndexBufferProperty):
305            return getattr(mesh, OpenGL.IndexBufferProperty)
306
307        buffer = QOpenGLBuffer(QOpenGLBuffer.IndexBuffer)
308        buffer.create()
309        buffer.bind()
310
311        data = cast(bytes, mesh.getIndicesAsByteArray()) # We check for None at the beginning of the method
312        if 'index_start' in kwargs and 'index_stop' in kwargs:
313            buffer.allocate(data[4 * kwargs['index_start']:4 * kwargs['index_stop']], 4*(kwargs['index_stop'] - kwargs['index_start']))
314        else:
315            buffer.allocate(data, len(data))
316        buffer.release()
317
318        setattr(mesh, OpenGL.IndexBufferProperty, buffer)
319
320        return buffer
321
322    __instance = None    # type: OpenGL
323
324    @classmethod
325    def getInstance(cls, *args, **kwargs) -> "OpenGL":
326        return cls.__instance
327